send service errors back over rpc instead of dropping the connection
the call outcome now rides the frame as Result<Response, WireError>, so a rejection reaches the caller with its message and the pooled connection stays up. only a real transport failure closes it now; a malformed request comes back as a rejection too.
This commit is contained in:
parent
00f68e804b
commit
5c35bef330
5 changed files with 81 additions and 7 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::error::{RpcError, RpcResult};
|
use crate::error::{RpcError, RpcResult, WireError};
|
||||||
use crate::server::{read_frame, write_frame};
|
use crate::server::{read_frame, write_frame};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
@ -56,7 +56,10 @@ where
|
||||||
read_frame(stream).await
|
read_frame(stream).await
|
||||||
};
|
};
|
||||||
match exchange.await {
|
match exchange.await {
|
||||||
Ok(payload) => return Ok(postcard::from_bytes(&payload)?),
|
Ok(payload) => {
|
||||||
|
let outcome: Result<Res, WireError> = postcard::from_bytes(&payload)?;
|
||||||
|
return outcome.map_err(RpcError::from);
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
*guard = None;
|
*guard = None;
|
||||||
last_error = Some(error);
|
last_error = Some(error);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
pub type RpcResult<T> = Result<T, RpcError>;
|
pub type RpcResult<T> = Result<T, RpcError>;
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum RpcError {
|
pub enum RpcError {
|
||||||
|
|
@ -14,3 +15,28 @@ pub enum RpcError {
|
||||||
#[error("service is unavailable: {0}")]
|
#[error("service is unavailable: {0}")]
|
||||||
Unavailable(String),
|
Unavailable(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
|
||||||
|
pub enum WireError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Rejected(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
Unavailable(String),
|
||||||
|
}
|
||||||
|
impl From<RpcError> for WireError {
|
||||||
|
fn from(error: RpcError) -> Self {
|
||||||
|
match error {
|
||||||
|
RpcError::Rejected(message) => WireError::Rejected(message),
|
||||||
|
RpcError::Unavailable(message) => WireError::Unavailable(message),
|
||||||
|
other => WireError::Unavailable(other.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<WireError> for RpcError {
|
||||||
|
fn from(error: WireError) -> Self {
|
||||||
|
match error {
|
||||||
|
WireError::Rejected(message) => RpcError::Rejected(message),
|
||||||
|
WireError::Unavailable(message) => RpcError::Unavailable(message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ pub mod server;
|
||||||
pub mod wire;
|
pub mod wire;
|
||||||
pub use auth::{AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, Session};
|
pub use auth::{AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, Session};
|
||||||
pub use client::RpcClient;
|
pub use client::RpcClient;
|
||||||
pub use error::{RpcError, RpcResult};
|
pub use error::{RpcError, RpcResult, WireError};
|
||||||
pub use game::{AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind};
|
pub use game::{AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind};
|
||||||
pub use server::{serve, RpcService};
|
pub use server::{serve, RpcService};
|
||||||
pub use wire::WireMessage;
|
pub use wire::WireMessage;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::error::{RpcError, RpcResult};
|
use crate::error::{RpcError, RpcResult, WireError};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
@ -31,9 +31,11 @@ async fn handle_connection<S: RpcService>(mut stream: TcpStream, service: Arc<S>
|
||||||
Err(RpcError::Closed) => return Ok(()),
|
Err(RpcError::Closed) => return Ok(()),
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
let decoded: S::Request = postcard::from_bytes(&request)?;
|
let outcome: Result<S::Response, WireError> = match postcard::from_bytes::<S::Request>(&request) {
|
||||||
let response = service.call(decoded).await?;
|
Ok(decoded) => service.call(decoded).await.map_err(WireError::from),
|
||||||
let encoded = postcard::to_allocvec(&response)?;
|
Err(error) => Err(WireError::Rejected(format!("undecodable request: {error}"))),
|
||||||
|
};
|
||||||
|
let encoded = postcard::to_allocvec(&outcome)?;
|
||||||
write_frame(&mut stream, &encoded).await?;
|
write_frame(&mut stream, &encoded).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
crates/service-rpc/tests/error_channel.rs
Normal file
43
crates/service-rpc/tests/error_channel.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use service_rpc::{serve, RpcClient, RpcError, RpcResult, RpcService};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
struct Doubler;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl RpcService for Doubler {
|
||||||
|
type Request = i32;
|
||||||
|
type Response = i32;
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"doubler"
|
||||||
|
}
|
||||||
|
async fn call(&self, request: i32) -> RpcResult<i32> {
|
||||||
|
if request == 0 {
|
||||||
|
return Err(RpcError::Rejected("zero is not allowed".to_owned()));
|
||||||
|
}
|
||||||
|
Ok(request * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_service_error_reaches_the_caller_and_keeps_the_connection() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||||
|
let endpoint = listener.local_addr().expect("addr").to_string();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = serve(listener, Arc::new(Doubler)).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let client: RpcClient<i32, i32> = RpcClient::with_pool_size(endpoint, 1);
|
||||||
|
|
||||||
|
assert_eq!(client.call(&5).await.expect("ok call"), 10);
|
||||||
|
|
||||||
|
match client.call(&0).await {
|
||||||
|
Err(RpcError::Rejected(message)) => assert_eq!(message, "zero is not allowed"),
|
||||||
|
other => panic!("expected a rejection, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.call(&7).await.expect("connection survived the error"),
|
||||||
|
14
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue