Compare commits

..

16 Commits

Author SHA1 Message Date
guojidan ebad748cdc Merge pull request #368 from guojidan/fix-sql
Fix scanner && lock
2025-08-09 06:37:36 -07:00
junxiang Mu b7e56ed92c Fix: clippy && fmt
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-08-09 21:16:56 +08:00
junxiang Mu 4811632751 Fix: fix scanner detect
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-08-09 21:06:17 +08:00
junxiang Mu 374a702f04 improve lock
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-08-09 21:05:46 +08:00
junxiang Mu e369e9f481 Feature: lock support auto release
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-08-09 17:52:08 +08:00
guojidan fe2e4a2274 Merge pull request #367 from guojidan/fix-sql
feat: enhance metadata extraction with object name for MIME type dete…
2025-08-08 21:53:12 -07:00
junxiang Mu b391272e94 feat: enhance metadata extraction with object name for MIME type detection
Signed-off-by: junxiang Mu <1948535941@qq.com>
2025-08-09 12:29:04 +08:00
majinghe c55c7a6373 feat: add docker usage for rustfs mcp (#365) 2025-08-08 17:18:20 +08:00
houseme 67f1c371a9 upgrade version 2025-08-08 11:33:32 +08:00
guojidan d987686c14 feat(lifecycle): Implement object lifecycle management functionality (#358)
* feat(lifecycle): Implement object lifecycle management functionality

Add a lifecycle module to automatically handle object expiration and transition during scanning
Modify the file metadata cache module to be publicly visible to support lifecycle operations
Adjust the scanning interval to a shorter time for testing lifecycle rules
Implement the parsing and execution logic for S3 lifecycle configurations
Add integration tests to verify the lifecycle expiration functionality
Update dependencies to support the new lifecycle features

Signed-off-by: junxiang Mu <1948535941@qq.com>

* fix cargo dependencies

Signed-off-by: junxiang Mu <1948535941@qq.com>

* fix fmt

Signed-off-by: junxiang Mu <1948535941@qq.com>

---------

Signed-off-by: junxiang Mu <1948535941@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-08-08 10:51:02 +08:00
houseme 48a9707110 fix: add tokio-test (#363)
* fix: add tokio-test

* fix: "called `unwrap` on `v` after checking its variant with `is_some`"

    = help: try using `if let` or `match`
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_unwrap
    = note: `-D clippy::unnecessary-unwrap` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::unnecessary_unwrap)]`

* fmt

* set toolchain 1.88.0

* fmt

* fix: cliip
2025-08-08 10:23:22 +08:00
bestgopher b89450f54d replace make with just (#349) 2025-08-07 22:37:05 +08:00
houseme e0c99bced4 chore: add tls log and removing unused crates (#359)
* chore: add tls log

* improve code for http

* improve code dependencies for `cargo.toml` and removing unused crates

* modify name

* improve code

* fix

* Update crates/config/src/constants/env.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* improve code

* fix

* add `is_enabled` and `is_disabled`

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-07 19:02:09 +08:00
houseme 130f85a575 chore: add tls log (#357) 2025-08-07 17:33:57 +08:00
shiro.lee c42fbed3d2 fix: Fixed an issue where the list_objects_v2 API did not return dire… (#352)
* fix: Fixed an issue where the list_objects_v2 API did not return directory names when they conflicted with file names in the same bucket (e.g., test/ vs. test.txt, aaa/ vs. aaa.csv) (#335)

* fix: adjusted the order of directory listings
2025-08-07 11:05:05 +08:00
安正超 fd539f0f0a Update dependabot.yml 2025-08-06 22:55:52 +08:00
56 changed files with 2516 additions and 571 deletions
+1
View File
@@ -0,0 +1 @@
target
+2 -2
View File
@@ -16,7 +16,7 @@ name: Continuous Integration
on:
push:
branches: [main]
branches: [ main ]
paths-ignore:
- "**.md"
- "**.txt"
@@ -36,7 +36,7 @@ on:
- ".github/workflows/audit.yml"
- ".github/workflows/performance.yml"
pull_request:
branches: [main]
branches: [ main ]
paths-ignore:
- "**.md"
- "**.txt"
+1
View File
@@ -20,3 +20,4 @@ profile.json
.docker/openobserve-otel/data
*.zst
.secrets
*.go
Generated
+94 -198
View File
@@ -119,9 +119,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.19"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -149,22 +149,22 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -260,7 +260,7 @@ dependencies = [
"chrono",
"chrono-tz",
"half",
"hashbrown 0.15.4",
"hashbrown 0.15.5",
"num",
]
@@ -522,9 +522,9 @@ dependencies = [
[[package]]
name = "async-lock"
version = "3.4.0"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
dependencies = [
"event-listener",
"event-listener-strategy",
@@ -674,9 +674,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.3"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0"
checksum = "483020b893cdef3d89637e428d588650c71cfae7ea2e6ecbaee4de4ff99fb2dd"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -704,9 +704,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.4"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe"
checksum = "1541072f81945fa1251f8795ef6c92c4282d74d59f88498ae7d4bf00f0ebdad9"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -739,9 +739,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.9"
version = "1.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7"
checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -764,9 +764,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
version = "1.100.0"
version = "1.101.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c5eafbdcd898114b839ba68ac628e31c4cfc3e11dfca38dc1b2de2f35bb6270"
checksum = "7b16efa59a199f5271bf21ab3e570c5297d819ce4f240e6cf0096d1dc0049c44"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -788,7 +788,7 @@ dependencies = [
"http 0.2.12",
"http 1.3.1",
"http-body 0.4.6",
"lru 0.12.5",
"lru",
"percent-encoding",
"regex-lite",
"sha2 0.10.9",
@@ -798,9 +798,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.78.0"
version = "1.79.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbd7bc4bd34303733bded362c4c997a39130eac4310257c79aae8484b1c4b724"
checksum = "0a847168f15b46329fa32c7aca4e4f1a2e072f9b422f0adb19756f2e1457f111"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -820,9 +820,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.79.0"
version = "1.80.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77358d25f781bb106c1a69531231d4fd12c6be904edb0c47198c604df5a2dbca"
checksum = "b654dd24d65568738593e8239aef279a86a15374ec926ae8714e2d7245f34149"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -842,9 +842,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.80.0"
version = "1.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e3ed2a9b828ae7763ddaed41d51724d2661a50c45f845b08967e52f4939cfc"
checksum = "c92ea8a7602321c83615c82b408820ad54280fb026e92de0eeea937342fafa24"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -865,9 +865,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "1.3.3"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfb9021f581b71870a17eac25b52335b82211cdc092e02b6876b2bcefa61666"
checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
@@ -904,9 +904,9 @@ dependencies = [
[[package]]
name = "aws-smithy-checksums"
version = "0.63.5"
version = "0.63.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab9472f7a8ec259ddb5681d2ef1cb1cf16c0411890063e67cdc7b62562cc496"
checksum = "9054b4cc5eda331cde3096b1576dec45365c5cbbca61d1fffa5f236e251dfce7"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@@ -935,9 +935,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.62.2"
version = "0.62.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c82ba4cab184ea61f6edaafc1072aad3c2a17dcf4c0fce19ac5694b90d8b5f"
checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
@@ -964,7 +964,7 @@ dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"h2 0.3.27",
"h2 0.4.11",
"h2 0.4.12",
"http 0.2.12",
"http 1.3.1",
"http-body 0.4.6",
@@ -1013,9 +1013,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.8.5"
version = "1.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6"
checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1037,9 +1037,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.8.5"
version = "1.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c"
checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1155,50 +1155,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-server"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab"
dependencies = [
"arc-swap",
"bytes",
"fs-err",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"rustls 0.23.31",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.2",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.75"
@@ -1529,9 +1485,9 @@ dependencies = [
[[package]]
name = "camino"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
dependencies = [
"serde",
]
@@ -1584,9 +1540,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.30"
version = "1.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
dependencies = [
"jobserver",
"libc",
@@ -1741,9 +1697,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.42"
version = "4.5.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1751,9 +1707,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.42"
version = "4.5.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
dependencies = [
"anstream",
"anstyle",
@@ -2337,12 +2293,12 @@ dependencies = [
[[package]]
name = "darling"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9"
checksum = "d6b136475da5ef7b6ac596c0e956e37bad51b85b987ff3d5e230e964936736b2"
dependencies = [
"darling_core 0.21.0",
"darling_macro 0.21.0",
"darling_core 0.21.1",
"darling_macro 0.21.1",
]
[[package]]
@@ -2361,9 +2317,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d"
checksum = "b44ad32f92b75fb438b04b68547e521a548be8acc339a6dacc4a7121488f53e6"
dependencies = [
"fnv",
"ident_case",
@@ -2386,11 +2342,11 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146"
checksum = "2b5be8a7a562d315a5b92a630c30cec6bcf663e6673f00fbb69cca66a6f521b9"
dependencies = [
"darling_core 0.21.0",
"darling_core 0.21.1",
"quote",
"syn 2.0.104",
]
@@ -3782,9 +3738,9 @@ dependencies = [
[[package]]
name = "event-listener"
version = "5.4.0"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
@@ -3970,16 +3926,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-err"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683"
dependencies = [
"autocfg",
"tokio",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -4055,9 +4001,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.6.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
@@ -4544,9 +4490,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.11"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [
"atomic-waker",
"bytes",
@@ -4590,9 +4536,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.4"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
@@ -4796,7 +4742,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.11",
"h2 0.4.12",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
@@ -5052,7 +4998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
"hashbrown 0.15.5",
]
[[package]]
@@ -5305,7 +5251,7 @@ dependencies = [
"dbus-secret-service",
"log",
"security-framework 2.11.1",
"security-framework 3.2.0",
"security-framework 3.3.0",
"windows-sys 0.60.2",
"zeroize",
]
@@ -5615,16 +5561,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.4",
]
[[package]]
name = "lru"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed"
dependencies = [
"hashbrown 0.15.4",
"hashbrown 0.15.5",
]
[[package]]
@@ -6020,9 +5957,9 @@ dependencies = [
[[package]]
name = "notify"
version = "8.1.0"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.1",
"fsevent-sys",
@@ -6736,7 +6673,7 @@ dependencies = [
"flate2",
"futures",
"half",
"hashbrown 0.15.4",
"hashbrown 0.15.5",
"lz4_flex",
"num",
"num-bigint",
@@ -7089,9 +7026,9 @@ dependencies = [
[[package]]
name = "polling"
version = "3.9.0"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7"
checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829"
dependencies = [
"cfg-if",
"concurrent-queue",
@@ -7383,9 +7320,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.38.0"
version = "0.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b"
checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4"
dependencies = [
"memchr",
"serde",
@@ -7680,9 +7617,9 @@ dependencies = [
[[package]]
name = "redox_users"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78eaea1f52c56d57821be178b2d47e09ff26481a6042e8e042fcb0ced068b470"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
@@ -7781,7 +7718,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2 0.4.11",
"h2 0.4.12",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -7889,9 +7826,9 @@ dependencies = [
[[package]]
name = "rmcp"
version = "0.3.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f0d0d5493be0d181a62db489eab7838669b81885972ca00ceca893cf6ac2883"
checksum = "2faf35b7d3c4b7f8c21c45bb014011b32a0ce6444bf6094da04daab01a8c3c34"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -7910,11 +7847,11 @@ dependencies = [
[[package]]
name = "rmcp-macros"
version = "0.3.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aebc912b8fa7d54999adc4e45601d1d95fe458f97eb0a1277eddcd6382cf4b1"
checksum = "ad9720d9d2a943779f1dc3d47fa9072c7eeffaff4e1a82f67eb9f7ea52696091"
dependencies = [
"darling 0.21.0",
"darling 0.21.1",
"proc-macro2",
"quote",
"serde_json",
@@ -8105,8 +8042,6 @@ dependencies = [
"atoi",
"atomic_enum",
"axum",
"axum-extra",
"axum-server",
"bytes",
"chrono",
"clap",
@@ -8173,26 +8108,22 @@ version = "0.0.5"
dependencies = [
"anyhow",
"async-trait",
"bytes",
"chrono",
"futures",
"lazy_static",
"once_cell",
"rmp-serde",
"rustfs-common",
"rustfs-ecstore",
"rustfs-filemeta",
"rustfs-lock",
"rustfs-madmin",
"rustfs-utils",
"s3s",
"serde",
"serde_json",
"serial_test",
"tempfile",
"thiserror 2.0.12",
"time",
"tokio",
"tokio-test",
"tokio-util",
"tracing",
"tracing-subscriber",
@@ -8217,19 +8148,12 @@ version = "0.0.5"
dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"crc-fast",
"hex",
"http 1.3.1",
"http-body 1.0.1",
"md-5",
"pin-project-lite",
"pretty_assertions",
"sha1 0.10.6",
"sha2 0.10.9",
"tokio",
"tracing",
"tracing-test",
]
[[package]]
@@ -8305,9 +8229,8 @@ dependencies = [
"num_cpus",
"once_cell",
"path-absolutize",
"path-clean",
"pin-project-lite",
"quick-xml 0.38.0",
"quick-xml 0.38.1",
"rand 0.9.2",
"reed-solomon-simd",
"regex",
@@ -8423,10 +8346,7 @@ dependencies = [
"async-trait",
"bytes",
"futures",
"lazy_static",
"lru 0.16.0",
"once_cell",
"rand 0.9.2",
"rustfs-protos",
"serde",
"serde_json",
@@ -8478,7 +8398,7 @@ dependencies = [
"form_urlencoded",
"futures",
"once_cell",
"quick-xml 0.38.0",
"quick-xml 0.38.1",
"reqwest",
"rumqttc",
"rustfs-config",
@@ -8656,12 +8576,9 @@ dependencies = [
"bytes",
"http 1.3.1",
"hyper 1.6.0",
"rand 0.9.2",
"rustfs-utils",
"s3s",
"serde",
"serde_urlencoded",
"tempfile",
"time",
"tracing",
]
@@ -8829,7 +8746,7 @@ dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework 3.2.0",
"security-framework 3.3.0",
]
[[package]]
@@ -9066,9 +8983,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "3.2.0"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.1",
@@ -9475,9 +9392,9 @@ dependencies = [
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
dependencies = [
"libc",
]
@@ -10382,9 +10299,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.15"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
dependencies = [
"bytes",
"futures-core",
@@ -10495,7 +10412,7 @@ dependencies = [
"base64 0.22.1",
"bytes",
"flate2",
"h2 0.4.11",
"h2 0.4.12",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -10724,27 +10641,6 @@ dependencies = [
"tracing-serde",
]
[[package]]
name = "tracing-test"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68"
dependencies = [
"tracing-core",
"tracing-subscriber",
"tracing-test-macro",
]
[[package]]
name = "tracing-test-macro"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
dependencies = [
"quote",
"syn 2.0.104",
]
[[package]]
name = "tracing-wasm"
version = "0.2.1"
@@ -12301,9 +12197,9 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.2"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [
"yoke",
"zerofrom",
+8 -13
View File
@@ -97,25 +97,22 @@ atoi = "2.0.0"
async-channel = "2.5.0"
async-recursion = "1.1.1"
async-trait = "0.1.88"
async-compression = { version = "0.4.0" }
async-compression = { version = "0.4.19" }
atomic_enum = "0.3.0"
aws-config = { version = "1.8.3" }
aws-sdk-s3 = "1.100.0"
aws-config = { version = "1.8.4" }
aws-sdk-s3 = "1.101.0"
axum = "0.8.4"
axum-extra = "0.10.1"
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
base64-simd = "0.8.0"
base64 = "0.22.1"
brotli = "8.0.1"
bytes = { version = "1.10.1", features = ["serde"] }
bytesize = "2.0.1"
byteorder = "1.5.0"
bytes-utils = "0.1.4"
cfg-if = "1.0.1"
crc-fast = "1.3.0"
chacha20poly1305 = { version = "0.10.1" }
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5.42", features = ["derive", "env"] }
clap = { version = "4.5.43", features = ["derive", "env"] }
const-str = { version = "0.6.4", features = ["std", "proc"] }
crc32fast = "1.5.0"
criterion = { version = "0.7", features = ["html_reports"] }
@@ -156,7 +153,6 @@ keyring = { version = "3.6.3", features = [
] }
lazy_static = "1.5.0"
libsystemd = { version = "0.7.2" }
lru = "0.16"
local-ip-address = "0.6.5"
lz4 = "1.28.1"
matchit = "0.8.4"
@@ -192,7 +188,7 @@ percent-encoding = "2.3.1"
pin-project-lite = "0.2.16"
prost = "0.14.1"
pretty_assertions = "1.4.1"
quick-xml = "0.38.0"
quick-xml = "0.38.1"
rand = "0.9.2"
rdkafka = { version = "0.38.0", features = ["tokio"] }
reed-solomon-simd = { version = "3.0.1" }
@@ -210,7 +206,7 @@ rfd = { version = "0.15.4", default-features = false, features = [
"xdg-portal",
"tokio",
] }
rmcp = { version = "0.3.2" }
rmcp = { version = "0.5.0" }
rmp = "0.8.14"
rmp-serde = "1.3.0"
rsa = "0.9.8"
@@ -254,7 +250,7 @@ tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17" }
tokio-tar = "0.3.1"
tokio-test = "0.4.4"
tokio-util = { version = "0.7.15", features = ["io", "compat"] }
tokio-util = { version = "0.7.16", features = ["io", "compat"] }
tonic = { version = "0.14.0", features = ["gzip"] }
tonic-prost = { version = "0.14.0" }
tonic-prost-build = { version = "0.14.0" }
@@ -266,7 +262,6 @@ tracing-core = "0.1.34"
tracing-error = "0.2.1"
tracing-opentelemetry = "0.31.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] }
tracing-test = "0.2.5"
transform-stream = "0.3.1"
url = "2.5.4"
urlencoding = "2.1.3"
@@ -283,7 +278,7 @@ zstd = "0.13.3"
[workspace.metadata.cargo-shear]
ignored = ["rustfs", "rust-i18n"]
ignored = ["rustfs", "rust-i18n", "rustfs-mcp"]
[profile.wasm-dev]
inherits = "dev"
+258
View File
@@ -0,0 +1,258 @@
DOCKER_CLI := env("DOCKER_CLI", "docker")
IMAGE_NAME := env("IMAGE_NAME", "rustfs:v1.0.0")
DOCKERFILE_SOURCE := env("DOCKERFILE_SOURCE", "Dockerfile.source")
DOCKERFILE_PRODUCTION := env("DOCKERFILE_PRODUCTION", "Dockerfile")
CONTAINER_NAME := env("CONTAINER_NAME", "rustfs-dev")
[group("📒 Help")]
[private]
default:
@just --list --list-heading $'🦀 RustFS justfile manual page:\n'
[doc("show help")]
[group("📒 Help")]
help: default
[doc("run `cargo fmt` to format codes")]
[group("👆 Code Quality")]
fmt:
@echo "🔧 Formatting code..."
cargo fmt --all
[doc("run `cargo fmt` in check mode")]
[group("👆 Code Quality")]
fmt-check:
@echo "📝 Checking code formatting..."
cargo fmt --all --check
[doc("run `cargo clippy`")]
[group("👆 Code Quality")]
clippy:
@echo "🔍 Running clippy checks..."
cargo clippy --all-targets --all-features --fix --allow-dirty -- -D warnings
[doc("run `cargo check`")]
[group("👆 Code Quality")]
check:
@echo "🔨 Running compilation check..."
cargo check --all-targets
[doc("run `cargo test`")]
[group("👆 Code Quality")]
test:
@echo "🧪 Running tests..."
cargo nextest run --all --exclude e2e_test
cargo test --all --doc
[doc("run `fmt` `clippy` `check` `test` at once")]
[group("👆 Code Quality")]
pre-commit: fmt clippy check test
@echo "✅ All pre-commit checks passed!"
[group("🤔 Git")]
setup-hooks:
@echo "🔧 Setting up git hooks..."
chmod +x .git/hooks/pre-commit
@echo "✅ Git hooks setup complete!"
[doc("use `release` mode for building")]
[group("🔨 Build")]
build:
@echo "🔨 Building RustFS using build-rustfs.sh script..."
./build-rustfs.sh
[doc("use `debug` mode for building")]
[group("🔨 Build")]
build-dev:
@echo "🔨 Building RustFS in development mode..."
./build-rustfs.sh --dev
[group("🔨 Build")]
[private]
build-target target:
@echo "🔨 Building rustfs for {{ target }}..."
@echo "💡 On macOS/Windows, use 'make build-docker' or 'make docker-dev' instead"
./build-rustfs.sh --platform {{ target }}
[doc("use `x86_64-unknown-linux-musl` target for building")]
[group("🔨 Build")]
build-musl: (build-target "x86_64-unknown-linux-musl")
[doc("use `x86_64-unknown-linux-gnu` target for building")]
[group("🔨 Build")]
build-gnu: (build-target "x86_64-unknown-linux-gnu")
[doc("use `aarch64-unknown-linux-musl` target for building")]
[group("🔨 Build")]
build-musl-arm64: (build-target "aarch64-unknown-linux-musl")
[doc("use `aarch64-unknown-linux-gnu` target for building")]
[group("🔨 Build")]
build-gnu-arm64: (build-target "aarch64-unknown-linux-gnu")
[doc("build and deploy to server")]
[group("🔨 Build")]
deploy-dev ip: build-musl
@echo "🚀 Deploying to dev server: {{ ip }}"
./scripts/dev_deploy.sh {{ ip }}
[group("🔨 Build")]
[private]
build-cross-all-pre:
@echo "🔧 Building all target architectures..."
@echo "💡 On macOS/Windows, use 'make docker-dev' for reliable multi-arch builds"
@echo "🔨 Generating protobuf code..."
-cargo run --bin gproto
[doc("build all targets at once")]
[group("🔨 Build")]
build-cross-all: build-cross-all-pre && build-gnu build-gnu-arm64 build-musl build-musl-arm64
# ========================================================================================
# Docker Multi-Architecture Builds (Primary Methods)
# ========================================================================================
[doc("build an image and run it")]
[group("🐳 Build Image")]
build-docker os="rockylinux9.3" cli=(DOCKER_CLI) dockerfile=(DOCKERFILE_SOURCE):
#!/usr/bin/env bash
SOURCE_BUILD_IMAGE_NAME="rustfs/rustfs-{{ os }}:v1"
SOURCE_BUILD_CONTAINER_NAME="rustfs-{{ os }}-build"
BUILD_CMD="/root/.cargo/bin/cargo build --release --bin rustfs --target-dir /root/s3-rustfs/target/{{ os }}"
echo "🐳 Building RustFS using Docker ({{ os }})..."
{{ cli }} buildx build -t $SOURCE_BUILD_IMAGE_NAME -f {{ dockerfile }} .
{{ cli }} run --rm --name $SOURCE_BUILD_CONTAINER_NAME -v $(pwd):/root/s3-rustfs -it $SOURCE_BUILD_IMAGE_NAME $BUILD_CMD
[doc("build an image")]
[group("🐳 Build Image")]
docker-buildx:
@echo "🏗️ Building multi-architecture production Docker images with buildx..."
./docker-buildx.sh
[doc("build an image and push it")]
[group("🐳 Build Image")]
docker-buildx-push:
@echo "🚀 Building and pushing multi-architecture production Docker images with buildx..."
./docker-buildx.sh --push
[doc("build an image with a version")]
[group("🐳 Build Image")]
docker-buildx-version version:
@echo "🏗️ Building multi-architecture production Docker images (version: {{ version }}..."
./docker-buildx.sh --release {{ version }}
[doc("build an image with a version and push it")]
[group("🐳 Build Image")]
docker-buildx-push-version version:
@echo "🚀 Building and pushing multi-architecture production Docker images (version: {{ version }}..."
./docker-buildx.sh --release {{ version }} --push
[doc("build an image with a version and push it to registry")]
[group("🐳 Build Image")]
docker-dev-push registry cli=(DOCKER_CLI) source=(DOCKERFILE_SOURCE):
@echo "🚀 Building and pushing multi-architecture development Docker images..."
@echo "💡 push to registry: {{ registry }}"
{{ cli }} buildx build \
--platform linux/amd64,linux/arm64 \
--file {{ source }} \
--tag {{ registry }}/rustfs:source-latest \
--tag {{ registry }}/rustfs:dev-latest \
--push \
.
# Local production builds using direct buildx (alternative to docker-buildx.sh)
[group("🐳 Build Image")]
docker-buildx-production-local cli=(DOCKER_CLI) source=(DOCKERFILE_PRODUCTION):
@echo "🏗️ Building single-architecture production Docker image locally..."
@echo "💡 Alternative to docker-buildx.sh for local testing"
{{ cli }} buildx build \
--file {{ source }} \
--tag rustfs:production-latest \
--tag rustfs:latest \
--load \
--build-arg RELEASE=latest \
.
# Development/Source builds using direct buildx commands
[group("🐳 Build Image")]
docker-dev cli=(DOCKER_CLI) source=(DOCKERFILE_SOURCE):
@echo "🏗️ Building multi-architecture development Docker images with buildx..."
@echo "💡 This builds from source code and is intended for local development and testing"
@echo "⚠️ Multi-arch images cannot be loaded locally, use docker-dev-push to push to registry"
{{ cli }} buildx build \
--platform linux/amd64,linux/arm64 \
--file {{ source }} \
--tag rustfs:source-latest \
--tag rustfs:dev-latest \
.
[group("🐳 Build Image")]
docker-dev-local cli=(DOCKER_CLI) source=(DOCKERFILE_SOURCE):
@echo "🏗️ Building single-architecture development Docker image for local use..."
@echo "💡 This builds from source code for the current platform and loads locally"
{{ cli }} buildx build \
--file {{ source }} \
--tag rustfs:source-latest \
--tag rustfs:dev-latest \
--load \
.
# ========================================================================================
# Single Architecture Docker Builds (Traditional)
# ========================================================================================
[group("🐳 Build Image")]
docker-build-production cli=(DOCKER_CLI) source=(DOCKERFILE_PRODUCTION):
@echo "🏗️ Building single-architecture production Docker image..."
@echo "💡 Consider using 'make docker-buildx-production-local' for multi-arch support"
{{ cli }} build -f {{ source }} -t rustfs:latest .
[group("🐳 Build Image")]
docker-build-source cli=(DOCKER_CLI) source=(DOCKERFILE_SOURCE):
@echo "🏗️ Building single-architecture source Docker image..."
@echo "💡 Consider using 'make docker-dev-local' for multi-arch support"
{{ cli }} build -f {{ source }} -t rustfs:source .
# ========================================================================================
# Development Environment
# ========================================================================================
[group("🏃 Running")]
dev-env-start cli=(DOCKER_CLI) source=(DOCKERFILE_SOURCE) container=(CONTAINER_NAME):
@echo "🚀 Starting development environment..."
{{ cli }} buildx build \
--file {{ source }} \
--tag rustfs:dev \
--load \
.
-{{ cli }} stop {{ container }} 2>/dev/null
-{{ cli }} rm {{ container }} 2>/dev/null
{{ cli }} run -d --name {{ container }} \
-p 9010:9010 -p 9000:9000 \
-v {{ invocation_directory() }}:/workspace \
-it rustfs:dev
[group("🏃 Running")]
dev-env-stop cli=(DOCKER_CLI) container=(CONTAINER_NAME):
@echo "🛑 Stopping development environment..."
-{{ cli }} stop {{ container }} 2>/dev/null
-{{ cli }} rm {{ container }} 2>/dev/null
[group("🏃 Running")]
dev-env-restart: dev-env-stop dev-env-start
[group("👍 E2E")]
e2e-server:
sh scripts/run.sh
[group("👍 E2E")]
probe-e2e:
sh scripts/probe.sh
[doc("inspect one image")]
[group("🚚 Other")]
docker-inspect-multiarch image cli=(DOCKER_CLI):
@echo "🔍 Inspecting multi-architecture image: {{ image }}"
{{ cli }} buildx imagetools inspect {{ image }}
+1 -1
View File
@@ -76,7 +76,7 @@ build-docker: SOURCE_BUILD_CONTAINER_NAME = rustfs-$(BUILD_OS)-build
build-docker: BUILD_CMD = /root/.cargo/bin/cargo build --release --bin rustfs --target-dir /root/s3-rustfs/target/$(BUILD_OS)
build-docker:
@echo "🐳 Building RustFS using Docker ($(BUILD_OS))..."
$(DOCKER_CLI) build -t $(SOURCE_BUILD_IMAGE_NAME) -f $(DOCKERFILE_SOURCE) .
$(DOCKER_CLI) buildx build -t $(SOURCE_BUILD_IMAGE_NAME) -f $(DOCKERFILE_SOURCE) .
$(DOCKER_CLI) run --rm --name $(SOURCE_BUILD_CONTAINER_NAME) -v $(shell pwd):/root/s3-rustfs -it $(SOURCE_BUILD_IMAGE_NAME) $(BUILD_CMD)
.PHONY: build-musl
+1 -6
View File
@@ -24,24 +24,19 @@ tracing = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
bytes = { workspace = true }
time = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["v4", "serde"] }
anyhow = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
url = { workspace = true }
rustfs-lock = { workspace = true }
s3s = { workspace = true }
lazy_static = { workspace = true }
chrono = { workspace = true }
[dev-dependencies]
rmp-serde = { workspace = true }
tokio-test = { workspace = true }
serde_json = { workspace = true }
serial_test = "3.2.0"
once_cell = { workspace = true }
tracing-subscriber = { workspace = true }
walkdir = "2.5.0"
tempfile = { workspace = true }
+15 -4
View File
@@ -133,8 +133,14 @@ impl HealStorageAPI for ECStoreHealStorage {
match self.ecstore.get_object_info(bucket, object, &Default::default()).await {
Ok(info) => Ok(Some(info)),
Err(e) => {
error!("Failed to get object meta: {}/{} - {}", bucket, object, e);
Err(Error::other(e))
// Map ObjectNotFound to None to align with Option return type
if matches!(e, rustfs_ecstore::error::StorageError::ObjectNotFound(_, _)) {
debug!("Object meta not found: {}/{}", bucket, object);
Ok(None)
} else {
error!("Failed to get object meta: {}/{} - {}", bucket, object, e);
Err(Error::other(e))
}
}
}
}
@@ -154,8 +160,13 @@ impl HealStorageAPI for ECStoreHealStorage {
}
},
Err(e) => {
error!("Failed to get object: {}/{} - {}", bucket, object, e);
Err(Error::other(e))
if matches!(e, rustfs_ecstore::error::StorageError::ObjectNotFound(_, _)) {
debug!("Object data not found: {}/{}", bucket, object);
Ok(None)
} else {
error!("Failed to get object: {}/{} - {}", bucket, object, e);
Err(Error::other(e))
}
}
}
}
+106 -8
View File
@@ -23,22 +23,23 @@ use ecstore::{
set_disk::SetDisks,
};
use rustfs_ecstore::{self as ecstore, StorageAPI, data_usage::store_data_usage_in_backend};
use rustfs_filemeta::MetacacheReader;
use rustfs_filemeta::{MetacacheReader, VersionType};
use tokio::sync::{Mutex, RwLock};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, warn};
use super::metrics::{BucketMetrics, DiskMetrics, MetricsCollector, ScannerMetrics};
use crate::heal::HealManager;
use crate::scanner::lifecycle::ScannerItem;
use crate::{
HealRequest,
error::{Error, Result},
get_ahm_services_cancel_token,
};
use rustfs_common::{
data_usage::DataUsageInfo,
metrics::{Metric, Metrics, globalMetrics},
};
use rustfs_common::data_usage::DataUsageInfo;
use rustfs_common::metrics::{Metric, Metrics, globalMetrics};
use rustfs_ecstore::cmd::bucket_targets::VersioningConfig;
use rustfs_ecstore::disk::RUSTFS_META_BUCKET;
@@ -290,7 +291,7 @@ impl Scanner {
/// Get global metrics from common crate
pub async fn get_global_metrics(&self) -> rustfs_madmin::metrics::ScannerMetrics {
globalMetrics.report().await
(*globalMetrics).report().await
}
/// Perform a single scan cycle
@@ -317,7 +318,7 @@ impl Scanner {
cycle_completed: vec![chrono::Utc::now()],
started: chrono::Utc::now(),
};
globalMetrics.set_cycle(Some(cycle_info)).await;
(*globalMetrics).set_cycle(Some(cycle_info)).await;
self.metrics.set_current_cycle(self.state.read().await.current_cycle);
self.metrics.increment_total_cycles();
@@ -431,8 +432,27 @@ impl Scanner {
}
if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() {
// First try the standard integrity check
// First check whether the object still logically exists.
// If it's already deleted (e.g., non-versioned bucket), do not trigger heal.
let object_opts = ecstore::store_api::ObjectOptions::default();
match ecstore.get_object_info(bucket, object, &object_opts).await {
Ok(_) => {
// Object exists logically, continue with verification below
}
Err(e) => {
if matches!(e, ecstore::error::StorageError::ObjectNotFound(_, _)) {
debug!(
"Object {}/{} not found logically (likely deleted), skip integrity check & heal",
bucket, object
);
return Ok(());
} else {
debug!("get_object_info error for {}/{}: {}", bucket, object, e);
// Fall through to existing logic which will handle accordingly
}
}
}
// First try the standard integrity check
let mut integrity_failed = false;
debug!("Running standard object verification for {}/{}", bucket, object);
@@ -1160,6 +1180,19 @@ impl Scanner {
/// This method collects all objects from a disk for a specific bucket.
/// It returns a map of object names to their metadata for later analysis.
async fn scan_volume(&self, disk: &DiskStore, bucket: &str) -> Result<HashMap<String, rustfs_filemeta::FileMeta>> {
let ecstore = match rustfs_ecstore::new_object_layer_fn() {
Some(ecstore) => ecstore,
None => {
error!("ECStore not available");
return Err(Error::Other("ECStore not available".to_string()));
}
};
let bucket_info = ecstore.get_bucket_info(bucket, &Default::default()).await.ok();
let versioning_config = bucket_info.map(|bi| Arc::new(VersioningConfig { enabled: bi.versioning }));
let lifecycle_config = rustfs_ecstore::bucket::metadata_sys::get_lifecycle_config(bucket)
.await
.ok()
.map(|(c, _)| Arc::new(c));
// Start global metrics collection for volume scan
let stop_fn = Metrics::time(Metric::ScanObject);
@@ -1247,6 +1280,15 @@ impl Scanner {
}
}
} else {
// Apply lifecycle actions
if let Some(lifecycle_config) = &lifecycle_config {
let mut scanner_item =
ScannerItem::new(bucket.to_string(), Some(lifecycle_config.clone()), versioning_config.clone());
if let Err(e) = scanner_item.apply_actions(&entry.name, entry.clone()).await {
error!("Failed to apply lifecycle actions for {}/{}: {}", bucket, entry.name, e);
}
}
// Store object metadata for later analysis
object_metadata.insert(entry.name.clone(), file_meta.clone());
}
@@ -1375,8 +1417,64 @@ impl Scanner {
let empty_vec = Vec::new();
let locations = object_locations.get(&key).unwrap_or(&empty_vec);
// If any disk reports this object as a latest delete marker (tombstone),
// it's a legitimate deletion. Skip missing-object heal to avoid recreating
// deleted objects. Optional: a metadata heal could be submitted to fan-out
// the delete marker, but we keep it conservative here.
let mut has_latest_delete_marker = false;
for &disk_idx in locations {
if let Some(bucket_map) = all_disk_objects.get(disk_idx) {
if let Some(file_map) = bucket_map.get(bucket) {
if let Some(fm) = file_map.get(object_name) {
if let Some(first_ver) = fm.versions.first() {
if first_ver.header.version_type == VersionType::Delete {
has_latest_delete_marker = true;
break;
}
}
}
}
}
}
if has_latest_delete_marker {
debug!(
"Object {}/{} is a delete marker on some disk(s), skipping heal for missing parts",
bucket, object_name
);
continue;
}
// Check if object is missing from some disks
if locations.len() < disks.len() {
// Before submitting heal, confirm the object still exists logically.
let should_heal = if let Some(store) = rustfs_ecstore::new_object_layer_fn() {
match store.get_object_info(bucket, object_name, &Default::default()).await {
Ok(_) => true, // exists -> propagate by heal
Err(e) => {
if matches!(e, rustfs_ecstore::error::StorageError::ObjectNotFound(_, _)) {
debug!(
"Object {}/{} not found logically (deleted), skip missing-disks heal",
bucket, object_name
);
false
} else {
debug!(
"Object {}/{} get_object_info errored ({}), conservatively skip heal",
bucket, object_name, e
);
false
}
}
}
} else {
// No store available; be conservative and skip to avoid recreating deletions
debug!("No ECStore available to confirm existence, skip heal for {}/{}", bucket, object_name);
false
};
if !should_heal {
continue;
}
objects_needing_heal += 1;
let missing_disks: Vec<usize> = (0..disks.len()).filter(|&i| !locations.contains(&i)).collect();
warn!("Object {}/{} missing from disks: {:?}", bucket, object_name, missing_disks);
+125
View File
@@ -0,0 +1,125 @@
// 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::sync::Arc;
use rustfs_common::metrics::IlmAction;
use rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_audit::LcEventSrc;
use rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::{apply_lifecycle_action, eval_action_from_lifecycle};
use rustfs_ecstore::bucket::metadata_sys::get_object_lock_config;
use rustfs_ecstore::cmd::bucket_targets::VersioningConfig;
use rustfs_ecstore::store_api::ObjectInfo;
use rustfs_filemeta::FileMetaVersion;
use rustfs_filemeta::metacache::MetaCacheEntry;
use s3s::dto::BucketLifecycleConfiguration as LifecycleConfig;
use tracing::info;
#[derive(Clone)]
pub struct ScannerItem {
bucket: String,
lifecycle: Option<Arc<LifecycleConfig>>,
versioning: Option<Arc<VersioningConfig>>,
}
impl ScannerItem {
pub fn new(bucket: String, lifecycle: Option<Arc<LifecycleConfig>>, versioning: Option<Arc<VersioningConfig>>) -> Self {
Self {
bucket,
lifecycle,
versioning,
}
}
pub async fn apply_actions(&mut self, object: &str, mut meta: MetaCacheEntry) -> anyhow::Result<()> {
info!("apply_actions called for object: {}", object);
if self.lifecycle.is_none() {
info!("No lifecycle config for object: {}", object);
return Ok(());
}
info!("Lifecycle config exists for object: {}", object);
let file_meta = match meta.xl_meta() {
Ok(meta) => meta,
Err(e) => {
tracing::error!("Failed to get xl_meta for {}: {}", object, e);
return Ok(());
}
};
let latest_version = file_meta.versions.first().cloned().unwrap_or_default();
let file_meta_version = FileMetaVersion::try_from(latest_version.meta.as_slice()).unwrap_or_default();
let obj_info = ObjectInfo {
bucket: self.bucket.clone(),
name: object.to_string(),
version_id: latest_version.header.version_id,
mod_time: latest_version.header.mod_time,
size: file_meta_version.object.as_ref().map_or(0, |o| o.size),
user_defined: serde_json::from_slice(file_meta.data.as_slice()).unwrap_or_default(),
..Default::default()
};
self.apply_lifecycle(&obj_info).await;
Ok(())
}
async fn apply_lifecycle(&mut self, oi: &ObjectInfo) -> (IlmAction, i64) {
let size = oi.size;
if self.lifecycle.is_none() {
return (IlmAction::NoneAction, size);
}
let (olcfg, rcfg) = if self.bucket != ".minio.sys" {
(
get_object_lock_config(&self.bucket).await.ok(),
None, // FIXME: replication config
)
} else {
(None, None)
};
let lc_evt = eval_action_from_lifecycle(
self.lifecycle.as_ref().unwrap(),
olcfg
.as_ref()
.and_then(|(c, _)| c.rule.as_ref().and_then(|r| r.default_retention.clone())),
rcfg.clone(),
oi,
)
.await;
info!("lifecycle: {} Initial scan: {}", oi.name, lc_evt.action);
let mut new_size = size;
match lc_evt.action {
IlmAction::DeleteVersionAction | IlmAction::DeleteAllVersionsAction | IlmAction::DelMarkerDeleteAllVersionsAction => {
new_size = 0;
}
IlmAction::DeleteAction => {
if let Some(vcfg) = &self.versioning {
if !vcfg.is_enabled() {
new_size = 0;
}
} else {
new_size = 0;
}
}
_ => (),
}
apply_lifecycle_action(&lc_evt, &LcEventSrc::Scanner, oi).await;
(lc_evt.action, new_size)
}
}
+1
View File
@@ -14,6 +14,7 @@
pub mod data_scanner;
pub mod histogram;
pub mod lifecycle;
pub mod metrics;
pub use data_scanner::Scanner;
@@ -0,0 +1,243 @@
use rustfs_ahm::scanner::{Scanner, data_scanner::ScannerConfig};
use rustfs_ecstore::{
bucket::metadata::BUCKET_LIFECYCLE_CONFIG,
bucket::metadata_sys,
disk::endpoint::Endpoint,
endpoints::{EndpointServerPools, Endpoints, PoolEndpoints},
store::ECStore,
store_api::{ObjectIO, ObjectOptions, PutObjReader, StorageAPI},
};
use serial_test::serial;
use std::sync::Once;
use std::sync::OnceLock;
use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::fs;
use tracing::info;
static GLOBAL_ENV: OnceLock<(Vec<PathBuf>, Arc<ECStore>)> = OnceLock::new();
static INIT: Once = Once::new();
fn init_tracing() {
INIT.call_once(|| {
let _ = tracing_subscriber::fmt::try_init();
});
}
/// Test helper: Create test environment with ECStore
async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>) {
init_tracing();
// Fast path: already initialized, just clone and return
if let Some((paths, ecstore)) = GLOBAL_ENV.get() {
return (paths.clone(), ecstore.clone());
}
// create temp dir as 4 disks with unique base dir
let test_base_dir = format!("/tmp/rustfs_ahm_lifecycle_test_{}", uuid::Uuid::new_v4());
let temp_dir = std::path::PathBuf::from(&test_base_dir);
if temp_dir.exists() {
fs::remove_dir_all(&temp_dir).await.ok();
}
fs::create_dir_all(&temp_dir).await.unwrap();
// create 4 disk dirs
let disk_paths = vec![
temp_dir.join("disk1"),
temp_dir.join("disk2"),
temp_dir.join("disk3"),
temp_dir.join("disk4"),
];
for disk_path in &disk_paths {
fs::create_dir_all(disk_path).await.unwrap();
}
// create EndpointServerPools
let mut endpoints = Vec::new();
for (i, disk_path) in disk_paths.iter().enumerate() {
let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap();
// set correct index
endpoint.set_pool_index(0);
endpoint.set_set_index(0);
endpoint.set_disk_index(i);
endpoints.push(endpoint);
}
let pool_endpoints = PoolEndpoints {
legacy: false,
set_count: 1,
drives_per_set: 4,
endpoints: Endpoints::from(endpoints),
cmd_line: "test".to_string(),
platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH),
};
let endpoint_pools = EndpointServerPools(vec![pool_endpoints]);
// format disks (only first time)
rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await.unwrap();
// create ECStore with dynamic port 0 (let OS assign) or fixed 9002 if free
let port = 9002; // for simplicity
let server_addr: std::net::SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let ecstore = ECStore::new(server_addr, endpoint_pools).await.unwrap();
// init bucket metadata system
let buckets_list = ecstore
.list_bucket(&rustfs_ecstore::store_api::BucketOptions {
no_metadata: true,
..Default::default()
})
.await
.unwrap();
let buckets = buckets_list.into_iter().map(|v| v.name).collect();
rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await;
// Initialize background expiry workers
rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::init_background_expiry(ecstore.clone()).await;
// Store in global once lock
let _ = GLOBAL_ENV.set((disk_paths.clone(), ecstore.clone()));
(disk_paths, ecstore)
}
/// Test helper: Create a test bucket
async fn create_test_bucket(ecstore: &Arc<ECStore>, bucket_name: &str) {
(**ecstore)
.make_bucket(bucket_name, &Default::default())
.await
.expect("Failed to create test bucket");
info!("Created test bucket: {}", bucket_name);
}
/// Test helper: Upload test object
async fn upload_test_object(ecstore: &Arc<ECStore>, bucket: &str, object: &str, data: &[u8]) {
let mut reader = PutObjReader::from_vec(data.to_vec());
let object_info = (**ecstore)
.put_object(bucket, object, &mut reader, &ObjectOptions::default())
.await
.expect("Failed to upload test object");
info!("Uploaded test object: {}/{} ({} bytes)", bucket, object, object_info.size);
}
/// Test helper: Set bucket lifecycle configuration
async fn set_bucket_lifecycle(bucket_name: &str) -> Result<(), Box<dyn std::error::Error>> {
// Create a simple lifecycle configuration XML with 0 days expiry for immediate testing
let lifecycle_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<LifecycleConfiguration>
<Rule>
<ID>test-rule</ID>
<Status>Enabled</Status>
<Filter>
<Prefix>test/</Prefix>
</Filter>
<Expiration>
<Days>0</Days>
</Expiration>
</Rule>
</LifecycleConfiguration>"#;
metadata_sys::update(bucket_name, BUCKET_LIFECYCLE_CONFIG, lifecycle_xml.as_bytes().to_vec()).await?;
Ok(())
}
/// Test helper: Check if object exists
async fn object_exists(ecstore: &Arc<ECStore>, bucket: &str, object: &str) -> bool {
((**ecstore).get_object_info(bucket, object, &ObjectOptions::default()).await).is_ok()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
async fn test_lifecycle_expiry_basic() {
let (_disk_paths, ecstore) = setup_test_env().await;
// Create test bucket and object
let bucket_name = "test-lifecycle-bucket";
let object_name = "test/object.txt"; // Match the lifecycle rule prefix "test/"
let test_data = b"Hello, this is test data for lifecycle expiry!";
create_test_bucket(&ecstore, bucket_name).await;
upload_test_object(&ecstore, bucket_name, object_name, test_data).await;
// Verify object exists initially
assert!(object_exists(&ecstore, bucket_name, object_name).await);
println!("✅ Object exists before lifecycle processing");
// Set lifecycle configuration with very short expiry (0 days = immediate expiry)
set_bucket_lifecycle(bucket_name)
.await
.expect("Failed to set lifecycle configuration");
println!("✅ Lifecycle configuration set for bucket: {bucket_name}");
// Verify lifecycle configuration was set
match rustfs_ecstore::bucket::metadata_sys::get(bucket_name).await {
Ok(bucket_meta) => {
assert!(bucket_meta.lifecycle_config.is_some());
println!("✅ Bucket metadata retrieved successfully");
}
Err(e) => {
println!("❌ Error retrieving bucket metadata: {e:?}");
}
}
// Create scanner with very short intervals for testing
let scanner_config = ScannerConfig {
scan_interval: Duration::from_millis(100),
deep_scan_interval: Duration::from_millis(500),
max_concurrent_scans: 1,
..Default::default()
};
let scanner = Scanner::new(Some(scanner_config), None);
// Start scanner
scanner.start().await.expect("Failed to start scanner");
println!("✅ Scanner started");
// Wait for scanner to process lifecycle rules
tokio::time::sleep(Duration::from_secs(2)).await;
// Manually trigger a scan cycle to ensure lifecycle processing
scanner.scan_cycle().await.expect("Failed to trigger scan cycle");
println!("✅ Manual scan cycle completed");
// Wait a bit more for background workers to process expiry tasks
tokio::time::sleep(Duration::from_secs(5)).await;
// Check if object has been expired (deleted)
let object_still_exists = object_exists(&ecstore, bucket_name, object_name).await;
println!("Object exists after lifecycle processing: {object_still_exists}");
if object_still_exists {
println!("❌ Object was not deleted by lifecycle processing");
// Let's try to get object info to see its details
match ecstore
.get_object_info(bucket_name, object_name, &rustfs_ecstore::store_api::ObjectOptions::default())
.await
{
Ok(obj_info) => {
println!(
"Object info: name={}, size={}, mod_time={:?}",
obj_info.name, obj_info.size, obj_info.mod_time
);
}
Err(e) => {
println!("Error getting object info: {e:?}");
}
}
} else {
println!("✅ Object was successfully deleted by lifecycle processing");
}
assert!(!object_still_exists);
println!("✅ Object successfully expired");
// Stop scanner
let _ = scanner.stop().await;
println!("✅ Scanner stopped");
println!("Lifecycle expiry basic test completed");
}
-7
View File
@@ -28,18 +28,11 @@ documentation = "https://docs.rs/rustfs-signer/latest/rustfs_checksum/"
[dependencies]
bytes = { workspace = true }
crc-fast = { workspace = true }
hex = { workspace = true }
http = { workspace = true }
http-body = { workspace = true }
base64-simd = { workspace = true }
md-5 = { workspace = true }
pin-project-lite = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
bytes-utils = { workspace = true }
pretty_assertions = { workspace = true }
tracing-test = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
+1
View File
@@ -24,6 +24,7 @@ pub const SHA_1_HEADER_NAME: &str = "x-amz-checksum-sha1";
pub const SHA_256_HEADER_NAME: &str = "x-amz-checksum-sha256";
pub const CRC_64_NVME_HEADER_NAME: &str = "x-amz-checksum-crc64nvme";
#[allow(dead_code)]
pub(crate) static MD5_HEADER_NAME: &str = "content-md5";
pub const CHECKSUM_ALGORITHMS_IN_PRIORITY_ORDER: [&str; 5] =
+1 -1
View File
@@ -294,7 +294,7 @@ impl Checksum for Sha256 {
Self::size()
}
}
#[allow(dead_code)]
#[derive(Debug, Default)]
struct Md5 {
hasher: md5::Md5,
+262
View File
@@ -19,3 +19,265 @@ pub const ENV_WORD_DELIMITER: &str = "_";
/// Medium-drawn lines separator
/// This is used to separate words in environment variable names.
pub const ENV_WORD_DELIMITER_DASH: &str = "-";
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub enum EnableState {
True,
False,
#[default]
Empty,
Yes,
No,
On,
Off,
Enabled,
Disabled,
Ok,
NotOk,
Success,
Failure,
Active,
Inactive,
One,
Zero,
}
impl std::fmt::Display for EnableState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for EnableState {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
s if s.eq_ignore_ascii_case("true") => Ok(EnableState::True),
s if s.eq_ignore_ascii_case("false") => Ok(EnableState::False),
"" => Ok(EnableState::Empty),
s if s.eq_ignore_ascii_case("yes") => Ok(EnableState::Yes),
s if s.eq_ignore_ascii_case("no") => Ok(EnableState::No),
s if s.eq_ignore_ascii_case("on") => Ok(EnableState::On),
s if s.eq_ignore_ascii_case("off") => Ok(EnableState::Off),
s if s.eq_ignore_ascii_case("enabled") => Ok(EnableState::Enabled),
s if s.eq_ignore_ascii_case("disabled") => Ok(EnableState::Disabled),
s if s.eq_ignore_ascii_case("ok") => Ok(EnableState::Ok),
s if s.eq_ignore_ascii_case("not_ok") => Ok(EnableState::NotOk),
s if s.eq_ignore_ascii_case("success") => Ok(EnableState::Success),
s if s.eq_ignore_ascii_case("failure") => Ok(EnableState::Failure),
s if s.eq_ignore_ascii_case("active") => Ok(EnableState::Active),
s if s.eq_ignore_ascii_case("inactive") => Ok(EnableState::Inactive),
"1" => Ok(EnableState::One),
"0" => Ok(EnableState::Zero),
_ => Err(()),
}
}
}
impl EnableState {
/// Returns the default value for the enum.
pub fn get_default() -> Self {
Self::default()
}
/// Returns the string representation of the enum.
pub fn as_str(&self) -> &str {
match self {
EnableState::True => "true",
EnableState::False => "false",
EnableState::Empty => "",
EnableState::Yes => "yes",
EnableState::No => "no",
EnableState::On => "on",
EnableState::Off => "off",
EnableState::Enabled => "enabled",
EnableState::Disabled => "disabled",
EnableState::Ok => "ok",
EnableState::NotOk => "not_ok",
EnableState::Success => "success",
EnableState::Failure => "failure",
EnableState::Active => "active",
EnableState::Inactive => "inactive",
EnableState::One => "1",
EnableState::Zero => "0",
}
}
/// is_enabled checks if the state represents an enabled condition.
pub fn is_enabled(self) -> bool {
matches!(
self,
EnableState::True
| EnableState::Yes
| EnableState::On
| EnableState::Enabled
| EnableState::Ok
| EnableState::Success
| EnableState::Active
| EnableState::One
)
}
/// is_disabled checks if the state represents a disabled condition.
pub fn is_disabled(self) -> bool {
matches!(
self,
EnableState::False
| EnableState::No
| EnableState::Off
| EnableState::Disabled
| EnableState::NotOk
| EnableState::Failure
| EnableState::Inactive
| EnableState::Zero
| EnableState::Empty
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_enable_state_display_and_fromstr() {
let cases = [
(EnableState::True, "true"),
(EnableState::False, "false"),
(EnableState::Empty, ""),
(EnableState::Yes, "yes"),
(EnableState::No, "no"),
(EnableState::On, "on"),
(EnableState::Off, "off"),
(EnableState::Enabled, "enabled"),
(EnableState::Disabled, "disabled"),
(EnableState::Ok, "ok"),
(EnableState::NotOk, "not_ok"),
(EnableState::Success, "success"),
(EnableState::Failure, "failure"),
(EnableState::Active, "active"),
(EnableState::Inactive, "inactive"),
(EnableState::One, "1"),
(EnableState::Zero, "0"),
];
for (variant, string) in cases.iter() {
assert_eq!(&variant.to_string(), string);
assert_eq!(EnableState::from_str(string).unwrap(), *variant);
}
// Test invalid string
assert!(EnableState::from_str("invalid").is_err());
}
#[test]
fn test_enable_state_enum() {
let cases = [
(EnableState::True, "true"),
(EnableState::False, "false"),
(EnableState::Empty, ""),
(EnableState::Yes, "yes"),
(EnableState::No, "no"),
(EnableState::On, "on"),
(EnableState::Off, "off"),
(EnableState::Enabled, "enabled"),
(EnableState::Disabled, "disabled"),
(EnableState::Ok, "ok"),
(EnableState::NotOk, "not_ok"),
(EnableState::Success, "success"),
(EnableState::Failure, "failure"),
(EnableState::Active, "active"),
(EnableState::Inactive, "inactive"),
(EnableState::One, "1"),
(EnableState::Zero, "0"),
];
for (variant, string) in cases.iter() {
assert_eq!(variant.to_string(), *string);
}
}
#[test]
fn test_enable_state_enum_from_str() {
let cases = [
("true", EnableState::True),
("false", EnableState::False),
("", EnableState::Empty),
("yes", EnableState::Yes),
("no", EnableState::No),
("on", EnableState::On),
("off", EnableState::Off),
("enabled", EnableState::Enabled),
("disabled", EnableState::Disabled),
("ok", EnableState::Ok),
("not_ok", EnableState::NotOk),
("success", EnableState::Success),
("failure", EnableState::Failure),
("active", EnableState::Active),
("inactive", EnableState::Inactive),
("1", EnableState::One),
("0", EnableState::Zero),
];
for (string, variant) in cases.iter() {
assert_eq!(EnableState::from_str(string).unwrap(), *variant);
}
}
#[test]
fn test_enable_state_default() {
let default_state = EnableState::get_default();
assert_eq!(default_state, EnableState::Empty);
assert_eq!(default_state.as_str(), "");
}
#[test]
fn test_enable_state_as_str() {
let cases = [
(EnableState::True, "true"),
(EnableState::False, "false"),
(EnableState::Empty, ""),
(EnableState::Yes, "yes"),
(EnableState::No, "no"),
(EnableState::On, "on"),
(EnableState::Off, "off"),
(EnableState::Enabled, "enabled"),
(EnableState::Disabled, "disabled"),
(EnableState::Ok, "ok"),
(EnableState::NotOk, "not_ok"),
(EnableState::Success, "success"),
(EnableState::Failure, "failure"),
(EnableState::Active, "active"),
(EnableState::Inactive, "inactive"),
(EnableState::One, "1"),
(EnableState::Zero, "0"),
];
for (variant, string) in cases.iter() {
assert_eq!(variant.as_str(), *string);
}
}
#[test]
fn test_enable_state_is_enabled() {
let enabled_states = [
EnableState::True,
EnableState::Yes,
EnableState::On,
EnableState::Enabled,
EnableState::Ok,
EnableState::Success,
EnableState::Active,
EnableState::One,
];
for state in enabled_states.iter() {
assert!(state.is_enabled());
}
let disabled_states = [
EnableState::False,
EnableState::No,
EnableState::Off,
EnableState::Disabled,
EnableState::NotOk,
EnableState::Failure,
EnableState::Inactive,
EnableState::Zero,
EnableState::Empty,
];
for state in disabled_states.iter() {
assert!(state.is_disabled());
}
}
}
+1
View File
@@ -14,3 +14,4 @@
pub(crate) mod app;
pub(crate) mod env;
pub(crate) mod tls;
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub const ENV_TLS_KEYLOG: &str = "RUSTFS_TLS_KEYLOG";
+2
View File
@@ -18,6 +18,8 @@ pub mod constants;
pub use constants::app::*;
#[cfg(feature = "constants")]
pub use constants::env::*;
#[cfg(feature = "constants")]
pub use constants::tls::*;
#[cfg(feature = "notify")]
pub mod notify;
#[cfg(feature = "observability")]
+133
View File
@@ -0,0 +1,133 @@
#![cfg(test)]
// 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 aws_config::meta::region::RegionProviderChain;
use aws_sdk_s3::Client;
use aws_sdk_s3::config::{Credentials, Region};
use bytes::Bytes;
use serial_test::serial;
use std::error::Error;
use tokio::time::sleep;
const ENDPOINT: &str = "http://localhost:9000";
const ACCESS_KEY: &str = "rustfsadmin";
const SECRET_KEY: &str = "rustfsadmin";
const BUCKET: &str = "test-basic-bucket";
async fn create_aws_s3_client() -> Result<Client, Box<dyn Error>> {
let region_provider = RegionProviderChain::default_provider().or_else(Region::new("us-east-1"));
let shared_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(region_provider)
.credentials_provider(Credentials::new(ACCESS_KEY, SECRET_KEY, None, None, "static"))
.endpoint_url(ENDPOINT)
.load()
.await;
let client = Client::from_conf(
aws_sdk_s3::Config::from(&shared_config)
.to_builder()
.force_path_style(true)
.build(),
);
Ok(client)
}
async fn setup_test_bucket(client: &Client) -> Result<(), Box<dyn Error>> {
match client.create_bucket().bucket(BUCKET).send().await {
Ok(_) => {}
Err(e) => {
let error_str = e.to_string();
if !error_str.contains("BucketAlreadyOwnedByYou") && !error_str.contains("BucketAlreadyExists") {
return Err(e.into());
}
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_bucket_lifecycle_configuration() -> Result<(), Box<dyn std::error::Error>> {
use aws_sdk_s3::types::{BucketLifecycleConfiguration, LifecycleExpiration, LifecycleRule, LifecycleRuleFilter};
use tokio::time::Duration;
let client = create_aws_s3_client().await?;
setup_test_bucket(&client).await?;
// Upload test object first
let test_content = "Test object for lifecycle expiration";
let lifecycle_object_key = "lifecycle-test-object.txt";
client
.put_object()
.bucket(BUCKET)
.key(lifecycle_object_key)
.body(Bytes::from(test_content.as_bytes()).into())
.send()
.await?;
// Verify object exists initially
let resp = client.get_object().bucket(BUCKET).key(lifecycle_object_key).send().await?;
assert!(resp.content_length().unwrap_or(0) > 0);
// Configure lifecycle rule: expire after current time + 3 seconds
let expiration = LifecycleExpiration::builder().days(0).build();
let filter = LifecycleRuleFilter::builder().prefix(lifecycle_object_key).build();
let rule = LifecycleRule::builder()
.id("expire-test-object")
.filter(filter)
.expiration(expiration)
.status(aws_sdk_s3::types::ExpirationStatus::Enabled)
.build()?;
let lifecycle = BucketLifecycleConfiguration::builder().rules(rule).build()?;
client
.put_bucket_lifecycle_configuration()
.bucket(BUCKET)
.lifecycle_configuration(lifecycle)
.send()
.await?;
// Verify lifecycle configuration was set
let resp = client.get_bucket_lifecycle_configuration().bucket(BUCKET).send().await?;
let rules = resp.rules();
assert!(rules.iter().any(|r| r.id().unwrap_or("") == "expire-test-object"));
// Wait for lifecycle processing (scanner runs every 1 second)
sleep(Duration::from_secs(3)).await;
// After lifecycle processing, the object should be deleted by the lifecycle rule
let get_result = client.get_object().bucket(BUCKET).key(lifecycle_object_key).send().await;
match get_result {
Ok(_) => {
panic!("Expected object to be deleted by lifecycle rule, but it still exists");
}
Err(e) => {
if let Some(service_error) = e.as_service_error() {
if service_error.is_no_such_key() {
println!("Lifecycle configuration test completed - object was successfully deleted by lifecycle rule");
} else {
panic!("Expected NoSuchKey error, but got: {e:?}");
}
} else {
panic!("Expected service error, but got: {e:?}");
}
}
}
println!("Lifecycle configuration test completed.");
Ok(())
}
+73
View File
@@ -38,6 +38,79 @@ fn get_cluster_endpoints() -> Vec<Endpoint> {
}]
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_guard_drop_releases_exclusive_lock_local() -> Result<(), Box<dyn Error>> {
// Single local client; no external server required
let client: Arc<dyn LockClient> = Arc::new(LocalClient::new());
let ns_lock = NamespaceLock::with_clients("e2e_guard_local".to_string(), vec![client]);
// Acquire exclusive guard
let g1 = ns_lock
.lock_guard("guard_exclusive", "owner1", Duration::from_millis(100), Duration::from_secs(5))
.await?;
assert!(g1.is_some(), "first guard acquisition should succeed");
// While g1 is alive, second exclusive acquisition should fail
let g2 = ns_lock
.lock_guard("guard_exclusive", "owner2", Duration::from_millis(50), Duration::from_secs(5))
.await?;
assert!(g2.is_none(), "second guard acquisition should fail while first is held");
// Drop first guard to trigger background release
drop(g1);
// Give the background unlock worker a short moment to process
sleep(Duration::from_millis(80)).await;
// Now acquisition should succeed
let g3 = ns_lock
.lock_guard("guard_exclusive", "owner2", Duration::from_millis(100), Duration::from_secs(5))
.await?;
assert!(g3.is_some(), "acquisition should succeed after guard drop releases the lock");
drop(g3);
Ok(())
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_guard_shared_then_write_after_drop() -> Result<(), Box<dyn Error>> {
// Two shared read guards should coexist; write should be blocked until they drop
let client: Arc<dyn LockClient> = Arc::new(LocalClient::new());
let ns_lock = NamespaceLock::with_clients("e2e_guard_rw".to_string(), vec![client]);
// Acquire two read guards
let r1 = ns_lock
.rlock_guard("rw_resource", "reader1", Duration::from_millis(100), Duration::from_secs(5))
.await?;
let r2 = ns_lock
.rlock_guard("rw_resource", "reader2", Duration::from_millis(100), Duration::from_secs(5))
.await?;
assert!(r1.is_some() && r2.is_some(), "both read guards should be acquired");
// Attempt write while readers hold the lock should fail
let w_fail = ns_lock
.lock_guard("rw_resource", "writer", Duration::from_millis(50), Duration::from_secs(5))
.await?;
assert!(w_fail.is_none(), "write should be blocked when read guards are active");
// Drop read guards to release
drop(r1);
drop(r2);
sleep(Duration::from_millis(80)).await;
// Now write should succeed
let w_ok = ns_lock
.lock_guard("rw_resource", "writer", Duration::from_millis(150), Duration::from_secs(5))
.await?;
assert!(w_ok.is_some(), "write should succeed after read guards are dropped");
drop(w_ok);
Ok(())
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
+1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod lifecycle;
mod lock;
mod node_interact_test;
mod sql;
+1 -2
View File
@@ -69,7 +69,6 @@ hmac = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
hex-simd = { workspace = true }
path-clean = { workspace = true }
tempfile.workspace = true
hyper.workspace = true
hyper-util.workspace = true
@@ -123,4 +122,4 @@ harness = false
[[bench]]
name = "comparison_benchmark"
harness = false
harness = false
@@ -516,7 +516,7 @@ impl TransitionState {
if let Err(err) = transition_object(api.clone(), &task.obj_info, LcAuditEvent::new(task.event.clone(), task.src.clone())).await {
if !is_err_version_not_found(&err) && !is_err_object_not_found(&err) && !is_network_or_host_down(&err.to_string(), false) && !err.to_string().contains("use of closed network connection") {
error!("Transition to {} failed for {}/{} version:{} with {}",
task.event.storage_class, task.obj_info.bucket, task.obj_info.name, task.obj_info.version_id.expect("err"), err.to_string());
task.event.storage_class, task.obj_info.bucket, task.obj_info.name, task.obj_info.version_id.map(|v| v.to_string()).unwrap_or_default(), err.to_string());
}
} else {
let mut ts = TierStats {
@@ -743,7 +743,7 @@ pub async fn transition_object(api: Arc<ECStore>, oi: &ObjectInfo, lae: LcAuditE
..Default::default()
},
//lifecycle_audit_event: lae,
version_id: Some(oi.version_id.expect("err").to_string()),
version_id: oi.version_id.map(|v| v.to_string()),
versioned: BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await,
version_suspended: BucketVersioningSys::prefix_suspended(&oi.bucket, &oi.name).await,
mod_time: oi.mod_time,
@@ -808,7 +808,7 @@ impl LifecycleOps for ObjectInfo {
lifecycle::ObjectOpts {
name: self.name.clone(),
user_tags: self.user_tags.clone(),
version_id: self.version_id.expect("err").to_string(),
version_id: self.version_id.map(|v| v.to_string()).unwrap_or_default(),
mod_time: self.mod_time,
size: self.size as usize,
is_latest: self.is_latest,
@@ -874,7 +874,11 @@ pub async fn eval_action_from_lifecycle(
if lock_enabled && enforce_retention_for_deletion(oi) {
//if serverDebugLog {
if oi.version_id.is_some() {
info!("lifecycle: {} v({}) is locked, not deleting", oi.name, oi.version_id.expect("err"));
info!(
"lifecycle: {} v({}) is locked, not deleting",
oi.name,
oi.version_id.map(|v| v.to_string()).unwrap_or_default()
);
} else {
info!("lifecycle: {} is locked, not deleting", oi.name);
}
@@ -928,7 +932,7 @@ pub async fn apply_expiry_on_non_transitioned_objects(
};
if lc_event.action.delete_versioned() {
opts.version_id = Some(oi.version_id.expect("err").to_string());
opts.version_id = oi.version_id.map(|v| v.to_string());
}
opts.versioned = BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await;
@@ -27,6 +27,7 @@ use std::env;
use std::fmt::Display;
use time::macros::{datetime, offset};
use time::{self, Duration, OffsetDateTime};
use tracing::info;
use crate::bucket::lifecycle::rule::TransitionOps;
@@ -279,7 +280,12 @@ impl Lifecycle for BucketLifecycleConfiguration {
async fn eval_inner(&self, obj: &ObjectOpts, now: OffsetDateTime) -> Event {
let mut events = Vec::<Event>::new();
info!(
"eval_inner: object={}, mod_time={:?}, now={:?}, is_latest={}, delete_marker={}",
obj.name, obj.mod_time, now, obj.is_latest, obj.delete_marker
);
if obj.mod_time.expect("err").unix_timestamp() == 0 {
info!("eval_inner: mod_time is 0, returning default event");
return Event::default();
}
@@ -418,7 +424,16 @@ impl Lifecycle for BucketLifecycleConfiguration {
}
}
if obj.is_latest && !obj.delete_marker {
info!(
"eval_inner: checking expiration condition - is_latest={}, delete_marker={}, version_id={:?}, condition_met={}",
obj.is_latest,
obj.delete_marker,
obj.version_id,
(obj.is_latest || obj.version_id.is_empty()) && !obj.delete_marker
);
// Allow expiration for latest objects OR non-versioned objects (empty version_id)
if (obj.is_latest || obj.version_id.is_empty()) && !obj.delete_marker {
info!("eval_inner: entering expiration check");
if let Some(ref expiration) = rule.expiration {
if let Some(ref date) = expiration.date {
let date0 = OffsetDateTime::from(date.clone());
@@ -435,22 +450,29 @@ impl Lifecycle for BucketLifecycleConfiguration {
});
}
} else if let Some(days) = expiration.days {
if days != 0 {
let expected_expiry: OffsetDateTime = expected_expiry_time(obj.mod_time.expect("err!"), days);
if now.unix_timestamp() == 0 || now.unix_timestamp() > expected_expiry.unix_timestamp() {
let mut event = Event {
action: IlmAction::DeleteAction,
rule_id: rule.id.clone().expect("err!"),
due: Some(expected_expiry),
noncurrent_days: 0,
newer_noncurrent_versions: 0,
storage_class: "".into(),
};
/*if rule.expiration.expect("err!").delete_all.val {
event.action = IlmAction::DeleteAllVersionsAction
}*/
events.push(event);
}
let expected_expiry: OffsetDateTime = expected_expiry_time(obj.mod_time.expect("err!"), days);
info!(
"eval_inner: expiration check - days={}, obj_time={:?}, expiry_time={:?}, now={:?}, should_expire={}",
days,
obj.mod_time.expect("err!"),
expected_expiry,
now,
now.unix_timestamp() > expected_expiry.unix_timestamp()
);
if now.unix_timestamp() == 0 || now.unix_timestamp() > expected_expiry.unix_timestamp() {
info!("eval_inner: object should expire, adding DeleteAction");
let mut event = Event {
action: IlmAction::DeleteAction,
rule_id: rule.id.clone().expect("err!"),
due: Some(expected_expiry),
noncurrent_days: 0,
newer_noncurrent_versions: 0,
storage_class: "".into(),
};
/*if rule.expiration.expect("err!").delete_all.val {
event.action = IlmAction::DeleteAllVersionsAction
}*/
events.push(event);
}
}
}
@@ -598,7 +620,7 @@ impl LifecycleCalculate for Transition {
pub fn expected_expiry_time(mod_time: OffsetDateTime, days: i32) -> OffsetDateTime {
if days == 0 {
return mod_time;
return OffsetDateTime::UNIX_EPOCH; // Return epoch time to ensure immediate expiry
}
let t = mod_time
.to_offset(offset!(-0:00:00))
@@ -54,8 +54,8 @@ pub fn get_object_retention_meta(meta: HashMap<String, String>) -> ObjectLockRet
}
if let Some(till_str) = till_str {
let t = OffsetDateTime::parse(till_str, &format_description::well_known::Iso8601::DEFAULT);
if t.is_err() {
retain_until_date = Date::from(t.expect("err")); //TODO: utc
if let Ok(parsed_time) = t {
retain_until_date = Date::from(parsed_time);
}
}
ObjectLockRetention {
+1 -1
View File
@@ -1897,7 +1897,7 @@ impl ReplicationState {
} else if !self.replica_status.is_empty() {
self.replica_status.clone()
} else {
return ReplicationStatusType::Unknown;
ReplicationStatusType::Unknown
}
}
+2 -2
View File
@@ -953,9 +953,8 @@ impl LocalDisk {
let name = path_join_buf(&[current, entry]);
if !dir_stack.is_empty() {
if let Some(pop) = dir_stack.pop() {
if let Some(pop) = dir_stack.last().cloned() {
if pop < name {
//
out.write_obj(&MetaCacheEntry {
name: pop.clone(),
..Default::default()
@@ -969,6 +968,7 @@ impl LocalDisk {
error!("scan_dir err {:?}", er);
}
}
dir_stack.pop();
}
}
}
+226 -47
View File
@@ -3211,6 +3211,20 @@ impl ObjectIO for SetDisks {
h: HeaderMap,
opts: &ObjectOptions,
) -> Result<GetObjectReader> {
// Acquire a shared read-lock early to protect read consistency
let mut _read_lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let guard_opt = self
.namespace_lock
.rlock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_read_lock_guard = guard_opt;
}
let (fi, files, disks) = self
.get_object_fileinfo(bucket, object, opts, true)
.await
@@ -3256,7 +3270,10 @@ impl ObjectIO for SetDisks {
let object = object.to_owned();
let set_index = self.set_index;
let pool_index = self.pool_index;
// Move the read-lock guard into the task so it lives for the duration of the read
let _guard_to_hold = _read_lock_guard; // moved into closure below
tokio::spawn(async move {
let _guard = _guard_to_hold; // keep guard alive until task ends
if let Err(e) = Self::get_object_with_fileinfo(
&bucket,
&object,
@@ -3284,16 +3301,18 @@ impl ObjectIO for SetDisks {
async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result<ObjectInfo> {
let disks = self.disks.read().await;
// Acquire per-object exclusive lock via RAII guard. It auto-releases asynchronously on drop.
let mut _object_lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let paths = vec![object.to_string()];
let lock_acquired = self
let guard_opt = self
.namespace_lock
.lock_batch(&paths, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if !lock_acquired {
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_object_lock_guard = guard_opt;
}
let mut user_defined = opts.user_defined.clone();
@@ -3500,14 +3519,6 @@ impl ObjectIO for SetDisks {
self.delete_all(RUSTFS_META_TMP_BUCKET, &tmp_dir).await?;
// Release lock if it was acquired
if !opts.no_lock {
let paths = vec![object.to_string()];
if let Err(err) = self.namespace_lock.unlock_batch(&paths, &self.locker_owner).await {
error!("Failed to unlock object {}: {}", object, err);
}
}
for (i, op_disk) in online_disks.iter().enumerate() {
if let Some(disk) = op_disk {
if disk.is_online().await {
@@ -3583,6 +3594,19 @@ impl StorageAPI for SetDisks {
return Err(StorageError::NotImplemented);
}
// Guard lock for source object metadata update
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
{
let guard_opt = self
.namespace_lock
.lock_guard(src_object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let disks = self.get_disks_internal().await;
let (mut metas, errs) = {
@@ -3676,6 +3700,18 @@ impl StorageAPI for SetDisks {
}
#[tracing::instrument(skip(self))]
async fn delete_object_version(&self, bucket: &str, object: &str, fi: &FileInfo, force_del_marker: bool) -> Result<()> {
// Guard lock for single object delete-version
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
{
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let disks = self.get_disks(0, 0).await?;
let write_quorum = disks.len() / 2 + 1;
@@ -3732,6 +3768,23 @@ impl StorageAPI for SetDisks {
del_errs.push(None)
}
// Per-object guards to keep until function end
let mut _guards: Vec<Option<rustfs_lock::LockGuard>> = Vec::with_capacity(objects.len());
// Acquire locks for all objects first; mark errors for failures
for (i, dobj) in objects.iter().enumerate() {
match self
.namespace_lock
.lock_guard(&dobj.object_name, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?
{
Some(g) => _guards.push(Some(g)),
None => {
del_errs[i] = Some(Error::other("can not get lock. please retry"));
_guards.push(None);
}
}
}
// let mut del_fvers = Vec::with_capacity(objects.len());
let mut vers_map: HashMap<&String, FileInfoVersions> = HashMap::new();
@@ -3788,7 +3841,10 @@ impl StorageAPI for SetDisks {
}
}
vers_map.insert(&dobj.object_name, v);
// Only add to vers_map if we hold the lock
if _guards[i].is_some() {
vers_map.insert(&dobj.object_name, v);
}
}
let mut vers = Vec::with_capacity(vers_map.len());
@@ -3830,6 +3886,18 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(skip(self))]
async fn delete_object(&self, bucket: &str, object: &str, opts: ObjectOptions) -> Result<ObjectInfo> {
// Guard lock for single object delete
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.delete_prefix {
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
if opts.delete_prefix {
self.delete_prefix(bucket, object)
.await
@@ -3837,7 +3905,90 @@ impl StorageAPI for SetDisks {
return Ok(ObjectInfo::default());
}
unimplemented!()
// Create a single object deletion request
let mut vr = FileInfo {
name: object.to_string(),
version_id: opts.version_id.as_ref().and_then(|v| Uuid::parse_str(v).ok()),
..Default::default()
};
// Handle versioning
let (suspended, versioned) = (opts.version_suspended, opts.versioned);
if opts.version_id.is_none() && (suspended || versioned) {
vr.mod_time = Some(OffsetDateTime::now_utc());
vr.deleted = true;
if versioned {
vr.version_id = Some(Uuid::new_v4());
}
}
let vers = vec![FileInfoVersions {
name: vr.name.clone(),
versions: vec![vr.clone()],
..Default::default()
}];
let disks = self.disks.read().await;
let disks = disks.clone();
let write_quorum = disks.len() / 2 + 1;
let mut futures = Vec::with_capacity(disks.len());
let mut errs = Vec::with_capacity(disks.len());
for disk in disks.iter() {
let vers = vers.clone();
futures.push(async move {
if let Some(disk) = disk {
disk.delete_versions(bucket, vers, DeleteOptions::default()).await
} else {
Err(DiskError::DiskNotFound)
}
});
}
let results = join_all(futures).await;
for result in results {
match result {
Ok(disk_errs) => {
// Handle errors from disk operations
for err in disk_errs.iter().flatten() {
warn!("delete_object disk error: {:?}", err);
}
errs.push(None);
}
Err(e) => {
errs.push(Some(e));
}
}
}
// Check write quorum
if let Some(err) = reduce_write_quorum_errs(&errs, OBJECT_OP_IGNORED_ERRS, write_quorum) {
return Err(to_object_err(err.into(), vec![bucket, object]));
}
// Create result ObjectInfo
let result_info = if vr.deleted {
ObjectInfo {
bucket: bucket.to_string(),
name: object.to_string(),
delete_marker: true,
mod_time: vr.mod_time,
version_id: vr.version_id,
..Default::default()
}
} else {
ObjectInfo {
bucket: bucket.to_string(),
name: object.to_string(),
version_id: vr.version_id,
..Default::default()
}
};
Ok(result_info)
}
#[tracing::instrument(skip(self))]
@@ -3869,33 +4020,18 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(skip(self))]
async fn get_object_info(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<ObjectInfo> {
// let mut _ns = None;
// if !opts.no_lock {
// let paths = vec![object.to_string()];
// let ns_lock = new_nslock(
// Arc::clone(&self.ns_mutex),
// self.locker_owner.clone(),
// bucket.to_string(),
// paths,
// self.lockers.clone(),
// )
// .await;
// if !ns_lock
// .0
// .write()
// .await
// .get_lock(&Options {
// timeout: Duration::from_secs(5),
// retry_interval: Duration::from_secs(1),
// })
// .await
// .map_err(|err| Error::other(err.to_string()))?
// {
// return Err(Error::other("can not get lock. please retry".to_string()));
// }
// _ns = Some(ns_lock);
// }
// Acquire a shared read-lock to protect consistency during info fetch
let mut _read_lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let guard_opt = self
.namespace_lock
.rlock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_read_lock_guard = guard_opt;
}
let (fi, _, _) = self
.get_object_fileinfo(bucket, object, opts, false)
@@ -3927,6 +4063,19 @@ impl StorageAPI for SetDisks {
async fn put_object_metadata(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<ObjectInfo> {
// TODO: nslock
// Guard lock for metadata update
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let disks = self.get_disks_internal().await;
let (metas, errs) = {
@@ -4017,12 +4166,18 @@ impl StorageAPI for SetDisks {
}
};
/*if !opts.no_lock {
let lk = self.new_ns_lock(bucket, object);
let lkctx = lk.get_lock(globalDeleteOperationTimeout)?;
//ctx = lkctx.Context()
//defer lk.Unlock(lkctx)
}*/
// Acquire write-lock early; hold for the whole transition operation scope
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let (mut fi, meta_arr, online_disks) = self.get_object_fileinfo(bucket, object, opts, true).await?;
/*if err != nil {
@@ -4140,6 +4295,18 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(level = "debug", skip(self))]
async fn restore_transitioned_object(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> {
// Acquire write-lock early for the restore operation
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let set_restore_header_fn = async move |oi: &mut ObjectInfo, rerr: Option<Error>| -> Result<()> {
if rerr.is_none() {
return Ok(());
@@ -4213,6 +4380,18 @@ impl StorageAPI for SetDisks {
#[tracing::instrument(level = "debug", skip(self))]
async fn put_object_tags(&self, bucket: &str, object: &str, tags: &str, opts: &ObjectOptions) -> Result<ObjectInfo> {
// Acquire write-lock for tag update (metadata write)
let mut _lock_guard: Option<rustfs_lock::LockGuard> = None;
if !opts.no_lock {
let guard_opt = self
.namespace_lock
.lock_guard(object, &self.locker_owner, Duration::from_secs(5), Duration::from_secs(10))
.await?;
if guard_opt.is_none() {
return Err(Error::other("can not get lock. please retry".to_string()));
}
_lock_guard = guard_opt;
}
let (mut fi, _, disks) = self.get_object_fileinfo(bucket, object, opts, false).await?;
fi.metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags.to_owned());
+7 -1
View File
@@ -165,7 +165,13 @@ impl Sets {
let lock_clients = create_unique_clients(&set_endpoints).await?;
let namespace_lock = rustfs_lock::NamespaceLock::with_clients(format!("set-{i}"), lock_clients);
// Bind lock quorum to EC write quorum for this set: data_shards (+1 if equal to parity) per default_write_quorum()
let mut write_quorum = set_drive_count - parity_count;
if write_quorum == parity_count {
write_quorum += 1;
}
let namespace_lock =
rustfs_lock::NamespaceLock::with_clients_and_quorum(format!("set-{i}"), lock_clients, write_quorum);
let set_disks = SetDisks::new(
Arc::new(namespace_lock),
+2 -2
View File
@@ -139,8 +139,8 @@ async fn init_format_erasure(
let idx = i * set_drive_count + j;
let mut newfm = fm.clone();
newfm.erasure.this = fm.erasure.sets[i][j];
if deployment_id.is_some() {
newfm.id = deployment_id.unwrap();
if let Some(id) = deployment_id {
newfm.id = id;
}
fms[idx] = Some(newfm);
+1 -1
View File
@@ -2363,7 +2363,7 @@ mod test {
assert!(stats.delete_markers > 0, "应该有删除标记");
// 测试版本合并功能
let merged = merge_file_meta_versions(1, false, 0, &[fm.versions.clone()]);
let merged = merge_file_meta_versions(1, false, 0, std::slice::from_ref(&fm.versions));
assert!(!merged.is_empty(), "合并后应该有版本");
}
+1 -1
View File
@@ -17,7 +17,7 @@ pub mod fileinfo;
mod filemeta;
mod filemeta_inline;
pub mod headers;
mod metacache;
pub mod metacache;
pub mod test_data;
+14 -12
View File
@@ -795,24 +795,26 @@ impl<T: Clone + Debug + Send + 'static> Cache<T> {
}
}
if self.opts.no_wait && v.is_some() && now - self.last_update_ms.load(AtomicOrdering::SeqCst) < self.ttl.as_secs() * 2 {
if self.updating.try_lock().is_ok() {
let this = Arc::clone(&self);
spawn(async move {
let _ = this.update().await;
});
if self.opts.no_wait && now - self.last_update_ms.load(AtomicOrdering::SeqCst) < self.ttl.as_secs() * 2 {
if let Some(value) = v {
if self.updating.try_lock().is_ok() {
let this = Arc::clone(&self);
spawn(async move {
let _ = this.update().await;
});
}
return Ok(value);
}
return Ok(v.unwrap());
}
let _ = self.updating.lock().await;
if let Ok(duration) =
SystemTime::now().duration_since(UNIX_EPOCH + Duration::from_secs(self.last_update_ms.load(AtomicOrdering::SeqCst)))
{
if let (Ok(duration), Some(value)) = (
SystemTime::now().duration_since(UNIX_EPOCH + Duration::from_secs(self.last_update_ms.load(AtomicOrdering::SeqCst))),
v,
) {
if duration < self.ttl {
return Ok(v.unwrap());
return Ok(value);
}
}
-3
View File
@@ -32,9 +32,7 @@ workspace = true
async-trait.workspace = true
bytes.workspace = true
futures.workspace = true
lazy_static.workspace = true
rustfs-protos.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
@@ -44,4 +42,3 @@ url.workspace = true
uuid.workspace = true
thiserror.workspace = true
once_cell.workspace = true
lru.workspace = true
+120
View File
@@ -0,0 +1,120 @@
// 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::sync::Arc;
use once_cell::sync::Lazy;
use tokio::sync::mpsc;
use crate::{client::LockClient, types::LockId};
#[derive(Debug, Clone)]
struct UnlockJob {
lock_id: LockId,
clients: Vec<Arc<dyn LockClient>>, // cloned Arcs; cheap and shares state
}
#[derive(Debug)]
struct UnlockRuntime {
tx: mpsc::Sender<UnlockJob>,
}
// Global unlock runtime with background worker
static UNLOCK_RUNTIME: Lazy<UnlockRuntime> = Lazy::new(|| {
// Larger buffer to reduce contention during bursts
let (tx, mut rx) = mpsc::channel::<UnlockJob>(8192);
// Spawn background worker when first used; assumes a Tokio runtime is available
tokio::spawn(async move {
while let Some(job) = rx.recv().await {
// Best-effort release across clients; try all, success if any succeeds
let mut any_ok = false;
let lock_id = job.lock_id.clone();
for client in job.clients.into_iter() {
if client.release(&lock_id).await.unwrap_or(false) {
any_ok = true;
}
}
if !any_ok {
tracing::warn!("LockGuard background release failed for {}", lock_id);
} else {
tracing::debug!("LockGuard background released {}", lock_id);
}
}
});
UnlockRuntime { tx }
});
/// A RAII guard that releases the lock asynchronously when dropped.
#[derive(Debug)]
pub struct LockGuard {
lock_id: LockId,
clients: Vec<Arc<dyn LockClient>>,
/// If true, Drop will not try to release (used if user manually released).
disarmed: bool,
}
impl LockGuard {
pub(crate) fn new(lock_id: LockId, clients: Vec<Arc<dyn LockClient>>) -> Self {
Self {
lock_id,
clients,
disarmed: false,
}
}
/// Get the lock id associated with this guard
pub fn lock_id(&self) -> &LockId {
&self.lock_id
}
/// Manually disarm the guard so dropping it won't release the lock.
/// Call this if you explicitly released the lock elsewhere.
pub fn disarm(&mut self) {
self.disarmed = true;
}
}
impl Drop for LockGuard {
fn drop(&mut self) {
if self.disarmed {
return;
}
let job = UnlockJob {
lock_id: self.lock_id.clone(),
clients: self.clients.clone(),
};
// Try a non-blocking send to avoid panics in Drop
if let Err(err) = UNLOCK_RUNTIME.tx.try_send(job) {
// Channel full or closed; best-effort fallback: spawn a detached task
let lock_id = self.lock_id.clone();
let clients = self.clients.clone();
tracing::warn!("LockGuard channel send failed ({}), spawning fallback unlock task for {}", err, lock_id);
// If runtime is not available, this will panic; but in RustFS we are inside Tokio contexts.
let handle = tokio::spawn(async move {
let futures_iter = clients.into_iter().map(|client| {
let id = lock_id.clone();
async move { client.release(&id).await.unwrap_or(false) }
});
let _ = futures::future::join_all(futures_iter).await;
});
// Explicitly drop the JoinHandle to acknowledge detaching the task.
std::mem::drop(handle);
}
}
}
+2
View File
@@ -27,6 +27,7 @@ pub mod local;
// Core Modules
pub mod error;
pub mod guard;
pub mod types;
// ============================================================================
@@ -39,6 +40,7 @@ pub use crate::{
client::{LockClient, local::LocalClient, remote::RemoteClient},
// Error types
error::{LockError, Result},
guard::LockGuard,
local::LocalLockMap,
// Main components
namespace::{NamespaceLock, NamespaceLockManager},
+331 -157
View File
@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::LockRequest;
@@ -29,6 +29,11 @@ pub struct LocalLockEntry {
pub readers: HashMap<String, usize>,
/// lock expiration time
pub expires_at: Option<Instant>,
/// number of writers waiting (for simple fairness against reader storms)
pub writer_pending: usize,
/// notifiers for readers/writers
pub notify_readers: Arc<Notify>,
pub notify_writers: Arc<Notify>,
}
/// local lock map
@@ -38,6 +43,10 @@ pub struct LocalLockMap {
pub locks: Arc<RwLock<HashMap<crate::types::LockId, Arc<RwLock<LocalLockEntry>>>>>,
/// Shutdown flag for background tasks
shutdown: Arc<AtomicBool>,
/// expiration schedule map: when -> lock_ids
expirations: Arc<Mutex<BTreeMap<Instant, Vec<crate::types::LockId>>>>,
/// notify expiry task when new earlier deadline arrives
exp_notify: Arc<Notify>,
}
impl Default for LocalLockMap {
@@ -52,6 +61,8 @@ impl LocalLockMap {
let map = Self {
locks: Arc::new(RwLock::new(HashMap::new())),
shutdown: Arc::new(AtomicBool::new(false)),
expirations: Arc::new(Mutex::new(BTreeMap::new())),
exp_notify: Arc::new(Notify::new()),
};
map.spawn_expiry_task();
map
@@ -61,56 +72,115 @@ impl LocalLockMap {
fn spawn_expiry_task(&self) {
let locks = self.locks.clone();
let shutdown = self.shutdown.clone();
let expirations = self.expirations.clone();
let exp_notify = self.exp_notify.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
if shutdown.load(Ordering::Relaxed) {
tracing::debug!("Expiry task shutting down");
break;
}
let now = Instant::now();
let mut to_remove = Vec::new();
// Find next deadline and drain due ids
let (due_ids, wait_duration) = {
let mut due = Vec::new();
let mut guard = expirations.lock().await;
let now = Instant::now();
let next_deadline = guard.first_key_value().map(|(k, _)| *k);
// drain all <= now
let mut keys_to_remove = Vec::new();
for (k, v) in guard.range(..=now).map(|(k, v)| (*k, v.clone())) {
due.extend(v);
keys_to_remove.push(k);
}
for k in keys_to_remove {
guard.remove(&k);
}
let wait = if due.is_empty() {
next_deadline.map(|dl| if dl > now { dl - now } else { Duration::from_millis(0) })
} else {
Some(Duration::from_millis(0))
};
(due, wait)
};
{
let locks_guard = locks.read().await;
for (key, entry) in locks_guard.iter() {
if let Ok(mut entry_guard) = entry.try_write() {
if let Some(exp) = entry_guard.expires_at {
if exp <= now {
entry_guard.writer = None;
entry_guard.readers.clear();
entry_guard.expires_at = None;
if !due_ids.is_empty() {
// process due ids without holding the map lock during awaits
let now = Instant::now();
// collect entries to process
let entries: Vec<(crate::types::LockId, Arc<RwLock<LocalLockEntry>>)> = {
let locks_guard = locks.read().await;
due_ids
.into_iter()
.filter_map(|id| locks_guard.get(&id).cloned().map(|e| (id, e)))
.collect()
};
if entry_guard.writer.is_none() && entry_guard.readers.is_empty() {
to_remove.push(key.clone());
}
let mut to_remove = Vec::new();
for (lock_id, entry) in entries {
let mut entry_guard = entry.write().await;
if let Some(exp) = entry_guard.expires_at {
if exp <= now {
entry_guard.writer = None;
entry_guard.readers.clear();
entry_guard.expires_at = None;
entry_guard.notify_writers.notify_waiters();
entry_guard.notify_readers.notify_waiters();
if entry_guard.writer.is_none() && entry_guard.readers.is_empty() {
to_remove.push(lock_id);
}
}
}
}
if !to_remove.is_empty() {
let mut locks_w = locks.write().await;
for id in to_remove {
let _ = locks_w.remove(&id);
}
}
continue; // immediately look for next
}
if !to_remove.is_empty() {
let mut locks_guard = locks.write().await;
for key in to_remove {
locks_guard.remove(&key);
// nothing due; wait for next deadline or notification
if let Some(dur) = wait_duration {
tokio::select! {
_ = tokio::time::sleep(dur) => {},
_ = exp_notify.notified() => {},
}
} else {
// no deadlines, wait for new schedule or shutdown tick
exp_notify.notified().await;
}
}
});
}
/// schedule an expiry time for the given lock id (inline, avoid per-acquisition spawn)
async fn schedule_expiry(&self, id: crate::types::LockId, exp: Instant) {
let mut guard = self.expirations.lock().await;
let is_earliest = match guard.first_key_value() {
Some((k, _)) => exp < *k,
None => true,
};
guard.entry(exp).or_insert_with(Vec::new).push(id);
drop(guard);
if is_earliest {
self.exp_notify.notify_waiters();
}
}
/// write lock with TTL, support timeout, use LockRequest
pub async fn lock_with_ttl_id(&self, request: &LockRequest) -> std::io::Result<bool> {
let start = Instant::now();
let expires_at = Some(Instant::now() + request.ttl);
loop {
// get or create lock entry
let entry = {
// get or create lock entry (double-checked to reduce write-lock contention)
let entry = if let Some(e) = {
let locks_guard = self.locks.read().await;
locks_guard.get(&request.lock_id).cloned()
} {
e
} else {
let mut locks_guard = self.locks.write().await;
locks_guard
.entry(request.lock_id.clone())
@@ -119,13 +189,17 @@ impl LocalLockMap {
writer: None,
readers: HashMap::new(),
expires_at: None,
writer_pending: 0,
notify_readers: Arc::new(Notify::new()),
notify_writers: Arc::new(Notify::new()),
}))
})
.clone()
};
// try to get write lock to modify state
if let Ok(mut entry_guard) = entry.try_write() {
// attempt acquisition or wait using Notify
let notify_to_wait = {
let mut entry_guard = entry.write().await;
// check expired state
let now = Instant::now();
if let Some(exp) = entry_guard.expires_at {
@@ -136,30 +210,68 @@ impl LocalLockMap {
}
}
// check if can get write lock
// try acquire
if entry_guard.writer.is_none() && entry_guard.readers.is_empty() {
entry_guard.writer = Some(request.owner.clone());
entry_guard.expires_at = expires_at;
let expires_at = Instant::now() + request.ttl;
entry_guard.expires_at = Some(expires_at);
tracing::debug!("Write lock acquired for resource '{}' by owner '{}'", request.resource, request.owner);
{
drop(entry_guard);
self.schedule_expiry(request.lock_id.clone(), expires_at).await;
}
return Ok(true);
}
}
// couldn't acquire now, mark as pending writer and choose notifier
entry_guard.writer_pending = entry_guard.writer_pending.saturating_add(1);
entry_guard.notify_writers.clone()
};
if start.elapsed() >= request.acquire_timeout {
// wait with remaining timeout
let elapsed = start.elapsed();
if elapsed >= request.acquire_timeout {
// best-effort decrement pending counter
if let Ok(mut eg) = entry.try_write() {
eg.writer_pending = eg.writer_pending.saturating_sub(1);
} else {
let mut eg = entry.write().await;
eg.writer_pending = eg.writer_pending.saturating_sub(1);
}
return Ok(false);
}
tokio::time::sleep(Duration::from_millis(10)).await;
let remaining = request.acquire_timeout - elapsed;
if tokio::time::timeout(remaining, notify_to_wait.notified()).await.is_err() {
// timeout; decrement pending before returning
if let Ok(mut eg) = entry.try_write() {
eg.writer_pending = eg.writer_pending.saturating_sub(1);
} else {
let mut eg = entry.write().await;
eg.writer_pending = eg.writer_pending.saturating_sub(1);
}
return Ok(false);
}
// woke up; decrement pending before retrying
if let Ok(mut eg) = entry.try_write() {
eg.writer_pending = eg.writer_pending.saturating_sub(1);
} else {
let mut eg = entry.write().await;
eg.writer_pending = eg.writer_pending.saturating_sub(1);
}
}
}
/// read lock with TTL, support timeout, use LockRequest
pub async fn rlock_with_ttl_id(&self, request: &LockRequest) -> std::io::Result<bool> {
let start = Instant::now();
let expires_at = Some(Instant::now() + request.ttl);
loop {
// get or create lock entry
let entry = {
// get or create lock entry (double-checked to reduce write-lock contention)
let entry = if let Some(e) = {
let locks_guard = self.locks.read().await;
locks_guard.get(&request.lock_id).cloned()
} {
e
} else {
let mut locks_guard = self.locks.write().await;
locks_guard
.entry(request.lock_id.clone())
@@ -168,13 +280,17 @@ impl LocalLockMap {
writer: None,
readers: HashMap::new(),
expires_at: None,
writer_pending: 0,
notify_readers: Arc::new(Notify::new()),
notify_writers: Arc::new(Notify::new()),
}))
})
.clone()
};
// try to get write lock to modify state
if let Ok(mut entry_guard) = entry.try_write() {
// attempt acquisition or wait using Notify
let notify_to_wait = {
let mut entry_guard = entry.write().await;
// check expired state
let now = Instant::now();
if let Some(exp) = entry_guard.expires_at {
@@ -185,189 +301,247 @@ impl LocalLockMap {
}
}
// check if can get read lock
if entry_guard.writer.is_none() {
// increase read lock count
if entry_guard.writer.is_none() && entry_guard.writer_pending == 0 {
*entry_guard.readers.entry(request.owner.clone()).or_insert(0) += 1;
entry_guard.expires_at = expires_at;
let expires_at = Instant::now() + request.ttl;
entry_guard.expires_at = Some(expires_at);
tracing::debug!("Read lock acquired for resource '{}' by owner '{}'", request.resource, request.owner);
{
drop(entry_guard);
self.schedule_expiry(request.lock_id.clone(), expires_at).await;
}
return Ok(true);
}
}
if start.elapsed() >= request.acquire_timeout {
// choose notifier: prefer waiting on writers if writers pending, else readers
if entry_guard.writer_pending > 0 {
entry_guard.notify_writers.clone()
} else {
entry_guard.notify_readers.clone()
}
};
// wait with remaining timeout
let elapsed = start.elapsed();
if elapsed >= request.acquire_timeout {
return Ok(false);
}
let remaining = request.acquire_timeout - elapsed;
if tokio::time::timeout(remaining, notify_to_wait.notified()).await.is_err() {
return Ok(false);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
/// unlock by LockId and owner - need to specify owner to correctly unlock
pub async fn unlock_by_id_and_owner(&self, lock_id: &crate::types::LockId, owner: &str) -> std::io::Result<()> {
println!("Unlocking lock_id: {lock_id:?}, owner: {owner}");
let mut need_remove = false;
{
// first, get the entry without holding the write lock on the map
let entry = {
let locks_guard = self.locks.read().await;
if let Some(entry) = locks_guard.get(lock_id) {
println!("Found lock entry, attempting to acquire write lock...");
match entry.try_write() {
Ok(mut entry_guard) => {
println!("Successfully acquired write lock for unlock");
// try to release write lock
if entry_guard.writer.as_ref() == Some(&owner.to_string()) {
println!("Releasing write lock for owner: {owner}");
entry_guard.writer = None;
}
// try to release read lock
else if let Some(count) = entry_guard.readers.get_mut(owner) {
println!("Releasing read lock for owner: {owner} (count: {count})");
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(owner);
println!("Removed owner {owner} from readers");
}
} else {
println!("Owner {owner} not found in writers or readers");
}
// check if need to remove
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
println!("Lock entry is empty, marking for removal");
entry_guard.expires_at = None;
need_remove = true;
} else {
println!(
"Lock entry still has content: writer={:?}, readers={:?}",
entry_guard.writer, entry_guard.readers
);
}
}
Err(_) => {
println!("Failed to acquire write lock for unlock - this is the problem!");
return Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"Failed to acquire write lock for unlock",
));
}
match locks_guard.get(lock_id) {
Some(e) => e.clone(),
None => return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Lock entry not found")),
}
};
let mut need_remove = false;
let (notify_writers, notify_readers, writer_pending, writer_none) = {
let mut entry_guard = entry.write().await;
// try to release write lock
if entry_guard.writer.as_ref() == Some(&owner.to_string()) {
entry_guard.writer = None;
}
// try to release read lock
else if let Some(count) = entry_guard.readers.get_mut(owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(owner);
}
} else {
println!("Lock entry not found for lock_id: {lock_id:?}");
// owner not found, treat as no-op
}
// check if need to remove
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
}
// capture notifications and state
(
entry_guard.notify_writers.clone(),
entry_guard.notify_readers.clone(),
entry_guard.writer_pending,
entry_guard.writer.is_none(),
)
};
if writer_pending > 0 && writer_none {
// Wake a single writer to preserve fairness and avoid thundering herd
notify_writers.notify_one();
} else if writer_none {
// No writers waiting, allow readers to proceed
notify_readers.notify_waiters();
}
// only here, entry's Ref is really dropped, can safely remove
if need_remove {
println!("Removing lock entry from map...");
let mut locks_guard = self.locks.write().await;
let removed = locks_guard.remove(lock_id);
println!("Lock entry removed: {:?}", removed.is_some());
let _ = locks_guard.remove(lock_id);
}
println!("Unlock operation completed");
Ok(())
}
/// unlock by LockId - smart release (compatible with old interface, but may be inaccurate)
pub async fn unlock_by_id(&self, lock_id: &crate::types::LockId) -> std::io::Result<()> {
let mut need_remove = false;
{
let entry = {
let locks_guard = self.locks.read().await;
if let Some(entry) = locks_guard.get(lock_id) {
if let Ok(mut entry_guard) = entry.try_write() {
// release write lock first
if entry_guard.writer.is_some() {
entry_guard.writer = None;
}
// if no write lock, release first read lock
else if let Some((owner, _)) = entry_guard.readers.iter().next() {
let owner = owner.clone();
if let Some(count) = entry_guard.readers.get_mut(&owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(&owner);
}
}
}
match locks_guard.get(lock_id) {
Some(e) => e.clone(),
None => return Ok(()), // nothing to do
}
};
// if completely idle, clean entry
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
let mut need_remove = false;
let (notify_writers, notify_readers, writer_pending, writer_none) = {
let mut entry_guard = entry.write().await;
// release write lock first
if entry_guard.writer.is_some() {
entry_guard.writer = None;
}
// if no write lock, release first read lock
else if let Some((owner, _)) = entry_guard.readers.iter().next() {
let owner = owner.clone();
if let Some(count) = entry_guard.readers.get_mut(&owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(&owner);
}
}
}
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
}
(
entry_guard.notify_writers.clone(),
entry_guard.notify_readers.clone(),
entry_guard.writer_pending,
entry_guard.writer.is_none(),
)
};
if writer_pending > 0 && writer_none {
notify_writers.notify_one();
} else if writer_none {
notify_readers.notify_waiters();
}
if need_remove {
let mut locks_guard = self.locks.write().await;
locks_guard.remove(lock_id);
let _ = locks_guard.remove(lock_id);
}
Ok(())
}
/// runlock by LockId and owner - need to specify owner to correctly unlock read lock
pub async fn runlock_by_id_and_owner(&self, lock_id: &crate::types::LockId, owner: &str) -> std::io::Result<()> {
let mut need_remove = false;
{
let entry = {
let locks_guard = self.locks.read().await;
if let Some(entry) = locks_guard.get(lock_id) {
if let Ok(mut entry_guard) = entry.try_write() {
// release read lock
if let Some(count) = entry_guard.readers.get_mut(owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(owner);
}
}
match locks_guard.get(lock_id) {
Some(e) => e.clone(),
None => return Ok(()),
}
};
// if completely idle, clean entry
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
}
let mut need_remove = false;
let (notify_writers, notify_readers, writer_pending, writer_none) = {
let mut entry_guard = entry.write().await;
// release read lock
if let Some(count) = entry_guard.readers.get_mut(owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(owner);
}
}
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
}
(
entry_guard.notify_writers.clone(),
entry_guard.notify_readers.clone(),
entry_guard.writer_pending,
entry_guard.writer.is_none(),
)
};
if writer_pending > 0 && writer_none {
notify_writers.notify_waiters();
} else if writer_none {
notify_readers.notify_waiters();
}
if need_remove {
let mut locks_guard = self.locks.write().await;
locks_guard.remove(lock_id);
let _ = locks_guard.remove(lock_id);
}
Ok(())
}
/// runlock by LockId - smart release read lock (compatible with old interface)
pub async fn runlock_by_id(&self, lock_id: &crate::types::LockId) -> std::io::Result<()> {
let mut need_remove = false;
{
let entry = {
let locks_guard = self.locks.read().await;
if let Some(entry) = locks_guard.get(lock_id) {
if let Ok(mut entry_guard) = entry.try_write() {
// release first read lock
if let Some((owner, _)) = entry_guard.readers.iter().next() {
let owner = owner.clone();
if let Some(count) = entry_guard.readers.get_mut(&owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(&owner);
}
}
}
match locks_guard.get(lock_id) {
Some(e) => e.clone(),
None => return Ok(()),
}
};
// if completely idle, clean entry
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
let mut need_remove = false;
let (notify_writers, notify_readers, writer_pending, writer_none) = {
let mut entry_guard = entry.write().await;
// release first read lock
if let Some((owner, _)) = entry_guard.readers.iter().next() {
let owner = owner.clone();
if let Some(count) = entry_guard.readers.get_mut(&owner) {
*count -= 1;
if *count == 0 {
entry_guard.readers.remove(&owner);
}
}
}
if entry_guard.readers.is_empty() && entry_guard.writer.is_none() {
entry_guard.expires_at = None;
need_remove = true;
}
(
entry_guard.notify_writers.clone(),
entry_guard.notify_readers.clone(),
entry_guard.writer_pending,
entry_guard.writer.is_none(),
)
};
if writer_pending > 0 && writer_none {
notify_writers.notify_waiters();
} else if writer_none {
notify_readers.notify_waiters();
}
if need_remove {
let mut locks_guard = self.locks.write().await;
locks_guard.remove(lock_id);
let _ = locks_guard.remove(lock_id);
}
Ok(())
}
+109 -39
View File
@@ -19,6 +19,7 @@ use std::time::Duration;
use crate::{
client::LockClient,
error::{LockError, Result},
guard::LockGuard,
types::{LockId, LockInfo, LockRequest, LockResponse, LockStatus, LockType},
};
@@ -60,6 +61,22 @@ impl NamespaceLock {
}
}
/// Create namespace lock with clients and an explicit quorum size.
/// Quorum will be clamped into [1, clients.len()]. For single client, quorum is always 1.
pub fn with_clients_and_quorum(namespace: String, clients: Vec<Arc<dyn LockClient>>, quorum: usize) -> Self {
let q = if clients.len() <= 1 {
1
} else {
quorum.clamp(1, clients.len())
};
Self {
clients,
namespace,
quorum: q,
}
}
/// Create namespace lock with client (compatibility)
pub fn with_client(client: Arc<dyn LockClient>) -> Self {
Self::with_clients("default".to_string(), vec![client])
@@ -86,54 +103,77 @@ impl NamespaceLock {
return self.clients[0].acquire_lock(request).await;
}
// Two-phase commit for distributed lock acquisition
self.acquire_lock_with_2pc(request).await
// Quorum-based acquisition for distributed mode
let (resp, _idxs) = self.acquire_lock_quorum(request).await?;
Ok(resp)
}
/// Two-phase commit lock acquisition: all nodes must succeed or all fail
async fn acquire_lock_with_2pc(&self, request: &LockRequest) -> Result<LockResponse> {
// Phase 1: Prepare - try to acquire lock on all clients
let futures: Vec<_> = self
/// Acquire a lock and return a RAII guard that will release asynchronously on Drop.
/// This is a thin wrapper around `acquire_lock` and will only create a guard when acquisition succeeds.
pub async fn acquire_guard(&self, request: &LockRequest) -> Result<Option<LockGuard>> {
if self.clients.is_empty() {
return Err(LockError::internal("No lock clients available"));
}
if self.clients.len() == 1 {
let resp = self.clients[0].acquire_lock(request).await?;
if resp.success {
return Ok(Some(LockGuard::new(
LockId::new_deterministic(&request.resource),
vec![self.clients[0].clone()],
)));
}
return Ok(None);
}
let (resp, idxs) = self.acquire_lock_quorum(request).await?;
if resp.success {
let subset: Vec<_> = idxs.into_iter().filter_map(|i| self.clients.get(i).cloned()).collect();
Ok(Some(LockGuard::new(LockId::new_deterministic(&request.resource), subset)))
} else {
Ok(None)
}
}
/// Convenience: acquire exclusive lock as a guard
pub async fn lock_guard(&self, resource: &str, owner: &str, timeout: Duration, ttl: Duration) -> Result<Option<LockGuard>> {
let req = LockRequest::new(self.get_resource_key(resource), LockType::Exclusive, owner)
.with_acquire_timeout(timeout)
.with_ttl(ttl);
self.acquire_guard(&req).await
}
/// Convenience: acquire shared lock as a guard
pub async fn rlock_guard(&self, resource: &str, owner: &str, timeout: Duration, ttl: Duration) -> Result<Option<LockGuard>> {
let req = LockRequest::new(self.get_resource_key(resource), LockType::Shared, owner)
.with_acquire_timeout(timeout)
.with_ttl(ttl);
self.acquire_guard(&req).await
}
/// Quorum-based lock acquisition: success if at least `self.quorum` clients succeed.
/// Returns the LockResponse and the indices of clients that acquired the lock.
async fn acquire_lock_quorum(&self, request: &LockRequest) -> Result<(LockResponse, Vec<usize>)> {
let futs: Vec<_> = self
.clients
.iter()
.enumerate()
.map(|(idx, client)| async move {
let result = client.acquire_lock(request).await;
(idx, result)
})
.map(|(idx, client)| async move { (idx, client.acquire_lock(request).await) })
.collect();
let results = futures::future::join_all(futures).await;
let results = futures::future::join_all(futs).await;
let mut successful_clients = Vec::new();
let mut failed_clients = Vec::new();
// Collect results
for (idx, result) in results {
match result {
Ok(response) if response.success => {
for (idx, res) in results {
if let Ok(resp) = res {
if resp.success {
successful_clients.push(idx);
}
_ => {
failed_clients.push(idx);
}
}
}
// Check if we have enough successful acquisitions for quorum
if successful_clients.len() >= self.quorum {
// Phase 2a: Commit - we have quorum, but need to ensure consistency
// If not all clients succeeded, we need to rollback for consistency
if successful_clients.len() < self.clients.len() {
// Rollback all successful acquisitions to maintain consistency
self.rollback_acquisitions(request, &successful_clients).await;
return Ok(LockResponse::failure(
"Partial success detected, rolled back for consistency".to_string(),
Duration::ZERO,
));
}
// All clients succeeded - lock acquired successfully
Ok(LockResponse::success(
let resp = LockResponse::success(
LockInfo {
id: LockId::new_deterministic(&request.resource),
resource: request.resource.clone(),
@@ -148,16 +188,17 @@ impl NamespaceLock {
wait_start_time: None,
},
Duration::ZERO,
))
);
Ok((resp, successful_clients))
} else {
// Phase 2b: Abort - insufficient quorum, rollback any successful acquisitions
if !successful_clients.is_empty() {
self.rollback_acquisitions(request, &successful_clients).await;
}
Ok(LockResponse::failure(
let resp = LockResponse::failure(
format!("Failed to acquire quorum: {}/{} required", successful_clients.len(), self.quorum),
Duration::ZERO,
))
);
Ok((resp, Vec::new()))
}
}
@@ -420,6 +461,33 @@ mod tests {
assert!(result.is_ok());
}
#[tokio::test]
async fn test_guard_acquire_and_drop_release() {
let ns_lock = NamespaceLock::with_client(Arc::new(LocalClient::new()));
// Acquire guard
let guard = ns_lock
.lock_guard("guard-resource", "owner", Duration::from_millis(100), Duration::from_secs(5))
.await
.unwrap();
assert!(guard.is_some());
let lock_id = guard.as_ref().unwrap().lock_id().clone();
// Drop guard to trigger background release
drop(guard);
// Give background worker a moment to process
tokio::time::sleep(Duration::from_millis(50)).await;
// Re-acquire should succeed (previous lock released)
let req = LockRequest::new(&lock_id.resource, LockType::Exclusive, "owner").with_ttl(Duration::from_secs(2));
let resp = ns_lock.acquire_lock(&req).await.unwrap();
assert!(resp.success);
// Cleanup
let _ = ns_lock.release_lock(&LockId::new_deterministic(&lock_id.resource)).await;
}
#[tokio::test]
async fn test_connection_health() {
let local_lock = NamespaceLock::new("test-namespace".to_string());
@@ -502,9 +570,11 @@ mod tests {
let client2: Arc<dyn LockClient> = Arc::new(LocalClient::new());
let clients = vec![client1, client2];
let ns_lock = NamespaceLock::with_clients("test-namespace".to_string(), clients);
// LocalClient shares a global in-memory map. For exclusive locks, only one can acquire at a time.
// In real distributed setups the quorum should be tied to EC write quorum. Here we use quorum=1 for success.
let ns_lock = NamespaceLock::with_clients_and_quorum("test-namespace".to_string(), clients, 1);
let request = LockRequest::new("test-resource", LockType::Exclusive, "test_owner").with_ttl(Duration::from_secs(10));
let request = LockRequest::new("test-resource", LockType::Shared, "test_owner").with_ttl(Duration::from_secs(2));
// This should succeed only if ALL clients can acquire the lock
let response = ns_lock.acquire_lock(&request).await.unwrap();
+15
View File
@@ -0,0 +1,15 @@
FROM rust:1.88 AS builder
WORKDIR /build
COPY . .
RUN cargo build --release -p rustfs-mcp
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /build/target/release/rustfs-mcp /app/
ENTRYPOINT ["/app/rustfs-mcp"]
+63
View File
@@ -98,7 +98,9 @@ rustfs-mcp --log-level debug --region us-west-2
```
### Integration with chat client
#### Option 1: Using Command Line Arguments
```json
{
"mcpServers": {
@@ -116,6 +118,7 @@ rustfs-mcp --log-level debug --region us-west-2
```
#### Option 2: Using Environment Variables
```json
{
"mcpServers": {
@@ -130,26 +133,84 @@ rustfs-mcp --log-level debug --region us-west-2
}
}
```
### Using MCP with Docker
#### Docker image build
Using MCP with docker will simply the usage of rustfs mcp. Building the docker image with below command:
```
docker build -f Dockerfile -t rustfs/rustfs-mcp ../../
```
Alternatively, if you want to build the image from the rustfs codebase root directory,run the command:
```
docker build -f crates/mcp/Dockerfile -t rustfs/rustfs-mcp .
```
#### IDE Configuration
Adding the following content in IDE MCP settings:
```
{
"mcpServers": {
"rustfs-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-e",
"AWS_ACCESS_KEY_ID",
"-e",
"AWS_SECRET_ACCESS_KEY",
"-e",
"AWS_REGION",
"-e",
"AWS_ENDPOINT_URL",
"rustfs/rustfs-mcp"
],
"env": {
"AWS_ACCESS_KEY_ID": "rustfs_access_key",
"AWS_SECRET_ACCESS_KEY": "rustfs_secret_key",
"AWS_REGION": "cn-east-1",
"AWS_ENDPOINT_URL": "rustfs_instance_url"
}
}
}
}
```
If success, MCP configure page will show the [available tools](#-available-tools).
## 🛠️ Available Tools
The MCP server exposes the following tools that AI assistants can use:
### `list_buckets`
List all S3 buckets accessible with the configured credentials.
**Parameters:** None
### `list_objects`
List objects in an S3 bucket with optional prefix filtering.
**Parameters:**
- `bucket_name` (string): Name of the S3 bucket
- `prefix` (string, optional): Prefix to filter objects
### `upload_file`
Upload a local file to S3 with automatic MIME type detection.
**Parameters:**
- `local_file_path` (string): Path to the local file
- `bucket_name` (string): Target S3 bucket
- `object_key` (string): S3 object key (destination path)
@@ -158,9 +219,11 @@ Upload a local file to S3 with automatic MIME type detection.
- `cache_control` (string, optional): Cache control header
### `get_object`
Retrieve an object from S3 with two operation modes: read content directly or download to a file.
**Parameters:**
- `bucket_name` (string): Source S3 bucket
- `object_key` (string): S3 object key
- `version_id` (string, optional): Version ID for versioned objects
+9 -9
View File
@@ -19,7 +19,7 @@ use crate::{
target::Target,
};
use futures::stream::{FuturesUnordered, StreamExt};
use rustfs_config::notify::{ENABLE_KEY, ENABLE_ON, NOTIFY_ROUTE_PREFIX};
use rustfs_config::notify::{ENABLE_KEY, NOTIFY_ROUTE_PREFIX};
use rustfs_config::{DEFAULT_DELIMITER, ENV_PREFIX};
use rustfs_ecstore::config::{Config, KVS};
use std::collections::{HashMap, HashSet};
@@ -111,10 +111,10 @@ impl TargetRegistry {
// 3.1. Instance discovery: Based on the '..._ENABLE_INSTANCEID' format
let enable_prefix = format!("{ENV_PREFIX}{NOTIFY_ROUTE_PREFIX}{target_type}_{ENABLE_KEY}_").to_uppercase();
for (key, value) in &all_env {
if value.eq_ignore_ascii_case(ENABLE_ON)
|| value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("1")
|| value.eq_ignore_ascii_case("yes")
if value.eq_ignore_ascii_case(rustfs_config::EnableState::One.as_str())
|| value.eq_ignore_ascii_case(rustfs_config::EnableState::On.as_str())
|| value.eq_ignore_ascii_case(rustfs_config::EnableState::True.as_str())
|| value.eq_ignore_ascii_case(rustfs_config::EnableState::Yes.as_str())
{
if let Some(id) = key.strip_prefix(&enable_prefix) {
if !id.is_empty() {
@@ -202,10 +202,10 @@ impl TargetRegistry {
let enabled = merged_config
.lookup(ENABLE_KEY)
.map(|v| {
v.eq_ignore_ascii_case(ENABLE_ON)
|| v.eq_ignore_ascii_case("true")
|| v.eq_ignore_ascii_case("1")
|| v.eq_ignore_ascii_case("yes")
v.eq_ignore_ascii_case(rustfs_config::EnableState::One.as_str())
|| v.eq_ignore_ascii_case(rustfs_config::EnableState::On.as_str())
|| v.eq_ignore_ascii_case(rustfs_config::EnableState::True.as_str())
|| v.eq_ignore_ascii_case(rustfs_config::EnableState::Yes.as_str())
})
.unwrap_or(false);
@@ -103,7 +103,7 @@ impl TableSource for TableSourceAdapter {
}
/// Called by [`InlineTableScan`]
fn get_logical_plan(&self) -> Option<Cow<LogicalPlan>> {
fn get_logical_plan(&self) -> Option<Cow<'_, LogicalPlan>> {
Some(Cow::Owned(self.plan.clone()))
}
}
-3
View File
@@ -31,14 +31,11 @@ bytes = { workspace = true }
http.workspace = true
time.workspace = true
hyper.workspace = true
serde.workspace = true
serde_urlencoded.workspace = true
rustfs-utils = { workspace = true, features = ["full"] }
s3s.workspace = true
[dev-dependencies]
tempfile = { workspace = true }
rand = { workspace = true }
[lints]
workspace = true
+13 -1
View File
@@ -21,7 +21,7 @@ use std::collections::HashMap;
use std::io::Error;
use std::path::Path;
use std::sync::Arc;
use std::{fs, io};
use std::{env, fs, io};
use tracing::{debug, warn};
/// Load public certificate from file.
@@ -194,6 +194,18 @@ pub fn create_multi_cert_resolver(
})
}
/// Checks if TLS key logging is enabled.
pub fn tls_key_log() -> bool {
env::var(rustfs_config::ENV_TLS_KEYLOG)
.map(|v| {
v.eq_ignore_ascii_case(rustfs_config::EnableState::One.as_str())
|| v.eq_ignore_ascii_case(rustfs_config::EnableState::On.as_str())
|| v.eq_ignore_ascii_case(rustfs_config::EnableState::True.as_str())
|| v.eq_ignore_ascii_case(rustfs_config::EnableState::Yes.as_str())
})
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
+1
View File
@@ -22,6 +22,7 @@ pub mod net;
#[cfg(feature = "net")]
pub use net::*;
#[cfg(all(feature = "net", feature = "io"))]
pub mod retry;
#[cfg(feature = "io")]
-2
View File
@@ -54,8 +54,6 @@ rustfs-s3select-query = { workspace = true }
atoi = { workspace = true }
atomic_enum = { workspace = true }
axum.workspace = true
axum-extra = { workspace = true }
axum-server = { workspace = true }
async-trait = { workspace = true }
bytes = { workspace = true }
chrono = { workspace = true }
+2 -1
View File
@@ -53,7 +53,6 @@ use s3s::stream::{ByteStream, DynByteStream};
use s3s::{Body, S3Error, S3Request, S3Response, S3Result, s3_error};
use s3s::{S3ErrorCode, StdError};
use serde::{Deserialize, Serialize};
use tracing::debug;
// use serde_json::to_vec;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
@@ -65,6 +64,7 @@ use tokio::sync::mpsc::{self};
use tokio::time::interval;
use tokio::{select, spawn};
use tokio_stream::wrappers::ReceiverStream;
use tracing::debug;
use tracing::{error, info, warn};
// use url::UrlQuery;
@@ -81,6 +81,7 @@ pub mod trace;
pub mod user;
use urlencoding::decode;
#[allow(dead_code)]
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "PascalCase", default)]
pub struct AccountInfo {
+2
View File
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
use crate::admin::router::Operation;
use crate::auth::{check_key_valid, get_session_token};
use http::{HeaderMap, StatusCode};
+1
View File
@@ -341,6 +341,7 @@ impl Operation for RemoveTier {
}
}
#[allow(dead_code)]
pub struct VerifyTier {}
#[async_trait::async_trait]
impl Operation for VerifyTier {
+2
View File
@@ -22,6 +22,7 @@ use tracing::warn;
use crate::admin::router::Operation;
#[allow(dead_code)]
fn extract_trace_options(uri: &Uri) -> S3Result<ServiceTraceOpts> {
let mut st_opts = ServiceTraceOpts::default();
st_opts
@@ -31,6 +32,7 @@ fn extract_trace_options(uri: &Uri) -> S3Result<ServiceTraceOpts> {
Ok(st_opts)
}
#[allow(dead_code)]
pub struct Trace {}
#[async_trait::async_trait]
+2 -4
View File
@@ -490,11 +490,9 @@ mod tests {
let result = check_claims_from_token("", &cred);
// This might fail due to global state dependencies, but should return error about global cred init
if result.is_ok() {
let claims = result.unwrap();
if let Ok(claims) = result {
assert!(claims.is_empty());
} else {
let error = result.unwrap_err();
} else if let Err(error) = result {
assert_eq!(error.code(), &S3ErrorCode::InternalError);
assert!(error.message().unwrap_or("").contains("action cred not init"));
}
+72 -14
View File
@@ -14,7 +14,6 @@
// Ensure the correct path for parse_license is imported
use crate::admin;
// use crate::admin::console::{CONSOLE_CONFIG, init_console_cfg};
use crate::auth::IAMAuth;
use crate::config;
use crate::server::hybrid::hybrid;
@@ -43,8 +42,6 @@ use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
use tokio_rustls::TlsAcceptor;
use tonic::{Request, Status, metadata::MetadataValue};
use tower::ServiceBuilder;
@@ -63,9 +60,6 @@ pub async fn start_http_server(
let server_port = server_addr.port();
let server_address = server_addr.to_string();
// The listening address and port are obtained from the parameters
// let listener = TcpListener::bind(server_address.clone()).await?;
// The listening address and port are obtained from the parameters
let listener = {
let mut server_addr = server_addr;
@@ -172,6 +166,7 @@ pub async fn start_http_server(
tokio::spawn(async move {
#[cfg(unix)]
let (mut sigterm_inner, mut sigint_inner) = {
use tokio::signal::unix::{SignalKind, signal};
// Unix platform specific code
let sigterm_inner = signal(SignalKind::terminate()).expect("Failed to create SIGTERM signal handler");
let sigint_inner = signal(SignalKind::interrupt()).expect("Failed to create SIGINT signal handler");
@@ -292,32 +287,55 @@ async fn setup_tls_acceptor(tls_path: &str) -> Result<Option<TlsAcceptor>> {
debug!("Found TLS directory, checking for certificates");
// 1. Try to load all certificates from the directory (multi-cert support)
// Make sure to use a modern encryption suite
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
if !cert_key_pairs.is_empty() {
debug!("Found {} certificates, creating multi-cert resolver", cert_key_pairs.len());
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
debug!("Found {} certificates, creating SNI-aware multi-cert resolver", cert_key_pairs.len());
// Create an SNI-enabled certificate resolver
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
// Configure the server to enable SNI support
let mut server_config = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?));
.with_cert_resolver(Arc::new(resolver));
// Configure ALPN protocol priority
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
// Log SNI requests
if rustfs_utils::tls_key_log() {
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
}
return Ok(Some(TlsAcceptor::from(Arc::new(server_config))));
}
}
// 2. Fallback to legacy single certificate mode
// 2. Revert to the traditional single-certificate mode
let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
if tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok() {
debug!("Found legacy single TLS certificate, starting with HTTPS");
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let certs = rustfs_utils::load_certs(&cert_path).map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
let key = rustfs_utils::load_private_key(&key_path).map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
let mut server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| rustfs_utils::certs_error(e.to_string()))?;
// Configure ALPN protocol priority
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
// Log SNI requests
if rustfs_utils::tls_key_log() {
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
}
return Ok(Some(TlsAcceptor::from(Arc::new(server_config))));
}
@@ -398,6 +416,10 @@ fn process_connection(
// Decide whether to handle HTTPS or HTTP connections based on the existence of TLS Acceptor
if let Some(acceptor) = tls_acceptor {
debug!("TLS handshake start");
let peer_addr = socket
.peer_addr()
.ok()
.map_or_else(|| "unknown".to_string(), |addr| addr.to_string());
match acceptor.accept(socket).await {
Ok(tls_socket) => {
debug!("TLS handshake successful");
@@ -408,8 +430,44 @@ fn process_connection(
}
}
Err(err) => {
error!(?err, "TLS handshake failed");
return; // Failed to end the task directly
// Detailed analysis of the reasons why the TLS handshake fails
let err_str = err.to_string();
let mut key_failure_type_str: &str = "UNKNOWN";
if err_str.contains("unexpected EOF") || err_str.contains("handshake eof") {
warn!(peer_addr = %peer_addr, "TLS handshake failed. If this client needs HTTP, it should connect to the HTTP port instead");
key_failure_type_str = "UNEXPECTED_EOF";
} else if err_str.contains("protocol version") {
error!(
peer_addr = %peer_addr,
"TLS handshake failed due to protocol version mismatch: {}", err
);
key_failure_type_str = "PROTOCOL_VERSION";
} else if err_str.contains("certificate") {
error!(
peer_addr = %peer_addr,
"TLS handshake failed due to certificate issues: {}", err
);
key_failure_type_str = "CERTIFICATE";
} else {
error!(
peer_addr = %peer_addr,
"TLS handshake failed: {}", err
);
}
info!(
counter.rustfs_tls_handshake_failures = 1_u64,
key_failure_type = key_failure_type_str,
"TLS handshake failure metric"
);
// Record detailed diagnostic information
debug!(
peer_addr = %peer_addr,
error_type = %std::any::type_name_of_val(&err),
error_details = %err,
"TLS handshake failure details"
);
return;
}
}
debug!("TLS handshake success");
+2 -2
View File
@@ -21,7 +21,7 @@ use crate::error::ApiError;
use crate::storage::access::ReqInfo;
use crate::storage::options::copy_dst_opts;
use crate::storage::options::copy_src_opts;
use crate::storage::options::{extract_metadata_from_mime, get_opts};
use crate::storage::options::{extract_metadata_from_mime_with_object_name, get_opts};
use bytes::Bytes;
use chrono::DateTime;
use chrono::Utc;
@@ -1412,7 +1412,7 @@ impl S3 for FS {
let mut metadata = metadata.unwrap_or_default();
extract_metadata_from_mime(&req.headers, &mut metadata);
extract_metadata_from_mime_with_object_name(&req.headers, &mut metadata, Some(&key));
if let Some(tags) = tagging {
metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags);
+118 -1
View File
@@ -186,6 +186,15 @@ pub fn extract_metadata(headers: &HeaderMap<HeaderValue>) -> HashMap<String, Str
/// Extracts metadata from headers and returns it as a HashMap.
pub fn extract_metadata_from_mime(headers: &HeaderMap<HeaderValue>, metadata: &mut HashMap<String, String>) {
extract_metadata_from_mime_with_object_name(headers, metadata, None);
}
/// Extracts metadata from headers and returns it as a HashMap with object name for MIME type detection.
pub fn extract_metadata_from_mime_with_object_name(
headers: &HeaderMap<HeaderValue>,
metadata: &mut HashMap<String, String>,
object_name: Option<&str>,
) {
for (k, v) in headers.iter() {
if let Some(key) = k.as_str().strip_prefix("x-amz-meta-") {
if key.is_empty() {
@@ -210,10 +219,42 @@ pub fn extract_metadata_from_mime(headers: &HeaderMap<HeaderValue>, metadata: &m
}
if !metadata.contains_key("content-type") {
metadata.insert("content-type".to_owned(), "binary/octet-stream".to_owned());
let default_content_type = if let Some(obj_name) = object_name {
detect_content_type_from_object_name(obj_name)
} else {
"binary/octet-stream".to_owned()
};
metadata.insert("content-type".to_owned(), default_content_type);
}
}
/// Detects content type from object name based on file extension.
pub(crate) fn detect_content_type_from_object_name(object_name: &str) -> String {
let lower_name = object_name.to_lowercase();
// Check for Parquet files specifically
if lower_name.ends_with(".parquet") {
return "application/vnd.apache.parquet".to_owned();
}
// Special handling for other data formats that mime_guess doesn't know
if lower_name.ends_with(".avro") {
return "application/avro".to_owned();
}
if lower_name.ends_with(".orc") {
return "application/orc".to_owned();
}
if lower_name.ends_with(".feather") {
return "application/feather".to_owned();
}
if lower_name.ends_with(".arrow") {
return "application/arrow".to_owned();
}
// Use mime_guess for standard file types
mime_guess::from_path(object_name).first_or_octet_stream().to_string()
}
/// List of supported headers.
static SUPPORTED_HEADERS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
vec![
@@ -646,4 +687,80 @@ mod tests {
assert_eq!(metadata.get("cache-control"), Some(&"public".to_string()));
assert!(!metadata.contains_key("authorization"));
}
#[test]
fn test_extract_metadata_from_mime_with_parquet_object_name() {
let headers = HeaderMap::new();
let mut metadata = HashMap::new();
extract_metadata_from_mime_with_object_name(&headers, &mut metadata, Some("data/test.parquet"));
assert_eq!(metadata.get("content-type"), Some(&"application/vnd.apache.parquet".to_string()));
}
#[test]
fn test_extract_metadata_from_mime_with_various_data_formats() {
let test_cases = vec![
("data.parquet", "application/vnd.apache.parquet"),
("data.PARQUET", "application/vnd.apache.parquet"), // 测试大小写不敏感
("file.avro", "application/avro"),
("file.orc", "application/orc"),
("file.feather", "application/feather"),
("file.arrow", "application/arrow"),
("file.json", "application/json"),
("file.csv", "text/csv"),
("file.txt", "text/plain"),
("file.unknownext", "application/octet-stream"), // 使用真正未知的扩展名
];
for (filename, expected_content_type) in test_cases {
let headers = HeaderMap::new();
let mut metadata = HashMap::new();
extract_metadata_from_mime_with_object_name(&headers, &mut metadata, Some(filename));
assert_eq!(
metadata.get("content-type"),
Some(&expected_content_type.to_string()),
"Failed for filename: {}",
filename
);
}
}
#[test]
fn test_extract_metadata_from_mime_with_existing_content_type() {
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("custom/type"));
let mut metadata = HashMap::new();
extract_metadata_from_mime_with_object_name(&headers, &mut metadata, Some("test.parquet"));
// 应该保留现有的 content-type,不被覆盖
assert_eq!(metadata.get("content-type"), Some(&"custom/type".to_string()));
}
#[test]
fn test_detect_content_type_from_object_name() {
// 测试 Parquet 文件(我们的自定义处理)
assert_eq!(detect_content_type_from_object_name("test.parquet"), "application/vnd.apache.parquet");
assert_eq!(detect_content_type_from_object_name("TEST.PARQUET"), "application/vnd.apache.parquet");
// 测试其他自定义数据格式
assert_eq!(detect_content_type_from_object_name("data.avro"), "application/avro");
assert_eq!(detect_content_type_from_object_name("data.orc"), "application/orc");
assert_eq!(detect_content_type_from_object_name("data.feather"), "application/feather");
assert_eq!(detect_content_type_from_object_name("data.arrow"), "application/arrow");
// 测试标准格式(mime_guess 处理)
assert_eq!(detect_content_type_from_object_name("data.json"), "application/json");
assert_eq!(detect_content_type_from_object_name("data.csv"), "text/csv");
assert_eq!(detect_content_type_from_object_name("data.txt"), "text/plain");
// 测试真正未知的格式(使用一个 mime_guess 不认识的扩展名)
assert_eq!(detect_content_type_from_object_name("unknown.unknownext"), "application/octet-stream");
// 测试没有扩展名的文件
assert_eq!(detect_content_type_from_object_name("noextension"), "application/octet-stream");
}
}