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