Fix ftruncate hang on large sparse files

This commit is contained in:
Marsman1996 2026-01-01 00:10:18 +08:00
parent 82ccfcd4c6
commit f9048977cd
2 changed files with 131 additions and 11 deletions

View File

@ -10,7 +10,7 @@ use ostd::{
};
use crate::{
SLOT_SIZE, XArray, XLockGuard,
BITS_PER_LAYER, SLOT_SIZE, XArray, XLockGuard,
entry::NodeEntryRef,
mark::{NoneMark, XMark},
node::{Height, XNode},
@ -226,6 +226,114 @@ impl<'a, P: NonNullPtr + Send + Sync, M> Cursor<'a, P, M> {
self.state.move_to(current_node, self.index);
self.continue_traverse_to_target();
}
/// Moves the cursor to the first present item at or after the current index.
/// If found, updates the cursor's index and state, and returns the index.
/// If not found, returns None.
pub fn next_present(&mut self) -> Option<u64> {
loop {
self.traverse_to_target();
let state = core::mem::take(&mut self.state);
if let CursorState::AtNode {
node,
operation_offset,
} = state
{
if node.is_leaf() {
// Check current slot
if node.entry_with(self.guard, operation_offset).is_some() {
self.state = CursorState::AtNode {
node,
operation_offset,
};
return Some(self.index);
}
// Check subsequent slots in this leaf
let mut off = operation_offset + 1;
while off < SLOT_SIZE as u8 {
if node.entry_with(self.guard, off).is_some() {
self.index += (off - operation_offset) as u64;
self.state = CursorState::AtNode {
node,
operation_offset: off,
};
return Some(self.index);
}
off += 1;
}
// Move to next leaf
let remaining_in_leaf = SLOT_SIZE as u64 - operation_offset as u64;
self.index = self.index.checked_add(remaining_in_leaf)?;
self.reset();
} else {
// Should not happen if traverse_to_target works as expected (it stops at leaf or inactive).
// But if it stops at internal node, it means missing child.
// We should treat it as Inactive logic.
self.reset();
}
} else {
// Inactive. Current index is empty.
// Find next present from root.
let head = self.xa.head.read_with(self.guard)?;
let max_index = head.height().max_index();
if self.index > max_index {
return None;
}
if let Some(next_idx) = self.find_next_from_root(self.index) {
self.index = next_idx;
// Loop will continue and traverse_to_target will succeed
} else {
return None;
}
}
}
}
fn find_next_from_root(&self, target: u64) -> Option<u64> {
let head = self.xa.head.read_with(self.guard)?;
self.find_next_in_node(&head, 0, target)
}
fn find_next_in_node(&self, node: &XNode<P>, node_base: u64, target: u64) -> Option<u64> {
let height = node.height();
let shift = (*height - 1) * BITS_PER_LAYER as u8;
let start_offset = if target > node_base {
((target - node_base) >> shift) as u8
} else {
0
};
for off in start_offset..SLOT_SIZE as u8 {
let child_base = node_base + ((off as u64) << shift);
if let Some(entry) = node.entry_with(self.guard, off) {
if *height == 1 {
// Leaf node.
if child_base >= target {
return Some(child_base);
}
} else {
// Internal node.
let child = entry.left().unwrap();
let search_start = if off == start_offset {
target
} else {
child_base
};
if let Some(found) = self.find_next_in_node(&child, child_base, search_start) {
return Some(found);
}
}
}
}
None
}
}
impl<P: NonNullPtr + Send + Sync, M: Into<XMark>> Cursor<'_, P, M> {

View File

@ -415,26 +415,38 @@ impl Vmo {
let page_idx_range = get_page_idx_range(&range);
let mut cursor = locked_pages.cursor_mut(page_idx_range.start as u64);
let Some(pager) = &self.pager else {
for _ in page_idx_range {
cursor.remove();
cursor.next();
// Ensure the cursor points to the first present item in the range
if cursor.load().is_none() {
if let Some(idx) = cursor.next_present() {
cursor.reset_to(idx);
} else {
return Ok(());
}
return Ok(());
};
}
let mut removed_page_idx = Vec::new();
for page_idx in page_idx_range {
while cursor.index() < page_idx_range.end as u64 {
if cursor.remove().is_some() {
removed_page_idx.push(page_idx);
removed_page_idx.push(cursor.index() as usize);
}
// Advance to the next index, then skip forward to the next present item if this slot is empty
cursor.next();
if cursor.load().is_none() {
if let Some(idx) = cursor.next_present() {
cursor.reset_to(idx);
} else {
break;
}
}
}
drop(locked_pages);
for page_idx in removed_page_idx {
pager.decommit_page(page_idx)?;
if let Some(pager) = &self.pager {
for page_idx in removed_page_idx {
pager.decommit_page(page_idx)?;
}
}
Ok(())