@@ -153,9 +153,10 @@ impl PageCache {
153153 }
154154
155155 fn evict_one ( & mut self ) -> Option < ( FileKey , u64 ) > {
156- // Walk the hand at most `capacity * 2` steps to bound worst case
157- // (every entry either visited or unevictable triggers a wrap).
158- let max_steps = self . capacity . saturating_mul ( 2 ) . max ( 1 ) ;
156+ // Two passes are sufficient to clear visited bits and then evict.
157+ // Use the live list length rather than the configured capacity because
158+ // pinned or dirty pages can temporarily grow the cache past capacity.
159+ let max_steps = self . map . len ( ) . saturating_mul ( 2 ) . max ( 1 ) ;
159160 let mut cur = self . hand . or ( self . tail ) ;
160161 for _ in 0 ..max_steps {
161162 let Some ( idx) = cur else {
@@ -293,6 +294,33 @@ impl PageCache {
293294 self . dirty . remove ( & key) ;
294295 }
295296 }
297+
298+ /// Drop every unpinned clean entry for `file`, leaving dirty entries
299+ /// available for a later flush and pinned entries available to readers.
300+ pub fn clear_clean_file ( & mut self , file : FileKey ) {
301+ let keys: Vec < ( FileKey , u64 ) > = self
302+ . map
303+ . keys ( )
304+ . filter ( |( f, _) | * f == file)
305+ . copied ( )
306+ . collect ( ) ;
307+ for key in keys {
308+ if self . pins . get ( & key) . copied ( ) . unwrap_or ( 0 ) > 0 || self . dirty . contains ( & key) {
309+ continue ;
310+ }
311+ if let Some ( idx) = self . map . remove ( & key) {
312+ let ( prev, next) = {
313+ let node = self . slab [ idx] . as_ref ( ) . expect ( "indexed node alive" ) ;
314+ ( node. prev , node. next )
315+ } ;
316+ if self . hand == Some ( idx) {
317+ self . hand = next;
318+ }
319+ self . unlink_node ( idx, prev, next) ;
320+ self . free_node ( idx) ;
321+ }
322+ }
323+ }
296324}
297325
298326#[ cfg( test) ]
@@ -368,6 +396,20 @@ mod tests {
368396 assert ! ( c. get( ( FileKey :: Main , 1 ) ) . is_some( ) ) ;
369397 }
370398
399+ #[ test]
400+ fn clear_clean_file_preserves_dirty_entries ( ) {
401+ let mut c = PageCache :: with_capacity ( 4 ) ;
402+ c. insert ( ( FileKey :: Main , 1 ) , page ( 1 ) ) ;
403+ c. insert ( ( FileKey :: Main , 2 ) , page ( 2 ) ) ;
404+ c. mark_dirty ( ( FileKey :: Main , 2 ) ) ;
405+
406+ c. clear_clean_file ( FileKey :: Main ) ;
407+
408+ assert ! ( c. get( ( FileKey :: Main , 1 ) ) . is_none( ) ) ;
409+ assert ! ( c. get( ( FileKey :: Main , 2 ) ) . is_some( ) ) ;
410+ assert ! ( c. is_dirty( ( FileKey :: Main , 2 ) ) ) ;
411+ }
412+
371413 #[ test]
372414 fn dirty_iter_is_sorted_ascending ( ) {
373415 let mut c = PageCache :: with_capacity ( 16 ) ;
@@ -396,4 +438,27 @@ mod tests {
396438 c. insert ( ( FileKey :: Main , 3 ) , page ( 3 ) ) ;
397439 assert_eq ! ( c. slab. len( ) , 2 , "slab reuses freed slot" ) ;
398440 }
441+
442+ #[ test]
443+ fn overgrown_cache_still_finds_clean_victim ( ) {
444+ let mut c = PageCache :: with_capacity ( 2 ) ;
445+
446+ for id in 1u8 ..=4 {
447+ let key = ( FileKey :: Main , u64:: from ( id) ) ;
448+ c. insert ( key, page ( id) ) ;
449+ c. pin ( key) ;
450+ }
451+ c. insert ( ( FileKey :: Main , 5 ) , page ( 5 ) ) ;
452+ assert_eq ! ( c. len( ) , 5 , "pinned pages may force temporary growth" ) ;
453+
454+ let evicted = c. insert ( ( FileKey :: Main , 6 ) , page ( 6 ) ) ;
455+ assert_eq ! (
456+ evicted,
457+ Some ( ( FileKey :: Main , 5 ) ) ,
458+ "eviction must scan the full over-capacity list for a clean victim"
459+ ) ;
460+ assert_eq ! ( c. len( ) , 5 ) ;
461+ assert ! ( c. get( ( FileKey :: Main , 5 ) ) . is_none( ) ) ;
462+ assert ! ( c. get( ( FileKey :: Main , 6 ) ) . is_some( ) ) ;
463+ }
399464}
0 commit comments