Skip to main content

ytsaurus_rpc/
connection.rs

1//! The connection actor.
2//!
3//! One TCP connection carries many concurrent requests, so the socket is owned
4//! by background tasks rather than by the caller: a writer task drains a
5//! **bounded** channel — so backpressure is real and a runaway caller cannot
6//! queue unbounded memory — and a reader task matches each response to the
7//! `oneshot` waiting for it, keyed by request id.
8//!
9//! Cancellation is protocol-level. Dropping the future returned by [`Connection::invoke`]
10//! sends the protocol's cancellation message, because a client-side-only
11//! timeout leaves the proxy doing work nobody will read.
12
13use std::collections::HashMap;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17use bytes::Bytes;
18use prost::Message;
19use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
20
21use crate::bus::packet::{Packet, PacketFlags, PacketType};
22use crate::bus::{Bus, BusReader, BusWriter};
23use crate::error::{Error, Result};
24use crate::guid::Guid;
25use crate::proto;
26use crate::rpc::{self, ResponseMessage};
27
28/// How many outbound messages may be queued before senders wait.
29const OUTBOUND_QUEUE: usize = 64;
30
31/// How many calls may be in flight on one connection.
32///
33/// An outbound queue bounds packets the writer has not picked up yet, but not
34/// requests the proxy has accepted and has not answered. This is the latter
35/// bound: one permit lives from registration through the response, or through
36/// the cancellation packet's write when the caller goes away.
37const MAX_IN_FLIGHT: usize = 256;
38
39/// How many cancellations may be queued.
40///
41/// A cancellation owns its call's in-flight permit until the writer has sent
42/// it. Consequently at most [`MAX_IN_FLIGHT`] can exist, so this channel can
43/// never fill while a new cancellation still needs a slot. The writer takes it
44/// first; a request backlog therefore cannot prevent cancellation.
45const CANCEL_QUEUE: usize = MAX_IN_FLIGHT;
46
47/// The callers waiting for responses, and whether the connection is still
48/// usable.
49///
50/// The two live under one lock on purpose. A caller registers itself and the
51/// reader task declares the connection dead; if those could interleave, a
52/// caller could register just after the reader cleared the map and then wait
53/// for a response no one will ever deliver.
54#[derive(Debug, Default)]
55struct Waiters {
56    closed: bool,
57    by_request: HashMap<Guid, oneshot::Sender<ResponseMessage>>,
58}
59
60impl Waiters {
61    /// Marks the connection dead and wakes everyone waiting on it. Dropping the
62    /// senders is what turns a lost connection into an error for each caller
63    /// rather than a hang.
64    fn close(&mut self) {
65        self.closed = true;
66        self.by_request.clear();
67    }
68}
69
70type Pending = Arc<Mutex<Waiters>>;
71type InFlight = Arc<Semaphore>;
72
73/// A protocol cancellation waiting for the writer.
74///
75/// The permit is deliberately carried with the packet rather than released by
76/// [`PendingGuard::drop`]. Releasing it at enqueue time permits a new call to
77/// time out and overflow this queue while an old cancellation is blocked on
78/// the socket.
79#[derive(Debug)]
80struct Cancellation {
81    packet: Packet,
82    _permit: OwnedSemaphorePermit,
83}
84
85enum Outgoing {
86    Request(Packet),
87    Cancellation(Cancellation),
88}
89
90/// A live connection to one RPC proxy.
91///
92/// Dropping it ends both background tasks and releases the socket. That is not
93/// automatic: the writer stops on its own once the last sender is gone, but the
94/// reader would stay parked in `receive()` holding the read half until the peer
95/// closed — and against a peer that never does, the task and its file
96/// descriptor would live as long as the process.
97#[derive(Debug)]
98pub struct Connection {
99    outbound: mpsc::Sender<Packet>,
100    cancels: mpsc::Sender<Cancellation>,
101    pending: Pending,
102    in_flight: InFlight,
103    address: String,
104    token: Option<String>,
105    closed: Arc<AtomicBool>,
106    reader_task: tokio::task::JoinHandle<()>,
107}
108
109impl Drop for Connection {
110    fn drop(&mut self) {
111        // Aborting skips the tail of `read_loop`, so `closed` is never set and
112        // the waiters are never woken on this path. That is sound only because
113        // every call borrows the `Connection`: there can be no waiter left to
114        // wake, and nobody can ask for one afterwards. Anything that hands out
115        // calls outliving the connection — an owned handle, a `'static`
116        // future — has to close the waiters here instead.
117        self.reader_task.abort();
118    }
119}
120
121impl Connection {
122    /// Connects to a proxy and starts the reader and writer tasks.
123    pub async fn connect(address: &str, token: Option<String>) -> Result<Self> {
124        let bus = Bus::connect(address).await?;
125        Ok(Self::from_bus(bus, address.to_owned(), token))
126    }
127
128    fn from_bus(bus: Bus, address: String, token: Option<String>) -> Self {
129        let Bus { reader, writer, .. } = bus;
130        let pending: Pending = Arc::default();
131        let in_flight = Arc::new(Semaphore::new(MAX_IN_FLIGHT));
132        let closed = Arc::new(AtomicBool::new(false));
133        let (outbound, outbound_receiver) = mpsc::channel(OUTBOUND_QUEUE);
134        let (cancels, cancel_receiver) = mpsc::channel(CANCEL_QUEUE);
135
136        tokio::spawn(write_loop(
137            writer,
138            outbound_receiver,
139            cancel_receiver,
140            Arc::clone(&pending),
141            Arc::clone(&in_flight),
142            Arc::clone(&closed),
143        ));
144        let reader_task = tokio::spawn(read_loop(
145            reader,
146            Arc::clone(&pending),
147            Arc::clone(&in_flight),
148            Arc::clone(&closed),
149        ));
150
151        Self {
152            outbound,
153            cancels,
154            pending,
155            in_flight,
156            address,
157            token,
158            closed,
159            reader_task,
160        }
161    }
162
163    /// The address this connection was opened to.
164    pub fn address(&self) -> &str {
165        &self.address
166    }
167
168    /// Whether the connection has failed or been closed.
169    pub fn is_closed(&self) -> bool {
170        self.closed.load(Ordering::Relaxed)
171    }
172
173    /// Calls one method and waits for its response.
174    ///
175    /// The timeout is sent to the server in the request header *and* applied
176    /// locally, so the two agree: a local-only timeout would leave the proxy
177    /// working, and a server-only one would leave the caller waiting if the
178    /// connection stalled.
179    pub async fn invoke<Response: Message + Default>(
180        &self,
181        method: &str,
182        body: &impl Message,
183        attachments: Vec<Bytes>,
184        timeout: Option<std::time::Duration>,
185        response_name: &'static str,
186    ) -> Result<(Response, Vec<Bytes>)> {
187        let response = self
188            .invoke_raw(rpc::API_SERVICE, method, body, attachments, timeout, None)
189            .await?;
190        let decoded = response.decode_body::<Response>(response_name)?;
191        Ok((decoded, response.attachments))
192    }
193
194    /// Calls one method, returning the whole response message.
195    pub async fn invoke_raw(
196        &self,
197        service: &str,
198        method: &str,
199        body: &impl Message,
200        attachments: Vec<Bytes>,
201        timeout: Option<std::time::Duration>,
202        mutation_id: Option<Guid>,
203    ) -> Result<ResponseMessage> {
204        let mut builder = rpc::RequestHeaderBuilder::new(service, method);
205        builder.timeout = timeout;
206        builder.mutation_id = mutation_id;
207        let request_id = builder.request_id;
208        let header = builder.build();
209
210        // The deadline covers the whole call, not just the wait for a reply.
211        // Queuing the request can block too — the outbound channel is bounded,
212        // and a peer that stops reading backs the writer up until it is full —
213        // so a deadline applied only to the reply would be no deadline at all
214        // in exactly the case a caller most needs one.
215        let deadline = timeout.map(|limit| tokio::time::Instant::now() + limit);
216        let timed_out = || Error::Timeout {
217            service: service.to_owned(),
218            method: method.to_owned(),
219            timeout: timeout.unwrap_or_default(),
220        };
221
222        // This belongs inside the call's deadline just as the outbound queue
223        // does. Otherwise a full in-flight set would recreate the unbounded
224        // wait the semaphore exists to prevent.
225        let permit = match deadline {
226            Some(deadline) => {
227                match tokio::time::timeout_at(deadline, Arc::clone(&self.in_flight).acquire_owned())
228                    .await
229                {
230                    Ok(Ok(permit)) => permit,
231                    Ok(Err(_)) => return Err(Error::ConnectionClosed { request_id }),
232                    Err(_) => return Err(timed_out()),
233                }
234            }
235            None => Arc::clone(&self.in_flight)
236                .acquire_owned()
237                .await
238                .map_err(|_| Error::ConnectionClosed { request_id })?,
239        };
240
241        let (sender, receiver) = oneshot::channel();
242        {
243            let mut waiters = self.pending.lock().await;
244            // Checked under the same lock the reader closes with, so a
245            // connection that has already died fails the call here instead of
246            // parking it for ever.
247            if waiters.closed {
248                return Err(Error::ConnectionClosed { request_id });
249            }
250            waiters.by_request.insert(request_id, sender);
251        }
252
253        // Armed from here on. However this function leaves — returning, or the
254        // caller dropping the future part-way — the guard removes the pending
255        // entry and, once the request has actually been queued, tells the
256        // server to stop working on a result nobody will read.
257        let mut guard = PendingGuard {
258            pending: Arc::clone(&self.pending),
259            cancels: self.cancels.clone(),
260            request_id,
261            service: service.to_owned(),
262            method: method.to_owned(),
263            completed: false,
264            sent: false,
265            permit: Some(permit),
266        };
267
268        let parts = rpc::encode_request(&header, self.token.as_deref(), body, attachments);
269        let packet = Packet::message(Guid::random(), parts, PacketFlags::NONE);
270        let queued = match deadline {
271            Some(deadline) => {
272                match tokio::time::timeout_at(deadline, self.outbound.send(packet)).await {
273                    Ok(queued) => queued,
274                    // Never queued, so there is nothing for the server to cancel;
275                    // the guard still removes the pending entry.
276                    Err(_) => return Err(timed_out()),
277                }
278            }
279            None => self.outbound.send(packet).await,
280        };
281        if queued.is_err() {
282            // Not `complete()`: the entry was inserted and still has to go. The
283            // guard removes it, and `sent` is still false, so nothing is
284            // cancelled for a request the server never received.
285            return Err(Error::ConnectionClosed { request_id });
286        }
287        guard.sent = true;
288
289        let response = match deadline {
290            Some(deadline) => match tokio::time::timeout_at(deadline, receiver).await {
291                Ok(received) => received,
292                // Dropping the guard sends the cancellation, so the timeout
293                // path needs nothing of its own.
294                Err(_) => return Err(timed_out()),
295            },
296            None => receiver.await,
297        };
298
299        let response = match response {
300            Ok(response) => response,
301            // The sender was dropped, which only happens when the reader task
302            // ended — the connection is gone, and there is nothing to cancel.
303            Err(_) => {
304                guard.complete();
305                return Err(Error::ConnectionClosed { request_id });
306            }
307        };
308        // The answer is in hand: nothing to remove and nothing to cancel.
309        guard.complete();
310
311        if let Some(error) = response.error() {
312            return Err(Error::response(service, method, error));
313        }
314        Ok(response)
315    }
316}
317
318/// Cleans up after an in-flight request however its future ends — including
319/// when the caller drops it part-way.
320///
321/// Two jobs. It removes the entry from the pending map, or the map grows
322/// without bound on a long-lived connection. And it sends the protocol's
323/// cancellation, because **cancellation is protocol-level**: a client that
324/// merely stops waiting leaves the proxy computing a result nobody will read,
325/// which is exactly the cost this crate exists to avoid.
326///
327/// Stood down once the response is in hand, since there is then nothing to
328/// remove and nothing to cancel.
329struct PendingGuard {
330    pending: Pending,
331    cancels: mpsc::Sender<Cancellation>,
332    request_id: Guid,
333    service: String,
334    method: String,
335    /// The call finished on its own; no cleanup is owed.
336    completed: bool,
337    /// The request reached the outbound queue, so the server may be working on
338    /// it. A request that never got that far has nothing to cancel, and saying
339    /// otherwise would send a cancellation for a request id the server has
340    /// never seen.
341    sent: bool,
342    /// Held until the call finishes, or moved into its cancellation packet.
343    permit: Option<OwnedSemaphorePermit>,
344}
345
346impl PendingGuard {
347    fn complete(&mut self) {
348        self.completed = true;
349    }
350}
351
352impl Drop for PendingGuard {
353    fn drop(&mut self) {
354        if self.completed {
355            return;
356        }
357
358        let pending = Arc::clone(&self.pending);
359        let request_id = self.request_id;
360        // `Drop` cannot await, so the removal is handed to the runtime — but
361        // only if there is one. `tokio::spawn` panics outside a runtime
362        // context, and a future can perfectly well be dropped there: polled
363        // inside `block_on` and released afterwards, or held in a struct that
364        // outlives it. A panic in `Drop` during unwinding aborts the process,
365        // so this checks first and falls back to the blocking path, which is
366        // sound because the lock is only ever held for a map operation.
367        match tokio::runtime::Handle::try_current() {
368            Ok(handle) => {
369                handle.spawn(async move {
370                    pending.lock().await.by_request.remove(&request_id);
371                });
372            }
373            Err(_) => {
374                if let Ok(mut waiters) = pending.try_lock() {
375                    waiters.by_request.remove(&request_id);
376                }
377                // A contended lock with no runtime to defer to leaves the entry
378                // for `Waiters::close` to sweep when the connection ends. That
379                // is bounded by the connection's lifetime, and unreachable in
380                // practice: the only other holders are the reader task and
381                // other callers, which need a runtime to be running at all.
382            }
383        }
384
385        if !self.sent {
386            return;
387        }
388
389        // Non-blocking, because dropping a future must not block. The permit
390        // moves with the packet and is released only after the writer handles
391        // it, so a full cancellation queue is impossible while a guard still
392        // owns a permit to turn into another cancellation.
393        let parts = rpc::encode_cancelation(request_id, &self.service, &self.method);
394        let cancellation = Cancellation {
395            packet: Packet::message(Guid::random(), parts, PacketFlags::NONE),
396            _permit: self
397                .permit
398                .take()
399                .expect("every unfinished call holds an in-flight permit"),
400        };
401        match self.cancels.try_send(cancellation) {
402            Ok(()) | Err(mpsc::error::TrySendError::Closed(_)) => {}
403            // With one permit in every queued cancellation and a channel as
404            // large as the semaphore, `Full` cannot occur. Keep Drop
405            // non-panicking even if a future maintenance change breaks that
406            // invariant; debug builds still flag it immediately.
407            Err(mpsc::error::TrySendError::Full(_)) => {
408                debug_assert!(false, "cancellation queue exceeded in-flight limit");
409            }
410        }
411    }
412}
413
414async fn write_loop(
415    mut writer: BusWriter,
416    mut outbound: mpsc::Receiver<Packet>,
417    mut cancels: mpsc::Receiver<Cancellation>,
418    pending: Pending,
419    in_flight: InFlight,
420    closed: Arc<AtomicBool>,
421) {
422    loop {
423        // `biased` so cancellations overtake queued requests. A cancellation
424        // frees work the proxy is doing for nobody, so it is worth more than
425        // the request behind it, and under load there is always a request
426        // behind it.
427        let outgoing = tokio::select! {
428            biased;
429            Some(cancellation) = cancels.recv() => Outgoing::Cancellation(cancellation),
430            Some(packet) = outbound.recv() => Outgoing::Request(packet),
431            else => break,
432        };
433        let packet = match &outgoing {
434            Outgoing::Request(packet) => packet,
435            Outgoing::Cancellation(cancellation) => &cancellation.packet,
436        };
437        if writer.send(packet).await.is_err() {
438            break;
439        }
440    }
441    closed.store(true, Ordering::Relaxed);
442    in_flight.close();
443    pending.lock().await.close();
444    let _ = writer.shutdown().await;
445}
446
447async fn read_loop(
448    mut reader: BusReader,
449    pending: Pending,
450    in_flight: InFlight,
451    closed: Arc<AtomicBool>,
452) {
453    loop {
454        let packet = match reader.receive().await {
455            Ok(packet) => packet,
456            Err(_) => break,
457        };
458
459        // Acks carry no payload and are only interesting when delivery
460        // tracking was requested, which this client does not request.
461        if packet.packet_type != PacketType::Message {
462            continue;
463        }
464
465        let Ok(response) = rpc::decode_response(packet.parts) else {
466            // A message that is not a response cannot be routed to anyone, and
467            // the connection is still usable for the requests that are.
468            continue;
469        };
470        let Some(request_id) = response.request_id() else {
471            continue;
472        };
473        if let Some(sender) = pending.lock().await.by_request.remove(&request_id) {
474            let _ = sender.send(response);
475        }
476    }
477
478    closed.store(true, Ordering::Relaxed);
479    in_flight.close();
480    // The reader is what delivers every response, so once it stops the
481    // connection is finished: waiters are woken with an error, and later calls
482    // are refused rather than parked for ever.
483    pending.lock().await.close();
484}
485
486/// Asks a proxy for the current set of RPC proxies.
487///
488/// This is the RPC `DiscoveryService`, not the HTTP `discover_proxies`
489/// command; it needs an already-connected proxy, so it refreshes a proxy list
490/// rather than bootstrapping one. See `docs/rpc-compatibility.md`.
491pub async fn discover_proxies(
492    connection: &Connection,
493    role: Option<&str>,
494    timeout: Option<std::time::Duration>,
495) -> Result<Vec<String>> {
496    let request = proto::api::TReqDiscoverProxies {
497        role: role.map(str::to_owned),
498        ..Default::default()
499    };
500    let response = connection
501        .invoke_raw(
502            rpc::DISCOVERY_SERVICE,
503            "DiscoverProxies",
504            &request,
505            Vec::new(),
506            timeout,
507            None,
508        )
509        .await?;
510    let decoded = response.decode_body::<proto::api::TRspDiscoverProxies>("TRspDiscoverProxies")?;
511    Ok(decoded.addresses)
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::bus::packet;
518    use bytes::BytesMut;
519    use tokio::io::{AsyncReadExt, AsyncWriteExt};
520    use tokio::net::TcpListener;
521
522    /// A stub proxy: completes the handshake, then answers each request through
523    /// the supplied closure. Enough to test routing, cancellation and
524    /// connection loss without a cluster.
525    ///
526    /// Dropping it really does drop the connection. The accepted socket is
527    /// owned by the spawned task, not by this struct, so without the explicit
528    /// abort the socket would stay open after the stub went out of scope and a
529    /// test waiting for the connection to fail would wait for ever.
530    struct StubProxy {
531        address: String,
532        seen: mpsc::UnboundedReceiver<Packet>,
533        task: tokio::task::JoinHandle<()>,
534        inject: mpsc::UnboundedSender<Packet>,
535    }
536
537    impl StubProxy {
538        /// Sends a packet the client never asked for.
539        async fn inject(&self, packet: Packet) {
540            let _ = self.inject.send(packet);
541            // Give the stub's loop a turn to pick it up.
542            tokio::time::sleep(std::time::Duration::from_millis(20)).await;
543        }
544    }
545
546    impl Drop for StubProxy {
547        fn drop(&mut self) {
548            self.task.abort();
549        }
550    }
551
552    async fn stub_proxy(
553        answer: impl Fn(&proto::rpc::TRequestHeader) -> Option<Vec<Option<Bytes>>> + Send + 'static,
554    ) -> StubProxy {
555        stub_proxy_with_batching(answer, 1).await
556    }
557
558    /// A stub that collects `batch` requests before answering any of them, and
559    /// then answers them in **reverse** order.
560    ///
561    /// With `batch = 1` this is an ordinary echo server. Above 1 it is the only
562    /// way to test that responses are routed by request id: a serial stub
563    /// replies in the order it was asked, so first-come-first-served dispatch
564    /// and id-keyed dispatch produce identical results and a test cannot tell
565    /// them apart.
566    async fn stub_proxy_with_batching(
567        answer: impl Fn(&proto::rpc::TRequestHeader) -> Option<Vec<Option<Bytes>>> + Send + 'static,
568        batch: usize,
569    ) -> StubProxy {
570        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
571        let address = listener.local_addr().unwrap().to_string();
572        let (seen_sender, seen) = mpsc::unbounded_channel();
573        let (inject, mut injected) = mpsc::unbounded_channel::<Packet>();
574
575        let task = tokio::spawn(async move {
576            let (stream, _) = listener.accept().await.unwrap();
577            let (mut read_half, mut write_half) = stream.into_split();
578            let mut buffer = BytesMut::new();
579            let mut handshaken = false;
580            let mut pending_replies: Vec<Vec<Option<Bytes>>> = Vec::new();
581
582            loop {
583                // Anything a test wants to push at the client, unsolicited.
584                while let Ok(packet) = injected.try_recv() {
585                    let mut out = BytesMut::new();
586                    packet::encode(&packet, &mut out).unwrap();
587                    if write_half.write_all(&out).await.is_err() {
588                        return;
589                    }
590                }
591
592                let decoded = packet::decode(&mut buffer, crate::bus::DEFAULT_MAX_MESSAGE_SIZE);
593                match decoded {
594                    Ok(Some(request)) => {
595                        if !handshaken {
596                            handshaken = true;
597                            let handshake = proto::bus::THandshake {
598                                connection_id: Guid::random().to_proto(),
599                                encryption_mode: Some(0),
600                                ..Default::default()
601                            };
602                            let mut part = Vec::new();
603                            part.extend_from_slice(&crate::bus::HANDSHAKE_SIGNATURE.to_le_bytes());
604                            handshake.encode(&mut part).unwrap();
605                            let reply = Packet::message(
606                                request.id,
607                                vec![Some(Bytes::from(part))],
608                                PacketFlags::NONE,
609                            );
610                            let mut out = BytesMut::new();
611                            packet::encode(&reply, &mut out).unwrap();
612                            if write_half.write_all(&out).await.is_err() {
613                                return;
614                            }
615                            continue;
616                        }
617
618                        // Tolerant on purpose: a test may put packets on this
619                        // connection that are not RPC requests, and a stub that
620                        // panicked on them would fail the test for the wrong
621                        // reason.
622                        let Some(Some(header_part)) = request.parts.first().cloned() else {
623                            continue;
624                        };
625                        let _ = seen_sender.send(request.clone());
626                        if header_part.len() < 4 {
627                            continue;
628                        }
629                        let Ok(header) = proto::rpc::TRequestHeader::decode(&header_part[4..])
630                        else {
631                            continue;
632                        };
633
634                        if let Some(parts) = answer(&header) {
635                            pending_replies.push(parts);
636                        }
637                        if pending_replies.len() >= batch {
638                            // Reversed: the last request asked is the first
639                            // answered.
640                            for parts in pending_replies.drain(..).rev() {
641                                let reply =
642                                    Packet::message(Guid::random(), parts, PacketFlags::NONE);
643                                let mut out = BytesMut::new();
644                                packet::encode(&reply, &mut out).unwrap();
645                                if write_half.write_all(&out).await.is_err() {
646                                    return;
647                                }
648                            }
649                        }
650                        continue;
651                    }
652                    Ok(None) => {}
653                    Err(_) => return,
654                }
655                if read_half.read_buf(&mut buffer).await.unwrap_or(0) == 0 {
656                    return;
657                }
658            }
659        });
660
661        StubProxy {
662            address,
663            seen,
664            task,
665            inject,
666        }
667    }
668
669    /// The next packet the stub saw, or `None` if none arrives promptly.
670    ///
671    /// Bounded on purpose. A bare `recv().await` turns "the client never sent
672    /// the thing this test is about" into a test that hangs for ever instead of
673    /// one that fails, which in CI is indistinguishable from a stuck runner.
674    async fn next_packet(stub: &mut StubProxy) -> Option<Packet> {
675        tokio::time::timeout(std::time::Duration::from_secs(5), stub.seen.recv())
676            .await
677            .ok()
678            .flatten()
679    }
680
681    fn success_reply(request_id: Guid, body: &impl Message) -> Vec<Option<Bytes>> {
682        let header = proto::rpc::TResponseHeader {
683            request_id: Some(request_id.to_proto()),
684            ..Default::default()
685        };
686        let mut header_part = Vec::new();
687        header_part.extend_from_slice(&(rpc::MessageType::Response as u32).to_le_bytes());
688        header.encode(&mut header_part).unwrap();
689        vec![
690            Some(Bytes::from(header_part)),
691            Some(Bytes::from(body.encode_to_vec())),
692        ]
693    }
694
695    fn error_reply(request_id: Guid, code: i32, message: &str) -> Vec<Option<Bytes>> {
696        let header = proto::rpc::TResponseHeader {
697            request_id: Some(request_id.to_proto()),
698            error: Some(proto::misc::TError {
699                code,
700                message: Some(message.to_owned()),
701                attributes: None,
702                inner_errors: vec![],
703            }),
704            ..Default::default()
705        };
706        let mut header_part = Vec::new();
707        header_part.extend_from_slice(&(rpc::MessageType::Response as u32).to_le_bytes());
708        header.encode(&mut header_part).unwrap();
709        vec![Some(Bytes::from(header_part))]
710    }
711
712    #[tokio::test]
713    async fn a_call_gets_its_own_response() {
714        let mut stub = stub_proxy(|header| {
715            let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
716            Some(success_reply(
717                request_id,
718                &proto::api::TRspPingTransaction::default(),
719            ))
720        })
721        .await;
722
723        let connection = Connection::connect(&stub.address, None).await.unwrap();
724        let request = proto::api::TReqPingTransaction {
725            transaction_id: Guid::random().to_proto(),
726            ..Default::default()
727        };
728        // Bounded, like every other await on a call in this file: a test that
729        // hangs when the client stops answering is indistinguishable from a
730        // stuck CI runner.
731        let (_response, attachments) = tokio::time::timeout(
732            std::time::Duration::from_secs(10),
733            connection.invoke::<proto::api::TRspPingTransaction>(
734                "PingTransaction",
735                &request,
736                Vec::new(),
737                None,
738                "TRspPingTransaction",
739            ),
740        )
741        .await
742        .expect("the stub answers immediately")
743        .unwrap();
744        assert!(attachments.is_empty(), "the stub sent no attachments");
745
746        // The stub answers only the request id it was given, so reaching here
747        // at all means the response was routed by id. Check the request that
748        // arrived really is the one that was made.
749        let sent = next_packet(&mut stub).await.expect("the request");
750        let header_part = sent.parts[0].as_ref().unwrap();
751        let header = proto::rpc::TRequestHeader::decode(&header_part[4..]).unwrap();
752        assert_eq!(header.method, "PingTransaction");
753        assert_eq!(header.service, rpc::API_SERVICE);
754        let body = proto::api::TReqPingTransaction::decode(sent.parts[1].as_ref().unwrap().clone())
755            .unwrap();
756        assert_eq!(body.transaction_id, request.transaction_id);
757    }
758
759    /// The point of the actor: several requests in flight on one connection,
760    /// answered **out of order**, each reaching its own caller.
761    ///
762    /// The reversal is what gives this test teeth. A stub that answers in the
763    /// order it was asked cannot distinguish routing by request id from
764    /// answering whoever asked first — both deliver the right bytes to the
765    /// right caller by accident. This one holds all four requests and replies
766    /// last-first, so first-come-first-served dispatch hands every caller
767    /// somebody else's answer.
768    #[tokio::test]
769    async fn concurrent_requests_are_routed_by_request_id() {
770        let stub = stub_proxy_with_batching(
771            |header| {
772                let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
773                // Echo the method name back inside the response so each caller can
774                // check it got *its* answer.
775                Some(success_reply(
776                    request_id,
777                    &proto::api::TRspGetNode {
778                        value: header.method.clone().into_bytes(),
779                    },
780                ))
781            },
782            4,
783        )
784        .await;
785
786        let connection = Arc::new(Connection::connect(&stub.address, None).await.unwrap());
787        let methods = ["GetNode", "ListNode", "ExistsNode", "SetNode"];
788        let mut handles = Vec::new();
789        for method in methods {
790            let connection = Arc::clone(&connection);
791            handles.push(tokio::spawn(async move {
792                connection
793                    .invoke::<proto::api::TRspGetNode>(
794                        method,
795                        &proto::api::TReqGetNode::default(),
796                        Vec::new(),
797                        None,
798                        "TRspGetNode",
799                    )
800                    .await
801                    .map(|(response, _)| String::from_utf8(response.value).unwrap())
802            }));
803        }
804
805        for (method, handle) in methods.iter().zip(handles) {
806            let answer = tokio::time::timeout(std::time::Duration::from_secs(10), handle)
807                .await
808                .expect("a call is stuck: the stub answers only once all four have arrived")
809                .unwrap()
810                .unwrap();
811            assert_eq!(
812                &answer, method,
813                "the caller for {method} was handed another call's answer"
814            );
815        }
816    }
817
818    #[tokio::test]
819    async fn a_server_error_becomes_a_rust_error_with_its_code() {
820        let stub = stub_proxy(|header| {
821            let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
822            Some(error_reply(
823                request_id,
824                crate::error::codes::NO_SUCH_TRANSACTION,
825                "no such transaction",
826            ))
827        })
828        .await;
829
830        let connection = Connection::connect(&stub.address, None).await.unwrap();
831        let error = tokio::time::timeout(
832            std::time::Duration::from_secs(10),
833            connection.invoke::<proto::api::TRspPingTransaction>(
834                "PingTransaction",
835                &proto::api::TReqPingTransaction {
836                    transaction_id: Guid::random().to_proto(),
837                    ..Default::default()
838                },
839                Vec::new(),
840                None,
841                "TRspPingTransaction",
842            ),
843        )
844        .await
845        .expect("the stub answers immediately")
846        .unwrap_err();
847
848        assert!(error.has_code(crate::error::codes::NO_SUCH_TRANSACTION));
849        assert!(
850            error
851                .to_string()
852                .contains("ApiService.PingTransaction failed")
853        );
854    }
855
856    #[tokio::test]
857    async fn a_timeout_reports_the_method_and_cancels_the_request() {
858        // Never answers, so the local timeout is what ends the call.
859        let mut stub = stub_proxy(|_| None).await;
860        let connection = Connection::connect(&stub.address, None).await.unwrap();
861
862        // Bounded well above the 50 ms deadline under test. Without this, a
863        // regression in that deadline makes the test hang instead of fail —
864        // which in CI is indistinguishable from a stuck runner, and is the
865        // exact shape this suite has already been caught in twice.
866        let error = tokio::time::timeout(
867            std::time::Duration::from_secs(10),
868            connection.invoke::<proto::api::TRspPingTransaction>(
869                "PingTransaction",
870                &proto::api::TReqPingTransaction {
871                    transaction_id: Guid::random().to_proto(),
872                    ..Default::default()
873                },
874                Vec::new(),
875                Some(std::time::Duration::from_millis(50)),
876                "TRspPingTransaction",
877            ),
878        )
879        .await
880        .expect("the local deadline did not fire: the call outlived it twentyfold")
881        .unwrap_err();
882        assert!(matches!(error, Error::Timeout { .. }), "got {error}");
883
884        // The request, then the cancellation for it.
885        let request = next_packet(&mut stub).await.expect("the request");
886        let header_part = request.parts[0].as_ref().unwrap();
887        assert_eq!(&header_part[0..4], b"rpci");
888        let header = proto::rpc::TRequestHeader::decode(&header_part[4..]).unwrap();
889        let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
890        // The header carries the deadline too, so the server stops on its own
891        // even if the cancellation is lost.
892        assert_eq!(header.timeout, Some(50_000));
893
894        let cancelation = next_packet(&mut stub)
895            .await
896            .expect("a cancellation must follow the timeout");
897        let part = cancelation.parts[0].as_ref().unwrap();
898        assert_eq!(&part[0..4], b"rpcc", "cancellation is an rpcc message");
899        let cancel_header = proto::rpc::TRequestCancelationHeader::decode(&part[4..]).unwrap();
900        assert_eq!(Guid::from_proto(&cancel_header.request_id), request_id);
901    }
902
903    /// A message the client cannot route must be ignored, not fatal.
904    ///
905    /// A proxy may send an ack, a response for a request that has already timed
906    /// out, or something this crate does not parse. Ending the read loop on any
907    /// of those would take down every other call on the connection — and both
908    /// `continue`s that prevent it survived mutation, so nothing was checking.
909    #[tokio::test]
910    async fn junk_from_the_peer_does_not_kill_the_connection() {
911        let stub = stub_proxy(|header| {
912            let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
913            Some(success_reply(
914                request_id,
915                &proto::api::TRspPingTransaction::default(),
916            ))
917        })
918        .await;
919
920        let connection = Connection::connect(&stub.address, None).await.unwrap();
921        let request = proto::api::TReqPingTransaction {
922            transaction_id: Guid::random().to_proto(),
923            ..Default::default()
924        };
925
926        // A response nobody is waiting for, and a message that is not a
927        // response at all: both arrive before any call is made.
928        let orphan = {
929            let header = proto::rpc::TResponseHeader {
930                request_id: Some(Guid::random().to_proto()),
931                ..Default::default()
932            };
933            let mut bytes = Vec::new();
934            bytes.extend_from_slice(&(rpc::MessageType::Response as u32).to_le_bytes());
935            header.encode(&mut bytes).unwrap();
936            Packet::message(
937                Guid::random(),
938                vec![Some(Bytes::from(bytes))],
939                PacketFlags::NONE,
940            )
941        };
942        let unparseable = Packet::message(
943            Guid::random(),
944            vec![Some(Bytes::from_static(b"not an rpc message at all"))],
945            PacketFlags::NONE,
946        );
947        let ack = Packet {
948            packet_type: PacketType::Ack,
949            flags: PacketFlags::NONE,
950            id: Guid::random(),
951            parts: Vec::new(),
952        };
953
954        for packet in [orphan, unparseable, ack] {
955            stub.inject(packet).await;
956        }
957        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
958
959        // The connection must still work.
960        assert!(!connection.is_closed(), "junk closed the connection");
961        tokio::time::timeout(
962            std::time::Duration::from_secs(10),
963            connection.invoke::<proto::api::TRspPingTransaction>(
964                "PingTransaction",
965                &request,
966                Vec::new(),
967                Some(std::time::Duration::from_secs(5)),
968                "TRspPingTransaction",
969            ),
970        )
971        .await
972        .expect("the connection stopped answering after the junk")
973        .expect("a call after the junk must still work");
974    }
975
976    #[tokio::test]
977    async fn a_dropped_connection_fails_the_calls_in_flight() {
978        let stub = stub_proxy(|_| None).await;
979        let connection = Connection::connect(&stub.address, None).await.unwrap();
980
981        let request = proto::api::TReqPingTransaction {
982            transaction_id: Guid::random().to_proto(),
983            ..Default::default()
984        };
985        let call = connection.invoke::<proto::api::TRspPingTransaction>(
986            "PingTransaction",
987            &request,
988            Vec::new(),
989            None,
990            "TRspPingTransaction",
991        );
992
993        // Killing the stub closes the socket, which must wake the caller with
994        // an error rather than leaving it parked forever.
995        drop(stub);
996        let error = tokio::time::timeout(std::time::Duration::from_secs(10), call)
997            .await
998            .expect("dropping the connection must fail the call, not park it")
999            .unwrap_err();
1000        assert!(
1001            matches!(error, Error::ConnectionClosed { .. }),
1002            "got {error}"
1003        );
1004    }
1005
1006    /// Dropping a call must tell the server to stop, not just stop listening.
1007    /// A client that only stops waiting leaves the proxy computing a result
1008    /// nobody will read — the exact cost this crate exists to avoid — which is
1009    /// why `TRequestHeader` has an `uncancelable` flag at all.
1010    #[tokio::test]
1011    async fn dropping_a_call_cancels_it_on_the_wire() {
1012        let mut stub = stub_proxy(|_| None).await;
1013        let connection = Connection::connect(&stub.address, None).await.unwrap();
1014
1015        let request = proto::api::TReqSelectRows {
1016            query: "* from [//tmp/t]".to_owned(),
1017            ..Default::default()
1018        };
1019        {
1020            let call = connection.invoke::<proto::api::TRspSelectRows>(
1021                "SelectRows",
1022                &request,
1023                Vec::new(),
1024                // No timeout: the drop is what has to do the cancelling.
1025                None,
1026                "TRspSelectRows",
1027            );
1028            let _ = tokio::time::timeout(std::time::Duration::from_millis(50), call).await;
1029        }
1030
1031        let sent = next_packet(&mut stub).await.expect("the request");
1032        let header_part = sent.parts[0].as_ref().unwrap();
1033        assert_eq!(&header_part[0..4], b"rpci");
1034        let header = proto::rpc::TRequestHeader::decode(&header_part[4..]).unwrap();
1035        let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
1036
1037        let cancelation = next_packet(&mut stub)
1038            .await
1039            .expect("dropping the future must send a cancellation");
1040        let part = cancelation.parts[0].as_ref().unwrap();
1041        assert_eq!(&part[0..4], b"rpcc");
1042        let cancel_header = proto::rpc::TRequestCancelationHeader::decode(&part[4..]).unwrap();
1043        assert_eq!(Guid::from_proto(&cancel_header.request_id), request_id);
1044        assert_eq!(cancel_header.method, "SelectRows");
1045    }
1046
1047    /// A completed call must NOT be cancelled: the answer is already in hand,
1048    /// and a stray cancellation for a finished request is noise on the wire.
1049    #[tokio::test]
1050    async fn a_completed_call_sends_no_cancellation() {
1051        let mut stub = stub_proxy(|header| {
1052            let request_id = Guid::from_proto(header.request_id.as_ref().unwrap());
1053            Some(success_reply(
1054                request_id,
1055                &proto::api::TRspPingTransaction::default(),
1056            ))
1057        })
1058        .await;
1059
1060        let connection = Connection::connect(&stub.address, None).await.unwrap();
1061        let request = proto::api::TReqPingTransaction {
1062            transaction_id: Guid::random().to_proto(),
1063            ..Default::default()
1064        };
1065        tokio::time::timeout(
1066            std::time::Duration::from_secs(10),
1067            connection.invoke::<proto::api::TRspPingTransaction>(
1068                "PingTransaction",
1069                &request,
1070                Vec::new(),
1071                None,
1072                "TRspPingTransaction",
1073            ),
1074        )
1075        .await
1076        .expect("the stub answers immediately")
1077        .unwrap();
1078
1079        let _request = next_packet(&mut stub).await.expect("the request");
1080        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1081        assert!(
1082            stub.seen.try_recv().is_err(),
1083            "a completed call must not be followed by a cancellation"
1084        );
1085    }
1086
1087    /// A request that never reached the outbound queue has nothing for the
1088    /// server to cancel: an `rpcc` naming a request id the proxy has never seen
1089    /// is noise at best, and at worst cancels an unrelated request that later
1090    /// reuses the id.
1091    ///
1092    /// Tested on the guard directly rather than through a stub, because the
1093    /// distinction is one flag and a stub cannot be held reliably in the state
1094    /// that exercises it — the writer drains the queue as fast as the peer
1095    /// reads. This is the mutation that survived round two: setting `sent` at
1096    /// construction left every other test in the crate green.
1097    #[tokio::test]
1098    async fn the_guard_cancels_only_what_it_actually_sent() {
1099        async fn drain(receiver: &mut mpsc::Receiver<Cancellation>) -> Vec<Packet> {
1100            // The guard defers its work to the runtime, so give it a turn.
1101            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1102            let mut packets = Vec::new();
1103            while let Ok(cancellation) = receiver.try_recv() {
1104                packets.push(cancellation.packet);
1105            }
1106            packets
1107        }
1108
1109        fn guard(
1110            pending: &Pending,
1111            cancels: &mpsc::Sender<Cancellation>,
1112            in_flight: &InFlight,
1113            request_id: Guid,
1114            sent: bool,
1115        ) -> PendingGuard {
1116            PendingGuard {
1117                pending: Arc::clone(pending),
1118                cancels: cancels.clone(),
1119                request_id,
1120                service: rpc::API_SERVICE.to_owned(),
1121                method: "LookupRows".to_owned(),
1122                completed: false,
1123                sent,
1124                permit: Some(
1125                    Arc::clone(in_flight)
1126                        .try_acquire_owned()
1127                        .expect("the test never holds more than one permit"),
1128                ),
1129            }
1130        }
1131
1132        let pending: Pending = Arc::default();
1133        let in_flight = Arc::new(Semaphore::new(1));
1134        let (cancels, mut receiver) = mpsc::channel(16);
1135        let request_id = Guid::random();
1136
1137        // Never queued: nothing may go out.
1138        drop(guard(&pending, &cancels, &in_flight, request_id, false));
1139        assert!(
1140            drain(&mut receiver).await.is_empty(),
1141            "cancelled a request the proxy never received"
1142        );
1143
1144        // Queued: the cancellation must name exactly that request.
1145        drop(guard(&pending, &cancels, &in_flight, request_id, true));
1146        let sent_packets = drain(&mut receiver).await;
1147        assert_eq!(sent_packets.len(), 1, "expected exactly one cancellation");
1148        let part = sent_packets[0].parts[0].as_ref().unwrap();
1149        assert_eq!(&part[0..4], b"rpcc");
1150        let header = proto::rpc::TRequestCancelationHeader::decode(&part[4..]).unwrap();
1151        assert_eq!(Guid::from_proto(&header.request_id), request_id);
1152
1153        // Completed: the answer is in hand, so neither removal nor cancellation.
1154        let mut done = guard(&pending, &cancels, &in_flight, request_id, true);
1155        done.complete();
1156        drop(done);
1157        assert!(
1158            drain(&mut receiver).await.is_empty(),
1159            "cancelled a call that had already returned"
1160        );
1161    }
1162
1163    /// Every cancellation keeps the permit until the writer consumes it. If
1164    /// permits were released by `PendingGuard::drop`, a permanently blocked
1165    /// writer would eventually fill this queue and later `try_send`s would be
1166    /// silently lost.
1167    #[tokio::test]
1168    async fn every_in_flight_call_has_room_for_its_cancellation() {
1169        let pending: Pending = Arc::default();
1170        let in_flight = Arc::new(Semaphore::new(MAX_IN_FLIGHT));
1171        let (cancels, mut receiver) = mpsc::channel(CANCEL_QUEUE);
1172
1173        for _ in 0..MAX_IN_FLIGHT {
1174            let guard = PendingGuard {
1175                pending: Arc::clone(&pending),
1176                cancels: cancels.clone(),
1177                request_id: Guid::random(),
1178                service: rpc::API_SERVICE.to_owned(),
1179                method: "LookupRows".to_owned(),
1180                completed: false,
1181                sent: true,
1182                permit: Some(
1183                    Arc::clone(&in_flight)
1184                        .try_acquire_owned()
1185                        .expect("the loop takes every permit exactly once"),
1186                ),
1187            };
1188            drop(guard);
1189        }
1190
1191        assert_eq!(receiver.len(), MAX_IN_FLIGHT);
1192        assert!(
1193            Arc::clone(&in_flight).try_acquire_owned().is_err(),
1194            "a queued cancellation must retain its call's permit"
1195        );
1196
1197        // Removing one cancellation also releases one permit, so the next
1198        // call will have both an in-flight slot and a cancellation slot.
1199        drop(receiver.recv().await.expect("the first cancellation"));
1200        assert!(
1201            Arc::clone(&in_flight).try_acquire_owned().is_ok(),
1202            "the consumed cancellation did not release its permit"
1203        );
1204    }
1205
1206    /// The pending map holds only callers that have acquired a permit. An
1207    /// outbound channel alone cannot provide this property: its writer can
1208    /// drain all packets while a proxy answers none.
1209    #[tokio::test]
1210    async fn the_in_flight_limit_bounds_pending_waiters() {
1211        let (outbound, _outbound_receiver) = mpsc::channel(MAX_IN_FLIGHT);
1212        let (cancels, _cancel_receiver) = mpsc::channel(CANCEL_QUEUE);
1213        let in_flight = Arc::new(Semaphore::new(MAX_IN_FLIGHT));
1214        let connection = Connection {
1215            outbound,
1216            cancels,
1217            pending: Arc::default(),
1218            in_flight: Arc::clone(&in_flight),
1219            address: "test".to_owned(),
1220            token: None,
1221            closed: Arc::new(AtomicBool::new(false)),
1222            reader_task: tokio::spawn(std::future::pending()),
1223        };
1224        let request = proto::api::TReqPingTransaction {
1225            transaction_id: Guid::random().to_proto(),
1226            ..Default::default()
1227        };
1228        let mut calls = Vec::with_capacity(MAX_IN_FLIGHT);
1229
1230        for _ in 0..MAX_IN_FLIGHT {
1231            let mut call = Box::pin(connection.invoke_raw(
1232                rpc::API_SERVICE,
1233                "PingTransaction",
1234                &request,
1235                Vec::new(),
1236                None,
1237                None,
1238            ));
1239            tokio::select! {
1240                biased;
1241                _ = call.as_mut() => panic!("the test connection cannot answer"),
1242                _ = tokio::task::yield_now() => {}
1243            }
1244            calls.push(call);
1245        }
1246        assert_eq!(
1247            connection.pending.lock().await.by_request.len(),
1248            MAX_IN_FLIGHT
1249        );
1250
1251        let mut overflow = Box::pin(connection.invoke_raw(
1252            rpc::API_SERVICE,
1253            "PingTransaction",
1254            &request,
1255            Vec::new(),
1256            None,
1257            None,
1258        ));
1259        tokio::select! {
1260            biased;
1261            _ = overflow.as_mut() => panic!("the overflow call cannot complete"),
1262            _ = tokio::task::yield_now() => {}
1263        }
1264        assert_eq!(
1265            connection.pending.lock().await.by_request.len(),
1266            MAX_IN_FLIGHT,
1267            "a call waiting for capacity must not register another waiter"
1268        );
1269        assert!(
1270            Arc::clone(&in_flight).try_acquire_owned().is_err(),
1271            "all in-flight permits should be held by the registered calls"
1272        );
1273
1274        drop(overflow);
1275        drop(calls);
1276    }
1277
1278    /// The same rule, but through `invoke_raw` — which is where the flag is
1279    /// actually set, and therefore the only place a mistake in setting it can
1280    /// be caught. Constructing a guard by hand, as the test above does, cannot
1281    /// catch it.
1282    ///
1283    /// The request queue is given capacity 1, filled, and left undrained, so
1284    /// the call cannot get its request out and times out *while queuing*. That
1285    /// is deterministic where a stalled real peer is not — a socket absorbs
1286    /// megabytes before it blocks. The cancellation channel is separate and
1287    /// empty, so a cancellation for a request that never left would be visible.
1288    #[tokio::test]
1289    async fn a_call_that_times_out_while_queuing_cancels_nothing() {
1290        let (outbound, _outbound_receiver) = mpsc::channel(1);
1291        let (cancels, mut cancel_receiver) = mpsc::channel(CANCEL_QUEUE);
1292        let connection = Connection {
1293            outbound,
1294            cancels,
1295            pending: Arc::default(),
1296            in_flight: Arc::new(Semaphore::new(MAX_IN_FLIGHT)),
1297            address: "test".to_owned(),
1298            token: None,
1299            closed: Arc::new(AtomicBool::new(false)),
1300            // Nothing to read: this test never reaches the wire.
1301            reader_task: tokio::spawn(std::future::pending()),
1302        };
1303
1304        connection
1305            .outbound
1306            .try_send(Packet::message(
1307                Guid::random(),
1308                vec![Some(Bytes::from_static(b"blocker"))],
1309                PacketFlags::NONE,
1310            ))
1311            .expect("the queue starts empty");
1312
1313        let request = proto::api::TReqPingTransaction {
1314            transaction_id: Guid::random().to_proto(),
1315            ..Default::default()
1316        };
1317        let error = tokio::time::timeout(
1318            std::time::Duration::from_secs(10),
1319            connection.invoke_raw(
1320                rpc::API_SERVICE,
1321                "PingTransaction",
1322                &request,
1323                Vec::new(),
1324                Some(std::time::Duration::from_millis(50)),
1325                None,
1326            ),
1327        )
1328        .await
1329        .expect("the deadline must end a call that cannot even be queued")
1330        .unwrap_err();
1331        assert!(matches!(error, Error::Timeout { .. }), "got {error}");
1332
1333        // Let the guard's deferred work run before looking.
1334        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1335        assert!(
1336            cancel_receiver.try_recv().is_err(),
1337            "cancelled a request that never left the queue"
1338        );
1339        assert!(
1340            connection.pending.lock().await.by_request.is_empty(),
1341            "the timed-out call left its entry behind"
1342        );
1343    }
1344
1345    /// Dropping a call outside a runtime must not panic.
1346    ///
1347    /// `Drop` cannot await, so the cleanup is normally handed to the runtime —
1348    /// but a future can be dropped with no runtime entered, and a panic while
1349    /// unwinding aborts the process.
1350    #[test]
1351    fn dropping_a_call_outside_a_runtime_does_not_panic() {
1352        let runtime = tokio::runtime::Builder::new_current_thread()
1353            .enable_all()
1354            .build()
1355            .unwrap();
1356
1357        // Built inside the runtime, but owned out here, so the call below can
1358        // borrow them and still be dropped on a plain thread.
1359        let (stub, connection) = runtime.block_on(async {
1360            let stub = stub_proxy(|_| None).await;
1361            let connection = Connection::connect(&stub.address, None).await.unwrap();
1362            (stub, connection)
1363        });
1364        let request = proto::api::TReqPingTransaction {
1365            transaction_id: Guid::random().to_proto(),
1366            ..Default::default()
1367        };
1368
1369        let mut call = Box::pin(connection.invoke_raw(
1370            rpc::API_SERVICE,
1371            "PingTransaction",
1372            &request,
1373            Vec::new(),
1374            None,
1375            None,
1376        ));
1377        // Polled far enough to register the waiter and arm the guard.
1378        runtime.block_on(async {
1379            let _ = tokio::time::timeout(std::time::Duration::from_millis(50), &mut call).await;
1380        });
1381
1382        // Dropped here, on a plain thread with no runtime entered: the case
1383        // that used to abort the process through a panic in `Drop`.
1384        drop(call);
1385        drop(connection);
1386        drop(stub);
1387    }
1388
1389    #[tokio::test]
1390    async fn the_pending_map_does_not_leak_when_a_call_is_dropped() {
1391        let stub = stub_proxy(|_| None).await;
1392        let connection = Connection::connect(&stub.address, None).await.unwrap();
1393
1394        let request = proto::api::TReqPingTransaction {
1395            transaction_id: Guid::random().to_proto(),
1396            ..Default::default()
1397        };
1398        {
1399            let call = connection.invoke::<proto::api::TRspPingTransaction>(
1400                "PingTransaction",
1401                &request,
1402                Vec::new(),
1403                None,
1404                "TRspPingTransaction",
1405            );
1406            // Give it long enough to register, then abandon it.
1407            let _ = tokio::time::timeout(std::time::Duration::from_millis(50), call).await;
1408        }
1409
1410        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1411        assert!(
1412            connection.pending.lock().await.by_request.is_empty(),
1413            "a dropped call left its entry in the pending map"
1414        );
1415    }
1416}