use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use bytes::Bytes;
use prost::Message;
use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
use crate::bus::packet::{Packet, PacketFlags, PacketType};
use crate::bus::{Bus, BusReader, BusWriter};
use crate::error::{Error, Result};
use crate::guid::Guid;
use crate::proto;
use crate::rpc::{self, ResponseMessage};
const OUTBOUND_QUEUE: usize = 64;
const MAX_IN_FLIGHT: usize = 256;
const CANCEL_QUEUE: usize = MAX_IN_FLIGHT;
#[derive(Debug, Default)]
struct Waiters {
closed: bool,
by_request: HashMap<Guid, oneshot::Sender<ResponseMessage>>,
}
impl Waiters {
fn close(&mut self) {
self.closed = true;
self.by_request.clear();
}
}
type Pending = Arc<Mutex<Waiters>>;
type InFlight = Arc<Semaphore>;
#[derive(Debug)]
struct Cancellation {
packet: Packet,
_permit: OwnedSemaphorePermit,
}
enum Outgoing {
Request(Packet),
Cancellation(Cancellation),
}
#[derive(Debug)]
pub struct Connection {
outbound: mpsc::Sender<Packet>,
cancels: mpsc::Sender<Cancellation>,
pending: Pending,
in_flight: InFlight,
address: String,
token: Option<String>,
closed: Arc<AtomicBool>,
reader_task: tokio::task::JoinHandle<()>,
}
impl Drop for Connection {
fn drop(&mut self) {
self.reader_task.abort();
}
}
impl Connection {
pub async fn connect(address: &str, token: Option<String>) -> Result<Self> {
let bus = Bus::connect(address).await?;
Ok(Self::from_bus(bus, address.to_owned(), token))
}
fn from_bus(bus: Bus, address: String, token: Option<String>) -> Self {
let Bus { reader, writer, .. } = bus;
let pending: Pending = Arc::default();
let in_flight = Arc::new(Semaphore::new(MAX_IN_FLIGHT));
let closed = Arc::new(AtomicBool::new(false));
let (outbound, outbound_receiver) = mpsc::channel(OUTBOUND_QUEUE);
let (cancels, cancel_receiver) = mpsc::channel(CANCEL_QUEUE);
tokio::spawn(write_loop(
writer,
outbound_receiver,
cancel_receiver,
Arc::clone(&pending),
Arc::clone(&in_flight),
Arc::clone(&closed),
));
let reader_task = tokio::spawn(read_loop(
reader,
Arc::clone(&pending),
Arc::clone(&in_flight),
Arc::clone(&closed),
));
Self {
outbound,
cancels,
pending,
in_flight,
address,
token,
closed,
reader_task,
}
}
pub fn address(&self) -> &str {
&self.address
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::Relaxed)
}
pub async fn invoke<Response: Message + Default>(
&self,
method: &str,
body: &impl Message,
attachments: Vec<Bytes>,
timeout: Option<std::time::Duration>,
response_name: &'static str,
) -> Result<(Response, Vec<Bytes>)> {
let response = self
.invoke_raw(rpc::API_SERVICE, method, body, attachments, timeout, None)
.await?;
let decoded = response.decode_body::<Response>(response_name)?;
Ok((decoded, response.attachments))
}
pub async fn invoke_raw(
&self,
service: &str,
method: &str,
body: &impl Message,
attachments: Vec<Bytes>,
timeout: Option<std::time::Duration>,
mutation_id: Option<Guid>,
) -> Result<ResponseMessage> {
let mut builder = rpc::RequestHeaderBuilder::new(service, method);
builder.timeout = timeout;
builder.mutation_id = mutation_id;
let request_id = builder.request_id;
let header = builder.build();
let deadline = timeout.map(|limit| tokio::time::Instant::now() + limit);
let timed_out = || Error::Timeout {
service: service.to_owned(),
method: method.to_owned(),
timeout: timeout.unwrap_or_default(),
};
let permit = match deadline {
Some(deadline) => {
match tokio::time::timeout_at(deadline, Arc::clone(&self.in_flight).acquire_owned())
.await
{
Ok(Ok(permit)) => permit,
Ok(Err(_)) => return Err(Error::ConnectionClosed { request_id }),
Err(_) => return Err(timed_out()),
}
}
None => Arc::clone(&self.in_flight)
.acquire_owned()
.await
.map_err(|_| Error::ConnectionClosed { request_id })?,
};
let (sender, receiver) = oneshot::channel();
{
let mut waiters = self.pending.lock().await;
if waiters.closed {
return Err(Error::ConnectionClosed { request_id });
}
waiters.by_request.insert(request_id, sender);
}
let mut guard = PendingGuard {
pending: Arc::clone(&self.pending),
cancels: self.cancels.clone(),
request_id,
service: service.to_owned(),
method: method.to_owned(),
completed: false,
sent: false,
permit: Some(permit),
};
let parts = rpc::encode_request(&header, self.token.as_deref(), body, attachments);
let packet = Packet::message(Guid::random(), parts, PacketFlags::NONE);
let queued = match deadline {
Some(deadline) => {
match tokio::time::timeout_at(deadline, self.outbound.send(packet)).await {
Ok(queued) => queued,
Err(_) => return Err(timed_out()),
}
}
None => self.outbound.send(packet).await,
};
if queued.is_err() {
return Err(Error::ConnectionClosed { request_id });
}
guard.sent = true;
let response = match deadline {
Some(deadline) => match tokio::time::timeout_at(deadline, receiver).await {
Ok(received) => received,
Err(_) => return Err(timed_out()),
},
None => receiver.await,
};
let response = match response {
Ok(response) => response,
Err(_) => {
guard.complete();
return Err(Error::ConnectionClosed { request_id });
}
};
guard.complete();
if let Some(error) = response.error() {
return Err(Error::response(service, method, error));
}
Ok(response)
}
}
struct PendingGuard {
pending: Pending,
cancels: mpsc::Sender<Cancellation>,
request_id: Guid,
service: String,
method: String,
completed: bool,
sent: bool,
permit: Option<OwnedSemaphorePermit>,
}
impl PendingGuard {
fn complete(&mut self) {
self.completed = true;
}
}
impl Drop for PendingGuard {
fn drop(&mut self) {
if self.completed {
return;
}
let pending = Arc::clone(&self.pending);
let request_id = self.request_id;
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
pending.lock().await.by_request.remove(&request_id);
});
}
Err(_) => {
if let Ok(mut waiters) = pending.try_lock() {
waiters.by_request.remove(&request_id);
}
}
}
if !self.sent {
return;
}
let parts = rpc::encode_cancelation(request_id, &self.service, &self.method);
let cancellation = Cancellation {
packet: Packet::message(Guid::random(), parts, PacketFlags::NONE),
_permit: self
.permit
.take()
.expect("every unfinished call holds an in-flight permit"),
};
match self.cancels.try_send(cancellation) {
Ok(()) | Err(mpsc::error::TrySendError::Closed(_)) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
debug_assert!(false, "cancellation queue exceeded in-flight limit");
}
}
}
}
async fn write_loop(
mut writer: BusWriter,
mut outbound: mpsc::Receiver<Packet>,
mut cancels: mpsc::Receiver<Cancellation>,
pending: Pending,
in_flight: InFlight,
closed: Arc<AtomicBool>,
) {
loop {
let outgoing = tokio::select! {
biased;
Some(cancellation) = cancels.recv() => Outgoing::Cancellation(cancellation),
Some(packet) = outbound.recv() => Outgoing::Request(packet),
else => break,
};
let packet = match &outgoing {
Outgoing::Request(packet) => packet,
Outgoing::Cancellation(cancellation) => &cancellation.packet,
};
if writer.send(packet).await.is_err() {
break;
}
}
closed.store(true, Ordering::Relaxed);
in_flight.close();
pending.lock().await.close();
let _ = writer.shutdown().await;
}
async fn read_loop(
mut reader: BusReader,
pending: Pending,
in_flight: InFlight,
closed: Arc<AtomicBool>,
) {
loop {
let packet = match reader.receive().await {
Ok(packet) => packet,
Err(_) => break,
};
if packet.packet_type != PacketType::Message {
continue;
}
let Ok(response) = rpc::decode_response(packet.parts) else {
continue;
};
let Some(request_id) = response.request_id() else {
continue;
};
if let Some(sender) = pending.lock().await.by_request.remove(&request_id) {
let _ = sender.send(response);
}
}
closed.store(true, Ordering::Relaxed);
in_flight.close();
pending.lock().await.close();
}
pub async fn discover_proxies(
connection: &Connection,
role: Option<&str>,
timeout: Option<std::time::Duration>,
) -> Result<Vec<String>> {
let request = proto::api::TReqDiscoverProxies {
role: role.map(str::to_owned),
..Default::default()
};
let response = connection
.invoke_raw(
rpc::DISCOVERY_SERVICE,
"DiscoverProxies",
&request,
Vec::new(),
timeout,
None,
)
.await?;
let decoded = response.decode_body::<proto::api::TRspDiscoverProxies>("TRspDiscoverProxies")?;
Ok(decoded.addresses)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::packet;
use bytes::BytesMut;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
struct StubProxy {
address: String,
seen: mpsc::UnboundedReceiver<Packet>,
task: tokio::task::JoinHandle<()>,
inject: mpsc::UnboundedSender<Packet>,
}
impl StubProxy {
async fn inject(&self, packet: Packet) {
let _ = self.inject.send(packet);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
}
impl Drop for StubProxy {
fn drop(&mut self) {
self.task.abort();
}
}
async fn stub_proxy(
answer: impl Fn(&proto::rpc::TRequestHeader) -> Option<Vec<Option<Bytes>>> + Send + 'static,
) -> StubProxy {
stub_proxy_with_batching(answer, 1).await
}
async fn stub_proxy_with_batching(
answer: impl Fn(&proto::rpc::TRequestHeader) -> Option<Vec<Option<Bytes>>> + Send + 'static,
batch: usize,
) -> StubProxy {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap().to_string();
let (seen_sender, seen) = mpsc::unbounded_channel();
let (inject, mut injected) = mpsc::unbounded_channel::<Packet>();
let task = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let (mut read_half, mut write_half) = stream.into_split();
let mut buffer = BytesMut::new();
let mut handshaken = false;
let mut pending_replies: Vec<Vec<Option<Bytes>>> = Vec::new();
loop {
while let Ok(packet) = injected.try_recv() {
let mut out = BytesMut::new();
packet::encode(&packet, &mut out).unwrap();
if write_half.write_all(&out).await.is_err() {
return;
}
}
let decoded = packet::decode(&mut buffer, crate::bus::DEFAULT_MAX_MESSAGE_SIZE);
match decoded {
Ok(Some(request)) => {
if !handshaken {
handshaken = true;
let handshake = proto::bus::THandshake {
connection_id: Guid::random().to_proto(),
encryption_mode: Some(0),
..Default::default()
};
let mut part = Vec::new();
part.extend_from_slice(&crate::bus::HANDSHAKE_SIGNATURE.to_le_bytes());
handshake.encode(&mut part).unwrap();
let reply = Packet::message(
request.id,
vec![Some(Bytes::from(part))],
PacketFlags::NONE,
);
let mut out = BytesMut::new();
packet::encode(&reply, &mut out).unwrap();
if write_half.write_all(&out).await.is_err() {
return;
}
continue;
}
let Some(Some(header_part)) = request.parts.first().cloned() else {
continue;
};
let _ = seen_sender.send(request.clone());
if header_part.len() < 4 {
continue;
}
let Ok(header) = proto::rpc::TRequestHeader::decode(&header_part[4..])
else {
continue;
};
if let Some(parts) = answer(&header) {
pending_replies.push(parts);
}
if pending_replies.len() >= batch {
for parts in pending_replies.drain(..).rev() {
let reply =
Packet::message(Guid::random(), parts, PacketFlags::NONE);
let mut out = BytesMut::new();
packet::encode(&reply, &mut out).unwrap();
if write_half.write_all(&out).await.is_err() {
return;
}
}
}
continue;
}
Ok(None) => {}
Err(_) => return,
}
if read_half.read_buf(&mut buffer).await.unwrap_or(0) == 0 {
return;
}
}
});
StubProxy {
address,
seen,
task,
inject,
}
}
async fn next_packet(stub: &mut StubProxy) -> Option<Packet> {
tokio::time::timeout(std::time::Duration::from_secs(5), stub.seen.recv())
.await
.ok()
.flatten()
}
fn success_reply(request_id: Guid, body: &impl Message) -> Vec<Option<Bytes>> {
let header = proto::rpc::TResponseHeader {
request_id: Some(request_id.to_proto()),
..Default::default()
};
let mut header_part = Vec::new();
header_part.extend_from_slice(&(rpc::MessageType::Response as u32).to_le_bytes());
header.encode(&mut header_part).unwrap();
vec![
Some(Bytes::from(header_part)),
Some(Bytes::from(body.encode_to_vec())),
]
}
fn error_reply(request_id: Guid, code: i32, message: &str) -> Vec<Option<Bytes>> {
let header = proto::rpc::TResponseHeader {
request_id: Some(request_id.to_proto()),
error: Some(proto::misc::TError {
code,
message: Some(message.to_owned()),
attributes: None,
inner_errors: vec![],
}),
..Default::default()
};
let mut header_part = Vec::new();
header_part.extend_from_slice(&(rpc::MessageType::Response as u32).to_le_bytes());
header.encode(&mut header_part).unwrap();
vec![Some(Bytes::from(header_part))]
}
#[tokio::test]
async fn a_call_gets_its_own_response() {
let mut stub = stub_proxy(|header| {
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
Some(success_reply(
request_id,
&proto::api::TRspPingTransaction::default(),
))
})
.await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
let (_response, attachments) = tokio::time::timeout(
std::time::Duration::from_secs(10),
connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&request,
Vec::new(),
None,
"TRspPingTransaction",
),
)
.await
.expect("the stub answers immediately")
.unwrap();
assert!(attachments.is_empty(), "the stub sent no attachments");
let sent = next_packet(&mut stub).await.expect("the request");
let header_part = sent.parts[0].as_ref().unwrap();
let header = proto::rpc::TRequestHeader::decode(&header_part[4..]).unwrap();
assert_eq!(header.method, "PingTransaction");
assert_eq!(header.service, rpc::API_SERVICE);
let body = proto::api::TReqPingTransaction::decode(sent.parts[1].as_ref().unwrap().clone())
.unwrap();
assert_eq!(body.transaction_id, request.transaction_id);
}
#[tokio::test]
async fn concurrent_requests_are_routed_by_request_id() {
let stub = stub_proxy_with_batching(
|header| {
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
Some(success_reply(
request_id,
&proto::api::TRspGetNode {
value: header.method.clone().into_bytes(),
},
))
},
4,
)
.await;
let connection = Arc::new(Connection::connect(&stub.address, None).await.unwrap());
let methods = ["GetNode", "ListNode", "ExistsNode", "SetNode"];
let mut handles = Vec::new();
for method in methods {
let connection = Arc::clone(&connection);
handles.push(tokio::spawn(async move {
connection
.invoke::<proto::api::TRspGetNode>(
method,
&proto::api::TReqGetNode::default(),
Vec::new(),
None,
"TRspGetNode",
)
.await
.map(|(response, _)| String::from_utf8(response.value).unwrap())
}));
}
for (method, handle) in methods.iter().zip(handles) {
let answer = tokio::time::timeout(std::time::Duration::from_secs(10), handle)
.await
.expect("a call is stuck: the stub answers only once all four have arrived")
.unwrap()
.unwrap();
assert_eq!(
&answer, method,
"the caller for {method} was handed another call's answer"
);
}
}
#[tokio::test]
async fn a_server_error_becomes_a_rust_error_with_its_code() {
let stub = stub_proxy(|header| {
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
Some(error_reply(
request_id,
crate::error::codes::NO_SUCH_TRANSACTION,
"no such transaction",
))
})
.await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let error = tokio::time::timeout(
std::time::Duration::from_secs(10),
connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
},
Vec::new(),
None,
"TRspPingTransaction",
),
)
.await
.expect("the stub answers immediately")
.unwrap_err();
assert!(error.has_code(crate::error::codes::NO_SUCH_TRANSACTION));
assert!(
error
.to_string()
.contains("ApiService.PingTransaction failed")
);
}
#[tokio::test]
async fn a_timeout_reports_the_method_and_cancels_the_request() {
let mut stub = stub_proxy(|_| None).await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let error = tokio::time::timeout(
std::time::Duration::from_secs(10),
connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
},
Vec::new(),
Some(std::time::Duration::from_millis(50)),
"TRspPingTransaction",
),
)
.await
.expect("the local deadline did not fire: the call outlived it twentyfold")
.unwrap_err();
assert!(matches!(error, Error::Timeout { .. }), "got {error}");
let request = next_packet(&mut stub).await.expect("the request");
let header_part = request.parts[0].as_ref().unwrap();
assert_eq!(&header_part[0..4], b"rpci");
let header = proto::rpc::TRequestHeader::decode(&header_part[4..]).unwrap();
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
assert_eq!(header.timeout, Some(50_000));
let cancelation = next_packet(&mut stub)
.await
.expect("a cancellation must follow the timeout");
let part = cancelation.parts[0].as_ref().unwrap();
assert_eq!(&part[0..4], b"rpcc", "cancellation is an rpcc message");
let cancel_header = proto::rpc::TRequestCancelationHeader::decode(&part[4..]).unwrap();
assert_eq!(Guid::from_proto(&cancel_header.request_id), request_id);
}
#[tokio::test]
async fn junk_from_the_peer_does_not_kill_the_connection() {
let stub = stub_proxy(|header| {
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
Some(success_reply(
request_id,
&proto::api::TRspPingTransaction::default(),
))
})
.await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
let orphan = {
let header = proto::rpc::TResponseHeader {
request_id: Some(Guid::random().to_proto()),
..Default::default()
};
let mut bytes = Vec::new();
bytes.extend_from_slice(&(rpc::MessageType::Response as u32).to_le_bytes());
header.encode(&mut bytes).unwrap();
Packet::message(
Guid::random(),
vec![Some(Bytes::from(bytes))],
PacketFlags::NONE,
)
};
let unparseable = Packet::message(
Guid::random(),
vec![Some(Bytes::from_static(b"not an rpc message at all"))],
PacketFlags::NONE,
);
let ack = Packet {
packet_type: PacketType::Ack,
flags: PacketFlags::NONE,
id: Guid::random(),
parts: Vec::new(),
};
for packet in [orphan, unparseable, ack] {
stub.inject(packet).await;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(!connection.is_closed(), "junk closed the connection");
tokio::time::timeout(
std::time::Duration::from_secs(10),
connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&request,
Vec::new(),
Some(std::time::Duration::from_secs(5)),
"TRspPingTransaction",
),
)
.await
.expect("the connection stopped answering after the junk")
.expect("a call after the junk must still work");
}
#[tokio::test]
async fn a_dropped_connection_fails_the_calls_in_flight() {
let stub = stub_proxy(|_| None).await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
let call = connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&request,
Vec::new(),
None,
"TRspPingTransaction",
);
drop(stub);
let error = tokio::time::timeout(std::time::Duration::from_secs(10), call)
.await
.expect("dropping the connection must fail the call, not park it")
.unwrap_err();
assert!(
matches!(error, Error::ConnectionClosed { .. }),
"got {error}"
);
}
#[tokio::test]
async fn dropping_a_call_cancels_it_on_the_wire() {
let mut stub = stub_proxy(|_| None).await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let request = proto::api::TReqSelectRows {
query: "* from [//tmp/t]".to_owned(),
..Default::default()
};
{
let call = connection.invoke::<proto::api::TRspSelectRows>(
"SelectRows",
&request,
Vec::new(),
None,
"TRspSelectRows",
);
let _ = tokio::time::timeout(std::time::Duration::from_millis(50), call).await;
}
let sent = next_packet(&mut stub).await.expect("the request");
let header_part = sent.parts[0].as_ref().unwrap();
assert_eq!(&header_part[0..4], b"rpci");
let header = proto::rpc::TRequestHeader::decode(&header_part[4..]).unwrap();
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
let cancelation = next_packet(&mut stub)
.await
.expect("dropping the future must send a cancellation");
let part = cancelation.parts[0].as_ref().unwrap();
assert_eq!(&part[0..4], b"rpcc");
let cancel_header = proto::rpc::TRequestCancelationHeader::decode(&part[4..]).unwrap();
assert_eq!(Guid::from_proto(&cancel_header.request_id), request_id);
assert_eq!(cancel_header.method, "SelectRows");
}
#[tokio::test]
async fn a_completed_call_sends_no_cancellation() {
let mut stub = stub_proxy(|header| {
let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
Some(success_reply(
request_id,
&proto::api::TRspPingTransaction::default(),
))
})
.await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
tokio::time::timeout(
std::time::Duration::from_secs(10),
connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&request,
Vec::new(),
None,
"TRspPingTransaction",
),
)
.await
.expect("the stub answers immediately")
.unwrap();
let _request = next_packet(&mut stub).await.expect("the request");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
stub.seen.try_recv().is_err(),
"a completed call must not be followed by a cancellation"
);
}
#[tokio::test]
async fn the_guard_cancels_only_what_it_actually_sent() {
async fn drain(receiver: &mut mpsc::Receiver<Cancellation>) -> Vec<Packet> {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let mut packets = Vec::new();
while let Ok(cancellation) = receiver.try_recv() {
packets.push(cancellation.packet);
}
packets
}
fn guard(
pending: &Pending,
cancels: &mpsc::Sender<Cancellation>,
in_flight: &InFlight,
request_id: Guid,
sent: bool,
) -> PendingGuard {
PendingGuard {
pending: Arc::clone(pending),
cancels: cancels.clone(),
request_id,
service: rpc::API_SERVICE.to_owned(),
method: "LookupRows".to_owned(),
completed: false,
sent,
permit: Some(
Arc::clone(in_flight)
.try_acquire_owned()
.expect("the test never holds more than one permit"),
),
}
}
let pending: Pending = Arc::default();
let in_flight = Arc::new(Semaphore::new(1));
let (cancels, mut receiver) = mpsc::channel(16);
let request_id = Guid::random();
drop(guard(&pending, &cancels, &in_flight, request_id, false));
assert!(
drain(&mut receiver).await.is_empty(),
"cancelled a request the proxy never received"
);
drop(guard(&pending, &cancels, &in_flight, request_id, true));
let sent_packets = drain(&mut receiver).await;
assert_eq!(sent_packets.len(), 1, "expected exactly one cancellation");
let part = sent_packets[0].parts[0].as_ref().unwrap();
assert_eq!(&part[0..4], b"rpcc");
let header = proto::rpc::TRequestCancelationHeader::decode(&part[4..]).unwrap();
assert_eq!(Guid::from_proto(&header.request_id), request_id);
let mut done = guard(&pending, &cancels, &in_flight, request_id, true);
done.complete();
drop(done);
assert!(
drain(&mut receiver).await.is_empty(),
"cancelled a call that had already returned"
);
}
#[tokio::test]
async fn every_in_flight_call_has_room_for_its_cancellation() {
let pending: Pending = Arc::default();
let in_flight = Arc::new(Semaphore::new(MAX_IN_FLIGHT));
let (cancels, mut receiver) = mpsc::channel(CANCEL_QUEUE);
for _ in 0..MAX_IN_FLIGHT {
let guard = PendingGuard {
pending: Arc::clone(&pending),
cancels: cancels.clone(),
request_id: Guid::random(),
service: rpc::API_SERVICE.to_owned(),
method: "LookupRows".to_owned(),
completed: false,
sent: true,
permit: Some(
Arc::clone(&in_flight)
.try_acquire_owned()
.expect("the loop takes every permit exactly once"),
),
};
drop(guard);
}
assert_eq!(receiver.len(), MAX_IN_FLIGHT);
assert!(
Arc::clone(&in_flight).try_acquire_owned().is_err(),
"a queued cancellation must retain its call's permit"
);
drop(receiver.recv().await.expect("the first cancellation"));
assert!(
Arc::clone(&in_flight).try_acquire_owned().is_ok(),
"the consumed cancellation did not release its permit"
);
}
#[tokio::test]
async fn the_in_flight_limit_bounds_pending_waiters() {
let (outbound, _outbound_receiver) = mpsc::channel(MAX_IN_FLIGHT);
let (cancels, _cancel_receiver) = mpsc::channel(CANCEL_QUEUE);
let in_flight = Arc::new(Semaphore::new(MAX_IN_FLIGHT));
let connection = Connection {
outbound,
cancels,
pending: Arc::default(),
in_flight: Arc::clone(&in_flight),
address: "test".to_owned(),
token: None,
closed: Arc::new(AtomicBool::new(false)),
reader_task: tokio::spawn(std::future::pending()),
};
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
let mut calls = Vec::with_capacity(MAX_IN_FLIGHT);
for _ in 0..MAX_IN_FLIGHT {
let mut call = Box::pin(connection.invoke_raw(
rpc::API_SERVICE,
"PingTransaction",
&request,
Vec::new(),
None,
None,
));
tokio::select! {
biased;
_ = call.as_mut() => panic!("the test connection cannot answer"),
_ = tokio::task::yield_now() => {}
}
calls.push(call);
}
assert_eq!(
connection.pending.lock().await.by_request.len(),
MAX_IN_FLIGHT
);
let mut overflow = Box::pin(connection.invoke_raw(
rpc::API_SERVICE,
"PingTransaction",
&request,
Vec::new(),
None,
None,
));
tokio::select! {
biased;
_ = overflow.as_mut() => panic!("the overflow call cannot complete"),
_ = tokio::task::yield_now() => {}
}
assert_eq!(
connection.pending.lock().await.by_request.len(),
MAX_IN_FLIGHT,
"a call waiting for capacity must not register another waiter"
);
assert!(
Arc::clone(&in_flight).try_acquire_owned().is_err(),
"all in-flight permits should be held by the registered calls"
);
drop(overflow);
drop(calls);
}
#[tokio::test]
async fn a_call_that_times_out_while_queuing_cancels_nothing() {
let (outbound, _outbound_receiver) = mpsc::channel(1);
let (cancels, mut cancel_receiver) = mpsc::channel(CANCEL_QUEUE);
let connection = Connection {
outbound,
cancels,
pending: Arc::default(),
in_flight: Arc::new(Semaphore::new(MAX_IN_FLIGHT)),
address: "test".to_owned(),
token: None,
closed: Arc::new(AtomicBool::new(false)),
reader_task: tokio::spawn(std::future::pending()),
};
connection
.outbound
.try_send(Packet::message(
Guid::random(),
vec![Some(Bytes::from_static(b"blocker"))],
PacketFlags::NONE,
))
.expect("the queue starts empty");
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
let error = tokio::time::timeout(
std::time::Duration::from_secs(10),
connection.invoke_raw(
rpc::API_SERVICE,
"PingTransaction",
&request,
Vec::new(),
Some(std::time::Duration::from_millis(50)),
None,
),
)
.await
.expect("the deadline must end a call that cannot even be queued")
.unwrap_err();
assert!(matches!(error, Error::Timeout { .. }), "got {error}");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
cancel_receiver.try_recv().is_err(),
"cancelled a request that never left the queue"
);
assert!(
connection.pending.lock().await.by_request.is_empty(),
"the timed-out call left its entry behind"
);
}
#[test]
fn dropping_a_call_outside_a_runtime_does_not_panic() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let (stub, connection) = runtime.block_on(async {
let stub = stub_proxy(|_| None).await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
(stub, connection)
});
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
let mut call = Box::pin(connection.invoke_raw(
rpc::API_SERVICE,
"PingTransaction",
&request,
Vec::new(),
None,
None,
));
runtime.block_on(async {
let _ = tokio::time::timeout(std::time::Duration::from_millis(50), &mut call).await;
});
drop(call);
drop(connection);
drop(stub);
}
#[tokio::test]
async fn the_pending_map_does_not_leak_when_a_call_is_dropped() {
let stub = stub_proxy(|_| None).await;
let connection = Connection::connect(&stub.address, None).await.unwrap();
let request = proto::api::TReqPingTransaction {
transaction_id: Guid::random().to_proto(),
..Default::default()
};
{
let call = connection.invoke::<proto::api::TRspPingTransaction>(
"PingTransaction",
&request,
Vec::new(),
None,
"TRspPingTransaction",
);
let _ = tokio::time::timeout(std::time::Duration::from_millis(50), call).await;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
connection.pending.lock().await.by_request.is_empty(),
"a dropped call left its entry in the pending map"
);
}
}