Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Protocol] Retry failed requests #95

Merged
merged 14 commits into from
Sep 25, 2023
Merged
10 changes: 8 additions & 2 deletions examples/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ mod test {
const ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);

fn test_game(game_name: &str) {
let timeout_settings =
Some(TimeoutSettings::new(Some(Duration::from_nanos(1)), Some(Duration::from_nanos(1))).unwrap());
let timeout_settings = Some(
TimeoutSettings::new(
Some(Duration::from_nanos(1)),
Some(Duration::from_nanos(1)),
None,
)
.unwrap(),
);
assert!(generic_query(game_name, &ADDR, None, timeout_settings, None).is_err());
}

Expand Down
6 changes: 5 additions & 1 deletion src/protocols/gamespy/protocols/one/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::buffer::Utf8Decoder;
use crate::protocols::gamespy::common::has_password;
use crate::GDErrorKind::TypeParse;

use crate::utils::retry_on_timeout;
use crate::{
buffer::Buffer,
protocols::{
Expand Down Expand Up @@ -198,7 +199,10 @@ pub fn query_vars(
/// Providing None to the timeout settings results in using the default values.
/// (TimeoutSettings::[default](TimeoutSettings::default)).
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let mut server_vars = query_vars(address, timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut server_vars = retry_on_timeout(retry_count, move || {
query_vars(address, timeout_settings.clone())
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The clone here is undesirable but needed because there is no struct to take ownership of it here.

})?;

let players_maximum: u32 = server_vars
.remove("maxplayers")
Expand Down
4 changes: 3 additions & 1 deletion src/protocols/gamespy/protocols/three/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{Player, Response, Team};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
Expand Down Expand Up @@ -340,8 +341,9 @@ fn parse_players_and_teams(packets: Vec<Vec<u8>>) -> GDResult<(Vec<Player>, Vec<
/// Providing None to the timeout settings results in using the default values.
/// (TimeoutSettings::[default](TimeoutSettings::default)).
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut client = GameSpy3::new(address, timeout_settings)?;
let packets = client.get_server_packets()?;
let packets = retry_on_timeout(retry_count, move || client.get_server_packets())?;

let (mut server_vars, remaining_data) = data_to_map(packets.get(0).ok_or(GDErrorKind::PacketBad)?)?;

Expand Down
4 changes: 3 additions & 1 deletion src/protocols/gamespy/protocols/two/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::two::{Player, Response, Team};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use byteorder::BigEndian;
Expand Down Expand Up @@ -155,8 +156,9 @@ fn get_players(bufferer: &mut Buffer<BigEndian>) -> GDResult<Vec<Player>> {
}

pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut client = GameSpy2::new(address, timeout_settings)?;
let (data, buf_index) = client.request_data()?;
let (data, buf_index) = retry_on_timeout(retry_count, move || client.request_data())?;

let mut buffer = Buffer::<BigEndian>::new(&data);
buffer.move_cursor(buf_index as isize)?;
Expand Down
6 changes: 4 additions & 2 deletions src/protocols/minecraft/protocol/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
types::TimeoutSettings,
},
socket::{Socket, UdpSocket},
utils::error_by_expected_size,
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, TypeParse},
GDResult,
};
Expand Down Expand Up @@ -97,6 +97,8 @@ impl Bedrock {
}

pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Self::new(address, timeout_settings)?.get_info()
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut mc_query = Self::new(address, timeout_settings)?;
retry_on_timeout(retry_count, move || mc_query.get_info())
}
}
8 changes: 7 additions & 1 deletion src/protocols/minecraft/protocol/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::retry_on_timeout,
GDErrorKind::{JsonParse, PacketBad},
GDResult,
};
Expand Down Expand Up @@ -160,6 +161,11 @@ impl Java {
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings, request_settings)?.get_info()
let retry_count = timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
let mut mc_query = Self::new(address, timeout_settings, request_settings)?;
retry_on_timeout(retry_count, move || mc_query.get_info())
}
}
6 changes: 4 additions & 2 deletions src/protocols/minecraft/protocol/legacy_bv1_8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::error_by_expected_size,
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
Expand Down Expand Up @@ -65,6 +65,8 @@ impl LegacyBV1_8 {
}

pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut mc_query = Self::new(address, timeout_settings)?;
retry_on_timeout(retry_count, move || mc_query.get_info())
}
}
6 changes: 4 additions & 2 deletions src/protocols/minecraft/protocol/legacy_v1_4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::error_by_expected_size,
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
Expand Down Expand Up @@ -68,6 +68,8 @@ impl LegacyV1_4 {
}

pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut mc_query = Self::new(address, timeout_settings)?;
retry_on_timeout(retry_count, move || mc_query.get_info())
}
}
6 changes: 4 additions & 2 deletions src/protocols/minecraft/protocol/legacy_v1_6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
types::TimeoutSettings,
},
socket::{Socket, TcpSocket},
utils::error_by_expected_size,
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
Expand Down Expand Up @@ -102,6 +102,8 @@ impl LegacyV1_6 {
}

pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let mut mc_query = Self::new(address, timeout_settings)?;
retry_on_timeout(retry_count, move || mc_query.get_info())
}
}
6 changes: 5 additions & 1 deletion src/protocols/quake/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::quake::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::retry_on_timeout;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use std::collections::HashMap;
Expand Down Expand Up @@ -95,7 +96,10 @@ pub fn client_query<Client: QuakeClient>(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response<Client::Player>> {
let data = get_data::<Client>(address, timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
let data = retry_on_timeout(retry_count, || {
get_data::<Client>(address, timeout_settings.clone())
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Undesirable clone (see before)

})?;
let mut bufferer = Buffer::<LittleEndian>::new(&data);

let mut server_vars = get_server_values(&mut bufferer)?;
Expand Down
34 changes: 29 additions & 5 deletions src/protocols/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,19 @@ pub struct CommonPlayerJson<'a> {
pub struct TimeoutSettings {
read: Option<Duration>,
write: Option<Duration>,
retries: usize,
}

impl TimeoutSettings {
/// Construct new settings, passing None will block indefinitely.
/// Passing zero Duration throws GDErrorKind::[InvalidInput].
pub fn new(read: Option<Duration>, write: Option<Duration>) -> GDResult<Self> {
///
/// The retry count is the number of extra tries once the original request
/// fails, so a value of "0" will only make a single request, whereas
/// "1" will try the request again once if it fails.
/// The retry count is per-request so for multi-request queries (valve) if a
/// single part fails that part can be retried up to `retries` times.
pub fn new(read: Option<Duration>, write: Option<Duration>, retries: Option<usize>) -> GDResult<Self> {
if let Some(read_duration) = read {
if read_duration == Duration::new(0, 0) {
return Err(InvalidInput.context("Read duration must not be 0"));
Expand All @@ -164,22 +171,39 @@ impl TimeoutSettings {
}
}

Ok(Self { read, write })
Ok(Self {
read,
write,
retries: retries.unwrap_or(0),
})
}

/// Get the read timeout.
pub const fn get_read(&self) -> Option<Duration> { self.read }

/// Get the write timeout.
pub const fn get_write(&self) -> Option<Duration> { self.write }

/// Get number of retries
pub const fn get_retries(&self) -> usize { self.retries }

/// Get the number of retries if there are timeout settings else fall back
/// to the default
pub fn get_retries_or_default(timeout_settings: &Option<TimeoutSettings>) -> usize {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This maybe shouldn't be in impl TimeoutSettings but you cannot implement functions on external types (Optional) so I put it here for organization. (Also probably doesn't need to be pub). Let me know what you think.

Copy link
Member

Choose a reason for hiding this comment

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

I think thats fine for now.

timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries())
}
}

impl Default for TimeoutSettings {
/// Default values are 4 seconds for both read and write.
/// Default values are 4 seconds for both read and write, no retries.
fn default() -> Self {
Self {
read: Some(Duration::from_secs(4)),
write: Some(Duration::from_secs(4)),
retries: 0,
}
}
}
Expand Down Expand Up @@ -273,7 +297,7 @@ mod tests {
let write_duration = Duration::from_secs(2);

// Create new TimeoutSettings with the valid durations
let timeout_settings = TimeoutSettings::new(Some(read_duration), Some(write_duration))?;
let timeout_settings = TimeoutSettings::new(Some(read_duration), Some(write_duration), None)?;

// Verify that the get_read and get_write methods return the expected values
assert_eq!(timeout_settings.get_read(), Some(read_duration));
Expand All @@ -291,7 +315,7 @@ mod tests {

// Try to create new TimeoutSettings with the zero read duration (this should
// fail)
let result = TimeoutSettings::new(Some(read_duration), Some(write_duration));
let result = TimeoutSettings::new(Some(read_duration), Some(write_duration), None);

// Verify that the function returned an error and that the error type is
// InvalidInput
Expand Down
24 changes: 19 additions & 5 deletions src/protocols/valve/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
},
},
socket::{Socket, UdpSocket},
utils::u8_lower_upper,
utils::{retry_on_timeout, u8_lower_upper},
GDErrorKind::{BadGame, Decompress, UnknownEnumCast},
GDResult,
};
Expand Down Expand Up @@ -120,16 +120,24 @@ impl SplitPacket {

pub(crate) struct ValveProtocol {
socket: UdpSocket,
retry_count: usize,
}

static PACKET_SIZE: usize = 6144;

impl ValveProtocol {
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address)?;
let retry_count = timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
socket.apply_timeout(timeout_settings)?;

Ok(Self { socket })
Ok(Self {
socket,
retry_count,
})
}

fn receive(&mut self, engine: &Engine, protocol: u8, buffer_size: usize) -> GDResult<Packet> {
Expand Down Expand Up @@ -260,7 +268,9 @@ impl ValveProtocol {

/// Get the server information's.
fn get_server_info(&mut self, engine: &Engine) -> GDResult<ServerInfo> {
let data = self.get_kind_request_data(engine, 0, Request::Info)?;
let data = retry_on_timeout(self.retry_count, || {
self.get_kind_request_data(engine, 0, Request::Info)
})?;
let mut buffer = Buffer::<LittleEndian>::new(&data);

if let Engine::GoldSrc(force) = engine {
Expand Down Expand Up @@ -354,7 +364,9 @@ impl ValveProtocol {

/// Get the server player's.
fn get_server_players(&mut self, engine: &Engine, protocol: u8) -> GDResult<Vec<ServerPlayer>> {
let data = self.get_kind_request_data(engine, protocol, Request::Players)?;
let data = retry_on_timeout(self.retry_count, || {
self.get_kind_request_data(engine, protocol, Request::Players)
})?;
let mut buffer = Buffer::<LittleEndian>::new(&data);

let count = buffer.read::<u8>()? as usize;
Expand Down Expand Up @@ -383,7 +395,9 @@ impl ValveProtocol {

/// Get the server's rules.
fn get_server_rules(&mut self, engine: &Engine, protocol: u8) -> GDResult<HashMap<String, String>> {
let data = self.get_kind_request_data(engine, protocol, Request::Rules)?;
let data = retry_on_timeout(self.retry_count, || {
self.get_kind_request_data(engine, protocol, Request::Rules)
})?;
let mut buffer = Buffer::<LittleEndian>::new(&data);

let count = buffer.read::<u16>()? as usize;
Expand Down
17 changes: 16 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::GDErrorKind::{PacketOverflow, PacketUnderflow};
use crate::GDErrorKind::{PacketOverflow, PacketReceive, PacketUnderflow};
use crate::GDResult;
use std::cmp::Ordering;

Expand All @@ -12,6 +12,21 @@ pub fn error_by_expected_size(expected: usize, size: usize) -> GDResult<()> {

pub const fn u8_lower_upper(n: u8) -> (u8, u8) { (n & 15, n >> 4) }

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,
Err(e) => return Err(e),
};
retry_count -= 1;
println!("Retry");
}
Err(last_err)
}

#[cfg(test)]
mod tests {
#[test]
Expand Down
Loading