refactor(utils): migrate remaining bounded retry loops

This commit is contained in:
Tunglies
2026-10-01 14:34:16 +08:00
committed by GitHub
parent 9d1b400299
commit 5375ce360f
11 changed files with 478 additions and 304 deletions
+53 -36
View File
@@ -109,44 +109,61 @@ fn current_user_home() -> anyhow::Result<PathBuf> {
}
.max(1024);
loop {
if buffer_size > MAX_BUFFER_SIZE {
anyhow::bail!("effective-user home lookup exceeded the maximum buffer size");
}
use crate::utils::retry::{RetryError, RetryPolicy, retry_sync};
retry_sync(
RetryPolicy::fixed(
std::num::NonZeroUsize::MIN.saturating_add(11),
std::time::Duration::ZERO,
),
|_| {
if buffer_size > MAX_BUFFER_SIZE {
return Err(RetryError::Stop(anyhow::anyhow!(
"effective-user home lookup exceeded the maximum buffer size"
)));
}
let mut passwd = std::mem::MaybeUninit::<libc::passwd>::zeroed();
let mut result = std::ptr::null_mut();
let mut buffer = vec![0_u8; buffer_size];
let code = unsafe {
libc::getpwuid_r(
uid,
passwd.as_mut_ptr(),
buffer.as_mut_ptr().cast(),
buffer.len(),
&mut result,
)
};
if code == libc::ERANGE {
buffer_size = buffer_size.saturating_mul(2);
continue;
}
if code != 0 {
return Err(std::io::Error::from_raw_os_error(code).into());
}
if result.is_null() {
anyhow::bail!("effective user {uid} has no password database entry");
}
let mut passwd = std::mem::MaybeUninit::<libc::passwd>::zeroed();
let mut result = std::ptr::null_mut();
let mut buffer = vec![0_u8; buffer_size];
let code = unsafe {
libc::getpwuid_r(
uid,
passwd.as_mut_ptr(),
buffer.as_mut_ptr().cast(),
buffer.len(),
&mut result,
)
};
if code == libc::ERANGE {
buffer_size = buffer_size.saturating_mul(2);
return Err(RetryError::Retry(anyhow::anyhow!(
"effective-user home lookup exceeded the maximum buffer size"
)));
}
if code != 0 {
return Err(RetryError::Stop(std::io::Error::from_raw_os_error(code).into()));
}
if result.is_null() {
return Err(RetryError::Stop(anyhow::anyhow!(
"effective user {uid} has no password database entry"
)));
}
let passwd = unsafe { passwd.assume_init() };
if passwd.pw_dir.is_null() {
anyhow::bail!("effective user {uid} has no home directory");
}
let home = unsafe { CStr::from_ptr(passwd.pw_dir) };
if home.to_bytes().is_empty() {
anyhow::bail!("effective user {uid} has an empty home directory");
}
return Ok(PathBuf::from(std::ffi::OsStr::from_bytes(home.to_bytes())));
}
let passwd = unsafe { passwd.assume_init() };
if passwd.pw_dir.is_null() {
return Err(RetryError::Stop(anyhow::anyhow!(
"effective user {uid} has no home directory"
)));
}
let home = unsafe { CStr::from_ptr(passwd.pw_dir) };
if home.to_bytes().is_empty() {
return Err(RetryError::Stop(anyhow::anyhow!(
"effective user {uid} has an empty home directory"
)));
}
Ok(PathBuf::from(std::ffi::OsStr::from_bytes(home.to_bytes())))
},
)
}
pub const fn move_decision(
+79 -1
View File
@@ -19,6 +19,14 @@ pub struct RetryPolicy {
}
impl RetryPolicy {
pub const fn fixed(attempts: NonZeroUsize, delay: Duration) -> Self {
Self {
attempts,
backoff: Backoff::Fixed(delay),
jitter: false,
}
}
fn delays(self) -> ExponentialBackoff {
let (base, cap, factor) = match self.backoff {
Backoff::Fixed(delay) => (delay, delay, 1.0),
@@ -38,7 +46,6 @@ impl RetryPolicy {
pub enum RetryError<E> {
Retry(E),
#[allow(dead_code)]
Stop(E),
}
@@ -89,6 +96,49 @@ pub async fn try_strategies<
.await
}
pub async fn retry_with_state<S: Send, T: Send, E: Send>(
policy: RetryPolicy,
mut state: S,
mut operation: impl for<'a> FnMut(&'a mut S, usize) -> futures::future::BoxFuture<'a, Result<T, RetryError<E>>> + Send,
) -> Result<T, E> {
let mut result = operation(&mut state, 0).await;
for (index, delay) in policy.delays().enumerate() {
match result {
Ok(_) | Err(RetryError::Stop(_)) => break,
Err(RetryError::Retry(_)) => {
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
}
}
result = operation(&mut state, index + 1).await;
}
result.map_err(|error| match error {
RetryError::Retry(error) | RetryError::Stop(error) => error,
})
}
pub fn retry_sync<T, E>(
policy: RetryPolicy,
mut operation: impl FnMut(usize) -> Result<T, RetryError<E>>,
) -> Result<T, E> {
let mut result = operation(0);
for (index, delay) in policy.delays().enumerate() {
match result {
Ok(_) | Err(RetryError::Stop(_)) => break,
Err(RetryError::Retry(_)) => {
if !delay.is_zero() {
std::thread::sleep(delay);
}
}
}
result = operation(index + 1);
}
result.map_err(|error| match error {
RetryError::Retry(error) | RetryError::Stop(error) => error,
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -141,6 +191,34 @@ mod tests {
assert_eq!(visited, ["direct", "clash", "system"]);
}
#[tokio::test]
async fn stateful_and_sync_retries_preserve_progress_and_stop() {
let policy = RetryPolicy::fixed(NonZeroUsize::MIN.saturating_add(2), Duration::ZERO);
let result = retry_with_state(policy, Vec::new(), |visited, attempt| {
Box::pin(async move {
visited.push(attempt);
if visited.len() == 3 {
Ok(visited.clone())
} else {
Err(RetryError::Retry(()))
}
})
})
.await;
assert_eq!(result, Ok(vec![0, 1, 2]));
let mut visited = Vec::new();
let result = retry_sync(policy, |attempt| {
visited.push(attempt);
if attempt == 1 {
Err::<(), _>(RetryError::Stop("terminal"))
} else {
Err(RetryError::Retry("retry"))
}
});
assert_eq!(result, Err("terminal"));
assert_eq!(visited, [0, 1]);
}
#[test]
fn backoff_keeps_the_attempt_budget_and_exponential_cap() {
let policy = RetryPolicy {
+28 -16
View File
@@ -54,22 +54,34 @@ pub async fn check_singleton() -> Result<SingletonDisposition> {
.context("failed to initialize singleton lock")?;
if !try_lock_instance(&lock).context("failed to acquire singleton lock")? {
let deadline = std::time::Instant::now() + Duration::from_secs(20);
loop {
if std::time::Instant::now() >= deadline {
bail!("another app instance is starting");
}
// A restarting instance releases this lock only after spawning this
// process, and its embedded server is already down, so waiting on
// the notify probe alone can never promote this instance.
if try_lock_instance(&lock).context("failed to acquire singleton lock")? {
break;
}
if let Ok(record) = read_instance_record(&record_path)
&& notify_existing_instance(&record).await
{
return Ok(SingletonDisposition::Secondary);
}
tokio::time::sleep(Duration::from_millis(50)).await;
use crate::utils::retry::{RetryError, RetryPolicy, retry};
let secondary = retry(
RetryPolicy::fixed(
std::num::NonZeroUsize::MIN.saturating_add(400),
Duration::from_millis(50),
),
|_| async {
if std::time::Instant::now() >= deadline {
return Err(RetryError::Stop(anyhow::anyhow!("another app instance is starting")));
}
// A restarting primary releases the lock after spawning us, with its server down.
if try_lock_instance(&lock)
.context("failed to acquire singleton lock")
.map_err(RetryError::Stop)?
{
return Ok(false);
}
if let Ok(record) = read_instance_record(&record_path)
&& notify_existing_instance(&record).await
{
return Ok(true);
}
Err(RetryError::Retry(anyhow::anyhow!("another app instance is starting")))
},
)
.await?;
if secondary {
return Ok(SingletonDisposition::Secondary);
}
}