Skip to main content

alien_bindings/providers/kv/
local.rs

1//! Local disk-persisted KV backed by turso (`localkv.v1`), multi-process safe.
2//!
3//! # Connection strategy
4//!
5//! turso is async-native and its `Connection` is `Send + Sync`, so there is no
6//! `spawn_blocking` boundary and no `Mutex<Connection>` anywhere. `LocalKv`
7//! holds one `turso::Database` handle on `<dataDir>/localkv.sqlite`; each
8//! operation opens its **own** short-lived connection from it and drops it
9//! when the operation completes, so no statement state leaks between
10//! operations.
11//!
12//! Correctness under concurrent access (multiple handles on one file, i.e.
13//! multiple processes) comes from turso's multi-process WAL mode — enabled
14//! explicitly, experimental upstream, and gated by the multi-handle tests
15//! below — plus a `busy_timeout` (writers wait for the write lock instead of
16//! failing with `Busy`). The schema is created once in [`LocalKv::new`]. Reads
17//! of live rows never take the write lock; a read that encounters an expired
18//! row escalates to a short delete (see `get` and `exists`). Conditional puts
19//! are a single atomic `INSERT ... ON CONFLICT DO UPDATE ... WHERE` so the
20//! race is resolved by the database, not by application-level locking.
21//!
22//! See `crates/alien-bindings/FORMAT.md` for the on-disk `localkv.v1` contract.
23use crate::error::{ErrorData, Result};
24use crate::providers::local_store::{
25    as_blob, as_i64, as_opt_i64, as_text, opt_i64_value, query_all, LocalStore, StoreSpec,
26};
27use crate::traits::{Binding, Kv, PutOptions, ScanResult};
28use alien_error::{AlienError, Context as _, IntoAlienError as _};
29use async_trait::async_trait;
30use chrono::Utc;
31use std::path::PathBuf;
32use turso::Connection;
33
34static KV_SPEC: StoreSpec = StoreSpec {
35    db_filename: "localkv.sqlite",
36    format_version: "localkv.v1",
37    binding_type: "local KV",
38    schema_ddl: "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value BLOB NOT NULL, expires_at INTEGER);",
39};
40
41#[derive(Debug)]
42pub struct LocalKv {
43    store: LocalStore,
44}
45
46/// Build the standard KV operation error context.
47fn kv_error(operation: &str, key: &str, reason: &str) -> ErrorData {
48    ErrorData::KvOperationFailed {
49        operation: operation.to_string(),
50        key: key.to_string(),
51        reason: reason.to_string(),
52    }
53}
54
55/// Delete a row that is already expired (`expires_at <= now`), used by the
56/// lazy-expiry paths of `get` and `exists`.
57async fn delete_expired(conn: &Connection, operation: &str, key: &str, now: i64) -> Result<()> {
58    conn.execute(
59        "DELETE FROM kv WHERE key = ?1 AND expires_at IS NOT NULL AND expires_at <= ?2",
60        (key, now),
61    )
62    .await
63    .into_alien_error()
64    .context(kv_error(operation, key, "failed to delete expired row"))?;
65    Ok(())
66}
67
68impl LocalKv {
69    pub async fn new(data_dir: PathBuf) -> Result<Self> {
70        Ok(Self {
71            store: LocalStore::open(data_dir, &KV_SPEC).await?,
72        })
73    }
74
75    /// Get the data directory path (the directory that holds `localkv.sqlite`).
76    pub fn data_dir(&self) -> &PathBuf {
77        self.store.data_dir()
78    }
79
80    /// Get the number of items currently stored (including expired items).
81    /// Useful for testing.
82    pub async fn len(&self) -> Result<usize> {
83        self.store
84            .with_conn(|conn| async move {
85                let rows = query_all(&conn, "SELECT COUNT(*) FROM kv", ())
86                    .await
87                    .into_alien_error()
88                    .context(kv_error("len", "*", "failed to count rows"))?;
89                let count = rows
90                    .first()
91                    .and_then(|row| row.first())
92                    .and_then(as_i64)
93                    .ok_or_else(|| {
94                        AlienError::new(kv_error("len", "*", "count query returned no value"))
95                    })?;
96                Ok(count as usize)
97            })
98            .await
99    }
100
101    /// Check if the store is empty (including expired items).
102    /// Useful for testing.
103    pub async fn is_empty(&self) -> Result<bool> {
104        Ok(self.len().await? == 0)
105    }
106
107    /// Clear all data from the store.
108    /// Useful for testing.
109    pub async fn clear(&self) -> Result<()> {
110        self.store
111            .with_conn(|conn| async move {
112                conn.execute("DELETE FROM kv", ())
113                    .await
114                    .into_alien_error()
115                    .context(kv_error("clear", "*", "failed to clear local KV store"))?;
116                Ok(())
117            })
118            .await
119    }
120
121    /// Get all keys currently in the store (including expired ones).
122    /// Useful for testing and debugging.
123    pub async fn keys(&self) -> Result<Vec<String>> {
124        self.store
125            .with_conn(|conn| async move {
126                let rows = query_all(&conn, "SELECT key FROM kv", ())
127                    .await
128                    .into_alien_error()
129                    .context(kv_error("keys", "*", "failed to scan keys"))?;
130                let mut keys = Vec::with_capacity(rows.len());
131                for row in &rows {
132                    keys.push(row.first().and_then(as_text).ok_or_else(|| {
133                        AlienError::new(kv_error("keys", "*", "failed to read key row"))
134                    })?);
135                }
136                Ok(keys)
137            })
138            .await
139    }
140
141    /// Validate key constraints using global KV validation.
142    fn validate_key(key: &str) -> Result<()> {
143        crate::providers::kv::validate_key(key)
144    }
145
146    /// Validate value constraints using global KV validation.
147    fn validate_value(value: &[u8]) -> Result<()> {
148        crate::providers::kv::validate_value(value)
149    }
150}
151
152impl Binding for LocalKv {}
153
154#[async_trait]
155impl Kv for LocalKv {
156    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
157        Self::validate_key(key)?;
158
159        self.store
160            .with_conn(|conn| async move {
161                let now = Utc::now().timestamp_millis();
162                let rows = query_all(
163                    &conn,
164                    "SELECT value, expires_at FROM kv WHERE key = ?1",
165                    (key,),
166                )
167                .await
168                .into_alien_error()
169                .context(kv_error("get", key, "failed to read value"))?;
170
171                let Some(row) = rows.first() else {
172                    return Ok(None);
173                };
174                let value = row.first().and_then(as_blob).ok_or_else(|| {
175                    AlienError::new(kv_error("get", key, "stored value is not a blob"))
176                })?;
177                let expires_at = row.get(1).and_then(as_opt_i64).ok_or_else(|| {
178                    AlienError::new(kv_error("get", key, "stored expires_at is not an integer"))
179                })?;
180
181                if matches!(expires_at, Some(exp) if exp <= now) {
182                    // Lazily remove the expired row.
183                    delete_expired(&conn, "get", key, now).await?;
184                    Ok(None)
185                } else {
186                    Ok(Some(value))
187                }
188            })
189            .await
190    }
191
192    async fn put(&self, key: &str, value: Vec<u8>, options: Option<PutOptions>) -> Result<bool> {
193        Self::validate_key(key)?;
194        Self::validate_value(&value)?;
195        let options = options.unwrap_or_default();
196
197        self.store
198            .with_conn(|conn| async move {
199                let now = Utc::now().timestamp_millis();
200                let expires_at: Option<i64> = options
201                    .ttl
202                    .map(|d| now.saturating_add(i64::try_from(d.as_millis()).unwrap_or(i64::MAX)));
203
204                if options.if_not_exists {
205                    // One atomic statement: insert if absent, otherwise overwrite
206                    // ONLY when the existing row is already expired. The changed
207                    // row count (returned by `execute`) is 1 for the winner, 0
208                    // for a loser.
209                    let changed = conn
210                        .execute(
211                            "INSERT INTO kv (key, value, expires_at) VALUES (?1, ?2, ?3) \
212                             ON CONFLICT(key) DO UPDATE SET value = ?2, expires_at = ?3 \
213                             WHERE kv.expires_at IS NOT NULL AND kv.expires_at <= ?4",
214                            (key, value, opt_i64_value(expires_at), now),
215                        )
216                        .await
217                        .into_alien_error()
218                        .context(kv_error("put", key, "failed conditional put"))?;
219                    Ok(changed == 1)
220                } else {
221                    conn.execute(
222                        "INSERT INTO kv (key, value, expires_at) VALUES (?1, ?2, ?3) \
223                         ON CONFLICT(key) DO UPDATE SET value = ?2, expires_at = ?3",
224                        (key, value, opt_i64_value(expires_at)),
225                    )
226                    .await
227                    .into_alien_error()
228                    .context(kv_error("put", key, "failed to upsert value"))?;
229                    Ok(true)
230                }
231            })
232            .await
233    }
234
235    async fn delete(&self, key: &str) -> Result<()> {
236        Self::validate_key(key)?;
237
238        self.store
239            .with_conn(|conn| async move {
240                conn.execute("DELETE FROM kv WHERE key = ?1", (key,))
241                    .await
242                    .into_alien_error()
243                    .context(kv_error("delete", key, "failed to delete key"))?;
244                Ok(())
245            })
246            .await
247    }
248
249    async fn exists(&self, key: &str) -> Result<bool> {
250        Self::validate_key(key)?;
251
252        self.store
253            .with_conn(|conn| async move {
254                let now = Utc::now().timestamp_millis();
255                let rows = query_all(&conn, "SELECT expires_at FROM kv WHERE key = ?1", (key,))
256                    .await
257                    .into_alien_error()
258                    .context(kv_error("exists", key, "failed to check existence"))?;
259
260                let Some(row) = rows.first() else {
261                    return Ok(false);
262                };
263                let expires_at = row.first().and_then(as_opt_i64).ok_or_else(|| {
264                    AlienError::new(kv_error(
265                        "exists",
266                        key,
267                        "stored expires_at is not an integer",
268                    ))
269                })?;
270
271                if matches!(expires_at, Some(exp) if exp <= now) {
272                    // Lazily remove the expired row.
273                    delete_expired(&conn, "exists", key, now).await?;
274                    Ok(false)
275                } else {
276                    Ok(true)
277                }
278            })
279            .await
280    }
281
282    async fn scan_prefix(
283        &self,
284        prefix: &str,
285        limit: Option<usize>,
286        cursor: Option<String>,
287    ) -> Result<ScanResult> {
288        Self::validate_key(prefix)?;
289
290        // Parse cursor (simple offset-based pagination for local).
291        let start_offset = if let Some(cursor_str) = cursor {
292            cursor_str.parse::<usize>().map_err(|_| {
293                AlienError::new(ErrorData::InvalidInput {
294                    operation_context: "KV scan cursor parsing".to_string(),
295                    details: format!("Invalid cursor format: {}", cursor_str),
296                    field_name: Some("cursor".to_string()),
297                })
298            })?
299        } else {
300            0
301        };
302
303        // Collect matching, non-expired items in sorted key order.
304        let matching: Vec<(String, Vec<u8>)> = self
305            .store
306            .with_conn(|conn| async move {
307                let now = Utc::now().timestamp_millis();
308                let rows = query_all(
309                    &conn,
310                    "SELECT key, value, expires_at FROM kv WHERE key >= ?1 ORDER BY key",
311                    (prefix,),
312                )
313                .await
314                .into_alien_error()
315                .context(kv_error(
316                    "scan_prefix",
317                    prefix,
318                    "failed to scan prefix",
319                ))?;
320
321                let mut matching = Vec::new();
322                for row in &rows {
323                    let k = row.first().and_then(as_text).ok_or_else(|| {
324                        AlienError::new(kv_error(
325                            "scan_prefix",
326                            prefix,
327                            "failed to read scan row key",
328                        ))
329                    })?;
330                    // Keys are ordered ascending starting at `prefix`; once a key
331                    // stops matching the prefix, no later key can match either.
332                    if !k.starts_with(prefix) {
333                        break;
334                    }
335                    let v = row.get(1).and_then(as_blob).ok_or_else(|| {
336                        AlienError::new(kv_error(
337                            "scan_prefix",
338                            prefix,
339                            "stored value is not a blob",
340                        ))
341                    })?;
342                    let exp = row.get(2).and_then(as_opt_i64).ok_or_else(|| {
343                        AlienError::new(kv_error(
344                            "scan_prefix",
345                            prefix,
346                            "stored expires_at is not an integer",
347                        ))
348                    })?;
349                    if matches!(exp, Some(e) if e <= now) {
350                        continue; // expired: treat as absent
351                    }
352                    matching.push((k, v));
353                }
354                Ok(matching)
355            })
356            .await?;
357
358        // Apply offset-based pagination (results are already sorted by key).
359        let total_items = matching.len();
360        let end_offset = start_offset + limit.unwrap_or(total_items);
361
362        let items = matching
363            .into_iter()
364            .skip(start_offset)
365            .take(limit.unwrap_or(usize::MAX))
366            .collect::<Vec<_>>();
367
368        let next_cursor = if end_offset < total_items {
369            Some(end_offset.to_string())
370        } else {
371            None
372        };
373
374        Ok(ScanResult { items, next_cursor })
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::providers::local_store::open_database;
382    use std::sync::Arc;
383    use std::time::Duration;
384    use tempfile::TempDir;
385    use tokio::time;
386
387    async fn create_test_kv() -> (LocalKv, TempDir) {
388        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
389        let kv = LocalKv::new(temp_dir.path().join("kv.db"))
390            .await
391            .expect("Failed to create LocalKv");
392        (kv, temp_dir)
393    }
394
395    #[tokio::test]
396    async fn test_basic_operations() {
397        let (kv, _temp_dir) = create_test_kv().await;
398
399        assert!(kv
400            .put("test_key", b"test_value".to_vec(), None)
401            .await
402            .unwrap());
403        let value = kv.get("test_key").await.unwrap();
404        assert_eq!(value, Some(b"test_value".to_vec()));
405
406        assert!(kv.exists("test_key").await.unwrap());
407        assert!(!kv.exists("nonexistent").await.unwrap());
408
409        kv.delete("test_key").await.unwrap();
410        assert!(!kv.exists("test_key").await.unwrap());
411        assert_eq!(kv.get("test_key").await.unwrap(), None);
412    }
413
414    #[tokio::test]
415    async fn test_conditional_put() {
416        let (kv, _temp_dir) = create_test_kv().await;
417
418        let options = Some(PutOptions {
419            ttl: None,
420            if_not_exists: true,
421        });
422        assert!(kv
423            .put("key", b"value1".to_vec(), options.clone())
424            .await
425            .unwrap());
426
427        assert!(!kv.put("key", b"value2".to_vec(), options).await.unwrap());
428
429        assert_eq!(kv.get("key").await.unwrap(), Some(b"value1".to_vec()));
430
431        assert!(kv.put("key", b"value3".to_vec(), None).await.unwrap());
432        assert_eq!(kv.get("key").await.unwrap(), Some(b"value3".to_vec()));
433    }
434
435    #[tokio::test]
436    async fn test_ttl_expiration() {
437        let (kv, _temp_dir) = create_test_kv().await;
438
439        let options = Some(PutOptions {
440            ttl: Some(Duration::from_millis(500)),
441            if_not_exists: false,
442        });
443
444        kv.put("expiring_key", b"value".to_vec(), options)
445            .await
446            .unwrap();
447
448        assert!(kv.exists("expiring_key").await.unwrap());
449        assert_eq!(
450            kv.get("expiring_key").await.unwrap(),
451            Some(b"value".to_vec())
452        );
453
454        time::sleep(Duration::from_millis(750)).await;
455
456        assert!(!kv.exists("expiring_key").await.unwrap());
457        assert_eq!(kv.get("expiring_key").await.unwrap(), None);
458    }
459
460    #[tokio::test]
461    async fn test_prefix_scanning() {
462        let (kv, _temp_dir) = create_test_kv().await;
463
464        kv.put("prefix:key1", b"value1".to_vec(), None)
465            .await
466            .unwrap();
467        kv.put("prefix:key2", b"value2".to_vec(), None)
468            .await
469            .unwrap();
470        kv.put("prefix:key3", b"value3".to_vec(), None)
471            .await
472            .unwrap();
473        kv.put("other:key", b"other".to_vec(), None).await.unwrap();
474
475        let result = kv.scan_prefix("prefix:", None, None).await.unwrap();
476        assert_eq!(result.items.len(), 3);
477        assert!(result.next_cursor.is_none());
478
479        assert_eq!(result.items[0].0, "prefix:key1");
480        assert_eq!(result.items[1].0, "prefix:key2");
481        assert_eq!(result.items[2].0, "prefix:key3");
482
483        let result = kv.scan_prefix("prefix:", Some(2), None).await.unwrap();
484        assert_eq!(result.items.len(), 2);
485        assert!(result.next_cursor.is_some());
486
487        let cursor = result.next_cursor.unwrap();
488        let result = kv
489            .scan_prefix("prefix:", Some(2), Some(cursor))
490            .await
491            .unwrap();
492        assert_eq!(result.items.len(), 1);
493        assert_eq!(result.items[0].0, "prefix:key3");
494        assert!(result.next_cursor.is_none());
495    }
496
497    #[tokio::test]
498    async fn test_persistence_across_reopens() {
499        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
500        let db_path = temp_dir.path().join("kv.db");
501
502        {
503            let kv = LocalKv::new(db_path.clone())
504                .await
505                .expect("Failed to create LocalKv");
506            kv.put("persistent_key", b"persistent_value".to_vec(), None)
507                .await
508                .unwrap();
509        }
510
511        {
512            let kv = LocalKv::new(db_path)
513                .await
514                .expect("Failed to reopen LocalKv");
515            let value = kv.get("persistent_key").await.unwrap();
516            assert_eq!(value, Some(b"persistent_value".to_vec()));
517        }
518    }
519
520    #[tokio::test]
521    async fn test_key_validation() {
522        let (kv, _temp_dir) = create_test_kv().await;
523
524        assert!(kv.put("", b"value".to_vec(), None).await.is_err());
525        assert!(kv.get("").await.is_err());
526
527        let long_key = "a".repeat(513);
528        assert!(kv.put(&long_key, b"value".to_vec(), None).await.is_err());
529
530        assert!(kv
531            .put("key with spaces", b"value".to_vec(), None)
532            .await
533            .is_err());
534        assert!(kv
535            .put("key\nwith\nnewlines", b"value".to_vec(), None)
536            .await
537            .is_err());
538        assert!(kv
539            .put("key/with/slashes", b"value".to_vec(), None)
540            .await
541            .is_err());
542
543        assert!(kv
544            .put("valid_key-123", b"value".to_vec(), None)
545            .await
546            .is_ok());
547        assert!(kv
548            .put("domain.com:8080", b"value".to_vec(), None)
549            .await
550            .is_ok());
551    }
552
553    #[tokio::test]
554    async fn test_value_validation() {
555        let (kv, _temp_dir) = create_test_kv().await;
556
557        let large_value = vec![0u8; 24_577];
558        assert!(kv.put("key", large_value, None).await.is_err());
559
560        let max_value = vec![0u8; 24_576];
561        assert!(kv.put("key", max_value, None).await.is_ok());
562    }
563
564    #[tokio::test]
565    async fn test_utility_methods() {
566        let (kv, _temp_dir) = create_test_kv().await;
567
568        assert!(kv.is_empty().await.unwrap());
569        assert_eq!(kv.len().await.unwrap(), 0);
570        assert_eq!(kv.keys().await.unwrap(), Vec::<String>::new());
571
572        kv.put("key1", b"value1".to_vec(), None).await.unwrap();
573        kv.put("key2", b"value2".to_vec(), None).await.unwrap();
574
575        assert!(!kv.is_empty().await.unwrap());
576        assert_eq!(kv.len().await.unwrap(), 2);
577
578        let mut keys = kv.keys().await.unwrap();
579        keys.sort();
580        assert_eq!(keys, vec!["key1", "key2"]);
581
582        kv.clear().await.unwrap();
583        assert!(kv.is_empty().await.unwrap());
584        assert_eq!(kv.len().await.unwrap(), 0);
585    }
586
587    #[tokio::test]
588    async fn test_unknown_format_rejected_on_open() {
589        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
590        let dir = temp_dir.path().join("kv");
591
592        // Create a valid store, then rewrite its format marker to a future version.
593        {
594            let kv = LocalKv::new(dir.clone()).await.expect("initial open");
595            kv.put("k", b"v".to_vec(), None).await.unwrap();
596        }
597        {
598            let db = open_database(&dir.join("localkv.sqlite"), "test")
599                .await
600                .expect("raw open");
601            let conn = db.connect().expect("raw connect");
602            conn.execute(
603                "UPDATE meta SET value = 'localkv.v2' WHERE key = 'format'",
604                (),
605            )
606            .await
607            .expect("format overwrite");
608        }
609
610        // Reopening must fail fast, naming both the found and expected formats.
611        let err = LocalKv::new(dir)
612            .await
613            .expect_err("unknown format must be rejected");
614        let msg = err.to_string();
615        assert!(
616            msg.contains("localkv.v2"),
617            "error must name the found format, got: {msg}"
618        );
619        assert!(
620            msg.contains("localkv.v1"),
621            "error must name the expected format, got: {msg}"
622        );
623    }
624
625    // ---- Multi-process-safety proofs ----
626
627    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
628    async fn test_conditional_put_atomicity_across_handles() {
629        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
630        let dir = temp_dir.path().join("kv");
631        // Two independent handles on the SAME data_dir == two processes sharing the file.
632        let kv_a = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle a"));
633        let kv_b = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle b"));
634
635        let n = 16;
636        let mut handles = Vec::new();
637        for i in 0..n {
638            let kv = if i % 2 == 0 {
639                kv_a.clone()
640            } else {
641                kv_b.clone()
642            };
643            handles.push(tokio::spawn(async move {
644                let val = format!("val-{i}").into_bytes();
645                let opts = Some(PutOptions {
646                    ttl: None,
647                    if_not_exists: true,
648                });
649                let won = kv.put("race", val.clone(), opts).await.expect("put ok");
650                (won, val)
651            }));
652        }
653
654        let mut winners = Vec::new();
655        for h in handles {
656            let (won, val) = h.await.expect("task join");
657            if won {
658                winners.push(val);
659            }
660        }
661
662        assert_eq!(
663            winners.len(),
664            1,
665            "exactly one conditional put must win across both handles"
666        );
667        let stored = kv_a.get("race").await.unwrap().expect("key present");
668        assert_eq!(stored, winners[0], "stored value must equal the winner");
669        assert_eq!(
670            kv_b.get("race").await.unwrap().expect("key present via b"),
671            winners[0]
672        );
673    }
674
675    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
676    async fn test_ttl_expiry_takeover_conditional_put() {
677        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
678        let dir = temp_dir.path().join("kv");
679        let kv_a = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle a"));
680        let kv_b = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle b"));
681
682        // Seed a short-lived key with a conditional put.
683        assert!(kv_a
684            .put(
685                "k",
686                b"initial".to_vec(),
687                Some(PutOptions {
688                    ttl: Some(Duration::from_millis(300)),
689                    if_not_exists: true,
690                }),
691            )
692            .await
693            .unwrap());
694
695        // While it is still live, a conditional put must lose.
696        assert!(!kv_b
697            .put(
698                "k",
699                b"early".to_vec(),
700                Some(PutOptions {
701                    ttl: None,
702                    if_not_exists: true,
703                }),
704            )
705            .await
706            .unwrap());
707
708        // Wait for the seeded key to expire.
709        time::sleep(Duration::from_millis(450)).await;
710
711        // Race the takeover: many conditional puts against the now-expired key.
712        let n = 12;
713        let mut handles = Vec::new();
714        for i in 0..n {
715            let kv = if i % 2 == 0 {
716                kv_a.clone()
717            } else {
718                kv_b.clone()
719            };
720            handles.push(tokio::spawn(async move {
721                let val = format!("takeover-{i}").into_bytes();
722                let won = kv
723                    .put(
724                        "k",
725                        val.clone(),
726                        Some(PutOptions {
727                            ttl: None,
728                            if_not_exists: true,
729                        }),
730                    )
731                    .await
732                    .expect("put ok");
733                (won, val)
734            }));
735        }
736
737        let mut winners = Vec::new();
738        for h in handles {
739            let (won, val) = h.await.expect("task join");
740            if won {
741                winners.push(val);
742            }
743        }
744
745        assert_eq!(
746            winners.len(),
747            1,
748            "exactly one takeover conditional put must win after expiry"
749        );
750        let stored = kv_b.get("k").await.unwrap().expect("key present");
751        assert_eq!(
752            stored, winners[0],
753            "stored value must equal the takeover winner"
754        );
755    }
756
757    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
758    async fn test_multi_handle_concurrent_smoke() {
759        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
760        let dir = temp_dir.path().join("kv");
761        let kv_a = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle a"));
762        let kv_b = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle b"));
763
764        let mut handles = Vec::new();
765        for i in 0..50 {
766            let kv = if i % 2 == 0 {
767                kv_a.clone()
768            } else {
769                kv_b.clone()
770            };
771            handles.push(tokio::spawn(async move {
772                let key = format!("key_{i}");
773                let val = format!("v{i}").into_bytes();
774                // No busy errors expected under multi-process WAL + busy_timeout.
775                kv.put(&key, val.clone(), None).await.expect("put ok");
776                let got = kv.get(&key).await.expect("get ok");
777                assert_eq!(got, Some(val));
778            }));
779        }
780        for h in handles {
781            h.await.expect("task join");
782        }
783
784        assert_eq!(kv_a.len().await.unwrap(), 50, "handle a sees all keys");
785        assert_eq!(kv_b.len().await.unwrap(), 50, "handle b sees all keys");
786    }
787}