@@ -18,6 +18,23 @@ use crate::shard::EntryOrPlaceholder;
1818pub use crate :: sync_placeholder:: { EntryAction , EntryResult , GuardResult , PlaceholderGuard } ;
1919use crate :: sync_placeholder:: { JoinFuture , JoinResult } ;
2020
21+ /// Error returned by non-blocking cache operations that do not consume their
22+ /// inputs when the relevant shard lock could not be acquired immediately.
23+ ///
24+ /// This is used by borrowed-key/read-path operations. Non-blocking operations
25+ /// that consume owned inputs (e.g. `try_insert`) instead return those inputs
26+ /// on contention so the caller can retry or discard without losing data.
27+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
28+ pub struct LockContention ;
29+
30+ impl std:: fmt:: Display for LockContention {
31+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
32+ write ! ( f, "Lock Contention" )
33+ }
34+ }
35+
36+ impl std:: error:: Error for LockContention { }
37+
2138/// A concurrent cache
2239///
2340/// The concurrent cache is internally composed of equally sized shards, each of which is independently
@@ -274,6 +291,23 @@ impl<
274291 . is_some_and ( |( shard, hash) | shard. read ( ) . contains ( hash, key) )
275292 }
276293
294+ /// Attempts to check if a key exists in the cache without blocking.
295+ /// Returns `Ok(true)` if present, `Ok(false)` if absent,
296+ /// or `Err(LockContention)` if the shard lock could not be acquired without blocking.
297+ pub fn try_contains_key < Q > ( & self , key : & Q ) -> Result < bool , LockContention >
298+ where
299+ Q : Hash + Equivalent < Key > + ?Sized ,
300+ {
301+ let Some ( ( shard, hash) ) = self . shard_for ( key) else {
302+ return Ok ( false ) ;
303+ } ;
304+
305+ match shard. try_read ( ) {
306+ Some ( guard) => Ok ( guard. contains ( hash, key) ) ,
307+ None => Err ( LockContention ) ,
308+ }
309+ }
310+
277311 /// Fetches an item from the cache whose key is `key`.
278312 pub fn get < Q > ( & self , key : & Q ) -> Option < Val >
279313 where
@@ -283,6 +317,23 @@ impl<
283317 shard. read ( ) . get ( hash, key) . cloned ( )
284318 }
285319
320+ /// Attempts to fetch an item from the cache whose key is `key`.
321+ /// Returns `Ok(Some(val))` if the key is present, `Ok(None)` if absent,
322+ /// or `Err(LockContention)` if the shard lock could not be acquired without blocking.
323+ pub fn try_get < Q > ( & self , key : & Q ) -> Result < Option < Val > , LockContention >
324+ where
325+ Q : Hash + Equivalent < Key > + ?Sized ,
326+ {
327+ let Some ( ( shard, hash) ) = self . shard_for ( key) else {
328+ return Ok ( None ) ;
329+ } ;
330+
331+ match shard. try_read ( ) {
332+ Some ( guard) => Ok ( guard. get ( hash, key) . cloned ( ) ) ,
333+ None => Err ( LockContention ) ,
334+ }
335+ }
336+
286337 /// Peeks an item from the cache whose key is `key`.
287338 /// Contrary to gets, peeks don't alter the key "hotness".
288339 pub fn peek < Q > ( & self , key : & Q ) -> Option < Val >
@@ -293,6 +344,23 @@ impl<
293344 shard. read ( ) . peek ( hash, key) . cloned ( )
294345 }
295346
347+ /// Attempts to peek an item from the cache whose key is `key`.
348+ /// Contrary to gets, peeks don't alter the key "hotness".
349+ /// Returns `Ok(Some(val))` if the key is present, `Ok(None)` if absent,
350+ /// or `Err(LockContention)` if the shard lock could not be acquired without blocking.
351+ pub fn try_peek < Q > ( & self , key : & Q ) -> Result < Option < Val > , LockContention >
352+ where
353+ Q : Hash + Equivalent < Key > + ?Sized ,
354+ {
355+ let Some ( ( shard, hash) ) = self . shard_for ( key) else {
356+ return Ok ( None ) ;
357+ } ;
358+ match shard. try_read ( ) {
359+ Some ( guard) => Ok ( guard. peek ( hash, key) . cloned ( ) ) ,
360+ None => Err ( LockContention ) ,
361+ }
362+ }
363+
296364 /// Remove an item from the cache whose key is `key`.
297365 /// Returns the removed entry, if any.
298366 pub fn remove < Q > ( & self , key : & Q ) -> Option < ( Key , Val ) >
@@ -303,6 +371,23 @@ impl<
303371 shard. write ( ) . remove ( hash, key)
304372 }
305373
374+ /// Attempts to remove an item from the cache whose key is `key`.
375+ /// Returns `Ok(Some(entry))` with the removed entry if present, `Ok(None)` if absent,
376+ /// or `Err(LockContention)` if the shard lock could not be acquired without blocking.
377+ pub fn try_remove < Q > ( & self , key : & Q ) -> Result < Option < ( Key , Val ) > , LockContention >
378+ where
379+ Q : Hash + Equivalent < Key > + ?Sized ,
380+ {
381+ let Some ( ( shard, hash) ) = self . shard_for ( key) else {
382+ return Ok ( None ) ;
383+ } ;
384+
385+ match shard. try_write ( ) {
386+ Some ( mut guard) => Ok ( guard. remove ( hash, key) ) ,
387+ None => Err ( LockContention ) ,
388+ }
389+ }
390+
306391 /// Remove an item from the cache whose key is `key` if `f(&value)` returns `true` for that entry.
307392 /// Compared to peek and remove, this method guarantees that no new value was inserted in-between.
308393 ///
@@ -364,6 +449,16 @@ impl<
364449 self . lifecycle . end_request ( lcs) ;
365450 }
366451
452+ /// Attempts to insert an item in the cache with key `key` without blocking.
453+ /// Returns `Ok(())` if the item was inserted, or `Err((key, value))` if the shard lock
454+ /// could not be acquired without blocking. Lock contention is the only failure
455+ /// mode: the inputs are returned so the caller can retry or discard them.
456+ pub fn try_insert ( & self , key : Key , value : Val ) -> Result < ( ) , ( Key , Val ) > {
457+ let lcs = self . try_insert_with_lifecycle ( key, value) ?;
458+ self . lifecycle . end_request ( lcs) ;
459+ Ok ( ( ) )
460+ }
461+
367462 /// Inserts an item in the cache with key `key`.
368463 pub fn insert_with_lifecycle ( & self , key : Key , value : Val ) -> L :: RequestState {
369464 let mut lcs = self . lifecycle . begin_request ( ) ;
@@ -376,6 +471,32 @@ impl<
376471 lcs
377472 }
378473
474+ /// Attempts to insert an item in the cache with key `key` without blocking.
475+ /// Returns `Ok(lcs)` with the lifecycle request state if the item was inserted,
476+ /// or `Err((key, value))` if the shard lock could not be acquired without blocking.
477+ /// Lock contention is the only failure mode: the inputs are returned so the
478+ /// caller can retry or discard them.
479+ pub fn try_insert_with_lifecycle (
480+ & self ,
481+ key : Key ,
482+ value : Val ,
483+ ) -> Result < L :: RequestState , ( Key , Val ) > {
484+ // Tradeoff: begin_request is called before acquiring the shard lock to avoid holding
485+ // the lock during potentially expensive lifecycle initialization.
486+ let mut lcs = self . lifecycle . begin_request ( ) ;
487+ let ( shard, hash) = self . shard_for ( & key) . unwrap ( ) ;
488+
489+ match shard. try_write ( ) {
490+ Some ( mut shard) => {
491+ let result = shard. insert ( & mut lcs, hash, key, value, InsertStrategy :: Insert ) ;
492+ // result cannot err with the Insert strategy
493+ debug_assert ! ( result. is_ok( ) ) ;
494+ Ok ( lcs)
495+ }
496+ _ => Err ( ( key, value) ) ,
497+ }
498+ }
499+
379500 /// Clear all items from the cache
380501 pub fn clear ( & self ) {
381502 for s in self . shards . iter ( ) {
@@ -1526,4 +1647,120 @@ mod tests {
15261647 }
15271648 }
15281649 }
1650+
1651+ // --- Non-blocking method tests ---
1652+ #[ test]
1653+ fn test_try_contains_key ( ) {
1654+ let cache = Cache :: new ( 100 ) ;
1655+ cache. insert ( 1 , 10 ) ;
1656+
1657+ assert ! ( cache. try_contains_key( & 1 ) . is_ok_and( |v| v) ) ;
1658+ assert ! ( cache. try_contains_key( & 2 ) . is_ok_and( |v| !v) ) ;
1659+ }
1660+
1661+ #[ test]
1662+ fn test_try_contains_key_contended ( ) {
1663+ let cache = Cache :: new ( 100 ) ;
1664+ cache. insert ( 1 , 10 ) ;
1665+ // Hold write locks on all shards so try_read is blocked.
1666+ let _guards: Vec < _ > = cache. shards . iter ( ) . map ( |s| s. write ( ) ) . collect ( ) ;
1667+ assert ! ( cache. try_contains_key( & 1 ) . is_err( ) ) ;
1668+ }
1669+
1670+ #[ test]
1671+ fn test_try_get ( ) {
1672+ let cache = Cache :: new ( 100 ) ;
1673+ cache. insert ( 1 , 10 ) ;
1674+
1675+ assert ! ( cache. try_get( & 1 ) . is_ok_and( |v| matches!( v, Some ( 10 ) ) ) ) ;
1676+ assert ! ( cache. try_get( & 2 ) . is_ok_and( |v| v. is_none( ) ) ) ;
1677+ }
1678+
1679+ #[ test]
1680+ fn test_try_get_contended ( ) {
1681+ let cache = Cache :: new ( 100 ) ;
1682+ cache. insert ( 1 , 10 ) ;
1683+ let _guards: Vec < _ > = cache. shards . iter ( ) . map ( |s| s. write ( ) ) . collect ( ) ;
1684+ assert ! ( cache. try_get( & 1 ) . is_err( ) ) ;
1685+ }
1686+
1687+ #[ test]
1688+ fn test_try_peek ( ) {
1689+ let cache = Cache :: new ( 100 ) ;
1690+ cache. insert ( 1 , 10 ) ;
1691+
1692+ assert ! ( cache. try_peek( & 1 ) . is_ok_and( |v| matches!( v, Some ( 10 ) ) ) ) ;
1693+ assert ! ( cache. try_peek( & 2 ) . is_ok_and( |v| v. is_none( ) ) ) ;
1694+ }
1695+
1696+ #[ test]
1697+ fn test_try_peek_contended ( ) {
1698+ let cache = Cache :: new ( 100 ) ;
1699+ cache. insert ( 1 , 10 ) ;
1700+ let _guards: Vec < _ > = cache. shards . iter ( ) . map ( |s| s. write ( ) ) . collect ( ) ;
1701+ assert ! ( cache. try_peek( & 1 ) . is_err( ) ) ;
1702+ }
1703+
1704+ #[ test]
1705+ fn test_try_remove ( ) {
1706+ let cache = Cache :: new ( 100 ) ;
1707+ cache. insert ( 1 , 10 ) ;
1708+
1709+ assert ! ( cache
1710+ . try_remove( & 1 )
1711+ . is_ok_and( |v| matches!( v, Some ( ( 1 , 10 ) ) ) ) ) ;
1712+ assert ! ( cache. try_remove( & 1 ) . is_ok_and( |v| v. is_none( ) ) ) ;
1713+ assert ! ( cache. try_remove( & 99 ) . is_ok_and( |v| v. is_none( ) ) ) ;
1714+ }
1715+
1716+ #[ test]
1717+ fn test_try_remove_contended ( ) {
1718+ let cache = Cache :: new ( 100 ) ;
1719+ cache. insert ( 1 , 10 ) ;
1720+ // Hold read locks on all shards so try_write is blocked.
1721+ let guards: Vec < _ > = cache. shards . iter ( ) . map ( |s| s. read ( ) ) . collect ( ) ;
1722+ assert ! ( cache. try_remove( & 1 ) . is_err( ) ) ;
1723+ drop ( guards) ;
1724+ // Item must still be present since the remove did not happen.
1725+ assert_eq ! ( cache. get( & 1 ) , Some ( 10 ) ) ;
1726+ }
1727+
1728+ #[ test]
1729+ fn test_try_insert ( ) {
1730+ let cache = Cache :: new ( 100 ) ;
1731+
1732+ assert_eq ! ( cache. try_insert( 1 , 10 ) , Ok ( ( ) ) ) ;
1733+ assert_eq ! ( cache. get( & 1 ) , Some ( 10 ) ) ;
1734+
1735+ // Insert same key overwrites the previous value.
1736+ assert_eq ! ( cache. try_insert( 1 , 20 ) , Ok ( ( ) ) ) ;
1737+ assert_eq ! ( cache. get( & 1 ) , Some ( 20 ) ) ;
1738+ }
1739+
1740+ #[ test]
1741+ fn test_try_insert_contended ( ) {
1742+ let cache = Cache :: new ( 100 ) ;
1743+ let guards: Vec < _ > = cache. shards . iter ( ) . map ( |s| s. read ( ) ) . collect ( ) ;
1744+ assert_eq ! ( cache. try_insert( 1 , 10 ) , Err ( ( 1 , 10 ) ) ) ;
1745+ drop ( guards) ;
1746+ assert_eq ! ( cache. get( & 1 ) , None ) ;
1747+ }
1748+
1749+ #[ test]
1750+ fn test_try_insert_with_lifecycle ( ) {
1751+ let cache = Cache :: new ( 100 ) ;
1752+
1753+ // Successful insert returns the lifecycle request state.
1754+ let result = cache. try_insert_with_lifecycle ( 1 , 10 ) ;
1755+ assert ! ( result. is_ok( ) ) ;
1756+ let lcs = result. ok ( ) . unwrap ( ) ;
1757+ cache. lifecycle . end_request ( lcs) ;
1758+ assert_eq ! ( cache. get( & 1 ) , Some ( 10 ) ) ;
1759+
1760+ // Contended when a read lock is held.
1761+ let guards: Vec < _ > = cache. shards . iter ( ) . map ( |s| s. read ( ) ) . collect ( ) ;
1762+ assert_eq ! ( cache. try_insert_with_lifecycle( 2 , 20 ) , Err ( ( 2 , 20 ) ) ) ;
1763+ drop ( guards) ;
1764+ assert_eq ! ( cache. get( & 2 ) , None ) ;
1765+ }
15291766}
0 commit comments