rightsize_modules/
memcached.rs1use std::time::Duration;
5
6use rightsize::wait::{WaitStrategy, WaitTarget};
7use rightsize::{Container, ContainerGuard, Result};
8use tokio::io::{AsyncReadExt, AsyncWriteExt};
9use tokio::net::TcpStream;
10
11pub struct MemcachedContainer(Container);
13
14impl MemcachedContainer {
15 const PORT: u16 = 11211;
17
18 pub fn new() -> Self {
20 Self::with_image("memcached:1.6-alpine")
21 }
22
23 pub fn with_image(image: &str) -> Self {
25 Self(
26 Container::new(image)
27 .with_exposed_ports(&[Self::PORT])
28 .waiting_for(MemcachedResponds),
29 )
30 }
31
32 pub async fn start(self) -> Result<MemcachedGuard> {
34 crate::register_default_backends();
35 Ok(MemcachedGuard(self.0.start().await?))
36 }
37}
38
39impl Default for MemcachedContainer {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45pub struct MemcachedGuard(ContainerGuard);
47
48impl MemcachedGuard {
49 pub fn address(&self) -> String {
51 format!(
52 "{}:{}",
53 self.0.host(),
54 self.0.get_mapped_port(MemcachedContainer::PORT).unwrap()
55 )
56 }
57
58 pub async fn stop(self) -> Result<()> {
60 self.0.stop().await
61 }
62}
63
64impl std::ops::Deref for MemcachedGuard {
65 type Target = ContainerGuard;
66 fn deref(&self) -> &ContainerGuard {
67 &self.0
68 }
69}
70
71struct MemcachedResponds;
77
78impl MemcachedResponds {
79 async fn probe_once(host: &str, port: u16) -> bool {
80 let Ok(connect) = tokio::time::timeout(
81 Duration::from_millis(1000),
82 TcpStream::connect((host, port)),
83 )
84 .await
85 else {
86 return false;
87 };
88 let Ok(mut stream) = connect else {
89 return false;
90 };
91 if tokio::time::timeout(
92 Duration::from_millis(1000),
93 stream.write_all(b"version\r\n"),
94 )
95 .await
96 .is_err()
97 {
98 return false;
99 }
100 let mut buf = [0u8; 64];
101 let Ok(Ok(n)) =
102 tokio::time::timeout(Duration::from_millis(1000), stream.read(&mut buf)).await
103 else {
104 return false;
105 };
106 if n == 0 {
107 return false;
108 }
109 String::from_utf8_lossy(&buf[..n]).starts_with("VERSION")
110 }
111}
112
113#[async_trait::async_trait]
114impl WaitStrategy for MemcachedResponds {
115 async fn wait_until_ready(&self, target: &dyn WaitTarget) -> Result<()> {
116 let guest_port = target
117 .exposed_guest_ports()
118 .first()
119 .copied()
120 .unwrap_or(MemcachedContainer::PORT);
121 let port = target.mapped_port(guest_port);
122 let host = target.host().to_string();
123 rightsize::wait::poll_until_ready(
124 target,
125 Duration::from_secs(60),
126 "a VERSION reply",
127 || {
128 let host = host.clone();
129 async move { Self::probe_once(&host, port).await }
130 },
131 )
132 .await
133 }
134
135 fn with_startup_timeout(self: Box<Self>, _timeout: Duration) -> Box<dyn WaitStrategy> {
136 self
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::sync::Arc;
145 use std::sync::atomic::{AtomicBool, Ordering};
146
147 #[test]
148 fn with_image_smoke() {
149 let _ = MemcachedContainer::new();
150 let _ = MemcachedContainer::with_image("memcached:1.6-alpine");
151 }
152
153 #[tokio::test]
157 async fn probe_once_true_on_a_version_reply_false_otherwise() {
158 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
159 let port = listener.local_addr().unwrap().port();
160 let ready = Arc::new(AtomicBool::new(false));
161 let ready_clone = ready.clone();
162 std::thread::spawn(move || {
163 listener.set_nonblocking(true).unwrap();
164 loop {
165 if let Ok((mut stream, _)) = listener.accept() {
166 use std::io::{Read, Write};
167 let mut buf = [0u8; 64];
168 let _ = stream.read(&mut buf);
169 if ready_clone.load(Ordering::SeqCst) {
170 let _ = stream.write_all(b"VERSION 1.6.31\r\n");
171 } else {
172 let _ = stream.write_all(b"ERROR\r\n");
173 }
174 return;
175 }
176 std::thread::sleep(Duration::from_millis(5));
177 }
178 });
179
180 assert!(!MemcachedResponds::probe_once("127.0.0.1", port).await);
181
182 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
183 let port = listener.local_addr().unwrap().port();
184 ready.store(true, Ordering::SeqCst);
185 std::thread::spawn(move || {
186 listener.set_nonblocking(true).unwrap();
187 loop {
188 if let Ok((mut stream, _)) = listener.accept() {
189 use std::io::{Read, Write};
190 let mut buf = [0u8; 64];
191 let _ = stream.read(&mut buf);
192 let _ = stream.write_all(b"VERSION 1.6.31\r\n");
193 return;
194 }
195 std::thread::sleep(Duration::from_millis(5));
196 }
197 });
198 assert!(MemcachedResponds::probe_once("127.0.0.1", port).await);
199 }
200}