| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use futures::{AsyncReadExt as _, AsyncWriteExt as _}; | |
| use libp2p::PeerId; | |
| use tokio::sync::{mpsc, oneshot, watch}; | |
| use super::{CompletedStream, PeerError, StreamCompletion}; | |
| const QUEUE_CAPACITY: usize = 64; | |
| const CHUNK_BYTES: usize = 64 * 1024; | |
| pub struct PeerStream { | |
| pub peer_id: PeerId, | |
| pub path: PeerConnectionPath, | |
| pub incoming: mpsc::Receiver<Result<Vec<u8>, PeerError>>, | |
| pub commands: mpsc::Sender<StreamCommand>, | |
| pub abort: watch::Sender<bool>, | |
| } | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| pub enum DirectTransport { | |
| Quic, | |
| Tcp, | |
| WebRtc, | |
| Other, | |
| } | |
| #[derive(Clone, Debug, PartialEq, Eq)] | |
| pub enum PeerConnectionPath { | |
| Direct(DirectTransport), | |
| Transit { relay_peer_id: PeerId }, | |
| } | |
| impl PeerConnectionPath { | |
| pub(super) fn relay_peer_id(&self) -> Option<PeerId> { | |
| match self { | |
| Self::Direct(_) => None, | |
| Self::Transit { relay_peer_id } => Some(*relay_peer_id), | |
| } | |
| } | |
| } | |
| pub enum StreamCommand { | |
| Write { | |
| bytes: Vec<u8>, | |
| result: oneshot::Sender<Result<(), PeerError>>, | |
| }, | |
| Close { | |
| result: oneshot::Sender<Result<(), PeerError>>, | |
| }, | |
| } | |
| pub(super) fn spawn_stream( | |
| peer_id: PeerId, | |
| path: PeerConnectionPath, | |
| stream: impl futures::AsyncRead + futures::AsyncWrite + Unpin + Send + 'static, | |
| completion: Option<(StreamCompletion, mpsc::Sender<CompletedStream>)>, | |
| ) -> PeerStream { | |
| let (incoming_tx, incoming_rx) = mpsc::channel(QUEUE_CAPACITY); | |
| let (command_tx, mut command_rx) = mpsc::channel(QUEUE_CAPACITY); | |
| let (abort_tx, mut abort_rx) = watch::channel(false); | |
| let abort_guard = abort_tx.clone(); | |
| tokio::spawn(async move { | |
| let _abort_guard = abort_guard; | |
| let (mut reader, mut writer) = stream.split(); | |
| // Drive both halves independently. Awaiting a backpressured write in | |
| // the read loop deadlocks when both peers send more than the window. | |
| let close_result = { | |
| let reading = async { | |
| let mut buffer = vec![0_u8; CHUNK_BYTES]; | |
| loop { | |
| match reader.read(&mut buffer).await { | |
| Ok(0) => break, | |
| Ok(size) => { | |
| if incoming_tx.send(Ok(buffer[..size].to_vec())).await.is_err() { | |
| break; | |
| } | |
| } | |
| Err(error) => { | |
| let _ = incoming_tx | |
| .send(Err(PeerError::new("peer_native_failed", error.to_string()))) | |
| .await; | |
| break; | |
| } | |
| } | |
| } | |
| }; | |
| let writing = async { | |
| while let Some(command) = command_rx.recv().await { | |
| match command { | |
| StreamCommand::Write { bytes, result } => { | |
| let outcome = async { | |
| writer.write_all(&bytes).await?; | |
| writer.flush().await | |
| } | |
| .await | |
| .map_err(|error: std::io::Error| { | |
| PeerError::new("peer_native_failed", error.to_string()) | |
| }); | |
| let failed = outcome.is_err(); | |
| let _ = result.send(outcome); | |
| if failed { | |
| break; | |
| } | |
| } | |
| StreamCommand::Close { result } => { | |
| let outcome = writer.close().await.map_err(|error| { | |
| PeerError::new("peer_native_failed", error.to_string()) | |
| }); | |
| return Some((result, outcome)); | |
| } | |
| } | |
| } | |
| None | |
| }; | |
| tokio::select! { | |
| biased; | |
| _ = abort_rx.changed() => None, | |
| result = writing => result, | |
| _ = reading => None, | |
| } | |
| }; | |
| if let Some((completion, completed)) = completion { | |
| let (acknowledged, acknowledgment) = oneshot::channel(); | |
| if completed | |
| .send(CompletedStream { | |
| kind: completion, | |
| acknowledged, | |
| }) | |
| .await | |
| .is_ok() | |
| { | |
| let _ = acknowledgment.await; | |
| } | |
| } | |
| if let Some((result, outcome)) = close_result { | |
| let _ = result.send(outcome); | |
| } | |
| }); | |
| PeerStream { | |
| peer_id, | |
| path, | |
| incoming: incoming_rx, | |
| commands: command_tx, | |
| abort: abort_tx, | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use tokio_util::compat::TokioAsyncReadCompatExt; | |
| #[tokio::test] | |
| async fn simultaneous_large_writes_keep_reading_under_backpressure() { | |
| let (left, right) = tokio::io::duplex(1024); | |
| let mut left = spawn_stream( | |
| PeerId::random(), | |
| PeerConnectionPath::Direct(DirectTransport::Tcp), | |
| left.compat(), | |
| None, | |
| ); | |
| let mut right = spawn_stream( | |
| PeerId::random(), | |
| PeerConnectionPath::Direct(DirectTransport::Tcp), | |
| right.compat(), | |
| None, | |
| ); | |
| let (left_result, left_done) = oneshot::channel(); | |
| let (right_result, right_done) = oneshot::channel(); | |
| const BYTES: usize = 512 * 1024; | |
| left.commands | |
| .send(StreamCommand::Write { | |
| bytes: vec![1; BYTES], | |
| result: left_result, | |
| }) | |
| .await | |
| .unwrap(); | |
| right | |
| .commands | |
| .send(StreamCommand::Write { | |
| bytes: vec![2; BYTES], | |
| result: right_result, | |
| }) | |
| .await | |
| .unwrap(); | |
| async fn receive(stream: &mut PeerStream, expected: u8) { | |
| let mut count = 0; | |
| while count < BYTES { | |
| let bytes = stream.incoming.recv().await.unwrap().unwrap(); | |
| assert!(bytes.iter().all(|byte| *byte == expected)); | |
| count += bytes.len(); | |
| } | |
| assert_eq!(count, BYTES); | |
| } | |
| let outcome = tokio::time::timeout(std::time::Duration::from_secs(3), async { | |
| let (left_ack, right_ack, _, _) = tokio::join!( | |
| left_done, | |
| right_done, | |
| receive(&mut left, 2), | |
| receive(&mut right, 1) | |
| ); | |
| left_ack.unwrap().unwrap(); | |
| right_ack.unwrap().unwrap(); | |
| }) | |
| .await; | |
| left.abort.send_replace(true); | |
| right.abort.send_replace(true); | |
| outcome.expect("simultaneous writes must not stop either reader"); | |
| } | |
| } |