Fix: stop retrying stale snapshot on transient errors - #1719
Conversation
|
Thank you for addressing this issue! But have you used the wrong branch in this PR? It shouldn't be so many changes. |
i used release-0.9; it is 1 file change now. btw, if it is valid, possible making a quick minor release? this is affecting our production |
Yes, make sense. |
|
any update on this? |
|
Thanks — the stale-snapshot loop is a real issue, but I think the fix is too coarse. Returning A more proportionate fix:
Also, the test asserts 2 distinct snapshot IDs but not retry cadence. Worth bounding the number of |
that makes sense; i will make change later on |
|
updated as per review |
drmingdrmer
left a comment
There was a problem hiding this comment.
@drmingdrmer reviewed all commit messages and made 3 comments.
Reviewable status: 0 of 3 files reviewed, 3 unresolved discussions (waiting on cliff0412).
openraft/src/network/snapshot_transport.rs line 102 at r2 (raw file):
const SNAPSHOT_CHUNK_MAX_RETRIES: u64 = 5; const SNAPSHOT_CHUNK_RETRY_BASE_MILLIS: u64 = 10;
use Duration::from_millis() to define a typed time duration, instead of using millis in u64:
const SNAPSHOT_CHUNK_RETRY_BASE: Duration = Duration::from_millis(10).
The same for SNAPSHOT_CHUNK_RETRY_MAX_MILLIS
openraft/src/network/snapshot_transport.rs line 109 at r2 (raw file):
let shift = consecutive_failures.saturating_sub(1).min(4) as u32; let millis = SNAPSHOT_CHUNK_RETRY_BASE_MILLIS.saturating_mul(1u64 << shift);
1u64<<shift may panic if shift is too large
openraft/src/network/snapshot_transport.rs line 132 at r2 (raw file):
let end = snapshot.snapshot.seek(SeekFrom::End(0)).await.sto_res(subject_verb)?; let mut consecutive_failures = 0; let mut unreachable_backoff = None::<Backoff>;
consecutive_failures and Backoff seems to be duplicated.
Keep just one of these two: either increase consecutive_failures and then use it to calculate the sleep time, or just use Backoff to set up incremental sleep interval series.
Code quote:
let mut consecutive_failures = 0;
let mut unreachable_backoff = None::<Backoff>;
regarding first 2 points; already made the changes; this mentioned that need to use this from Unreachable node, which defaults to 500ms. i think we can stick to that and use longer retry delay; but for |
drmingdrmer
left a comment
There was a problem hiding this comment.
@drmingdrmer reviewed all commit messages and resolved 1 discussion.
Reviewable status: 0 of 3 files reviewed, 2 unresolved discussions (waiting on cliff0412).
d171738 to
06af8b8
Compare
06af8b8 to
83c8176
Compare
cliff0412
left a comment
There was a problem hiding this comment.
@cliff0412 made 2 comments.
Reviewable status: 0 of 7 files reviewed, 2 unresolved discussions (waiting on drmingdrmer).
openraft/src/network/snapshot_transport.rs line 109 at r2 (raw file):
Previously, drmingdrmer (张炎泼) wrote…
1u64<<shiftmay panic if shift is too large
Done.
openraft/src/network/snapshot_transport.rs line 132 at r2 (raw file):
Previously, cliff0412 (cliff.li) wrote…
regarding first 2 points; already made the changes;
for the third one, forUnreachableerror, at filenetwork/network.rsline 161/// Build a backoff instance if the target node is temporarily(or permanently) unreachable. /// /// When a [`Unreachable`](`crate::error::Unreachable`) error is returned from the `Network` /// methods, Openraft does not retry connecting to a node immediately. Instead, it sleeps /// for a while and retries. The duration of the sleep is determined by the backoff /// instance. /// /// The backoff is an infinite iterator that returns the ith sleep interval before the ith /// retry. The returned instance will be dropped if a successful RPC is made. /// /// By default it returns a constant backoff of 500 ms. fn backoff(&self) -> Backoff { Backoff::new(std::iter::repeat(Duration::from_millis(500))) }this mentioned that need to use this from Unreachable node, which defaults to 500ms. i think we can stick to that and use longer retry delay; but for
TimeoutandNetworkerror, we can use the shorter retry delay as defined in this PR
DONE
83c8176 to
07a0f77
Compare
`Chunked::send_snapshot` used to swallow every transient `RPCError` variant — `Timeout`, `Unreachable`, `Network`, and even remote `Fatal` — then `continue` the loop without changing `offset`. A flaky target therefore streamed the same snapshot forever, even after the leader had built a newer one. The outer `C::timeout(hard_ttl, ...)` expiring also just `continue`d, making a tight loop bounded only by a 1 ms per-iteration sleep. Retry policy: - `Timeout` / `Network`: module-local exponential backoff (`SNAPSHOT_CHUNK_RETRY_BASE` = 10 ms doubling to `SNAPSHOT_CHUNK_RETRY_MAX` = 200 ms). These errors typically clear within a packet-loss burst — a tight curve rides it out without involving the caller. - `Unreachable`: caller's `RaftNetwork::backoff()` iterator, cached per outage and dropped on the next successful chunk. An unreachable target usually stays so for seconds to minutes, a cadence the application should pick. - `PayloadTooLarge`: fail fast. Retrying the same chunk at the same size cannot make progress, and the append-entries shrink path in `ReplicationCore` is not reusable here yet. - `RemoteError::Fatal`: propagate verbatim. - `SnapshotMismatch`: reset offset + retry state and continue. Bail out on `SNAPSHOT_CHUNK_MAX_RETRIES` (5) consecutive transient failures and surface the underlying error. The replication layer then unwinds and drives the next attempt with a fresh snapshot — exactly what the new integration test `t91_snapshot_retry_uses_latest_snapshot` verifies end-to-end. The outer `hard_ttl` timeout also returns `NetworkError` immediately rather than looping: stacking in-flight RPCs under the same deadline cannot make progress, and the replication layer drives the next attempt on its own timer. Changes: - Add `SNAPSHOT_CHUNK_MAX_RETRIES`, `SNAPSHOT_CHUNK_RETRY_BASE`, `SNAPSHOT_CHUNK_RETRY_MAX`, `SNAPSHOT_CHUNK_UNREACHABLE_FALLBACK` constants, and `snapshot_chunk_retry_delay()` - Track `consecutive_failures` and a cached `unreachable_backoff` across retries; reset both on success or mismatch - Propagate remote `Fatal` by re-wrapping the `RemoteError` - Unit tests covering retry-resume, budget exhaustion, outer-timeout fast-fail, `PayloadTooLarge` fast-fail, and mismatch reset - Integration test `t91_snapshot_retry_uses_latest_snapshot` exercising the replication layer's re-drive path
07a0f77 to
30240a5
Compare
drmingdrmer
left a comment
There was a problem hiding this comment.
@drmingdrmer reviewed 7 files and all commit messages, and resolved 2 discussions.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on cliff0412).
| continue; | ||
| } | ||
| continue; | ||
| err @ (RPCError::Timeout(_) | RPCError::Network(_) | RPCError::Unreachable(_)) => err, |
There was a problem hiding this comment.
Why not just use _ => err
There was a problem hiding this comment.
in case there are new error variants added.
There was a problem hiding this comment.
I don't like this style, there is only one branch continue, others all return error.
drmingdrmer
left a comment
There was a problem hiding this comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on cliff0412).
When snapshot transmission hits a transient transport error, openraft currently keeps retrying inside the same snapshot task. This means the leader can keep retrying an old snapshot even after a newer snapshot has already been built.
In the worst case, if the follower is effectively crashed and the transport keeps failing quickly, the snapshot task can stay in a very tight retry loop. With the current chunked transport, that loop sleeps only 1ms between attempts, so the leader may keep hammering the same stale snapshot over and over.
This patch changes snapshot retry behavior so transient snapshot send errors return to
ReplicationCore. The next retry then fetches the current snapshot again.This change is