@@ -168,9 +168,10 @@ impl PageCache {
168168 }
169169
170170 fn evict_one ( & mut self ) -> Option < ( FileKey , u64 ) > {
171- // Walk the hand at most `capacity * 2` steps to bound worst case
172- // (every entry either visited or unevictable triggers a wrap).
173- let max_steps = self . capacity . saturating_mul ( 2 ) . max ( 1 ) ;
171+ // Two passes are sufficient to clear visited bits and then evict.
172+ // Use the live list length rather than the configured capacity because
173+ // pinned or dirty pages can temporarily grow the cache past capacity.
174+ let max_steps = self . map . len ( ) . saturating_mul ( 2 ) . max ( 1 ) ;
174175 let mut cur = self . hand . or ( self . tail ) ;
175176 for _ in 0 ..max_steps {
176177 let Some ( idx) = cur else {
@@ -318,6 +319,33 @@ impl PageCache {
318319 self . dirty . remove ( & key) ;
319320 }
320321 }
322+
323+ /// Drop every unpinned clean entry for `file`, leaving dirty entries
324+ /// available for a later flush and pinned entries available to readers.
325+ pub fn clear_clean_file ( & mut self , file : FileKey ) {
326+ let keys: Vec < ( FileKey , u64 ) > = self
327+ . map
328+ . keys ( )
329+ . filter ( |( f, _) | * f == file)
330+ . copied ( )
331+ . collect ( ) ;
332+ for key in keys {
333+ if self . pins . get ( & key) . copied ( ) . unwrap_or ( 0 ) > 0 || self . dirty . contains ( & key) {
334+ continue ;
335+ }
336+ if let Some ( idx) = self . map . remove ( & key) {
337+ let ( prev, next) = {
338+ let node = self . slab [ idx] . as_ref ( ) . expect ( "indexed node alive" ) ;
339+ ( node. prev , node. next )
340+ } ;
341+ if self . hand == Some ( idx) {
342+ self . hand = next;
343+ }
344+ self . unlink_node ( idx, prev, next) ;
345+ self . free_node ( idx) ;
346+ }
347+ }
348+ }
321349}
322350
323351#[ cfg( test) ]
@@ -393,6 +421,20 @@ mod tests {
393421 assert ! ( c. get( ( FileKey :: Main , 1 ) ) . is_some( ) ) ;
394422 }
395423
424+ #[ test]
425+ fn clear_clean_file_preserves_dirty_entries ( ) {
426+ let mut c = PageCache :: with_capacity ( 4 ) ;
427+ c. insert ( ( FileKey :: Main , 1 ) , page ( 1 ) ) ;
428+ c. insert ( ( FileKey :: Main , 2 ) , page ( 2 ) ) ;
429+ c. mark_dirty ( ( FileKey :: Main , 2 ) ) ;
430+
431+ c. clear_clean_file ( FileKey :: Main ) ;
432+
433+ assert ! ( c. get( ( FileKey :: Main , 1 ) ) . is_none( ) ) ;
434+ assert ! ( c. get( ( FileKey :: Main , 2 ) ) . is_some( ) ) ;
435+ assert ! ( c. is_dirty( ( FileKey :: Main , 2 ) ) ) ;
436+ }
437+
396438 #[ test]
397439 fn dirty_iter_is_sorted_ascending ( ) {
398440 let mut c = PageCache :: with_capacity ( 16 ) ;
@@ -421,4 +463,27 @@ mod tests {
421463 c. insert ( ( FileKey :: Main , 3 ) , page ( 3 ) ) ;
422464 assert_eq ! ( c. slab. len( ) , 2 , "slab reuses freed slot" ) ;
423465 }
466+
467+ #[ test]
468+ fn overgrown_cache_still_finds_clean_victim ( ) {
469+ let mut c = PageCache :: with_capacity ( 2 ) ;
470+
471+ for id in 1u8 ..=4 {
472+ let key = ( FileKey :: Main , u64:: from ( id) ) ;
473+ c. insert ( key, page ( id) ) ;
474+ c. pin ( key) ;
475+ }
476+ c. insert ( ( FileKey :: Main , 5 ) , page ( 5 ) ) ;
477+ assert_eq ! ( c. len( ) , 5 , "pinned pages may force temporary growth" ) ;
478+
479+ let evicted = c. insert ( ( FileKey :: Main , 6 ) , page ( 6 ) ) ;
480+ assert_eq ! (
481+ evicted,
482+ Some ( ( FileKey :: Main , 5 ) ) ,
483+ "eviction must scan the full over-capacity list for a clean victim"
484+ ) ;
485+ assert_eq ! ( c. len( ) , 5 ) ;
486+ assert ! ( c. get( ( FileKey :: Main , 5 ) ) . is_none( ) ) ;
487+ assert ! ( c. get( ( FileKey :: Main , 6 ) ) . is_some( ) ) ;
488+ }
424489}
0 commit comments