rust-gamedig/src/utils.rs
Tom c3281be419
[Protocol] Retry failed requests (#95)
* Add retry count to TimeoutSettings

This can be used to specify how many times to re-send requests that
fail. The default value is "1" so the if the first request fails, 1 more
attempt will be made.

* Add retries to valve queries

* [Protocol] &Optional<TimeoutSettings> add get_retries_or_default

Allow fetching the number of retries or the default retries value from a
borrowed optional TimeoutSettings.

* [Protocol] Add retries to minecraft protocol

* [Protocol] Add retries to quake

* [Protocol] Add retries to gamespy

* [Protocol] Update TimeoutSettings docs, and change default retries to 0

* Remove logging from retry_on_timeout

* [Protocol] TimeoutSettings make retries non-optional

* [Protocol] Move retry logic into lower level query functions

Retries are now implemented as wrappers on the single function that
would need to be retried on timeout.

In order to avoid cloning of TimeoutSettings, Socket::apply_timeouts()
was changed to accept a borrowed TimeoutSettings. And extra helpers were
added to the TimeoutSettings impl to reduce repetition.

* [Examples] Add retries to the generic example

* Also retry on PacketSend error

Sending packets could also timeout and until error_generic_member_access
is stable we have no way of determining the type of the underlying
`std::error::Error`.

* Add retry unit tests

* [Docs] Update changelog
2023-09-25 22:12:54 +03:00

108 lines
3.1 KiB
Rust

use crate::GDErrorKind::{PacketOverflow, PacketReceive, PacketSend, PacketUnderflow};
use crate::GDResult;
use std::cmp::Ordering;
pub fn error_by_expected_size(expected: usize, size: usize) -> GDResult<()> {
match size.cmp(&expected) {
Ordering::Greater => Err(PacketOverflow.into()),
Ordering::Less => Err(PacketUnderflow.into()),
Ordering::Equal => Ok(()),
}
}
pub const fn u8_lower_upper(n: u8) -> (u8, u8) { (n & 15, n >> 4) }
/// Run a closure `retry_count+1` times while it returns [PacketReceive] or
/// [PacketSend] errors, returning the first success, other Error, or after
/// `retry_count+1` tries the last [PacketReceive] or [PacketSend] error.
pub fn retry_on_timeout<T>(mut retry_count: usize, mut fetch: impl FnMut() -> GDResult<T>) -> GDResult<T> {
let mut last_err = PacketReceive.context("Retry count was 0");
retry_count += 1;
while retry_count > 0 {
last_err = match fetch() {
Ok(r) => return Ok(r),
Err(e) if e.kind == PacketReceive || e.kind == PacketSend => e,
Err(e) => return Err(e),
};
retry_count -= 1;
}
Err(last_err)
}
#[cfg(test)]
mod tests {
use super::retry_on_timeout;
use crate::{
GDErrorKind::{PacketBad, PacketReceive, PacketSend},
GDResult,
};
#[test]
fn u8_lower_upper() {
assert_eq!(super::u8_lower_upper(171), (11, 10));
}
#[test]
fn error_by_expected_size() {
assert!(super::error_by_expected_size(69, 69).is_ok());
assert!(super::error_by_expected_size(69, 68).is_err());
assert!(super::error_by_expected_size(69, 70).is_err());
}
#[test]
fn retry_success_on_first() {
let r = retry_on_timeout(0, || Ok(()));
assert!(r.is_ok());
}
#[test]
fn retry_no_success() {
let r: GDResult<()> = retry_on_timeout(100, || Err(PacketSend.context("test")));
assert!(r.is_err());
assert_eq!(r.unwrap_err().kind, PacketSend);
}
#[test]
fn retry_success_on_third() {
let mut i = 0u8;
let r = retry_on_timeout(2, || {
i += 1;
if i < 3 {
Err(PacketReceive.context("test"))
} else {
Ok(())
}
});
assert!(r.is_ok());
}
#[test]
fn retry_success_on_third_but_less_retries() {
let mut i = 0u8;
let r = retry_on_timeout(1, || {
i += 1;
if i < 3 {
Err(PacketReceive.context("test"))
} else {
Ok(())
}
});
assert!(r.is_err());
assert_eq!(r.unwrap_err().kind, PacketReceive);
}
#[test]
fn retry_with_non_timeout_error() {
let mut i = 0u8;
let r = retry_on_timeout(50, || {
i += 1;
match i {
1 => Err(PacketSend.context("test")),
2 => Err(PacketBad.context("test")),
_ => Ok(()),
}
});
assert!(r.is_err());
assert_eq!(r.unwrap_err().kind, PacketBad);
}
}