Skip to main content

ringline_memcache/
sharded.rs

1//! Ketama-sharded client for ringline-memcache.
2//!
3//! Routes commands to independent Memcache instances using consistent hashing
4//! (ketama). Each server has a pool of connections with round-robin dispatch
5//! and lazy reconnection.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use ringline_memcache::{ShardedClient, ShardedConfig};
11//!
12//! async fn example() -> Result<(), ringline_memcache::Error> {
13//!     let config = ShardedConfig {
14//!         servers: vec![
15//!             "127.0.0.1:11211".parse().unwrap(),
16//!             "127.0.0.1:11212".parse().unwrap(),
17//!         ],
18//!         pool_size: 2,
19//!         connect_timeout_ms: 1000,
20//!         tls_server_name: None,
21//!     };
22//!     let mut sharded = ShardedClient::new(config);
23//!     sharded.connect_all().await?;
24//!     sharded.set("hello", "world").await?;
25//!     let val = sharded.get("hello").await?;
26//!     assert_eq!(val.unwrap().data.as_ref(), b"world");
27//!     sharded.close_all();
28//!     Ok(())
29//! }
30//! ```
31
32use std::net::SocketAddr;
33
34use memcache_proto::ResponseBytes as McResponseBytes;
35use ringline::ConnCtx;
36
37use crate::{
38    Client, Error, GetValue, Value, check_error_bytes, encode_add, encode_request, encode_set,
39};
40use memcache_proto::Request as McRequest;
41
42/// Configuration for a ketama-sharded client.
43pub struct ShardedConfig {
44    /// List of independent Memcache server addresses.
45    pub servers: Vec<SocketAddr>,
46    /// Number of connections per server (default: 1).
47    pub pool_size: usize,
48    /// Connect timeout in milliseconds. 0 means no timeout.
49    pub connect_timeout_ms: u64,
50    /// TLS server name (SNI) for outbound connections. `None` means plain TCP.
51    pub tls_server_name: Option<String>,
52}
53
54enum ShardConn {
55    Connected(ConnCtx),
56    Disconnected,
57}
58
59struct Shard {
60    addr: SocketAddr,
61    conns: Vec<ShardConn>,
62    next: usize,
63}
64
65/// A ketama-sharded Memcache client.
66///
67/// Commands are routed to independent Memcache instances using consistent
68/// hashing. Each server has a pool of connections with round-robin
69/// dispatch and lazy reconnection.
70pub struct ShardedClient {
71    shards: Vec<Shard>,
72    ring: ketama::Ring,
73    connect_timeout_ms: u64,
74    tls_server_name: Option<String>,
75}
76
77impl ShardedClient {
78    /// Create a new sharded client. All connections start disconnected.
79    pub fn new(config: ShardedConfig) -> Self {
80        let pool_size = config.pool_size.max(1);
81
82        let server_ids: Vec<String> = config.servers.iter().map(|a| a.to_string()).collect();
83        let ring = ketama::Ring::build(&server_ids.iter().map(|s| s.as_str()).collect::<Vec<_>>());
84
85        let shards = config
86            .servers
87            .iter()
88            .map(|&addr| {
89                let mut conns = Vec::with_capacity(pool_size);
90                for _ in 0..pool_size {
91                    conns.push(ShardConn::Disconnected);
92                }
93                Shard {
94                    addr,
95                    conns,
96                    next: 0,
97                }
98            })
99            .collect();
100
101        Self {
102            shards,
103            ring,
104            connect_timeout_ms: config.connect_timeout_ms,
105            tls_server_name: config.tls_server_name,
106        }
107    }
108
109    /// Eagerly connect all connections on all shards.
110    pub async fn connect_all(&mut self) -> Result<(), Error> {
111        let opts = self.connect_opts();
112        for shard in &mut self.shards {
113            for slot in &mut shard.conns {
114                let conn = do_connect(shard.addr, &opts).await?;
115                *slot = ShardConn::Connected(conn);
116            }
117        }
118        Ok(())
119    }
120
121    /// Close all connections on all shards.
122    pub fn close_all(&mut self) {
123        for shard in &mut self.shards {
124            for slot in &mut shard.conns {
125                if let ShardConn::Connected(c) = slot {
126                    c.close();
127                }
128                *slot = ShardConn::Disconnected;
129            }
130        }
131    }
132
133    /// Number of shards (servers).
134    pub fn shard_count(&self) -> usize {
135        self.shards.len()
136    }
137
138    fn connect_opts(&self) -> ConnectOpts {
139        ConnectOpts {
140            connect_timeout_ms: self.connect_timeout_ms,
141            tls_server_name: self.tls_server_name.clone(),
142        }
143    }
144
145    /// Get a [`Client`] for a specific shard by index (for node-level commands).
146    pub async fn shard_client(&mut self, index: usize) -> Result<Client, Error> {
147        let opts = self.connect_opts();
148        let shard = &mut self.shards[index];
149        let conn = get_conn(shard, &opts).await?;
150        Ok(Client::new(conn))
151    }
152
153    // -- Core routing --------------------------------------------------------
154
155    /// Route an encoded command to the shard owning `key`.
156    async fn route_command(
157        &mut self,
158        key: &[u8],
159        encoded: &[u8],
160    ) -> Result<McResponseBytes, Error> {
161        let opts = self.connect_opts();
162        let shard_idx = self.ring.route(key);
163        let shard = &mut self.shards[shard_idx];
164        let size = shard.conns.len();
165
166        for attempt in 0..size {
167            let idx = (shard.next + attempt) % size;
168            let conn = match &shard.conns[idx] {
169                ShardConn::Connected(c) => *c,
170                ShardConn::Disconnected => match do_connect(shard.addr, &opts).await {
171                    Ok(c) => {
172                        shard.conns[idx] = ShardConn::Connected(c);
173                        c
174                    }
175                    Err(_) => continue,
176                },
177            };
178
179            if let Err(e) = conn.send(encoded) {
180                shard.conns[idx] = ShardConn::Disconnected;
181                conn.close();
182                return Err(Error::Io(e));
183            }
184            match Client::new(conn).read_response().await {
185                Ok(response) => {
186                    shard.next = (idx + 1) % size;
187                    check_error_bytes(&response)?;
188                    return Ok(response);
189                }
190                Err(Error::ConnectionClosed) => {
191                    shard.conns[idx] = ShardConn::Disconnected;
192                    continue;
193                }
194                Err(e) => return Err(e),
195            }
196        }
197
198        Err(Error::AllConnectionsFailed)
199    }
200
201    /// Verify all keys route to the same shard. Returns the common shard index.
202    fn require_same_shard(&self, keys: &[&[u8]]) -> Result<usize, Error> {
203        let first = self.ring.route(keys[0]);
204        for key in &keys[1..] {
205            if self.ring.route(key) != first {
206                return Err(Error::Memcache(
207                    "keys in request don't route to the same shard".into(),
208                ));
209            }
210        }
211        Ok(first)
212    }
213
214    // -- Commands ------------------------------------------------------------
215
216    /// Get the value of a key. Returns `None` on cache miss.
217    pub async fn get(&mut self, key: impl AsRef<[u8]>) -> Result<Option<Value>, Error> {
218        let key = key.as_ref();
219        let encoded = encode_request(&memcache_proto::Request::get(key));
220        let response = self.route_command(key, &encoded).await?;
221        match response {
222            McResponseBytes::Values(mut values) => {
223                if values.is_empty() {
224                    Ok(None)
225                } else {
226                    let v = values.swap_remove(0);
227                    Ok(Some(Value {
228                        data: v.data,
229                        flags: v.flags,
230                    }))
231                }
232            }
233            _ => Err(Error::UnexpectedResponse),
234        }
235    }
236
237    /// Get values for multiple keys. All keys must route to the same shard.
238    /// Returns only hits, each with its key and CAS token.
239    pub async fn gets(&mut self, keys: &[&[u8]]) -> Result<Vec<GetValue>, Error> {
240        if keys.is_empty() {
241            return Ok(Vec::new());
242        }
243        self.require_same_shard(keys)?;
244        let encoded = encode_request(&McRequest::gets(keys));
245        let response = self.route_command(keys[0], &encoded).await?;
246        match response {
247            McResponseBytes::Values(values) => Ok(values
248                .into_iter()
249                .map(|v| GetValue {
250                    key: v.key,
251                    data: v.data,
252                    flags: v.flags,
253                    cas: v.cas,
254                })
255                .collect()),
256            _ => Err(Error::UnexpectedResponse),
257        }
258    }
259
260    /// Set a key-value pair with default flags (0) and no expiration.
261    pub async fn set(
262        &mut self,
263        key: impl AsRef<[u8]>,
264        value: impl AsRef<[u8]>,
265    ) -> Result<(), Error> {
266        self.set_with_options(key, value, 0, 0).await
267    }
268
269    /// Set a key-value pair with custom flags and expiration time.
270    pub async fn set_with_options(
271        &mut self,
272        key: impl AsRef<[u8]>,
273        value: impl AsRef<[u8]>,
274        flags: u32,
275        exptime: u32,
276    ) -> Result<(), Error> {
277        let key = key.as_ref();
278        let value = value.as_ref();
279        let encoded = encode_set(key, value, flags, exptime);
280        let response = self.route_command(key, &encoded).await?;
281        match response {
282            McResponseBytes::Stored => Ok(()),
283            _ => Err(Error::UnexpectedResponse),
284        }
285    }
286
287    /// Store a key only if it does not already exist (ADD command).
288    /// Returns `true` if stored, `false` if the key already exists.
289    pub async fn add(
290        &mut self,
291        key: impl AsRef<[u8]>,
292        value: impl AsRef<[u8]>,
293    ) -> Result<bool, Error> {
294        let key = key.as_ref();
295        let value = value.as_ref();
296        let encoded = encode_add(key, value);
297        let response = self.route_command(key, &encoded).await?;
298        match response {
299            McResponseBytes::Stored => Ok(true),
300            McResponseBytes::NotStored => Ok(false),
301            _ => Err(Error::UnexpectedResponse),
302        }
303    }
304
305    /// Store a key only if it already exists (REPLACE command).
306    /// Returns `true` if stored, `false` if the key does not exist.
307    pub async fn replace(
308        &mut self,
309        key: impl AsRef<[u8]>,
310        value: impl AsRef<[u8]>,
311    ) -> Result<bool, Error> {
312        let key = key.as_ref();
313        let value = value.as_ref();
314        let encoded = encode_request(&McRequest::Replace {
315            key,
316            value,
317            flags: 0,
318            exptime: 0,
319        });
320        let response = self.route_command(key, &encoded).await?;
321        match response {
322            McResponseBytes::Stored => Ok(true),
323            McResponseBytes::NotStored => Ok(false),
324            _ => Err(Error::UnexpectedResponse),
325        }
326    }
327
328    /// Increment a numeric value by delta. Returns the new value after incrementing.
329    /// Returns `None` if the key does not exist.
330    pub async fn incr(&mut self, key: impl AsRef<[u8]>, delta: u64) -> Result<Option<u64>, Error> {
331        let key = key.as_ref();
332        let encoded = encode_request(&McRequest::incr(key, delta));
333        let response = self.route_command(key, &encoded).await?;
334        match response {
335            McResponseBytes::Numeric(val) => Ok(Some(val)),
336            McResponseBytes::NotFound => Ok(None),
337            _ => Err(Error::UnexpectedResponse),
338        }
339    }
340
341    /// Decrement a numeric value by delta. Returns the new value after decrementing.
342    /// Returns `None` if the key does not exist.
343    pub async fn decr(&mut self, key: impl AsRef<[u8]>, delta: u64) -> Result<Option<u64>, Error> {
344        let key = key.as_ref();
345        let encoded = encode_request(&McRequest::decr(key, delta));
346        let response = self.route_command(key, &encoded).await?;
347        match response {
348            McResponseBytes::Numeric(val) => Ok(Some(val)),
349            McResponseBytes::NotFound => Ok(None),
350            _ => Err(Error::UnexpectedResponse),
351        }
352    }
353
354    /// Append data to an existing item's value.
355    /// Returns `true` if stored, `false` if the key does not exist.
356    pub async fn append(
357        &mut self,
358        key: impl AsRef<[u8]>,
359        value: impl AsRef<[u8]>,
360    ) -> Result<bool, Error> {
361        let key = key.as_ref();
362        let value = value.as_ref();
363        let encoded = encode_request(&McRequest::append(key, value));
364        let response = self.route_command(key, &encoded).await?;
365        match response {
366            McResponseBytes::Stored => Ok(true),
367            McResponseBytes::NotStored => Ok(false),
368            _ => Err(Error::UnexpectedResponse),
369        }
370    }
371
372    /// Prepend data to an existing item's value.
373    /// Returns `true` if stored, `false` if the key does not exist.
374    pub async fn prepend(
375        &mut self,
376        key: impl AsRef<[u8]>,
377        value: impl AsRef<[u8]>,
378    ) -> Result<bool, Error> {
379        let key = key.as_ref();
380        let value = value.as_ref();
381        let encoded = encode_request(&McRequest::prepend(key, value));
382        let response = self.route_command(key, &encoded).await?;
383        match response {
384            McResponseBytes::Stored => Ok(true),
385            McResponseBytes::NotStored => Ok(false),
386            _ => Err(Error::UnexpectedResponse),
387        }
388    }
389
390    /// Compare-and-swap: store the value only if the CAS token matches.
391    /// Returns `Ok(true)` if stored, `Ok(false)` if the CAS token didn't match (EXISTS),
392    /// or `Err` if the key was not found or another error occurred.
393    pub async fn cas(
394        &mut self,
395        key: impl AsRef<[u8]>,
396        value: impl AsRef<[u8]>,
397        cas_unique: u64,
398    ) -> Result<bool, Error> {
399        let key = key.as_ref();
400        let value = value.as_ref();
401        let encoded = encode_request(&McRequest::cas(key, value, cas_unique));
402        let response = self.route_command(key, &encoded).await?;
403        match response {
404            McResponseBytes::Stored => Ok(true),
405            McResponseBytes::Exists => Ok(false),
406            McResponseBytes::NotFound => Err(Error::Memcache("NOT_FOUND".into())),
407            _ => Err(Error::UnexpectedResponse),
408        }
409    }
410
411    /// Delete a key. Returns `true` if deleted, `false` if not found.
412    pub async fn delete(&mut self, key: impl AsRef<[u8]>) -> Result<bool, Error> {
413        let key = key.as_ref();
414        let encoded = encode_request(&McRequest::delete(key));
415        let response = self.route_command(key, &encoded).await?;
416        match response {
417            McResponseBytes::Deleted => Ok(true),
418            McResponseBytes::NotFound => Ok(false),
419            _ => Err(Error::UnexpectedResponse),
420        }
421    }
422
423    /// Flush all items on all shards.
424    pub async fn flush_all(&mut self) -> Result<(), Error> {
425        let opts = self.connect_opts();
426        for shard in &mut self.shards {
427            let conn = get_conn(shard, &opts).await?;
428            Client::new(conn).flush_all().await?;
429        }
430        Ok(())
431    }
432
433    /// Get the version string from any connected shard.
434    pub async fn version(&mut self) -> Result<String, Error> {
435        let opts = self.connect_opts();
436        for shard in &mut self.shards {
437            if let Ok(conn) = get_conn(shard, &opts).await {
438                return Client::new(conn).version().await;
439            }
440        }
441        Err(Error::AllConnectionsFailed)
442    }
443}
444
445/// Connection options cloned from config to avoid borrow conflicts.
446#[derive(Clone)]
447struct ConnectOpts {
448    connect_timeout_ms: u64,
449    tls_server_name: Option<String>,
450}
451
452/// Get a ConnCtx from a shard, lazily reconnecting if needed.
453async fn get_conn(shard: &mut Shard, opts: &ConnectOpts) -> Result<ConnCtx, Error> {
454    let size = shard.conns.len();
455    for _ in 0..size {
456        let idx = shard.next;
457        shard.next = (shard.next + 1) % size;
458
459        match &shard.conns[idx] {
460            ShardConn::Connected(c) => return Ok(*c),
461            ShardConn::Disconnected => {
462                if let Ok(conn) = do_connect(shard.addr, opts).await {
463                    shard.conns[idx] = ShardConn::Connected(conn);
464                    return Ok(conn);
465                }
466            }
467        }
468    }
469    Err(Error::AllConnectionsFailed)
470}
471
472async fn do_connect(addr: SocketAddr, opts: &ConnectOpts) -> Result<ConnCtx, Error> {
473    let conn = if let Some(ref sni) = opts.tls_server_name {
474        let fut = if opts.connect_timeout_ms > 0 {
475            ringline::connect_tls_with_timeout(addr, sni, opts.connect_timeout_ms)?
476        } else {
477            ringline::connect_tls(addr, sni)?
478        };
479        fut.await?
480    } else {
481        let fut = if opts.connect_timeout_ms > 0 {
482            ringline::connect_with_timeout(addr, opts.connect_timeout_ms)?
483        } else {
484            ringline::connect(addr)?
485        };
486        fut.await?
487    };
488
489    Ok(conn)
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    #[test]
497    fn test_single_server_always_routes_to_zero() {
498        let config = ShardedConfig {
499            servers: vec!["127.0.0.1:11211".parse().unwrap()],
500            pool_size: 1,
501            connect_timeout_ms: 0,
502            tls_server_name: None,
503        };
504        let client = ShardedClient::new(config);
505        assert_eq!(client.ring.route(b"any-key"), 0);
506        assert_eq!(client.ring.route(b"another-key"), 0);
507        assert_eq!(client.ring.route(b""), 0);
508    }
509
510    #[test]
511    fn test_deterministic_routing() {
512        let config = ShardedConfig {
513            servers: vec![
514                "127.0.0.1:11211".parse().unwrap(),
515                "127.0.0.1:11212".parse().unwrap(),
516                "127.0.0.1:11213".parse().unwrap(),
517            ],
518            pool_size: 1,
519            connect_timeout_ms: 0,
520            tls_server_name: None,
521        };
522        let client = ShardedClient::new(config);
523
524        let a = client.ring.route(b"test-key");
525        let b = client.ring.route(b"test-key");
526        assert_eq!(a, b);
527
528        let c = client.ring.route(b"other-key");
529        let d = client.ring.route(b"other-key");
530        assert_eq!(c, d);
531    }
532
533    #[test]
534    fn test_config_defaults() {
535        let config = ShardedConfig {
536            servers: vec![
537                "127.0.0.1:11211".parse().unwrap(),
538                "127.0.0.1:11212".parse().unwrap(),
539            ],
540            pool_size: 4,
541            connect_timeout_ms: 500,
542            tls_server_name: None,
543        };
544        let client = ShardedClient::new(config);
545        assert_eq!(client.shard_count(), 2);
546        assert_eq!(client.ring.node_count(), 2);
547        assert_eq!(client.shards[0].conns.len(), 4);
548        assert_eq!(client.shards[1].conns.len(), 4);
549    }
550
551    #[test]
552    fn test_pool_size_minimum() {
553        let config = ShardedConfig {
554            servers: vec!["127.0.0.1:11211".parse().unwrap()],
555            pool_size: 0,
556            connect_timeout_ms: 0,
557            tls_server_name: None,
558        };
559        let client = ShardedClient::new(config);
560        assert_eq!(client.shards[0].conns.len(), 1);
561    }
562
563    #[test]
564    fn test_require_same_shard_matching() {
565        let config = ShardedConfig {
566            servers: vec![
567                "127.0.0.1:11211".parse().unwrap(),
568                "127.0.0.1:11212".parse().unwrap(),
569            ],
570            pool_size: 1,
571            connect_timeout_ms: 0,
572            tls_server_name: None,
573        };
574        let client = ShardedClient::new(config);
575        let keys: &[&[u8]] = &[b"same-key", b"same-key"];
576        assert!(client.require_same_shard(keys).is_ok());
577    }
578
579    #[test]
580    fn test_require_same_shard_single_key() {
581        let config = ShardedConfig {
582            servers: vec![
583                "127.0.0.1:11211".parse().unwrap(),
584                "127.0.0.1:11212".parse().unwrap(),
585            ],
586            pool_size: 1,
587            connect_timeout_ms: 0,
588            tls_server_name: None,
589        };
590        let client = ShardedClient::new(config);
591        let keys: &[&[u8]] = &[b"anykey"];
592        assert!(client.require_same_shard(keys).is_ok());
593    }
594}