use crate::deadline::instant_from_unix_ms;
use crate::error::{ErrorKind, RpcError};
use crate::frame::{AsyncFrameReader, write_frame};
use crate::jsonrpc::{Envelope, Notification, Request, RequestId, Response};
use crate::protocol::{
CancelParams, EmptyParams, InitializeParams, InitializeResult, Limits, Product, rpc,
};
use crate::{ABSOLUTE_MAX_FRAME_BYTES, Error, Result};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
use tokio::task::JoinSet;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use zeroize::Zeroizing;
pub type RpcResult<T> = std::result::Result<T, RpcError>;
const MAX_ABANDONED_CALLBACKS: usize = crate::MAX_IN_FLIGHT * 4;
const COMMIT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
pub struct RequestContext {
pub request_id: RequestId,
pub deadline: Instant,
pub cancellation: CancellationToken,
pub peer: Peer,
}
#[derive(Debug, Clone)]
pub struct Peer {
inner: Arc<PeerInner>,
}
#[derive(Debug)]
struct PeerInner {
writer: mpsc::WeakSender<WriterCommand>,
calls: Mutex<PeerCalls>,
request_order: Mutex<()>,
next_id: AtomicU64,
limit: AtomicUsize,
semaphore: std::sync::RwLock<Arc<Semaphore>>,
capabilities: std::sync::RwLock<HashSet<String>>,
}
#[derive(Debug, Default)]
struct PeerCalls {
pending: HashMap<RequestId, oneshot::Sender<Response>>,
abandoned: HashMap<RequestId, OwnedSemaphorePermit>,
}
impl Peer {
fn new(writer: &mpsc::Sender<WriterCommand>) -> Self {
Self {
inner: Arc::new(PeerInner {
writer: writer.downgrade(),
calls: Mutex::new(PeerCalls::default()),
request_order: Mutex::new(()),
next_id: AtomicU64::new(1),
limit: AtomicUsize::new(ABSOLUTE_MAX_FRAME_BYTES),
semaphore: std::sync::RwLock::new(Arc::new(Semaphore::new(1))),
capabilities: std::sync::RwLock::new(HashSet::new()),
}),
}
}
pub fn detached() -> Self {
let (writer, _) = mpsc::channel(1);
Self::new(&writer)
}
pub fn supports(&self, method: &str) -> bool {
self.inner
.capabilities
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.contains(method)
}
pub async fn call<P: serde::Serialize, R: serde::de::DeserializeOwned>(
&self,
method: &str,
params: &P,
context: &RequestContext,
) -> RpcResult<R> {
if context.cancellation.is_cancelled() {
return Err(RpcError::new(ErrorKind::Cancelled));
}
if !self.supports(method) {
return Err(RpcError::new(ErrorKind::CapabilityRequired));
}
let semaphore = self
.inner
.semaphore
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone();
let permit = semaphore
.try_acquire_owned()
.map_err(|_| RpcError::unavailable(None))?;
let params =
serde_json::to_value(params).map_err(|_| RpcError::new(ErrorKind::Internal))?;
let deadline_unix_ms =
crate::deadline::clamp_unix_ms(crate::deadline::unix_ms_from_instant(context.deadline));
let order = tokio::select! {
biased;
_ = context.cancellation.cancelled() => return Err(RpcError::new(ErrorKind::Cancelled)),
order = tokio::time::timeout_at(context.deadline, self.inner.request_order.lock()) =>
order.map_err(|_| RpcError::new(ErrorKind::DeadlineExceeded))?,
};
let id = RequestId::new(self.inner.next_id.fetch_add(1, Ordering::Relaxed))
.map_err(|_| RpcError::new(ErrorKind::Internal))?;
let request = Request::new(id, method, deadline_unix_ms, params)
.map_err(|_| RpcError::new(ErrorKind::Internal))?
.with_parent_request_id(context.request_id);
let limit = self.inner.limit.load(Ordering::Acquire);
let payload = Zeroizing::new(
Envelope::Request(request)
.to_vec()
.map_err(|_| RpcError::new(ErrorKind::Internal))?,
);
if payload.is_empty() || payload.len() > limit {
return Err(RpcError::new(ErrorKind::MessageTooLarge));
}
let Some(writer) = self.inner.writer.upgrade() else {
return Err(RpcError::new(ErrorKind::Unavailable));
};
let (sender, receiver) = oneshot::channel();
self.inner.calls.lock().await.pending.insert(id, sender);
let command = WriterCommand {
payload,
limit,
committed: None,
};
let queued = tokio::select! {
biased;
_ = context.cancellation.cancelled() => {
self.inner.calls.lock().await.pending.remove(&id);
return Err(RpcError::new(ErrorKind::Cancelled));
}
_ = tokio::time::sleep_until(context.deadline) => {
self.inner.calls.lock().await.pending.remove(&id);
return Err(RpcError::new(ErrorKind::DeadlineExceeded));
}
queued = writer.send(command) => queued,
};
if queued.is_err() {
self.inner.calls.lock().await.pending.remove(&id);
return Err(RpcError::new(ErrorKind::Unavailable));
}
drop(order);
let response = tokio::select! {
biased;
_ = context.cancellation.cancelled() => {
self.abandon_call(id, permit).await;
return Err(RpcError::new(ErrorKind::Cancelled));
}
response = tokio::time::timeout_at(context.deadline, receiver) => response,
};
match response {
Ok(Ok(Response::Success(response))) => serde_json::from_value(response.result)
.map_err(|_| RpcError::new(ErrorKind::OperationFailed)),
Ok(Ok(Response::Error(response))) => Err(response.error),
Ok(Err(_)) => Err(RpcError::new(ErrorKind::Unavailable)),
Err(_) => {
self.abandon_call(id, permit).await;
Err(RpcError::new(ErrorKind::DeadlineExceeded))
}
}
}
async fn deliver(&self, response: Response) -> bool {
let Some(id) = response.id() else {
return false;
};
let mut calls = self.inner.calls.lock().await;
match calls.pending.remove(&id) {
Some(sender) => {
calls.abandoned.remove(&id);
let _ = sender.send(response);
true
}
None => calls.abandoned.remove(&id).is_some(),
}
}
async fn abandon_call(&self, id: RequestId, permit: OwnedSemaphorePermit) {
let mut calls = self.inner.calls.lock().await;
if calls.pending.remove(&id).is_some() && calls.abandoned.len() < MAX_ABANDONED_CALLBACKS {
calls.abandoned.insert(id, permit);
}
}
async fn fail_all(&self) {
let mut calls = self.inner.calls.lock().await;
calls.pending.clear();
calls.abandoned.clear();
}
}
#[async_trait]
pub trait ApplicationHandler: Send + Sync + 'static {
fn protocol(&self) -> &'static str;
fn versions(&self) -> &'static [u32] {
&[1]
}
fn capabilities(&self) -> Vec<String>;
fn validate_capabilities(&self, _capabilities: &[String]) -> RpcResult<()> {
Ok(())
}
async fn initialize(&self, context: &RequestContext, application: Value) -> RpcResult<Value>;
async fn call(&self, context: RequestContext, method: &str, params: Value) -> RpcResult<Value>;
async fn request_finished(&self, _request_id: RequestId, _committed: bool) {}
async fn shutdown(&self) {}
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub product: Product,
pub limits: Limits,
pub startup_timeout: Duration,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
product: Product {
name: "secretspec-ipc".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
limits: Limits {
max_frame_bytes: ABSOLUTE_MAX_FRAME_BYTES,
max_in_flight: 8,
},
startup_timeout: Duration::from_secs(5),
}
}
}
struct WriterCommand {
payload: Zeroizing<Vec<u8>>,
limit: usize,
committed: Option<oneshot::Sender<std::result::Result<(), ()>>>,
}
pub async fn serve<R, W, H>(
reader: R,
mut writer: W,
handler: Arc<H>,
config: ServerConfig,
) -> Result<()>
where
R: AsyncRead + Unpin + Send + 'static,
W: AsyncWrite + Unpin + Send + 'static,
H: ApplicationHandler,
{
config.product.validate()?;
config.limits.validate()?;
let mut reader = AsyncFrameReader::new(reader);
let (writer_tx, mut writer_rx) =
mpsc::channel::<WriterCommand>(config.limits.max_in_flight + 2);
let disconnected = CancellationToken::new();
let writer_disconnected = disconnected.clone();
let mut writer_task = tokio::spawn(async move {
while let Some(command) = writer_rx.recv().await {
let outcome = write_frame(&mut writer, &command.payload, command.limit)
.await
.map_err(|_| ());
if let Some(committed) = command.committed {
let _ = committed.send(outcome);
}
if outcome.is_err() {
writer_disconnected.cancel();
break;
}
}
use tokio::io::AsyncWriteExt;
let _ = writer.shutdown().await;
});
let peer = Peer::new(&writer_tx);
let mut active_limit = ABSOLUTE_MAX_FRAME_BYTES;
let mut initialized = false;
let mut advertised_capabilities = HashSet::new();
let mut last_seen_id: Option<RequestId> = None;
let inflight: Arc<Mutex<HashMap<RequestId, CancellationToken>>> =
Arc::new(Mutex::new(HashMap::new()));
let mut tasks = JoinSet::new();
let mut semaphore = Arc::new(Semaphore::new(1));
let mut shutdown_request: Option<(RequestId, Instant)> = None;
let mut drained = false;
let mut fatal: Option<Error> = None;
'session: loop {
if shutdown_request.is_some() && tasks.is_empty() {
drained = true;
break;
}
let frame = {
let read = reader.read_frame(active_limit);
tokio::pin!(read);
loop {
tokio::select! {
biased;
_ = disconnected.cancelled() => break 'session,
_ = async {
match shutdown_request {
Some((_, deadline)) => tokio::time::sleep_until(deadline).await,
None => std::future::pending().await,
}
} => {
drained = true;
break 'session;
}
joined = tasks.join_next(), if !tasks.is_empty() => {
if joined.is_some_and(|result| result.is_err()) {
fatal = Some(Error::Protocol("application task failed"));
break 'session;
}
if shutdown_request.is_some() && tasks.is_empty() {
drained = true;
break 'session;
}
}
frame = &mut read => match frame {
Ok(frame) => break frame,
Err(error) => {
fatal = Some(error);
break 'session;
}
},
}
}
};
let Some(frame) = frame else {
break;
};
let envelope = match Envelope::parse_classified(&frame) {
Ok(envelope) => envelope,
Err((_, kind)) => {
let error = Response::error(None, RpcError::new(kind));
let _ = commit(&writer_tx, error, active_limit).await;
break;
}
};
match envelope {
Envelope::Response(response) => {
if !initialized || !peer.deliver(response).await {
break;
}
}
Envelope::Notification(notification) => {
handle_notification(notification, &inflight).await;
}
Envelope::Request(request) => {
if last_seen_id.is_some_and(|last| request.id <= last) {
let response =
Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidRequest));
let _ = commit(&writer_tx, response, active_limit).await;
break;
}
last_seen_id = Some(request.id);
if !initialized {
if request.method == rpc::DISCOVER {
if let Err(error) = discover(
&request,
handler.as_ref(),
&config,
&writer_tx,
active_limit,
)
.await
{
fatal = Some(error);
break;
}
continue;
}
if request.method != rpc::INITIALIZE {
let response = Response::error(
Some(request.id),
RpcError::new(ErrorKind::InvalidRequest),
);
let _ = commit(&writer_tx, response, active_limit).await;
break;
}
let selected = match initialize(
&request,
handler.as_ref(),
&config,
&writer_tx,
&peer,
&mut reader,
)
.await
{
Ok(selected) => selected,
Err(error) => {
fatal = Some(error);
break;
}
};
let Some((limits, capabilities)) = selected else {
break;
};
peer.inner
.limit
.store(limits.max_frame_bytes, Ordering::Release);
*peer
.inner
.semaphore
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) =
Arc::new(Semaphore::new(limits.max_in_flight));
active_limit = limits.max_frame_bytes;
semaphore = Arc::new(Semaphore::new(limits.max_in_flight));
advertised_capabilities = capabilities.into_iter().collect();
initialized = true;
continue;
}
if request.method == rpc::INITIALIZE {
let response =
Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidRequest));
let _ = commit(&writer_tx, response, active_limit).await;
break;
}
if request.method == rpc::DISCOVER {
if let Err(error) = discover(
&request,
handler.as_ref(),
&config,
&writer_tx,
active_limit,
)
.await
{
fatal = Some(error);
break;
}
continue;
}
if shutdown_request.is_some() {
let response = Response::error(Some(request.id), RpcError::unavailable(None));
let _ = commit(&writer_tx, response, active_limit).await;
continue;
}
if request.method == rpc::SHUTDOWN {
if serde_json::from_value::<EmptyParams>(request.params.clone()).is_err() {
let response = Response::error(
Some(request.id),
RpcError::new(ErrorKind::InvalidParams),
);
let _ = commit(&writer_tx, response, active_limit).await;
break;
}
shutdown_request = Some((request.id, request_deadline(&request)));
continue;
}
if !request.method.starts_with("resolver.")
&& !request.method.starts_with("provider.")
{
let response =
Response::error(Some(request.id), RpcError::new(ErrorKind::MethodNotFound));
let _ = commit(&writer_tx, response, active_limit).await;
continue;
}
if !advertised_capabilities.contains(&request.method) {
let response = Response::error(
Some(request.id),
RpcError::new(ErrorKind::CapabilityRequired),
);
let _ = commit(&writer_tx, response, active_limit).await;
continue;
}
let deadline = request_deadline(&request);
if deadline <= Instant::now() {
let response = Response::error(
Some(request.id),
RpcError::new(ErrorKind::DeadlineExceeded),
);
let _ = commit(&writer_tx, response, active_limit).await;
continue;
}
let permit = match semaphore.clone().try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
let response =
Response::error(Some(request.id), RpcError::unavailable(None));
let _ = commit(&writer_tx, response, active_limit).await;
continue;
}
};
let cancellation = CancellationToken::new();
inflight
.lock()
.await
.insert(request.id, cancellation.clone());
let context = RequestContext {
request_id: request.id,
deadline,
cancellation: cancellation.clone(),
peer: peer.clone(),
};
let task_handler = handler.clone();
let task_writer = writer_tx.clone();
let task_inflight = inflight.clone();
let task_disconnected = disconnected.clone();
tasks.spawn(async move {
let _permit = permit;
run_call(
request,
context,
task_handler,
task_writer,
task_inflight,
task_disconnected,
active_limit,
)
.await;
});
}
}
}
peer.fail_all().await;
for cancellation in inflight.lock().await.values() {
cancellation.cancel();
}
tasks.abort_all();
while tasks.join_next().await.is_some() {}
if initialized {
let _ = tokio::time::timeout(config.startup_timeout, handler.shutdown()).await;
}
if drained && let Some((id, deadline)) = shutdown_request {
let _ = tokio::time::timeout_at(
deadline,
commit(&writer_tx, Response::success(id, json!({})), active_limit),
)
.await;
}
drop(writer_tx);
if tokio::time::timeout(config.startup_timeout, &mut writer_task)
.await
.is_err()
{
writer_task.abort();
let _ = writer_task.await;
}
match fatal {
Some(error) => Err(error),
None => Ok(()),
}
}
async fn discover<H: ApplicationHandler>(
request: &Request,
handler: &H,
config: &ServerConfig,
writer: &mpsc::Sender<WriterCommand>,
limit: usize,
) -> Result<()> {
let _: EmptyParams = match serde_json::from_value(request.params.clone()) {
Ok(params) => params,
Err(_) => {
return commit(
writer,
Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidParams)),
limit,
)
.await;
}
};
if request_deadline(request) <= Instant::now() {
return commit(
writer,
Response::error(Some(request.id), RpcError::new(ErrorKind::DeadlineExceeded)),
limit,
)
.await;
}
let methods = handler.capabilities();
if crate::protocol::validate_capabilities(&methods).is_err()
|| handler.validate_capabilities(&methods).is_err()
{
return commit(
writer,
Response::error(Some(request.id), RpcError::new(ErrorKind::Internal)),
limit,
)
.await;
}
let document = crate::description::openrpc(
handler.protocol(),
handler.versions(),
&config.product,
&methods,
)?;
commit(writer, Response::success(request.id, document), limit).await
}
async fn initialize<R, H: ApplicationHandler>(
request: &Request,
handler: &H,
config: &ServerConfig,
writer: &mpsc::Sender<WriterCommand>,
peer: &Peer,
reader: &mut AsyncFrameReader<R>,
) -> Result<Option<(Limits, Vec<String>)>>
where
R: AsyncRead + Unpin,
{
let params: InitializeParams<Value> = match serde_json::from_value(request.params.clone()) {
Ok(params) => params,
Err(_) => {
let response =
Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidParams));
commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
return Ok(None);
}
};
if params.protocol != handler.protocol() {
let response = Response::error(
Some(request.id),
RpcError::new(ErrorKind::UnsupportedVersion),
);
commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
return Ok(None);
}
if params.validate_common(handler.protocol()).is_err() {
let response = Response::error(Some(request.id), RpcError::new(ErrorKind::InvalidParams));
commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
return Ok(None);
}
let Some(version) = params
.versions
.iter()
.copied()
.filter(|version| handler.versions().contains(version))
.max()
else {
let response = Response::error(
Some(request.id),
RpcError::new(ErrorKind::UnsupportedVersion),
);
commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
return Ok(None);
};
let capabilities = handler.capabilities();
if crate::protocol::validate_capabilities(&capabilities).is_err() {
let response = Response::error(Some(request.id), RpcError::new(ErrorKind::Internal));
commit(writer, response, ABSOLUTE_MAX_FRAME_BYTES).await?;
return Ok(None);
}
if let Err(error) = handler.validate_capabilities(&capabilities) {
commit(
writer,
Response::error(Some(request.id), error),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
let limits = config.limits.select(params.limits)?;
*peer
.inner
.capabilities
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) =
params.client_methods.iter().cloned().collect();
let cancellation = CancellationToken::new();
let context = RequestContext {
request_id: request.id,
deadline: request_deadline(request).min(Instant::now() + config.startup_timeout),
cancellation,
peer: peer.clone(),
};
if context.deadline <= Instant::now() {
commit(
writer,
Response::error(Some(request.id), RpcError::new(ErrorKind::DeadlineExceeded)),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
let mut operation = Box::pin(handler.initialize(&context, params.application));
let application = loop {
let outcome = tokio::select! {
biased;
_ = tokio::time::sleep_until(context.deadline) => {
context.cancellation.cancel();
commit(
writer,
Response::error(
Some(request.id),
RpcError::new(ErrorKind::DeadlineExceeded),
),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
_ = context.cancellation.cancelled() => {
commit(
writer,
Response::error(Some(request.id), RpcError::new(ErrorKind::Cancelled)),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
outcome = &mut operation => Some(outcome),
frame = reader.read_frame(ABSOLUTE_MAX_FRAME_BYTES) => {
let Some(frame) = frame? else {
context.cancellation.cancel();
return Err(Error::Closed);
};
match Envelope::parse_classified(&frame) {
Ok(Envelope::Response(response)) => {
if !peer.deliver(response).await {
context.cancellation.cancel();
return Err(Error::Protocol(
"unmatched callback response during initialization",
));
}
None
}
Ok(Envelope::Notification(notification)) => {
if notification.method == rpc::CANCEL
&& serde_json::from_value::<CancelParams>(notification.params)
.is_ok_and(|params| params.id == request.id)
{
context.cancellation.cancel();
}
None
}
Ok(Envelope::Request(invalid)) => {
context.cancellation.cancel();
commit(
writer,
Response::error(
Some(invalid.id),
RpcError::new(ErrorKind::InvalidRequest),
),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
Err((_, kind)) => {
context.cancellation.cancel();
commit(
writer,
Response::error(None, RpcError::new(kind)),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
}
}
};
let Some(outcome) = outcome else { continue };
break match outcome {
Ok(application) => application,
Err(error) => {
context.cancellation.cancel();
commit(
writer,
Response::error(Some(request.id), error),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
return Ok(None);
}
};
};
let result = InitializeResult {
protocol: handler.protocol().to_string(),
version,
server: config.product.clone(),
methods: capabilities.clone(),
capabilities: Default::default(),
limits,
application,
};
let value = match serde_json::to_value(result) {
Ok(value) => value,
Err(_) => {
context.cancellation.cancel();
return Err(Error::Protocol("serialize initialize result"));
}
};
context.cancellation.cancel();
commit(
writer,
Response::success(request.id, value),
ABSOLUTE_MAX_FRAME_BYTES,
)
.await?;
Ok(Some((limits, capabilities)))
}
async fn handle_notification(
notification: Notification,
inflight: &Arc<Mutex<HashMap<RequestId, CancellationToken>>>,
) {
if notification.method != rpc::CANCEL {
return;
}
let Ok(params) = serde_json::from_value::<CancelParams>(notification.params) else {
return;
};
if let Some(cancellation) = inflight.lock().await.get(¶ms.id) {
cancellation.cancel();
}
}
async fn run_call<H: ApplicationHandler>(
request: Request,
context: RequestContext,
handler: Arc<H>,
writer: mpsc::Sender<WriterCommand>,
inflight: Arc<Mutex<HashMap<RequestId, CancellationToken>>>,
disconnected: CancellationToken,
limit: usize,
) {
let mut operation = Box::pin(handler.call(context.clone(), &request.method, request.params));
let response = tokio::select! {
biased;
_ = tokio::time::sleep_until(context.deadline) => {
context.cancellation.cancel();
let response = Response::error(Some(request.id), RpcError::new(ErrorKind::DeadlineExceeded));
let _ = send_terminal(&writer, response, limit).await;
let _ = operation.await;
handler.request_finished(request.id, false).await;
inflight.lock().await.remove(&request.id);
return;
}
_ = context.cancellation.cancelled() => {
let response = Response::error(Some(request.id), RpcError::new(ErrorKind::Cancelled));
let _ = send_terminal(&writer, response, limit).await;
let _ = operation.await;
handler.request_finished(request.id, false).await;
inflight.lock().await.remove(&request.id);
return;
}
result = &mut operation => response_from_result(request.id, result),
};
let committed = commit_application_before(
&writer,
response,
limit,
context.deadline,
&context.cancellation,
&disconnected,
)
.await;
handler.request_finished(request.id, committed).await;
inflight.lock().await.remove(&request.id);
}
async fn send_terminal(
writer: &mpsc::Sender<WriterCommand>,
response: Response,
limit: usize,
) -> Result<()> {
let payload = encode_response(&response, limit)?;
writer
.send(WriterCommand {
payload,
limit,
committed: None,
})
.await
.map_err(|_| Error::Closed)
}
fn encode_response(response: &Response, limit: usize) -> Result<Zeroizing<Vec<u8>>> {
let payload = serde_json::to_vec(response)
.map_err(|_| Error::Protocol("failed to serialize response"))?;
if !payload.is_empty() && payload.len() <= limit {
return Ok(Zeroizing::new(payload));
}
drop(Zeroizing::new(payload));
let replacement = Response::error(response.id(), RpcError::new(ErrorKind::MessageTooLarge));
serde_json::to_vec(&replacement)
.map(Zeroizing::new)
.map_err(|_| Error::Protocol("failed to serialize response"))
}
fn response_from_result(id: RequestId, result: RpcResult<Value>) -> Response {
match result {
Ok(value) => Response::success(id, value),
Err(error) => Response::error(Some(id), error),
}
}
async fn commit_application_before(
writer: &mpsc::Sender<WriterCommand>,
response: Response,
limit: usize,
deadline: Instant,
cancellation: &CancellationToken,
disconnected: &CancellationToken,
) -> bool {
let encoded = serde_json::to_vec(&response).map(Zeroizing::new).ok();
let application_response = encoded
.as_ref()
.is_some_and(|payload| !payload.is_empty() && payload.len() <= limit);
let request_id = response.id();
let payload = match encoded.filter(|_| application_response) {
Some(payload) => payload,
None => {
let replacement =
Response::error(request_id, RpcError::new(ErrorKind::MessageTooLarge));
match serde_json::to_vec(&replacement).map(Zeroizing::new) {
Ok(payload) => payload,
Err(_) => return false,
}
}
};
let (committed_tx, committed_rx) = oneshot::channel();
let command = WriterCommand {
payload,
limit,
committed: Some(committed_tx),
};
let enqueued = tokio::select! {
biased;
_ = tokio::time::sleep_until(deadline) => {
cancellation.cancel();
let response = Response::error(request_id, RpcError::new(ErrorKind::DeadlineExceeded));
let _ = send_terminal(writer, response, limit).await;
false
}
_ = cancellation.cancelled() => {
let response = Response::error(request_id, RpcError::new(ErrorKind::Cancelled));
let _ = send_terminal(writer, response, limit).await;
false
}
result = writer.send(command) => result.is_ok(),
};
if !enqueued {
return false;
}
cancellation.cancel();
match tokio::time::timeout_at(deadline, committed_rx).await {
Ok(Ok(Ok(()))) => application_response,
Ok(Ok(Err(()))) | Ok(Err(_)) | Err(_) => {
disconnected.cancel();
false
}
}
}
async fn commit(
writer: &mpsc::Sender<WriterCommand>,
response: Response,
limit: usize,
) -> Result<()> {
let payload = encode_response(&response, limit)?;
tokio::time::timeout(COMMIT_TIMEOUT, async {
let (committed_tx, committed_rx) = oneshot::channel();
writer
.send(WriterCommand {
payload,
limit,
committed: Some(committed_tx),
})
.await
.map_err(|_| Error::Closed)?;
committed_rx
.await
.map_err(|_| Error::Closed)?
.map_err(|_| Error::Closed)
})
.await
.map_err(|_| Error::DeadlineExceeded)?
}
fn request_deadline(request: &Request) -> Instant {
instant_from_unix_ms(request.deadline_unix_ms())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_peer_does_not_keep_the_writer_channel_open() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(1);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
drop(writer_tx);
assert!(
writer_rx.recv().await.is_none(),
"the peer kept the writer channel open after the session dropped its sender"
);
let context = RequestContext {
request_id: RequestId::new(1).unwrap(),
deadline: Instant::now() + Duration::from_secs(30),
cancellation: CancellationToken::new(),
peer: peer.clone(),
};
let error = peer
.call::<_, Value>("client.prompt", &json!({}), &context)
.await
.unwrap_err();
assert_eq!(error.data.kind, ErrorKind::Unavailable);
}
#[tokio::test]
async fn callbacks_obey_the_pre_negotiation_outbound_limit() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(4);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
let cancellation = CancellationToken::new();
let context = RequestContext {
request_id: RequestId::new(1).unwrap(),
deadline: Instant::now() + Duration::from_secs(2),
cancellation: cancellation.clone(),
peer: peer.clone(),
};
let first_peer = peer.clone();
let first_context = context.clone();
let first = tokio::spawn(async move {
first_peer
.call::<_, Value>("client.prompt", &json!({}), &first_context)
.await
});
let _first_frame = writer_rx.recv().await.expect("first callback frame");
let second = peer
.call::<_, Value>("client.prompt", &json!({}), &context)
.await;
assert!(matches!(
second,
Err(ref error) if error.data.kind == ErrorKind::Unavailable
));
assert!(writer_rx.try_recv().is_err(), "a second callback was sent");
cancellation.cancel();
assert!(matches!(
first.await.unwrap(),
Err(ref error) if error.data.kind == ErrorKind::Cancelled
));
}
#[tokio::test]
async fn pre_negotiation_callbacks_use_the_absolute_frame_limit() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(2);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
let context = RequestContext {
request_id: RequestId::new(1).unwrap(),
deadline: Instant::now() + Duration::from_secs(2),
cancellation: CancellationToken::new(),
peer: peer.clone(),
};
let outcome = peer
.call::<_, Value>(
"client.prompt",
&json!({"value": "x".repeat(ABSOLUTE_MAX_FRAME_BYTES)}),
&context,
)
.await;
assert!(matches!(
outcome,
Err(ref error) if error.data.kind == ErrorKind::MessageTooLarge
));
assert!(writer_rx.try_recv().is_err(), "oversized callback was sent");
}
#[tokio::test]
async fn callbacks_switch_to_the_negotiated_outbound_limit() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(6);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
*peer.inner.semaphore.write().unwrap() = Arc::new(Semaphore::new(2));
peer.inner.limit.store(32 * 1024, Ordering::Release);
let cancellation = CancellationToken::new();
let context = RequestContext {
request_id: RequestId::new(1).unwrap(),
deadline: Instant::now() + Duration::from_secs(2),
cancellation: cancellation.clone(),
peer: peer.clone(),
};
let mut calls = Vec::new();
for _ in 0..2 {
let peer = peer.clone();
let context = context.clone();
calls.push(tokio::spawn(async move {
peer.call::<_, Value>("client.prompt", &json!({}), &context)
.await
}));
writer_rx.recv().await.expect("negotiated callback frame");
}
let third = peer
.call::<_, Value>("client.prompt", &json!({}), &context)
.await;
assert!(matches!(
third,
Err(ref error) if error.data.kind == ErrorKind::Unavailable
));
cancellation.cancel();
for call in calls {
assert!(matches!(
call.await.unwrap(),
Err(ref error) if error.data.kind == ErrorKind::Cancelled
));
}
}
#[tokio::test]
async fn a_callback_cannot_start_after_its_parent_is_terminal() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(2);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
let cancellation = CancellationToken::new();
cancellation.cancel();
let context = RequestContext {
request_id: RequestId::new(1).unwrap(),
deadline: Instant::now() + Duration::from_secs(2),
cancellation,
peer: peer.clone(),
};
let outcome = peer
.call::<_, Value>("client.prompt", &json!({}), &context)
.await;
assert!(matches!(
outcome,
Err(ref error) if error.data.kind == ErrorKind::Cancelled
));
assert!(writer_rx.try_recv().is_err(), "late callback was sent");
}
#[test]
fn expired_deadline_is_not_started() {
let request = Request::new(RequestId::new(1).unwrap(), "test.call", 1, json!({})).unwrap();
assert!(request_deadline(&request) <= Instant::now());
}
#[tokio::test]
async fn an_expired_callback_consumes_its_terminal_response() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(1);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
for raw_id in 1..=64 {
let context = RequestContext {
request_id: RequestId::new(10).unwrap(),
deadline: Instant::now(),
cancellation: CancellationToken::new(),
peer: peer.clone(),
};
let error = peer
.call::<_, Value>("client.prompt", &json!({}), &context)
.await
.unwrap_err();
assert_eq!(error.data.kind, ErrorKind::DeadlineExceeded);
let _request = writer_rx.recv().await.unwrap();
let id = RequestId::new(raw_id).unwrap();
let terminal = Response::error(Some(id), RpcError::new(ErrorKind::DeadlineExceeded));
assert!(peer.deliver(terminal.clone()).await);
assert!(!peer.deliver(terminal).await, "duplicates remain invalid");
}
assert!(peer.inner.calls.lock().await.abandoned.is_empty());
}
#[tokio::test]
async fn an_abandoned_callback_keeps_its_slot_until_the_client_answers() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(4);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
let context = |deadline| RequestContext {
request_id: RequestId::new(10).unwrap(),
deadline,
cancellation: CancellationToken::new(),
peer: peer.clone(),
};
let error = peer
.call::<_, Value>("client.prompt", &json!({}), &context(Instant::now()))
.await
.unwrap_err();
assert_eq!(error.data.kind, ErrorKind::DeadlineExceeded);
let _request = writer_rx.recv().await.unwrap();
let error = peer
.call::<_, Value>(
"client.prompt",
&json!({}),
&context(Instant::now() + Duration::from_secs(300)),
)
.await
.unwrap_err();
assert_eq!(error.data.kind, ErrorKind::Unavailable);
let terminal = Response::error(
Some(RequestId::new(1).unwrap()),
RpcError::new(ErrorKind::DeadlineExceeded),
);
assert!(peer.deliver(terminal).await);
let error = peer
.call::<_, Value>("client.prompt", &json!({}), &context(Instant::now()))
.await
.unwrap_err();
assert_eq!(error.data.kind, ErrorKind::DeadlineExceeded);
let _request = writer_rx.recv().await.unwrap();
}
#[tokio::test]
async fn a_cancelled_callback_also_consumes_its_terminal_response() {
let (writer_tx, mut writer_rx) = mpsc::channel::<WriterCommand>(1);
let peer = Peer::new(&writer_tx);
peer.inner
.capabilities
.write()
.unwrap()
.insert("client.prompt".to_string());
let cancellation = CancellationToken::new();
let context = RequestContext {
request_id: RequestId::new(10).unwrap(),
deadline: Instant::now() + Duration::from_secs(1),
cancellation: cancellation.clone(),
peer: peer.clone(),
};
let call_peer = peer.clone();
let call = tokio::spawn(async move {
call_peer
.call::<_, Value>("client.prompt", &json!({}), &context)
.await
});
let _request = writer_rx.recv().await.unwrap();
cancellation.cancel();
let error = call.await.unwrap().unwrap_err();
assert_eq!(error.data.kind, ErrorKind::Cancelled);
let terminal = Response::error(
Some(RequestId::new(1).unwrap()),
RpcError::new(ErrorKind::Cancelled),
);
assert!(peer.deliver(terminal).await);
assert!(peer.inner.calls.lock().await.abandoned.is_empty());
}
}