Skip to content

fix(sandbox): remove state patch clear that races with new patches#14893

Merged
darioush merged 2 commits into
masterfrom
fix-sandbox-patch-state-race
Jan 16, 2026
Merged

fix(sandbox): remove state patch clear that races with new patches#14893
darioush merged 2 commits into
masterfrom
fix-sandbox-patch-state-race

Conversation

@r-near

@r-near r-near commented Jan 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes an unconditional clear() call that caused sandbox_patch_state RPC to silently drop patches.

Problem

When a new sandbox state patch was submitted while a block was being processed, the patch would be cleared by postprocess_ready_block() before any block could consume it. The RPC would return success, but the patch was never applied.

Fix

Remove the clear() call. Patches are already consumed via take() in start_process_block_impl, making this clear() both unnecessary and harmful.

The unconditional clear() in postprocess_ready_block() could wipe
sandbox state patches that were submitted during block processing,
causing sandbox_patch_state RPC to return success while the patch
was silently dropped.

Patches are already consumed via take() in start_process_block_impl,
so this clear() is unnecessary and harmful.
Copilot AI review requested due to automatic review settings January 16, 2026 03:40
@r-near r-near requested a review from a team as a code owner January 16, 2026 03:40
@r-near r-near requested a review from darioush January 16, 2026 03:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a race condition in sandbox state patching where patches submitted during block processing were silently dropped. The clear() call in postprocess_ready_block() was unconditionally removing pending state patches, including newly submitted ones that had not yet been consumed.

Changes:

  • Removed the unnecessary self.pending_state_patch.clear() call that was causing patches to be lost
  • Patches are already properly consumed via take() in start_process_block_impl, making the clear() both redundant and harmful

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented Jan 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.62%. Comparing base (a64d491) to head (688e348).

Additional details and impacted files
@@             Coverage Diff             @@
##           master   #14893       +/-   ##
===========================================
+ Coverage    1.30%   68.62%   +67.31%     
===========================================
  Files         726      920      +194     
  Lines      140324   200084    +59760     
  Branches   140324   200084    +59760     
===========================================
+ Hits         1829   137301   +135472     
+ Misses     138442    56842    -81600     
- Partials       53     5941     +5888     
Flag Coverage Δ
pytests-nightly 1.30% <ø> (+<0.01%) ⬆️
unittests 68.35% <ø> (?)
unittests-nightly 68.14% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

r-near added a commit to near/near-sandbox-rs that referenced this pull request Jan 16, 2026
Adds a test that verifies sandbox_patch_state RPC returns only after
the patched state is queryable. This test will fail on nearcore versions
prior to the fix in near/nearcore#14893

@darioush darioush left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, the clear call seems to be a leftover from when pending_state_patch used to be an Option<Vec<_>> and now it's just Vec<_>, so it is not needed.
The race was seemingly introduced when apply chunks got refactored to async.

The clear method is not needed any other place so I removed it.

@darioush darioush enabled auto-merge January 16, 2026 18:07
@darioush darioush added this pull request to the merge queue Jan 16, 2026
Merged via the queue into master with commit e47762f Jan 16, 2026
26 checks passed
@darioush darioush deleted the fix-sandbox-patch-state-race branch January 16, 2026 18:35
github-merge-queue Bot pushed a commit that referenced this pull request Feb 19, 2026
- Add sandbox_fast_forward and sandbox_patch_state bug fixes to 2.11
changelog (#14899, #14893)
pugachAG pushed a commit that referenced this pull request Feb 19, 2026
- Add sandbox_fast_forward and sandbox_patch_state bug fixes to 2.11
changelog (#14899, #14893)
r-near added a commit to near/near-sandbox-rs that referenced this pull request Apr 1, 2026
Adds a test that verifies sandbox_patch_state RPC returns only after
the patched state is queryable. This test will fail on nearcore versions
prior to the fix in near/nearcore#14893
r-near added a commit to near/near-sandbox-rs that referenced this pull request Apr 1, 2026
Adds a test that verifies sandbox_patch_state RPC returns only after
the patched state is queryable. This test will fail on nearcore versions
prior to the fix in near/nearcore#14893
github-merge-queue Bot pushed a commit that referenced this pull request Apr 16, 2026
## Background

`sandbox_patch_state` sometimes permanently loses patches — the patched
account never appears, even on retry. This was reported in the [Zulip
thread](https://near.zulipchat.com/#narrow/channel/308695-nearone.2Fprivate/topic/Sandbox.20patch_state.20issues)
and reproduced with
[near-sandbox-rs#51](near/near-sandbox-rs#51).

Three interrelated bugs:

- **Patch lost on block failure**: `pending_state_patch.take()` drains
the queue at the start of block processing. If preprocessing fails
(orphan, missing chunks, optimistic deferral), the patch is gone
forever.
- **RPC returns too early**: `patch_state_in_progress()` checks
`is_empty()`, which is true right after `take()` — before the patch is
actually committed to state.
- **Startup race**: Block 1 after genesis has no new chunks. The
runtime's `apply()` early-returns for old chunks and silently drops the
`state_patch`.

## What changed

Introduces `SandboxPatchTracker` — a type that encapsulates the pending
patch, a generation counter, and a committed-generation counter. Like
`SandboxStatePatch`, it uses the ZST pattern: real struct on sandbox
builds, zero-sized no-op on non-sandbox builds. This replaces the three
separate fields on `Chain` (`pending_state_patch`,
`sandbox_patch_generation`, `sandbox_patch_committed_gen`) with a single
`sandbox_patches: SandboxPatchTracker`.

The key design change is **taking the patch late** — at point of use
inside the per-shard loop of `apply_chunks_preprocessing`, instead of at
the top of `start_process_block_impl`. This eliminates the need for
backup/clone/restore on error paths, since any error before the take
leaves the patch in the tracker. The generation counter (`in_progress()`
checks `generation != committed_gen`, not `is_empty()`) ensures the RPC
doesn't return prematurely despite the late take.

This removes the `state_patch` parameter from `preprocess_block` and
`apply_chunks_preprocessing`, and results in zero `#[cfg(feature =
"sandbox")]` blocks in `chain.rs`.

## Related

- #15536 — alternative fix by @r-near that addresses the same three bugs
with a backup/restore approach and 22 cfg-gates. This PR takes a
different approach (take-late + tracker) to achieve the same result with
less code deviation.
- #14893 — fixed a different patch loss path (`clear()` in
`postprocess_ready_block`)
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.

3 participants