mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-01 19:12:14 +00:00
cc2442ccb7c623710c9384cfbe5212bbdc710226
403 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d8ad5caee | ci: drop cla.yml to least privilege (#5548) | ||
|
|
2b31bda6d1 |
ci: clear the checkout token in every job that does not push (#5547)
actions/checkout writes its token into .git/config as an http extraheader, where it stays for the rest of the job. That matters more here than usual: pull_request jobs run on self-hosted runners and execute the PR's own build.rs, proc-macros and tests, any of which can read that file. Test and Lint holds actions: write on top of that, so its token can cancel runs and delete the Actions caches the whole pipeline now depends on — it was given persist-credentials: false when that permission was added, and this extends the same treatment to the other 45 checkouts. Two are exempt because the token IS the credential the job needs. helm-package's publish job pushes to rustfs/helm with it; clearing it would break chart publishing. nix-flake-update is exempt pending verification: it passes FLAKE_UPDATE_TOKEN to create-pull-request directly rather than reusing .git/config, so it very likely does not need persistence, but that is unproven and a broken weekly bot is not worth the guess. Both carry a persist-credentials-exempt comment saying which. scripts/security/check_persist_credentials.sh requires every checkout to either clear its credentials or carry that comment, so the decision stays visible in review rather than being an omission nobody notices. Refs: rustfs/backlog#1598, rustfs/backlog#1602 |
||
|
|
48c2fcb62b |
ci: add an opt-in cargo --timings run to decide the sccache question (#5545)
sccache can only cache compilation units whose --emit includes link. That covers workspace rlibs and nothing else: clippy is metadata-only, and the ~100 test binaries, the rustfs bin and every build script invoke the system linker. So the headline claim that started this — s3select-query costing 17m13s inside a 19m32s nextest build — does not by itself justify sccache, because cargo prints "Compiling" when a crate starts and never when it finishes: that number is the whole compilation tail, not one crate. Adding --timings behind a workflow_dispatch input on Cache Warm answers it with data instead. Read two shares off the report: workspace lib codegen against the whole build, and s3select-query's own rlib. The plan in rustfs/backlog#1601 adopts sccache only above 50% and 25%; if linking dominates instead, the answer is mold/lld plus split-debuginfo, which is precisely the region sccache cannot reach. Off by default: it doubles the ci-dev build, and this only needs running once per question. Nothing about the normal warm path changes. Refs: rustfs/backlog#1598, rustfs/backlog#1601 |
||
|
|
428fde069d |
ci: drop the docker layer cache and stop installing unused tooling (#5544)
Two cleanups, neither changing what is built or published. docker.yml loses its type=gha layer cache. The image build compiles nothing — it downloads a release zip and runs apk/apt — so the cache could only save the minute or two those take, against a real correctness problem: with RELEASE=latest the binary URL is resolved by curl inside a RUN layer, and the layer key does not include what it resolved to. A rebuild at the same RELEASE value (a dispatch with version=latest, or a re-run of the same version) would hit the old layer and ship the previous release's binary. mode=max also drew on the same repo-wide 10GB Actions cache quota the Rust lanes are contending for. Only RELEASE is passed as a build-arg now; it is the sole one the Dockerfiles declare besides TARGETARCH. BUILDTIME, VERSION, BUILD_TYPE, REVISION and CHANNEL were read by no stage, and BUILDTIME's $(date ...) was a literal string in the YAML block rather than a substitution. The DOCKER_CHANNEL computation that fed CHANNEL evaluated to "release" down every branch and is gone with it. BUILD_DATE and VCS_REF stay unset even though the Dockerfiles declare them: supplying them would change the published image labels. The setup composite stops installing tools its callers do not use. protobuf- compiler comes out of the apt list entirely — setup-protoc installs 34.1 into the tool cache and prepends it to PATH, so the apt build was shadowed on every run and never used; the same duplicate install is removed from the io_uring lane. musl-tools, zip and unzip move behind install-build-packaging-tools, off for the CI and coverage lanes and left on for build.yml, whose native musl leg needs musl-gcc and whose packaging steps need zip. cargo-nextest and the rustfmt/clippy components move behind install-test-tools, off only for build.yml, which runs no tests and no lints; coverage and the nightly replication lane keep them because both invoke nextest. The action's github-token input is deleted along with its 20 call sites. Its runs block never referenced it — setup-protoc uses github.token directly — so it was 20 places handing a token to something that ignored it. Refs: rustfs/backlog#1598, rustfs/backlog#1600, rustfs/backlog#1603 |
||
|
|
3f4f31129e |
ci: narrow the io_uring lane and make the sampler attributable (#5540)
Two independent changes to the Test and Lint area. The io_uring lane compiled 7 integration binaries to run none of their tests. Job log 91309868055 shows the lib target reporting "18 passed; 3453 filtered out" while every binary under crates/ecstore/tests/ reported "running 0 tests". Adding --lib drops them from the build without changing the selected set. The `uring_` filter itself must not be touched. libtest matches on substring, so it also selects names containing `during_` — 6 of the 18 selected tests are such incidental matches, and narrowing the filter to `io_uring` would silently drop them. scripts/check_uring_lane_lib_only.sh asserts the precondition --lib depends on: no test function whose name contains `uring_` may live under crates/ecstore/tests/. A count floor would not do, because the dangerous case — someone adding a matching test there — leaves the lib count unchanged and CI green. The resource sampler was measuring the wrong machine. The runners are ARC pods, so /proc/loadavg, /proc/pressure/*, `free` and `df` are node-level and include every other runner pod on the same Kubernetes node: one sample reported loadavg 6.67 with 832 threads node-wide while `ps` inside the pod showed about 10 processes. Judging a CARGO_BUILD_JOBS change on those numbers cannot work. The sampler moves to scripts/ci/resource_sampler.sh and now records /sys/fs/cgroup cpu.max, memory.max, memory.peak and the cpu/io/memory pressure files, which are this pod's own. The node-level readings stay — co-tenancy is a real cause of stalls, and a 9m57s plain `git checkout` was traced to it — but are labelled NODE-LEVEL so nobody reads them as this job's load. Phase markers are written on start, and clippy is now sampled too: it is the natural control arm for a CARGO_BUILD_JOBS experiment, since --all-targets is check-only for workspace members and never links the ~100 test binaries the limit throttles. No numbers are changed in this commit. CARGO_BUILD_JOBS stays at 2 until there is attributable data to change it on. Refs: rustfs/backlog#1598, rustfs/backlog#1601 |
||
|
|
6c99d4fe22 |
ci: stop the weekly flake.lock bot from running the whole pipeline (#5539)
nix-flake-update opens a PR every Sunday that changes flake.lock and nothing else. flake.lock is consumed only by Nix packaging — cargo never reads it — yet it was in no paths filter, so each of those PRs ran the full Continuous Integration pipeline and the merge then ran Build and Release too. Added to all four lists that have to agree: ci.yml's push and pull_request paths-ignore, build.yml's push paths-ignore, and ci-docs-only.yml's paths. The cron stays. flake.lock still has consumers — anyone running `nix build` or `nix develop` — so stopping the updates would remove the workflow's output, not just its CI cost. Those four lists drifting is a silent failure, so scripts/check_ci_paths_sync.sh now asserts the pair that matters: ci.yml's pull_request paths-ignore must equal ci-docs-only.yml's paths. An entry present only in the first means a PR touching those files triggers neither workflow, nobody reports "Test and Lint" or "Quick Checks", and the PR waits on a required check forever. It also asserts both companion job names still exist, since renaming one produces the same hang. The push list is not compared: no required check is reported for push events. Also in this cleanup: - nix-flake-update's GITHUB_TOKEN drops to contents: read. The branch push and the pull request are both made by update-flake-lock with the FLAKE_UPDATE_TOKEN PAT, so the write scopes were an unused repo-write credential on an unattended weekly job. - build.yml's build_docker dispatch input is documented as advisory. docker.yml triggers on workflow_run and requires the triggering event to be a tag push, so a manual dispatch never reaches it whatever this input says. - The nine disabled workflow files get a banner saying so. Their disabled_manually state lives in GitHub's UI and is invisible when reading the file, which has already misled one audit into treating dead workflows as live. Refs: rustfs/backlog#1598, rustfs/backlog#1603 |
||
|
|
e2b2bdcc34 |
ci: bound every job's runtime and stop pasting inputs into shell (#5537)
Three hardening changes with no effect on what any workflow produces. Declare timeout-minutes on the 25 jobs that lacked it. GitHub's default is 360 minutes, and this repository has a history of runners stalling intermittently (#5394) plus a measured 9m57s plain `git checkout` under node-level I/O contention, so one wedged job could hold a runner for six hours out of a pool of roughly 15-21. Budgets follow what the jobs actually do: 10 minutes for echo-only and guard-script jobs, 30 for anything calling the GitHub API, uploading release assets or pushing over the network. scripts/security/check_job_timeouts.sh keeps it that way, checking only jobs that declare runs-on so reusable-workflow callers are not flagged. Pass workflow inputs and workflow_run fields through env instead of `${{ }}` interpolation in run blocks. A git ref name may contain `$(...)` — any string without a space is a legal tag — and interpolation pastes it into the script where bash evaluates it. The worst instance was helm-package's final commit message: it is built from the triggering tag name inside the job that holds the cross-repository push token with rustfs/helm already checked out. Also converted in build.yml, docker.yml and performance-ab.yml; the last is currently disabled, but a disabled workflow can be re-enabled. Not touched: helm-package's `contains(head_branch, '.')` tag test, since GitHub expressions have no regex and this repository's tags carry no `v` prefix, so rewriting the condition would change which builds publish a chart. Give audit.yml a scheduled-failure alert and run it daily. A scheduled cargo-deny failure usually means the dependency tree just matched a newly published RustSec advisory — the most important signal this workflow produces, and until now it was visible only to whoever happened to open the Actions tab. coverage.yml and e2e-replication-nightly.yml already use this ci-8 mechanism. The cron moves from weekly to daily so a new advisory against an unchanged tree surfaces within a day instead of seven; the check list is untouched, since splitting it into a light daily run and a weekly full run would create runs where sources, bans and licenses go unverified. Refs: rustfs/backlog#1598, rustfs/backlog#1602 |
||
|
|
1524ed891f |
ci: write the Rust caches from a workflow that is not cancelled (#5536)
#5532 consolidated ci.yml's nine cache sites into four keys with one writer each, but the writers never run to completion: ci.yml cancels superseded runs on main, and merges land far faster than its 70-minute pipeline. Measured over 15 consecutive main pushes — 12 cancelled, 2 failed, 0 succeeded. A cancelled run never reaches rust-cache's post step (cache-on-failure does not cover cancellation), so nothing was being saved and every PR paid a cold restore: 11.8-20.9 minutes of "Setup Rust environment" against 0.7-3.4 warm. Move cache writing into its own workflow whose concurrency group does not cancel in progress, and make every lane in ci.yml a pure reader. ci.yml keeps cancelling superseded runs, which is correct — nobody needs test results for a commit that is already three merges behind — while the caches still get written. Not simply disabling cancel-in-progress for main pushes in ci.yml: that would run several full pipelines concurrently on a 15-21 runner self-hosted pool that is already the bottleneck, which is the opposite of the goal. GitHub keeps at most one running plus one pending run per concurrency group, so the new workflow collapses a burst of merges into "current finishes, newest queued follows" and occupies one runner at a time. The warm builds are supersets of what the reading lanes compile, because a reader restores only what the writer saved: --all-targets for the test binaries nextest builds (including e2e_test, which test-and-lint excludes), plus the e2e-test-hooks and rio-v2 feature combinations, whose different feature resolution yields a different -Cmetadata. The ci-dev build carries the same CARGO_BUILD_JOBS limit ci.yml puts on nextest, since it links the same ~100 test binaries and three concurrent links can wedge Cargo (#5394). ci-uring is warmed on ubuntu-latest to match its reader: rust-cache's key covers runner.os and arch but not the runner label or image. Refs: rustfs/backlog#1598, rustfs/backlog#1600 |
||
|
|
7354a5663d |
ci: fit the Rust caches back inside the 10GB quota (#5532)
The repository has 18 rust-cache families of 1.2-3.1GB each against GitHub's fixed 10GB per-repo quota. Measured usage sat at 9.72GB, 9.96GB and 11.63GB on three samples, so LRU eviction is continuous and the main lanes lose: ci-test and ci-e2e were repeatedly absent from the surviving entries. That is what makes "Setup Rust environment" bimodal — 0.7-3.4 minutes warm against 11.8-20.9 minutes cold. Four changes, all cache-only. No job builds or tests anything different. Collapse ci.yml's nine cache sites into four keys, each with exactly one writer: ci-dev (test-and-lint writes; ILM, debug-binary, e2e-tests and e2e-full read), ci-feat-rio (rio-v2 lint writes, its debug-binary reads), ci-feat-proto (the swift leg writes for both protocol legs), and ci-uring, which stays alone. e2e-full is easy to miss here: it already shared ci-e2e with e2e-tests and both saved, so without an explicit 'false' it would have become a second unnamed writer of ci-dev. rio/swift/sftp are deliberately NOT merged — measured at 2365/2307/1200MB they are not near-identical, and none is a superset of the others. Give each writer a superset warm-up on main. A writer's own steps are not automatically a superset of its readers': clippy emits metadata only, the nextest pass excludes e2e_test, and no lint lane enables e2e-test-hooks, whose different feature resolution yields a different -Cmetadata. Without these builds the readers would restore a cache missing precisely what they need. Guarded to main, so the PR critical path is unaffected. Stop writing tag-scoped caches in build.yml. A cache saved on refs/tags/X can only be restored by a re-run of that same tag, so each release cycle wrote up to 12 unreadable 1-2GB entries that evicted the hot lanes. Tag builds still restore the main-scoped cache. The one real cost is that re-running a failed leg of the same tag now falls back to main's cache. Flip the composite action's cache-save-if default to 'false' and make the input mandatory in practice. audit.yml was relying on the old "true" default: every PR touching Cargo.toml or Cargo.lock saved a second, PR-scoped copy (~843MB measured, job 91048468127) that pushed main-scoped lanes out of the quota. The fail-safe direction is a cold cache, not a stolen quota slice. scripts/security/check_cache_save_if.sh now asserts every call site states it, wired into audit.yml next to the existing pin check. While there, cargo-deny stops pulling the full setup composite. It compiles nothing, so apt, protoc, flatc and nextest were pure overhead — but it does run cargo metadata, and Cargo.toml pins datafusion and s3s as git dependencies that must be materialised into ~/.cargo/git, so the cache itself stays. Refs: rustfs/backlog#1598, rustfs/backlog#1600 |
||
|
|
790bdc0e63 |
ci: gate the seven expensive jobs on Quick Checks (#5529)
* ci: stop running expensive jobs that cannot inform the result Three independent fixes that all avoid burning self-hosted runners on work whose outcome is already determined. None of them changes what is tested. - ci-docs-only: add a "Quick Checks" companion job. It is a prerequisite for gating ci.yml's expensive jobs behind quick-checks (rustfs/backlog#1599): once "Quick Checks" is a required check, a docs-only PR would otherwise wait on it forever. The steps are a byte-identical copy of ci.yml's quick-checks rather than an `echo`, so that on a mixed PR the two same-named check runs execute the same commands against the same merge ref and cannot disagree — GitHub has no written contract for how it picks between same-named required check runs, and the real job only takes 45-51s, leaving no timing margin to rely on. - ci: guard uring-integration with the same `closed` check every other job already has. The pull_request trigger includes `closed` only so the concurrency group cancels in-flight runs; this job had no guard and no `needs`, so every closed or merged PR ran the full io_uring suite (4m17s, 7m19s and 7m31s on runs 30678272341, 30678117601 and 30662728539). - ci: gate s3-lifecycle-behavior-tests on e2e-tests, matching s3-implemented-tests. Both lanes only download the prebuilt debug binary, and s3-implemented-tests already finishes later, so a green PR's wall clock is unchanged; a red one stops holding a sm-standard-4 for up to 30 minutes. Refs: rustfs/backlog#1598, rustfs/backlog#1599 * ci: gate the seven expensive jobs on Quick Checks Every expensive job started in parallel with quick-checks, so a formatting or architecture-guard failure still paid for the full pipeline. On run 30673292690 Quick Checks failed after 0.8 minutes and the run went on to burn 424.5 runner-minutes — 99.8% of it after the gate had already failed. The self-hosted pool is 15-21 ARC runners and one full PR run needs about seven sm-standard-4 concurrently, so those minutes come straight out of other PRs' queue time (six runs measured 72-488 minutes queued). quick-checks itself is compile-free and takes 45-51s, so a passing PR pays about a minute of extra critical path. REQUIRES the branch ruleset to list "Quick Checks" as a required check BEFORE this merges. Adding `needs` gives these jobs a `skipped` conclusion for the first time, and GitHub treats a skipped required check as satisfied — with required_approving_review_count=0, a failing quick-checks would otherwise let a broken PR merge. Ordering is tracked in rustfs/backlog#1599. Refs: rustfs/backlog#1598, rustfs/backlog#1599 * ci: stop a PR run once Test and Lint has failed (#5530) On run 30674613104 the e2e, ILM and sftp lanes had all failed while Test and Lint and the rio-v2 variant kept running past 70 minutes. The run's verdict was settled; the remaining lanes were spending sm-standard-4 time on a result nobody could act on, and with one full PR run needing about seven of those runners, that time comes out of other PRs' queue time. Two mechanisms, both scoped to pull_request so main pushes, the merge queue and the weekly schedule keep the full failure signal: - test-and-lint-protocols: fail-fast on PRs, so one failing protocol leg stops its sibling. This is the only part that also covers fork PRs, since it needs no token. - test-and-lint: on failure, cancel the run through the REST API. Only test-and-lint may cancel. The lanes that are not required checks (protocols, ILM, e2e, s3-tests) must never hold that power: a flake in one of them would turn the required "Test and Lint" into `cancelled`, which blocks the merge. A maintainer can merge today with sftp red, and that has to stay true. The cancel step uses curl, not `gh`: every existing `gh` call in this repo runs on ubuntu-latest, and the sm-standard-* images are custom and trimmed, so `gh` is not known to exist there. Fork PRs are excluded by an explicit condition rather than left to fail, since their GITHUB_TOKEN is forced read-only and job-level permissions cannot raise it. Job-level permissions must list contents: read alongside actions: write — job-level permissions replace the workflow block instead of merging with it, and dropping contents would break this job's checkout and the repo-token the setup action passes to setup-protoc. Because that token can now cancel runs and delete Actions caches, the checkout also sets persist-credentials: false so a PR's own build.rs or proc-macro cannot read it back out of .git/config. Refs: rustfs/backlog#1598, rustfs/backlog#1599 * ci: correct the companion-workflow comments Addresses review feedback on #5528, which merged before these fixes were pushed. The ci-docs-only header claimed the ruleset already requires "Quick Checks". It does not — that ruleset change is a separate step, and this file's whole purpose is to land first so that change does not strand docs-only PRs. Say what is true today. Also move the byte-identical requirement onto ci.yml's quick-checks job, which is the more likely edit site, instead of pointing at a comment that was not there. |
||
|
|
707d062174 |
ci: stop running expensive jobs that cannot inform the result (#5528)
Three independent fixes that all avoid burning self-hosted runners on work whose outcome is already determined. None of them changes what is tested. - ci-docs-only: add a "Quick Checks" companion job. It is a prerequisite for gating ci.yml's expensive jobs behind quick-checks (rustfs/backlog#1599): once "Quick Checks" is a required check, a docs-only PR would otherwise wait on it forever. The steps are a byte-identical copy of ci.yml's quick-checks rather than an `echo`, so that on a mixed PR the two same-named check runs execute the same commands against the same merge ref and cannot disagree — GitHub has no written contract for how it picks between same-named required check runs, and the real job only takes 45-51s, leaving no timing margin to rely on. - ci: guard uring-integration with the same `closed` check every other job already has. The pull_request trigger includes `closed` only so the concurrency group cancels in-flight runs; this job had no guard and no `needs`, so every closed or merged PR ran the full io_uring suite (4m17s, 7m19s and 7m31s on runs 30678272341, 30678117601 and 30662728539). - ci: gate s3-lifecycle-behavior-tests on e2e-tests, matching s3-implemented-tests. Both lanes only download the prebuilt debug binary, and s3-implemented-tests already finishes later, so a green PR's wall clock is unchanged; a red one stops holding a sm-standard-4 for up to 30 minutes. Refs: rustfs/backlog#1598, rustfs/backlog#1599 |
||
|
|
145d38133b |
fix(ci): generate release notes with the correct baseline (#5467)
fix(ci): generate release notes with correct baseline |
||
|
|
b83c9c4663 | ci(release): isolate preview releases from latest channels (#5462) | ||
|
|
abc5f2e818 |
fix(scanner): persist portable usage cache keys (#5444)
* fix(scanner): persist portable usage cache keys * fix(scanner): validate complete bucket cache graphs Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> |
||
|
|
3c0d315a9c |
ci: sample runner state during the nextest step (#5440)
The Test and Lint lane intermittently stalls until the inner 75m timeout kills the cargo process group (issue #5394). The evidence collected since #5402 shows the stall can begin in the build phase before any test runs (run 30449339653: last output at minute 4 of the nextest step, then 71 silent minutes until SIGTERM), but the post-mortem pgrep always reports nothing because GNU timeout has already terminated the whole process group by the time it runs. Add a background sampler to the nextest step that appends system and process snapshots (loadavg, PSI, memory, disk, top-RSS processes, cargo/rustc/linker/build-script processes, D-state processes) to the existing test-and-lint artifact every 60 seconds, and record kernel OOM/kill events in the post-mortem diagnostics. The last samples before a timeout identify the wedged process or the resource pressure that caused the stall. This only instruments the failure mode where the runner survives; jobs whose runner disappears entirely still upload no artifacts and need runner-pool-side logs. Refs #5394 |
||
|
|
cb62079ba6 |
test(ilm): make active cancellation deterministic (#5403)
* ci: preserve timeout evidence for workspace tests * test(ilm): make active cancellation deterministic * ci: allow cold doctest compilation to finish --------- Co-authored-by: houseme <housemecn@gmail.com> |
||
|
|
a1a65ad65d | fix(action): change quay.io image repository name (#5417) | ||
|
|
358af6a8de |
ci: preserve timeout evidence for workspace tests (#5402)
* ci: preserve timeout evidence for workspace tests * ci: allow cold doctest compilation to finish * ci: allow cold nextest compilation to finish * ci: allow cold nextest compilation to finish --------- Co-authored-by: houseme <housemecn@gmail.com> |
||
|
|
775279b6fd |
fix(notify): defer disabled bootstrap storage refresh (#5373)
Co-authored-by: heihutu <heihutu@gmail.com> |
||
|
|
3ed682be42 |
feat: offline log fault-analysis system (rustfs diagnose) (#4876)
* feat(log-analyzer): add crate skeleton and unified event model
Implements LA-1 (rustfs/backlog#1282) of the log fault-analysis system
(rustfs/backlog#1281): new synchronous rustfs-log-analyzer crate with the
LogEvent/LogLevel/SourceRef/EventKind/ParseStats model shared by all
later stages. No tokio, no rustfs-* internal deps by design.
Note: thiserror listed in the issue is deferred until a stage actually
defines error types (LA-3/LA-4) to avoid an unused dependency.
* feat(log-analyzer): add line parsing layer
Implements LA-2 (rustfs/backlog#1283): four parse channels tried in order
per line — native tracing JSON, container-prefix stripping (K8s CRI /
docker compose / journald) with JSON retry, multi-line Rust panic block
folding (both pre- and post-1.65 formats, stderr has no JSON logger), and
a plain-text fallback that never fails. Parse accounting feeds the report
parse-ratio disclosure.
* feat(log-analyzer): add ingest layer for directories and archives
Implements LA-3 (rustfs/backlog#1284): expands customer inputs (files,
directories, zip/tar/tar.gz/.zst/.gz, stdin-like readers) into parsed
events. Magic-byte detection with extension fallback, recursive archive
walking with depth/entry/byte/memory caps (every capped input disclosed
in IngestReport.skipped), first-level directory names become node labels,
and nothing is ever extracted to disk so hostile entry paths are inert.
Adds tar 0.4 to workspace deps (sync; the async astral-tokio-tar used by
rustfs-zip does not fit this crate's no-tokio contract).
* feat(log-analyzer): add rule model, matching engine, and finding aggregation
Implements LA-4 (rustfs/backlog#1285): owned serde-round-trippable Rule/
Matcher/Severity types (external JSON rules deserialize into the same
types later), fail-fast RuleSet validation that reports every problem at
once, a linear-scan engine with regexes compiled once, and an
order-independent FindingsCollector (commutative aggregates only; the
test asserts byte-identical output across shuffled input orders).
* feat(log-analyzer): add built-in seed rule library (68 rules, 12 categories)
Implements LA-5 (rustfs/backlog#1286): the 2026-07 repository-wide
failure-log survey distilled into rules across disk health, erasure/
bitrot, quorum, network/RPC, distributed locks, heal, scanner, IAM,
startup/config/TLS, capacity, decommission/rebalance, and process panics.
Every anchor was verified verbatim against the source tree (94/94 hits,
zero corrections needed). Quorum rules pre-fill implies_root_cause for
the Phase-2 folding (rustfs/backlog#1290); client-side rules carry burst
thresholds (min_count) so isolated client mistakes don't clutter reports.
Tests: one realistic positive sample per rule (table-driven), exact-set
smoke samples including the intentional internode/client signature
double-hit, and negative cases.
* feat(log-analyzer): add analysis orchestration, report rendering, and redaction
Implements LA-6 (rustfs/backlog#1287): a single-pass Analyzer that does
rule matching, minute-bucket timelines (gap-filled, merged to <=60
buckets), unmatched WARN/ERROR template clustering (placeholders for
numbers/uuids/paths/addresses/quotes, 5000-template cap disclosed as
<overflow>), mixed-UTC-offset detection, and below-min_count demotion to
a low-confidence section. Renderers: pipe-friendly terminal text, stable
JSON (schema_version=1), and ticket-pasteable Markdown. --redact hashes
customer identifiers (stable h:sha256[..8]) in samples/evidence/messages
while keeping rule ids, targets, and panic locations intact.
* feat(rustfs): add 'rustfs diagnose' subcommand for offline log fault analysis
Implements LA-7 (rustfs/backlog#1288): wires rustfs-log-analyzer into the
main binary as a diagnose subcommand that short-circuits before
observability/storage init (same pattern as 'info' / 'tls inspect') so
the report on stdout is never wrapped by the JSON logger.
rustfs diagnose <paths>... [--format text|json|md] [--since 24h]
[--until ...] [--min-level warn] [--redact] [--top N] [--samples N]
Accepts files, directories, archives (.zip/.tar/.tar.gz/.zst/.gz) and '-'
for stdin. Exit codes: 0 = diagnosis completed (findings never fail the
process), 2 = bad arguments / no readable input.
diagnose_e2e covers the six MVP acceptance scenarios from
rustfs/backlog#1281 (directory+zst archive, multi-node zip attribution,
CRI-prefixed kubectl logs, panic folding, stable JSON schema, CLI parsing
incl. the legacy 'rustfs <volume>' preprocessor regression); the
full-binary smoke test is #[ignore]d (run with -- --ignored).
Usage doc: docs/operations/log-diagnose.md.
* ci(log-analyzer): guard rule anchors against log-message drift
Implements LA-8 (rustfs/backlog#1289): every seed-rule anchor must exist
verbatim in the rustfs source tree, so changing a log message without
updating its rule fails the gate instead of silently killing the rule.
- la-dump-anchors bin emits 'rule_id<TAB>anchor' TSV;
- scripts/check_log_analyzer_rules.sh greps each anchor (fixed-string,
*.rs only, excluding crates/log-analyzer itself to avoid self-matches);
- RuleSet::new now rejects anchors that are blank, contain tab/newline,
or are shorter than 8 bytes (no discriminating power); the '[FATAL]'
anchor gained its trailing space to meet the floor while still matching
the emit_fatal_stderr format string;
- wired as log-analyzer-rules-check into the pre-pr gate (it compiles the
crate, so it stays out of the fast pre-commit set).
Negative self-test: breaking an anchor makes the script exit 1 naming the
rule ('MISSING anchor for rule inconsistent-drive: zzz-not-exist-anchor').
* refactor(log-analyzer): use root-relative provenance for directory inputs
Binary smoke run showed report samples citing full absolute paths, which
drowns the useful part. Directory inputs now label sources as
"<root-name>/<relative-path>" (e.g. "smoke-logs/node1/rustfs.log");
archives and single files keep their existing provenance.
* chore(log-analyzer): reword comment to satisfy the typos gate
* fix(log-analyzer): declare chrono serde feature locally after workspace feature localization
* fix(log-analyzer): bound line reads so a newline-less input cannot bypass the byte cap
read_until grew the line buffer with the entire remaining stream before the
max_total_bytes check ran, so a single multi-GB line (decompression bomb or
corrupt file) could allocate unboundedly. Replace it with a capped reader that
enforces the remaining global budget chunk-by-chunk and adds a per-line cap
(IngestOptions::max_line_bytes, default 1 MiB); over-cap tails are discarded
but still charged, and truncation is disclosed once per file as the new
line_too_long skip reason. Flagged by Codex review on #4876.
* fix(log-analyzer): redact field-shaped identifiers inside message text and widen the hash to 64 bits
--redact only hashed IPv4 literals in unstructured message text, so
bucket/object/access-key values embedded in messages (access_key=AK123,
'bucket: media, object: private/a.bin') leaked into reports documented as
safe to forward. Apply the SENSITIVE_FIELDS list to key=value / key: value
shapes in message text with the same hash as structured fields, and extend
the hash from 8 to 16 hex chars so cross-identifier collisions stay
negligible. Flagged by Codex and Copilot review on #4876.
* fix(log-analyzer): strip collector prefixes before panic-block absorption
An open panic block tested continuation lines against absorbs() before their
CRI/compose/journald prefix was stripped, so containerized panics stored the
prefix in the payload and split into a truncated panic plus text noise as soon
as the note/backtrace lines arrived. Stripping now happens once at the top of
feed() and every channel judges the payload. Flagged by Codex review on #4876.
* fix(log-analyzer): remove two input-order dependencies in representative selection
The unmatched-cluster target stayed pinned to the first-seen event while the
representative sample could be replaced, so sample and target could come from
different events and vary with input order; the pair now updates together by
lexicographic (sample, target) min. Sample selection tie-broke on line number
alone, which is only unique within one file; the key now includes the source
file. Flagged by Copilot review on #4876.
* fix(rustfs): reject negative relative times in diagnose --since/--until
parse_time_arg accepted "-24h" and produced a future timestamp, contradicting
the documented 'counted back from now' semantics. The amount now parses as
unsigned, so a leading '-' fails with the usual invalid-time error. Flagged by
Copilot review on #4876.
* feat(log-analyzer): Phase 2 — causal folding, timeline anomalies, external rules (LA-9) (#4942)
* feat(log-analyzer): collapse cascade symptoms under their root-cause finding
Phase-2 sub-item A (rustfs/backlog#1290): a finding whose rule declares
implies_root_cause edges folds under a qualifying root — root.first_seen <=
symptom.first_seen + 5min and root.last_seen >= symptom.first_seen - 30min,
existence-based when either side has no timestamps (pure stderr panics).
Findings gain collapsed_into/caused; text/markdown render the root block with
an indented cascade line and stop listing collapsed symptoms flat, while JSON
keeps every finding. Roots are promoted to the most severe position among
their block so the report top still answers 'the most likely cause'.
* feat(log-analyzer): detect timeline/clock anomalies (schema v2)
Phase-2 sub-item B (rustfs/backlog#1290): three deterministic hints rendered
between the summary and findings — mixed UTC offsets (with a clock-skew note
when signature-mismatch findings coexist), per-node time ranges that do not
overlap at all (both nodes >100 timestamped events), and log gaps of at least
max(15min, 3x bucket width) after >=3 consecutive active minutes, upgraded to
restart evidence when a startup-class finding begins within 5min after the
gap. JSON gains timeline_anomalies and schema_version bumps to 2.
* feat(diagnose): load external rules with --rules <file.json>
Phase-2 sub-item C (rustfs/backlog#1290): an external JSON rule file
({schema_version: 1, rules: [Rule...]}, the exact serde shape of the built-in
rules) merges over the seed library, with same-id rules replacing built-ins so
the support team can hotfix a misfiring rule without a release. The merged set
validates as a whole and any problem (bad regex, duplicate id, empty matcher
group, wrong schema version) prints every error and exits 2 — analysis never
runs on a half-broken set. External anchors are exempt from the CI anchor
guard, documented as author-owned quality. Adds the custom-rules section to
docs/operations/log-diagnose.md.
* fix(log-analyzer): address PR #4876 review (redaction coverage, order-independence, guards)
Redaction (--redact) now honours its "forwardable" intent across every report surface instead of a 15-name field whitelist applied over a subset:
- redact_event scrubs the full fields map (sensitive names hashed whole, every other value run through redact_text) plus provenance, so the JSON/Markdown full-sample dump no longer leaks non-whitelisted fields (client_ip, url, user, ...).
- node labels are hashed once at ingestion, so summary.nodes, per-node timeline ranges, samples and timeline anomalies stay consistent and correlatable under one stable hash.
- evidence values, unmatched-cluster templates and skipped-input paths are now redacted; peer/disk/drive/volume/node/user added to the sensitive set; IPv6 literals are hashed (without touching `rust::paths` or HH:MM:SS clocks); provenance keeps the leaf filename and hashes the customer directory/archive prefix.
- redact.rs and docs/operations/log-diagnose.md reworded to "best-effort identifier scrubbing", not an anonymization guarantee.
Order-independence (the crate's headline contract):
- the evidence value cap keeps the lexicographically smallest N distinct values instead of the first-N-by-arrival (previously order-dependent).
- first_seen/last_seen break equal-instant ties on the offset, so the serialized RFC3339 offset no longer depends on input order.
Parsing:
- a new-format panic header no longer swallows the line immediately after it when that line is itself a JSON event or a second panic header (previously dropped an interleaved ERROR in merged stdout/stderr, or merged a panic-during-panic); trailing "note: ..." backtrace lines now fold into the block.
CLI:
- diagnose --since/--until reject absurd relative amounts via checked_sub_signed instead of panicking; the exit-code doc now matches actual behaviour.
CI:
- check_log_analyzer_rules.sh is wired into the ci.yml test-and-lint job (it was only in make pre-pr, so anchor drift from other PRs could merge green).
Markdown table cells escape '|' so customer log text cannot break the table structure.
---------
Co-authored-by: houseme <housemecn@gmail.com>
|
||
|
|
4faea7fcbc |
ci: bump repo-visuals-action to v1.3.0 (#5014)
Backward-compatible feature release; existing chart-style/animate/contributors inputs are unchanged and continue to work. |
||
|
|
53728a03d3 | chore: self-host contributor wall (#5006) | ||
|
|
825bf0e2d8 |
ci: self-host star history chart (#5001)
* ci: self-host star history chart * ci: use animated gradient star chart |
||
|
|
cf9e9c6fd5 |
fix(ilm): implement expire_restored delete semantics for restore expiry (#4950)
DeleteRestoredAction is supposed to demote a restored object back to its pure transitioned state: remove only the local restored copy, strip the x-amz-restore headers, and leave the version (and the remote tier data) untouched. expire_transitioned_object set opts.transition.expire_restored accordingly, but no delete path ever read the flag, so delete_object ran an ordinary delete: on unversioned buckets the whole object vanished and the free-version record scheduled remote tier cleanup (tier data loss); on versioned buckets the latest version got a spurious delete marker that replication propagated. Route expire_restored explicitly in SetDisks::delete_object before delete-marker resolution and replication dispatch: target the found version with FileInfo.expire_restored=true and return early. The FileMeta::delete_version layer already implements the semantics (strip restore headers, keep the version, hand back the local data dir); this wires it up. Also fix the action matching in expire_transitioned_object (extracted into transitioned_object_delete_opts): DeleteRestoredVersionAction previously fell through to the full transitioned-object delete, which removed the remote tier data of a noncurrent restored version. It now routes through the same restored-copy cleanup with the exact version id, matching MinIO's Action.DeleteVersioned()/DeleteRestored() dispatch. Re-enable test_restore_chain_local_read_expiry_keeps_remote_and_allows_ re_restore in the ILM Integration (serial) lane; add unit tests pinning the event->options routing and the filemeta expire_restored branch. Closes rustfs/backlog#1302 |
||
|
|
c818177b54 |
test(ilm): re-enable test_transition_and_restore_flows; fix test-util disk-open and restore error-path lock (#4945)
test(ilm): re-enable test_transition_and_restore_flows; fix test-util disk-open and restore error-path lock (rustfs/backlog#1303) The excluded test's 'missing xl.meta ... on disk2' was NOT an EC metadata-distribution issue: after a transition all four shard disks hold a fully consistent xl.meta (verified by decoding each shard). The panic came from the tier test util's open_disk, which hardcoded disk_index 0 for every disk path; LocalDisk::new validates the endpoint's (set_idx, disk_idx) against the disk's own format.json and rejected every non-slot-0 disk with InconsistentDisk, which read_transition_meta collapsed into 'missing xl.meta'. Derive the real indices from format.json instead. This also un-breaks free_version_count / wait_for_free_version_absence for non-first disks (silently 0 before). With that fixed, the test advanced to the #4877 restore self-deadlock, whose main paths #4886 already fixed. Complete that fix on the one path it missed: update_restore_metadata (the restore-failure metadata rewrite) still rebuilt copy_object options with no_lock=false and would re-acquire the object write lock the restore handler already holds. Propagate the caller's no_lock there too. Remove the test from the serial-lane exclusion list; the four remaining exclusions are unrelated known issues and stay. |
||
|
|
701c3eee5b |
fix(ilm): replace whole-copy-back restore lock with accept-path CAS (#4956)
fix(ilm): serialize RestoreObject accepts with a CAS guard, not the whole copy-back Implements the backlog#1304 decision: replace #4877's object write lock held across the entire tier copy-back with an atomic compare-and-set on the restore ongoing flag. - ecstore: add ECStore::acquire_restore_accept_guard + RestoreAcceptGuard (opaque, purpose-scoped object write lock with an is_lock_lost fence); handle_restore_transitioned_object no longer locks the copy-back, so HEAD/get_object_info stay non-blocking during a restore and the inner put_object/complete_multipart_upload commit locks no longer self-deadlock. - API: execute_restore_object holds the guard across the restore-status read-check-write (no_lock inside the scope), drops it before spawning the copy-back; SELECT-type restores keep the plain read-locked path; the copy-back pins the resolved version so a concurrent PUT cannot strand the flagged version at ongoing=true; concurrent restores are rejected with 409 RestoreAlreadyInProgress (was a retryable 500) and guard contention maps to 503 SlowDown. - tests: drop the #4877 entry-blocking unit test (semantics deliberately reversed), pin accept-guard mutual exclusion, update the ilm-8 restore integration test to the final semantics, and add a concurrent double-POST test asserting exactly one acceptance and one tier GET. |
||
|
|
dc4b85eb2d | ci: log console version and completion after asset download (#4910) | ||
|
|
381639fbbc | fix(release): require embedded console assets (#4907) | ||
|
|
d5baaa67b8 |
ci: restore latest.json updates for prerelease tags (#4903)
PR #4582 gated the update-latest-version job to stable release tags so prereleases could not overwrite the stable version pointer. But the project currently ships prerelease tags only (beta previews), so the job never runs and latest.json has been stale since. Run the job for every release tag again, and keep the honest half of the #4582 fix by writing release_type from the actual build type instead of hardcoding "stable". |
||
|
|
8a126bb176 |
ci: drop macOS x86_64 release target (#4899)
Remove the Intel macOS entry from the Build and Release platform matrix so official release builds only publish the Apple Silicon macOS binary. Co-authored-by: heihutu <heihutu@gmail.com> |
||
|
|
ae15f5804d |
test(ilm): fix restore integration test object key to match transition filter (#4886)
* test(ilm): fix restore test object key to match transition filter restore_object_usecase_reports_ongoing_conflict_and_completion used the object key "restore/api-object.bin", but the shared set_bucket_lifecycle_transition_with_tier helper only transitions objects under the "test/" prefix. enqueue_transition_for_existing_objects therefore matched nothing and wait_for_transition timed out at 15s, failing the test deterministically. The test was added in #4860 but its ILM Integration (serial) lane is skipped on regular PRs, so it merged red and has failed on every main run since. Move the object under the test/ prefix like every passing sibling test in this file. * ci(ilm): exclude broken RestoreObject API test from serial lane restore_object_usecase_reports_ongoing_conflict_and_completion exposes a real regression, not a test bug: the RestoreObject copy-back (handle_restore_transitioned_object) now holds the object write lock added in #4877 across the entire tier read-back, so it never releases in time and the test's concurrent get_object_info times out with Lock(Timeout, 5s). The failure is deterministic and independent of the mock tier's injected latency. This is the same class of known-broken restore/transition failure already tracked under backlog#1148 (three sibling scanner tests are excluded here by name for the same reason), so exclude this one the same way until the restore copy-back path is fixed or the #4877 lock scope is revisited. The prior commit keeps its correct fix (the object key must live under the test/ transition prefix); that was masking this deeper issue by never letting the object transition in the first place. Restore copy-back deadlock/hang under the #4877 lock is escalated separately for a product-level decision (fix the copy-back vs. narrow/revert #4877). * test(ilm): fix scanner restore test object keys to match transition filter test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore and test_multipart_restore_preserves_parts_and_etag (both added in #4860) keyed their objects under restore/ instead of the test/ prefix that set_bucket_lifecycle_transition_with_tier filters on, so the objects never transitioned and wait_for_transition timed out at 15s. These surfaced only after the prior commit excluded the rustfs-side restore API test: nextest runs -j1 fail-fast, so that earlier failure stopped the run before these scanner tests executed. Unlike the excluded API test, both call restore_transitioned_object().await sequentially and only read afterwards, so they don't hit the concurrent-read-vs-#4877-write-lock timeout; the key prefix was their only problem. * ci(ilm): exclude the two remaining #4877-broken restore tests test_multipart_restore_preserves_parts_and_etag and test_restore_chain_local_read_expiry_keeps_remote_and_allows_re_restore both call restore_transitioned_object().await, which since #4877 acquires the object write lock and deterministically times out (Lock Timeout, 5s) against an already-held lock, so restore never completes. They surfaced one at a time because nextest runs -j1 fail-fast. The earlier prefix fix was necessary but only advanced them from the transition wait to this restore-lock timeout. Exclude both by name alongside their already-excluded sibling test_transition_and_restore_flows (same root cause, tracked under backlog#1148) so the ILM Integration (serial) lane goes green. The #4877 lock scope still needs a product fix before any of these re-enable. * docs(ilm): describe the excluded restore tests' symptom as a lock timeout, not a deadlock The #4877 write lock is held across the tier read-back and outlives the 5s lock timeout; nothing proves a true deadlock. Wording flagged by Copilot review. * fix(ecstore): stop restore copy-back self-deadlocking on the #4877 write lock #4877 made handle_restore_transitioned_object hold the object write lock for the whole restore and forward no_lock=true so the set layer would not reacquire it. But the set-level copy-back rebuilds its own options (put_restore_opts -> ropts, and the complete_multipart_upload opts) that default no_lock=false, so the inner put_object / new_multipart_upload / complete_multipart_upload each re-acquire this object's write lock in their commit phase and block on the lock the restore already holds -> Lock(Timeout, 5s), and restore never completes. Confirmed via RUSTFS_OBJECT_LOCK_DIAG_ENABLE: restore_transitioned_object acquires the write lock, then holds it ~10.5s across two nested 5s acquire timeouts before failing. This is a real product deadlock: a RestoreObject on any transitioned object (multipart especially) hangs, not just the tests. Propagate no_lock into the copy-back options so the inner writes inherit the already-held lock. Use opts.no_lock (not a hardcoded true) so a caller that restores without the outer lock still locks correctly. put_object_part is left as-is: it locks the multipart upload-id resource, not the object key, so it does not conflict. Verified test_multipart_restore_preserves_parts_and_etag now passes (3.6s, was a 15s+ hang). * ci(ilm): re-enable multipart restore test; scope remaining exclusions The prior commit fixes the #4877 restore self-deadlock, so test_multipart_restore_preserves_parts_and_etag passes again - drop it from the serial-lane exclusion list and remove its 'currently excluded' note. The other restore/transition tests still fail, but each on a DIFFERENT, independent issue unrelated to the (now-fixed) lock, verified locally: - test_restore_chain_...: DeleteRestoredAction sets expire_restored but no delete path reads it, so cleanup deletes the whole object (unimplemented semantics), not the local restored copy only. - test_transition_and_restore_flows: transition xl.meta missing on one drive (EC metadata distribution), not restore. - restore_object_usecase_reports_ongoing_conflict_and_completion: asserts a concurrent mid-restore ongoing=true read that #4877's read-vs-restore serialization rules out (backlog#1148 ilm-8 criterion 1, an API-semantics decision). Comments and #[ignore] reasons updated to reflect each real cause. All remain tracked under backlog#1148. |
||
|
|
61dbaf60d3 | ci: install zip and aws CLI on self-hosted runners (#4897) | ||
|
|
7b3d9e0c04 | ci: install C build tools before compiling s3s-e2e on custom runners (#4894) | ||
|
|
0fa6dc5946 |
ci: switch Linux builds to custom runners (#4884)
Co-authored-by: heihutu <heihutu@gmail.com> |
||
|
|
b3a781bce0 | test(replication): pin SSE replication contracts (#4883) | ||
|
|
49e04bf343 | test(replication): fix nightly site peer setup (#4882) | ||
|
|
299b739f9f | fix(replication): stop active-active replay loops (#4878) | ||
|
|
35c5693486 | ci: switch linux builds to ubicloud runners (#4873) | ||
|
|
8f15dd2cef |
ci: add zip packaging fallback (#4872)
ci: add python zip packaging fallback |
||
|
|
5ef2731a6b |
ci: tune build workflow runners (#4871)
* chore: extend build timeout and trim dev deps * ci: switch linux builds to ubicloud runners * ci: use larger linux build runners |
||
|
|
242424b0fc |
ci(perf): fit the baseline cache build to the post-LTO profile and self-heal nightly misses (#4841)
* ci(perf): fit the baseline cache build to the post-LTO profile and self-heal nightly misses
Every build-baseline-cache run on 2026-07-15 died on its 60min ceiling
('exceeded the maximum execution time of 1h0m0s'): #4806 put thin LTO +
codegen-units=1 on [profile.release], pushing a single release build past
60min on sm-standard-2, so the baseline cache never populated. The measured
binary must keep the production profile, so raise the job budget to 100min
instead of weakening the profile.
Also make the A/B job self-heal a same-commit cache miss: when the candidate
commit equals the baseline commit (nightly/dispatch on main) and the restore
misses, build the binary once, reuse it for both phases, and save it back to
the cache under rustfs-baseline-<sha> — previously that miss made the rig
build the identical commit twice, which no job budget fits post-#4806. The
PR-context miss (candidate != baseline, opt-in gate) keeps the double-build
fallback and now documents that it will overrun and alert.
Refs rustfs/backlog#1152 (perf-3 follow-up).
* ci(perf): latest-wins concurrency for the baseline cache build
Consumers only restore the binary for the current origin/main tip, so a
superseded push build's output is dead weight; cancel it instead of stacking
~65min jobs on the shared sm-standard-2 pool when main merges quickly. A
skipped intermediate SHA at most costs one same-commit self-heal in the A/B
job.
|
||
|
|
f29455b32b |
ci: add e2e-full merge-gate job running the full single-node e2e suite (#4847)
* ci: add e2e-full merge-gate job (single-node full e2e via nextest) Adds the [profile.e2e-full] nextest profile and a matching e2e-full job in ci.yml (push main + merge_group + workflow_dispatch) that runs the never-automated user-visible e2e suites (KMS, object_lock, multipart_auth, quota, checksum, encryption, security-boundary, ...) the fast PR e2e-smoke subset skips. The filter is the whole e2e_test crate minus the sets other lanes own: protocols:: (ci-6/ci-7), the 6 RustFSTestClusterEnvironment cluster suites (ci-7 nightly), replication_extension_test (repl-1's smoke + repl-nightly lanes), and #[ignore]d tests (ci-13). The 4-disk reliability tests are serialized, mirroring the ci profile. Reuses the downloaded debug binary and uploads junit. backlog#1149 ci-5. * ci: exclude characterized known-failures from e2e-full with tracked issues First-ever automated run of the full suite (dispatch 29381309848: 341 ran / 32 failed / 460s) surfaced five real product-bug families, each now tracked: rustfs#4842 (extract 500s, mtime=0 OffsetDateTime), rustfs#4843 (path-limit vs ignore-errors), rustfs#4844 (anonymous POST SSE-S3 500), rustfs#4845 (object-lock POST + list metadata=true 403), rustfs#4846 (lock quorum misclassified as timeout under load). Deterministic failures cannot be retried away, so each family is excluded with its OPEN issue cited and the fixing PR contract to delete the exclusion — passing negative-path siblings stay in as regression guards. |
||
|
|
5fd2e8b6a1 |
ci(coverage): add weekly cargo-llvm-cov baseline workflow (#4820)
ci(coverage): weekly cargo-llvm-cov workspace baseline (non-blocking) Add the weekly line-coverage report (backlog#1153 infra-5): - .github/workflows/coverage.yml — Sunday 07:00 UTC + workflow_dispatch; runs `cargo llvm-cov nextest --workspace --exclude e2e_test` under NEXTEST_PROFILE=ci (same scope and profile as the ci.yml test gate), writes a per-crate line-coverage table to the job summary, uploads lcov + JSON as a 90-day artifact, and routes scheduled failures through the shared schedule-failure-issue action (ci-8). Runs only on schedule/dispatch, so it can never become a required PR check. - scripts/coverage_per_crate.py — stdlib-only aggregation of the llvm-cov JSON export into the per-crate markdown table (worst-first, TOTAL row), shared by the workflow and the local target. - make coverage (.config/make/coverage.mak) — local equivalent with the same command sequence; fails with install hints when cargo-llvm-cov or cargo-nextest are missing. Listed in make help. - docs/testing/README.md — Coverage section: cadence, where the table and artifacts live, the trend-comparison method, and what is not measured (doctests, e2e_test). |
||
|
|
04616e32c8 | ci: route build jobs through matrix runner labels (#4830) | ||
|
|
d73c8a783a |
ci(perf): cache the main baseline binary to cut the nightly double build (#4816)
Every push to main now builds the release binary once and stores it in the actions cache as rustfs-baseline-<sha> (build-baseline-cache job). The A/B job restores it for origin/main and passes --baseline-bin; on the nightly, where the candidate commit equals the baseline, one cached binary serves both phases with --skip-build and the run does zero source builds. A cache miss falls back to the source double-build via --baseline-ref origin/main. This removes the ~32min-per-side double build that pushed the 24-cell nightly past its 120min ceiling (2026-07-11..07-14 all cancelled on timeout). Also: - alert-on-failure now fires on cancelled/timed-out, not just failure, so a timed-out nightly is no longer silent (the composite action already reports cancelled jobs); removed the stale perf-2 TODO now that the alert job exists. - run_hotpath_warp_ab.sh appends a Provenance section to gate.md (baseline and candidate SHAs + binary source, runner, warp version, matrix params) via a repeatable --provenance-note; the gate exit code is preserved. Refs rustfs/backlog#1152 (perf-3). |
||
|
|
0e7b8ea16b |
test(security): wire negative-auth suites into e2e-smoke with a count-floor guard (#4815)
The header-SigV4 (sec-1), presigned-URL (sec-2), and admin-gate (sec-4) negative auth-rejection e2e suites merged earlier but only presigned_negative was actually selected by any CI profile; negative_sigv4_test and admin_auth_test compiled and sat unrun. Add both to the e2e-smoke default-filter so all three attacker-facing S3 auth-rejection suites execute on every PR. They already meet the smoke admission criteria (RustFSTestEnvironment, random ports, parallel-safe, no #[ignore], no feature gates), so this is a pure filterset change — the single e2e-in-CI wiring mechanism (backlog#1149 ci-4), not a new job. Because the filter selects by module name, a rename or deletion could silently drop a suite out of the security gate with no CI signal. Add scripts/check_security_smoke_count.sh (infra-12 count-floor mechanism): it lists what the e2e-smoke profile selects and fails if the count of security auth-rejection tests drops below the committed floor in .config/security-smoke-floor.txt (16). Invoked from the e2e-tests job, before the smoke run, so the nextest list compiles the binaries the run reuses. The GHSA-3p3x FTPS/WebDAV constant-time e2e (protocols::test_protocol_core_suite) stays out by topology: it binds fixed ports, needs the ftps,webdav features, and is #[serial], so it cannot join the random-port default-feature smoke profile as a filterset change (global ruling G5). Its GHSA-r5qv sibling is a unit test that already runs in the default CI pass. docs/testing/security-regressions.md now carries the full CI-execution map and flags the GHSA-3p3x e2e CI-lane gap as a ci-domain follow-up. Refs: backlog#1151 (sec-5) |
||
|
|
c111d25a6c |
ci(mint): set RUSTFS_UNSAFE_BYPASS_DISK_CHECK so the mint container boots (#4814)
ci(mint): bypass local physical-disk-independence guard so mint boots The mint container maps four RUSTFS_VOLUMES data dirs onto a single runner device, so the startup physical-disk-independence guard aborts with a FATAL before mint can run. The scheduled run 29183544431 (2026-07-12) crashed at 'Wait for RustFS ready' with 'local erasure endpoints must use distinct physical disks'. Set RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true on the container, the CI use the guard explicitly sanctions -- mirroring the e2e-s3tests harness fixed in #4768. Completes the ci-2 acceptance (a mint run that actually produces log.json and the per-suite summary). |
||
|
|
ea2e24ac13 |
test/ci(ecstore): fix MinIO SSE interop size assertion + nightly dockerized interop check (#4809)
* test(ecstore): assert decrypted_size for MinIO SSE interop round-trip The ignored MinIO interop round-trip tests asserted `ObjectInfo.size` against the plaintext length. For SSE objects `size` is the on-disk DARE-encrypted size (plaintext + 32 bytes per 64 KiB block), so the assertion can never hold once real fixtures are present — the two `#[ignore]` tests failed the moment a real MinIO-written fixture was fed in, even though the decoded data was byte-identical. The client-visible object size comes from `decrypted_size()` / `get_actual_size()`, which correctly reads MinIO's `x-*-internal-actual-size` metadata (verified: both SSE-S3 and SSE-KMS 8 MiB multipart fixtures now report 8388608). Assert against that instead and keep the plaintext length and SHA-256 data checks. With real 4-drive MinIO fixtures (RELEASE.2025-09-07) all four tests pass, confirming RustFS reads MinIO erasure-coded SSE objects with byte-identical data and correct logical size. Co-Authored-By: heihutu <heihutu@gmail.com> * ci(ecstore): nightly MinIO interop check + dockerized fixture capture Wire the ignored MinIO on-disk interop reader tests into a nightly, non-required CI job, and make their fixtures reproducible without a host MinIO install. - Dockerfile + capture_via_docker.sh: build a throwaway image carrying the official MinIO server binary (pinned RELEASE.2025-09-07) plus the fixture lab on a small Python base, then run `lab.py capture-matrix` to write the SSE-S3 / SSE-KMS multipart fixtures the tests consume. lab.py drives MinIO's S3 API directly, so no `mc` is needed. - .github/workflows/minio-interop.yml: nightly + manual workflow on GitHub-hosted ubuntu-latest (reliable Docker + Python, unlike the self-hosted fleet — see e2e-s3tests.yml infra note). Regenerates the gitignored fixtures each run and executes the #[ignore] reader tests. Not a PR gate. - README: document the Docker capture path. Validated end to end: the script builds the image, captures the two multipart cases, and `cargo nextest run --run-ignored ignored-only` passes all four interop tests. Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> |
||
|
|
1553dc3f62 |
Address P2 follow-ups from the 2026-07-10..12 merged-PR review (backlog#1210-1220) (#4783)
* fix(obs): open cleaner compression source with O_NOFOLLOW The compressor opened the source log via File::open, which follows a symlink at the final path component. Between the scanner selecting a regular file and this open, an attacker with write access to the log directory could swap the entry for a symlink (TOCTOU) pointing at, say, /etc/shadow, whose contents would then be copied into an archive. Open the source with O_NOFOLLOW on Unix so such a swap fails with ELOOP; the temp/archive path already refused symlinks, this closes the source side. Refs rustfs/backlog#1210 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(obs): recompress instead of trusting leftover cleaner archives archive_header_ok only checked the first 2-4 magic bytes before treating an existing .gz/.zst as a completed prior result and letting the caller delete the source log. A file with valid magic but a truncated or forged body passes that check, so an attacker with write access to the log directory (or a crashed prior run) could plant such a stub and make the cleaner delete the real log without ever producing a usable archive — silent audit-data loss. Chosen fix: stop trusting cross-process leftovers entirely and always recompress the source in this pass, rather than fully decoding every leftover to validate it. Full-decode validation would add real CPU cost and decode-bug surface for a rare crash-recovery case; the existing atomic create_new+rename already overwrites whatever sits at the archive path (a planted symlink is replaced, never followed) with a freshly written, fsync'd archive, so a partial/forged leftover can never gate source deletion. This is the lowest-regression option. Refs rustfs/backlog#1211 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(object-data-cache): cap memory-gate reservation at cache growth headroom The memory gate subtracts `admitted_since_refresh` from the snapshot's available bytes so a burst arriving faster than the 5 s refresh cannot over-allocate. That counter is GROSS: it only rolls over on the refresh and never rolls back when a fill is later evicted, cancelled, or loses the invalidation race. Under sustained high-throughput churn (net footprint flat and far below `max_capacity`) the raw counter balloons past the memory the cache actually holds, so `effective_available` collapses and the gate reports false memory pressure — skipping the hottest fills with SkippedMemoryPressure until the next 5 s refresh. This only lowers hit rate; it never returns wrong data and self-heals each refresh. Fix direction 1 (minimal regression): cap the reservation deduction at the cache's own growth headroom (`max_capacity - weighted_size()`) instead of letting the unbounded gross counter shrink the system-available budget. The cache can never hold more than `max_capacity`, so a burst adds at most that headroom of real memory before moka evicts to stay bounded (net-zero churn beyond that point) — capping the deduction there keeps the reservation honest without treating gross churn as growth. Chosen over net-accounting (direction 2, releasing bytes on every failure/cancel/eviction path) because that only plugs the leak on failed fills and would not address the core defect: churn of *successful* insert/evict fills over the 5 s window. It also touches only the gate plus one call site rather than every failure path in moka_backend. The cap only ever raises `effective_available`, so real memory pressure (a low snapshot at refresh) still suppresses fills; when the cache is at capacity the headroom is 0 and the deduction vanishes, correctly reflecting net-zero churn. `MokaBackend` now stores `max_capacity` and passes the live headroom into `allows_fill`. Adds targeted gate tests: gross churn far above headroom no longer falsely suppresses, yet the reservation still bounds a burst while the cache can genuinely grow. Refs rustfs/backlog#1212 Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): assert native O_DIRECT path runs in uring read test uring_preserves_o_direct_for_eligible_reads only compared bytes through LocalDisk::read_file_mmap_copy. On a filesystem that rejects O_DIRECT the read silently degrades to the buffered StdBackend fallback and the byte check still passes, so the test could go green without the native read_at_direct path ever executing -- a vacuous pass. Add a per-disk native_direct_reads counter on UringBackend, incremented only when pread_uring_direct completes, and rebuild the test to drive a real UringBackend's pread_bytes and assert the counter is non-zero (every eligible read went through the native tier). When io_uring or O_DIRECT is unavailable on the host filesystem (restricted CI runners, tmpfs), the test skips loudly via eprintln instead of asserting a tautology, while still checking byte-correctness on whatever tier served the read. The counter also gives a gray release a positive signal that the O_DIRECT tier is serving reads, not just a fallback count. Refs rustfs/backlog#1213 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): warn + count read-time EINVAL on native O_DIRECT reads classify_direct_read_error is only reached from the read side: the O_DIRECT open in pread_uring_direct already succeeded (an open-time refusal is handled earlier as DirectOpenError::ODirectRefused). So an EINVAL/EOPNOTSUPP arriving here is a read-time error on an fd the kernel accepted for O_DIRECT -- far more likely an alignment bug in the aligned read path than an unsupported filesystem. The old code latched the disk's native path off with only a once-per-disk debug trace, hiding a potential correctness regression behind a silent buffered-read downgrade. Diagnostics only: the fallback behaviour is unchanged (the native path is still latched off and the caller still reads via StdBackend). This adds a rustfs_io_uring_direct_read_einval_total counter and promotes the once-per-disk trace from debug to warn so an operator can see an alignment regression instead of an unexplained latency/CPU shift. Refs rustfs/backlog#1214 Co-Authored-By: heihutu <heihutu@gmail.com> * docs(ecstore): document data-blocks-first default and its tail-latency cost DEFAULT_RUSTFS_GET_DATA_BLOCKS_FIRST_READER_SETUP is true and must stay true: deferred-parity is the deliberate, already-rolled-out full-object GET default from backlog#1159/#923. Flipping it back to false in code would silently revert that rollout for every deployment that has not set the env var, so this commit only documents -- no behaviour change. The added notes explain what data-blocks-first does (schedule data shards up front, engage parity lazily on a missing/corrupt data shard), the known trade-off (parity is engaged late, so a slow-but-not-dead data drive raises GET p99 because the faster parity shards are not raced against it until a data shard is declared missing), and the operational rollback switch (RUSTFS_GET_DATA_BLOCKS_FIRST_READER_SETUP=false), which is intentionally an env override rather than a code default change. No metric was added: the low-risk observability hook for "slow data drive engaged deferred parity" would live at the deferred-stripe engage point, which is out of this file's scope; this change stays documentation-only to avoid touching the hot GET path. Refs rustfs/backlog#1215 Co-Authored-By: heihutu <heihutu@gmail.com> * docs(ecstore): document wide-directory walk stall hazard and tuning list_dir enumerates a whole directory in one os::read_dir call (count = -1), and the walk caller bounds that entire enumeration with the per-read stall budget (default 5s) as if it were a single read. For a wide, flat prefix -- one directory holding millions of immediate children -- a single readdir can exceed the budget on a healthy disk, trip DiskError::Timeout, and surface as a ListObjects 500 quorum failure though the drive is fine (a #2999 sub-class). This is documented, not rewritten: turning the one-shot readdir into a streaming/batched enumeration that refreshes the stall deadline between chunks is an architecture-level change with high regression surface (ordering, the count contract, quorum merge) and belongs in a separate follow-up. The supported mitigation today is operational, so the comments point wide-directory deployments at RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS and the high-latency drive-timeout profile, which widen the budget with no code change. Notes were added at list_dir, the scan_dir call site, and get_drive_walkdir_stall_timeout. No behaviour change. Refs rustfs/backlog#1216 Co-Authored-By: heihutu <heihutu@gmail.com> * docs(ecstore): document consumer-peek vs producer-stall coupling In list_path_raw the consumer's peek_timeout is drawn from the same source and same value (walkdir_stall_timeout, default 5s) as the producer-side walk stall budget, but the two measure different things: the producer stall bounds a single drive read, while the consumer peek bounds the gap between two ADJACENT entries arriving from a reader. Because they share a value, the consumer cannot wait meaningfully longer for the next entry than the producer is allowed to spend producing one. Walking a region dense with non-listable internal items can make a HEALTHY drive miss the budget between visible entries; the consumer then declares it stalled and detaches it, dropping a good drive from the merge and capping the "large prefix succeeds" guarantee. Documented, not decoupled: giving the consumer peek an independent, strictly-larger budget would cut these false detaches but equally delays detaching a genuinely dead drive and shifts listing tail-latency semantics, so it wants soak data before changing the default. The comment records the invariant any such follow-up must keep -- consumer peek >= producer stall, never stricter -- so it can never fail a drive before the producer would. No behaviour change. Refs rustfs/backlog#1217 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(io-metrics): add time-based trigger for low-IOPS latency percentiles Percentiles were recomputed only every 128 IOs and seeded to 0, so a low-traffic deployment exported p95/p99 = 0/stale for a long time after startup. Add a 10s wall-clock trigger alongside the count throttle so the first recompute can fire before 128 samples accrue. Hot-path per-op mean update is unchanged. Refs rustfs/backlog#1218 Co-Authored-By: heihutu <heihutu@gmail.com> * test(e2e): cover codec-streaming parity under fault injection and NoSuchKey The codec-streaming compat A/B previously ran only against a healthy 4-disk EC set with successful full GETs: the DiskFaultHarness was constructed but never faulted, the error path was untested, and the range assertion silently compared legacy-vs-legacy (ranges always fall back to the duplex path), overstating what it proved. Add two genuinely-failable scenarios reusing the existing harness and fixtures: - Parity reconstruction A/B: take one data disk offline and re-run the full object matrix on both phases while the EC 2+2 set rebuilds each large object from the surviving shards. Assert codec == legacy byte-for-byte (sha256) and header-for-header, and assert the codec phase served the reconstructed objects with zero duplex-pipe fallback (the reader gate is drive-health-independent, so the codec fast path is really exercised through reconstruction). - NoSuchKey negative path: compare the HTTP status + S3 error code of a missing-key GET across the legacy and codec phases and require them to be identical (404/NoSuchKey), guarding against the codec env perturbing the error path. Also clarify the range-phase comment so it is not misread as codec-range correctness coverage: both sides are served by the same legacy range path, so the assertion only proves ranges keep working and keep falling back to legacy with the gates open. Verified: cargo check/--no-run pass and the test passes locally (1 passed; dup_codec=0 confirms the codec path ran). Refs rustfs/backlog#1219 Co-Authored-By: heihutu <heihutu@gmail.com> * ci(ecstore): exercise native O_DIRECT read path on an ext4 loopback The uring-integration leg ran on the runner's default TMPDIR, which may sit on tmpfs/overlayfs where open(O_DIRECT) fails and the native read_at_direct path silently latches off to the aligned StdBackend fallback. Mount a dedicated ext4 loopback and point TMPDIR at it so the real io_uring dep (bumped git->0.1.0->0.2.0->0.2.1) and the native O_DIRECT read path are actually covered rather than validated only by signature diffing. Refs rustfs/backlog#1220 Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> |
||
|
|
f84ba243a1 |
chore(ci): guard against committed planning docs and remove re-added ones (#4777)
chore(ci): guard against committed planning docs; remove re-added ones PR #4771 removed the docs/superpowers planning-doc archive and added a rule, but #4765 force-added two more (git add -f bypasses .gitignore): docs/superpowers/plans/2026-07-12-observability-single-writer.md and docs/superpowers/specs/2026-07-12-observability-single-writer-design.md — an agentic implementation plan and its design spec. Delete both. Close the enforcement gap so this cannot recur: - New scripts/check_no_planning_docs.sh fails if anything is tracked under docs/superpowers/, regardless of how it was added. - Wire it into make pre-commit/pre-pr/dev-check. - Run it in CI: ci.yml for code/mixed PRs, and ci-docs-only.yml (which was a green stub) so docs-only PRs can no longer slip a planning doc past the required 'Test and Lint' check. - Document the guard in AGENTS.md and the arch-checks skill. |