Repository: qdrant/qdrant
File: lib/segment/src/id_tracker/mutable_id_tracker.rs (Rust)

Code before:
fn store_mapping_changes(
    mappings_path: &Path,
    changes: &Vec<MappingChange>,
    persisted_mappings_size: Arc<AtomicU64>,
) -> OperationResult<()> {
    // Create or open file in append mode to write new changes to the end
    let mut file = File::options()
        .create(true)
        .write(true)
        .open(mappings_path)?;

    let file_len = file
        .metadata()
        .map_err(|err| {
            OperationError::service_error(format!(
                "Failed to get ID tracker mappings file size: {err}"
            ))
        })?
        .len();

    // Seek to the end of the persisted part of the file.
    // If previously call of `store_mapping_changes` failed,
    // `persisted_mappings_size` may be less than actual file size.
    let file_start_appending = persisted_mappings_size.load(std::sync::atomic::Ordering::Relaxed);
    if file_start_appending <= file_len {
        file.seek(io::SeekFrom::Start(file_start_appending))
            .map_err(|err| {
                OperationError::service_error(format!(
                    "Failed to seek to the persisted position of ID tracker mappings: {err}"
                ))
            })?;
    } else {
        return Err(OperationError::service_error(format!(
            "Mutable ID tracker mappings file size is less than persisted mappings size, cannot append new mappings (file size: {file_len}, persisted mappings size: {file_start_appending})",
        )));
    }

    let mut writer = BufWriter::new(file);

    write_mapping_changes(&mut writer, changes).map_err(|err| {
        OperationError::service_error(format!(
            "Failed to persist ID tracker point mappings ({}): {err}",
            mappings_path.display(),
        ))
    })?;

    writer.flush()?;
    let mut file = writer.into_inner().map_err(|err| {
        OperationError::service_error(format!(
            "Failed to flush ID tracker point mappings write buffer: {err}"
        ))

Review comment (suggestion):
Idea: how about we truncate the file here instead (or do both). Then we also 'repair' the file for the next load if we don't finish this flush.

If we hit this case, we expect to always write the exact same sequence of bytes. And so it should not corrupt the file even _more_. But truncating may be a bit safer.

Code after:
                .enumerate()
                .map(|(i, version)| (i as PointOffsetType, *version)),
        )
    }

    fn name(&self) -> &'static str {
        "mutable id tracker"
    }

    #[inline]
    fn files(&self) -> Vec<PathBuf> {
        Self::segment_files(&self.segment_path)
    }
}

pub(crate) fn mappings_path(segment_path: &Path) -> PathBuf {
    segment_path.join(FILE_MAPPINGS)
}

fn versions_path(segment_path: &Path) -> PathBuf {
    segment_path.join(FILE_VERSIONS)
}

/// Store new mapping changes, appending them to the given file
fn store_mapping_changes(
    mappings_path: &Path,
    changes: &Vec<MappingChange>,
    persisted_mappings_size: &AtomicU64,
) -> OperationResult<()> {
    // Create or open file in append mode to write new changes to the end
    let file = File::options()
        .create(true)
        .append(true)
        .open(mappings_path)?;

    // Ensure correct file length to not corrupt mappings when appending
    let file_len = file
        .metadata()
        .map_err(|err| {
            OperationError::service_error(format!(
                "Failed to get ID tracker mappings file size: {err}"
            ))
        })?
        .len();
    let file_start_appending = persisted_mappings_size.load(std::sync::atomic::Ordering::Relaxed);
    match file_len.cmp(&file_start_appending) {
        // File size is what we expect, continue normally
        Ordering::Equal => {}
        // File is larger than expected, previous flush might not have completed properly
        // Clean up by truncating to what we expect, then append
        // May happen if system is out of disk space and the file cannot be grown