mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
refactor(utils): migrate remaining bounded retry loops
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user