Skip to content

feat(workload): ranged GETs with regenerated-slice verification#30

Merged
overtrue merged 3 commits into
mainfrom
feat/ranged-get-workload
Jul 15, 2026
Merged

feat(workload): ranged GETs with regenerated-slice verification#30
overtrue merged 3 commits into
mainfrom
feat/ranged-get-workload

Conversation

@overtrue

Copy link
Copy Markdown
Contributor

Summary

The workload issued only whole-object GETs, so the ranged read path — offset decoding, per-shard positioning, partial-content framing — was never exercised under fault. This is the second half of workload-realism finding D4-5 in rustfs/backlog#1100 (the first half, EC-shard payload weights, landed as the suite profile guidance in the #26 review).

Stacked on #28 (needs its per-key committed-write history for the concurrency legs; merge #28 first and this rebases clean).

Workload side

  • RUSTFS_FAULT_TEST_WORKLOAD_RANGED_GET_PERCENT (0-100, default 0 = behavior unchanged) turns a deterministic, seed-stable slice of mixed-workload GETs into ranged reads. The (offset, length) pair is a pure function of (seed, index) — always in bounds, covering single-byte/interior/suffix reads, and reruns replay identically.
  • get_object_range_result expects 206 framing, records the returned slice hash with a new range field on the history record, and fails at the client when the byte count doesn't match the request (a silent 200-full-body would otherwise let every range read degenerate into a whole-object read).
  • Committed writes now record payload_ref { seed, index }: every non-multipart body comes from the seeded generator (seeded_bytes), so the exact bytes are reproducible after the fact. Both new record fields are optional/backward-compatible.

Checker side (what makes this a detector, not just traffic)

  • Ranged records are intercepted before the whole-object hash comparison, which would otherwise always misfire on slice hashes.
  • verify_ranged_get regenerates the expected slice for every committed value the GET could legally observe — the latest write, writes whose completion window overlaps the GET, and the immediately-previous value while the latest was in flight (the same concurrency legs as fix(checker): stop flagging concurrent committed overwrites as corruption #28's whole-object path) — and flags corruption when the returned slice matches none of them, or when the byte count lies.
  • Candidates without payload_ref (multipart bodies, legacy artifacts) make a mismatch inconclusive rather than a false positive.

Why this matters for coverage

RustFS's ranged reads on sharded objects walk a different path than full streams (shard offset math, partial reconstruct). With the EC-shard payload profile + this knob, faults now run against ranged reads at arbitrary offsets with content actually verified against the committed bytes — not just "the request didn't error".

Validation

  • cargo test: 245+15 pass — clean latest-slice read; concurrent-variant slice read (racing overwrite); garbage slice → corruption with the range in the message; unreproducible-candidate inconclusive; length mismatch flagged; sampler determinism/bounds/rate.
  • cargo clippy --all-targets / cargo fmt --check clean.
  • Default-off knob: with the env unset, zero behavioral change (all existing tests pass untouched).

Copilot AI review requested due to automatic review settings July 11, 2026 05:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@overtrue

Copy link
Copy Markdown
Contributor Author

E2E evidence (minikube, rustfs:1.0.0-beta.8, stress-cpu, RANGED_GET_PERCENT=40):

  • 226 GETs total, 5 ranged — every one answered with a genuine 206 and the exact requested byte count (zero client-side Failed), so beta.8's Range handling holds up under CPU stress.
  • Checker: passed=true, 59/59 committed objects verified, successful_corrupted_reads=[] — all 5 slices matched the regenerated bytes of their committed values (the interception + regeneration path ran end-to-end, no whole-object-hash misfires).
  • Deterministic sampler replayed in-bounds ranges as designed.

Known follow-up (deliberate v1 scope): the percent knob is env-only; a rangedGetPercent field in suite workloadProfiles (with the same ambient-isolation rule as percent, cf. #19) is the natural next slice.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ea782d94f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/fault/checker.rs Outdated
Comment on lines +1970 to +1973
committed_history
.get(key)
.map(Vec::as_slice)
.unwrap_or_default(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include ambiguous writes in ranged-read verification

When a ranged GET follows a timed-out/unknown overwrite that actually took effect, this new branch verifies only committed_history and then continues before the existing ambiguous-write handling below can run. The whole-object path can classify matching ambiguous bodies, but for range records the recorded hash is only a slice; if that slice belongs to the ambiguous overwrite rather than the last committed value, verify_ranged_get reports successful_corrupted_reads and fails the checker even though the observed value is a legal materialized ambiguous write. Please include active/superseded ambiguous attempts as reproducible candidates for ranged GETs, or make those cases inconclusive.

Useful? React with 👍 / 👎.

Comment thread src/fault/workload.rs
Comment on lines +848 to +852
recorder.finish(
record,
OperationOutcome::Failed,
Some(206),
Some(error.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report wrong-length ranged responses as corruption

When the service returns a response body whose length does not match the requested range, this records the GET as Failed; however successful_read_anomalies only inspects OperationKind::Get records with outcome Ok, so the checker never emits the length-mismatch corruption that verify_ranged_get was written to detect. In fault runs where client disruptions are allowed or expected, a successful HTTP response with truncated/expanded range data is only counted as workload disruption and can still let the checker pass. Please either keep these records checkable by the ranged verifier or have the checker explicitly consume failed range records with bodies.

Useful? React with 👍 / 👎.

Comment thread src/fault/checker.rs Outdated
Comment on lines +1875 to +1878
if latest.ended_at_ms >= record.started_at_ms
&& let Some(previous) = prior.last()
{
candidates.push(previous);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the stable pre-overwrite slice for overlapping writes

When a ranged GET starts while two or more overwrites are still in flight, prior.last() can be the earlier in-flight write rather than the stable value that existed before the overlap. For example, if w1 settled and w2/w3 both finish after this GET starts, a read that legally returns w1’s slice is not in candidates and is reported as corruption. Please walk back to the last write that settled before the overlapping write window instead of only adding the immediate predecessor.

Useful? React with 👍 / 👎.

@overtrue

Copy link
Copy Markdown
Contributor Author

Pushed a fix from a pre-merge adversarial self-review: verify_ranged_get consulted only committed writes, never ambiguous (timeout/unknown) ones — so a ranged GET legally returning the slice of a materialized-but-unconfirmed overwrite (routine under fault injection) was falsely reported as corrupted_reads, spuriously failing any run with ranged GETs. Now mirrors the whole-object path: ambiguous-slice match → unknown_writes_materialized; unreproducible/spanning ambiguous → inconclusive; pending-ambiguous no-match → unknown_write_value_conflicts (not visible_deleted). Added regression tests for both arms. Committed-value verification and real corruption detection unchanged.

@overtrue

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main. Stacked on #28 — merge #28 first. Note #28 is complementary to the already-merged #32 (stable_live_objects_at_read_starts handles GET-returns-stable-at-start value; #28's concurrent_committed_read additionally handles GET-returns-overlapping-write value — I verified #28's race test still FAILs on main-with-#32, so #28 is still needed). Both mechanisms coexist post-rebase; 255+15 tests pass.

@overtrue
overtrue added this pull request to the merge queue Jul 15, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jul 15, 2026
@overtrue
overtrue force-pushed the feat/ranged-get-workload branch from 527f359 to 5b4141c Compare July 15, 2026 07:04
overtrue added 3 commits July 15, 2026 15:07
The workload issued only whole-object GETs, so the ranged read path —
offset decoding, per-shard positioning, partial-content framing — was
never exercised under fault (backlog#1100 workload-realism gap, second
half of finding D4-5).

Workload side:
- RUSTFS_FAULT_TEST_WORKLOAD_RANGED_GET_PERCENT (0-100, default 0 =
  behavior unchanged) turns a deterministic, seed-stable slice of mixed-
  workload GETs into ranged reads with in-bounds (offset, length) derived
  purely from (seed, index) — reruns replay identical operations.
- get_object_range_result records the returned slice hash with a new
  range field, treats a wrong byte count as Failed at the client, and
  expects 206 framing.
- Committed writes now record payload_ref { seed, index }: every
  non-multipart body comes from the seeded generator, so the checker can
  regenerate the exact bytes later.

Checker side (the part that makes this a detector, not just traffic):
- Ranged records are intercepted before the whole-object comparison
  (which would otherwise always misfire on slice hashes).
- verify_ranged_get regenerates the expected slice for every committed
  value the GET could legally observe — latest write, writes overlapping
  the GET, and the previous value while the latest was in flight (same
  concurrency legs as the whole-object path) — and flags corruption when
  the returned slice matches none of them and byte count when it lies.
- Candidates without payload_ref (multipart bodies, legacy records) make
  a mismatch inconclusive rather than a false positive.

cargo test: 245+15 pass (new: clean latest-slice read, concurrent-
variant slice read, garbage slice -> corruption with the range in the
message, unreproducible-candidate inconclusive, length mismatch,
sampler determinism/bounds/rate).
Adversarial self-review before merge (backlog#1100 policy) caught a
false-positive: verify_ranged_get only compared the returned slice
against committed_history, never against ambiguous (timeout/unknown)
writes -- unlike the whole-object read path. So a ranged GET that legally
returned the slice of a materialized-but-unconfirmed overwrite (the
routine ambiguous-write case under fault injection) matched no committed
value and was reported as corrupted_reads -- the harness's harshest
verdict -- spuriously failing any fault run with ranged GETs enabled. The
byte-identical whole-object GET is classified unknown_writes_materialized
(non-corruption).

Record payload_ref on ambiguous write attempts (they already come from
the seeded generator) and, in verify_ranged_get, mirror the whole-object
path: a slice matching a materialized ambiguous write ->
unknown_writes_materialized; an unreproducible/range-spanning ambiguous
write -> inconclusive; a pending ambiguous write with no clean match ->
unknown_write_value_conflicts (not visible_deleted, closing the second
divergence the review flagged). Committed-value verification and real
corruption detection are unchanged.

cargo test 247+15 pass (new: materialized ambiguous ranged read is not
corruption; only-ambiguous-write ranged read is inconclusive).
…s exemptions in ranged verification

verify_ranged_get reimplemented only the concurrency exemptions of the
whole-object path and produced two classes of false positives:

- A ranged GET that started before an overlapping committed delete
  linearized and legally returned the pre-delete value was flagged as
  visible-deleted/resurrection, because the delete wipes the key's
  committed history before the GET record is processed. Carry
  payload_ref (and size) into the stable-at-read-start model and add
  the stably-live-at-start value as a candidate leg, mirroring the
  whole-object stable_live_at_start exemption. A GET that starts after
  the delete settled is still flagged.

- A slice matching a materialized ambiguous (timeout) write that a
  later committed overwrite already superseded landed in
  corrupted_reads, because only pending ambiguous attempts were
  consulted. Classify it as unknown_writes_materialized plus
  unknown_write_value_conflicts, exactly like the whole-object path,
  and treat mismatches with any ambiguous attempt outstanding as
  unknown_write_value_conflicts rather than corruption. A slice
  matching nothing with no ambiguous writes remains corruption.

Also fix the ranged_get_of_only_ambiguous_write_is_inconclusive test,
whose comment claimed the spans-range-inconclusive branch while the
record's payload_ref actually exercised the ambiguous-mismatch branch,
and add dedicated coverage for the unreproducible spans-range branch.
@overtrue
overtrue force-pushed the feat/ranged-get-workload branch from 5b4141c to 6dcfa7b Compare July 15, 2026 07:08
@overtrue
overtrue added this pull request to the merge queue Jul 15, 2026
Merged via the queue into main with commit a4881ba Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants