scatter_gather/
scatter_gather.rs1use std::{io, net::SocketAddr, rc::Rc, time::Duration, time::Instant};
34
35use compio::{
36 BufResult,
37 io::{AsyncRead, AsyncWriteExt},
38 net::{TcpListener, TcpStream},
39 runtime::Runtime,
40};
41use compio_pool::{LocalPool, ManageConnection, Pool};
42
43struct Line {
47 stream: TcpStream,
48 buf: Vec<u8>,
49 healthy: bool,
50}
51
52impl Line {
53 fn new(stream: TcpStream) -> Self {
54 Self {
55 stream,
56 buf: Vec::with_capacity(1024),
57 healthy: true,
58 }
59 }
60
61 async fn read_line(&mut self) -> io::Result<Option<String>> {
64 loop {
65 if let Some(pos) = self.buf.iter().position(|&b| b == b'\n') {
66 let line: Vec<u8> = self.buf.drain(..=pos).collect();
67 let text = String::from_utf8_lossy(&line[..line.len() - 1])
68 .trim_end_matches('\r')
69 .to_string();
70 return Ok(Some(text));
71 }
72 if self.buf.len() == self.buf.capacity() {
74 self.buf.reserve(1024);
75 }
76 let taken = std::mem::take(&mut self.buf);
77 let BufResult(res, b) = self.stream.read(taken).await;
78 self.buf = b;
79 match res {
80 Ok(0) => return Ok(None),
81 Ok(_) => {}
82 Err(e) => return Err(e),
83 }
84 }
85 }
86
87 async fn write_line(&mut self, text: &str) -> io::Result<()> {
89 let mut bytes = text.as_bytes().to_vec();
90 bytes.push(b'\n');
91 let BufResult(res, _) = self.stream.write_all(bytes).await;
92 res
93 }
94}
95
96struct Shard {
98 addr: SocketAddr,
99}
100
101impl ManageConnection for Shard {
102 type Connection = Line;
103 type Error = io::Error;
104
105 async fn connect(&self) -> io::Result<Line> {
106 let stream = TcpStream::connect(self.addr).await?;
107 stream.set_nodelay(true)?;
108 Ok(Line::new(stream))
109 }
110
111 async fn is_valid(&self, conn: &mut Line) -> io::Result<()> {
112 if conn.healthy {
113 Ok(())
114 } else {
115 Err(io::Error::new(io::ErrorKind::BrokenPipe, "shard conn marked dead"))
116 }
117 }
118
119 fn has_broken(&self, conn: &mut Line) -> bool {
120 !conn.healthy
121 }
122}
123
124fn main() -> io::Result<()> {
125 let listen: SocketAddr = std::env::args()
126 .nth(1)
127 .unwrap_or_else(|| "127.0.0.1:9000".into())
128 .parse()
129 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad listen addr: {e}")))?;
130 let shard_count: usize = std::env::args()
131 .nth(2)
132 .and_then(|s| s.parse().ok())
133 .unwrap_or(4);
134 let capacity: u32 = std::env::args()
135 .nth(3)
136 .and_then(|s| s.parse().ok())
137 .unwrap_or(8);
138 let delay_ms: u64 = std::env::var("DELAY_MS")
139 .ok()
140 .and_then(|s| s.parse().ok())
141 .unwrap_or(5);
142
143 let runtime = Runtime::builder().build().expect("build compio runtime");
144 runtime.block_on(async move {
145 let mut shard_pools = Vec::with_capacity(shard_count);
147 for id in 0..shard_count {
148 let any: SocketAddr = "127.0.0.1:0".parse().unwrap();
149 let listener = TcpListener::bind(any).await?;
150 let addr = listener.local_addr()?;
151 compio::runtime::spawn(shard_loop(listener, id, delay_ms)).detach();
152
153 let pool = Pool::builder().max_size(capacity).build(Shard { addr });
154 shard_pools.push(pool.local());
155 }
156 let shards = Rc::new(shard_pools);
157
158 let server = TcpListener::bind(listen).await?;
159 println!(
160 "scatter_gather on {listen}: {shard_count} shards x up to {capacity} pooled conns \
161 each, {delay_ms}ms/shard work"
162 );
163 println!("try: printf 'needle\\n' | nc -q1 {listen}");
164
165 loop {
166 let Ok((stream, _peer)) = server.accept().await else {
167 continue;
168 };
169 let _ = stream.set_nodelay(true);
170 compio::runtime::spawn(handle_client(shards.clone(), stream)).detach();
171 }
172 })
173}
174
175async fn handle_client(shards: Rc<Vec<LocalPool<Shard>>>, client_stream: TcpStream) {
178 let mut client = Line::new(client_stream);
179 loop {
180 let query = match client.read_line().await {
181 Ok(Some(q)) if !q.is_empty() => q,
182 Ok(Some(_)) => continue, _ => return, };
185
186 let started = Instant::now();
189 let mut tasks = Vec::with_capacity(shards.len());
190 for pool in shards.iter() {
191 let pool = pool.clone();
192 let q = query.clone();
193 tasks.push(compio::runtime::spawn(query_shard(pool, q)));
194 }
195
196 let mut parts = Vec::with_capacity(tasks.len());
198 for (id, task) in tasks.into_iter().enumerate() {
199 match task.await {
200 Ok(Some(reply)) => parts.push(reply),
201 _ => parts.push(format!("shard{id}=ERR")),
202 }
203 }
204 let elapsed = started.elapsed();
205
206 let merged = format!(
207 "{} | {} shards in {:.1}ms",
208 parts.join(" "),
209 parts.len(),
210 elapsed.as_secs_f64() * 1000.0,
211 );
212 if client.write_line(&merged).await.is_err() {
213 return;
214 }
215 }
216}
217
218async fn query_shard(pool: LocalPool<Shard>, query: String) -> Option<String> {
222 let mut conn = match pool.get().await {
223 Ok(conn) => conn,
224 Err(_) => return None,
225 };
226 if conn.write_line(&query).await.is_err() {
227 conn.healthy = false;
228 return None;
229 }
230 match conn.read_line().await {
231 Ok(Some(reply)) => Some(reply),
232 _ => {
233 conn.healthy = false;
234 None
235 }
236 }
237}
238
239async fn shard_loop(listener: TcpListener, id: usize, delay_ms: u64) {
241 loop {
242 let Ok((stream, _peer)) = listener.accept().await else {
243 continue;
244 };
245 let _ = stream.set_nodelay(true);
246 compio::runtime::spawn(shard_conn(stream, id, delay_ms)).detach();
247 }
248}
249
250async fn shard_conn(stream: TcpStream, id: usize, delay_ms: u64) {
252 let mut conn = Line::new(stream);
253 loop {
254 let query = match conn.read_line().await {
255 Ok(Some(q)) => q,
256 _ => return,
257 };
258 if delay_ms > 0 {
259 compio::time::sleep(Duration::from_millis(delay_ms)).await;
260 }
261 let buckets = 10 + id as u64;
264 let hits = query.bytes().map(u64::from).sum::<u64>() % buckets;
265 if conn.write_line(&format!("shard{id}={hits}")).await.is_err() {
266 return;
267 }
268 }
269}