blob: afb60c8209318640aeb7684850413663ff6390e0 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Server-owned consumer offset recovery.
//!
//! Forked from `server::streaming::partitions::storage` (the legacy
//! `load_consumer_offsets` / `load_consumer_group_offsets`) so server
//! owns the loaders for the offset files its own persistence path writes,
//! without depending on the legacy `server` crate. One file per consumer (numeric
//! file name = consumer id) holding a little-endian `u64` offset then a checksum over
//! it; see [`partitions::offset_storage`]. The legacy server stays compatible both
//! ways: it reads the first eight bytes and stops, and a file it wrote itself decodes
//! here as unchecksummed.
use iggy_common::{ConsumerGroupId, ConsumerKind, ConsumerOffset, IggyError};
use partitions::offset_storage::{OffsetRecord, decode_offset_record};
use std::sync::atomic::AtomicU64;
use tracing::{error, trace, warn};
const COMPONENT: &str = "STREAMING_PARTITIONS";
pub fn load_consumer_offsets(path: &str) -> Result<Vec<ConsumerOffset>, IggyError> {
trace!("Loading consumer offsets from path: {path}...");
let Ok(dir_entries) = std::fs::read_dir(path) else {
return Err(IggyError::CannotReadConsumerOffsets(path.to_owned()));
};
let mut consumer_offsets = Vec::new();
for dir_entry in dir_entries {
let dir_entry = match dir_entry {
Ok(entry) => entry,
Err(e) => {
warn!(
"Failed to read directory entry in consumer offsets path: {path}, \
error: {e}, skipping."
);
continue;
}
};
let metadata = match dir_entry.metadata() {
Ok(m) => m,
Err(e) => {
warn!(
"Failed to read metadata for entry in consumer offsets path: {path}, \
error: {e}, skipping."
);
continue;
}
};
if metadata.is_dir() {
continue;
}
let name = dir_entry.file_name().to_string_lossy().to_string();
let Ok(consumer_id) = name.parse::<u32>() else {
warn!(
"Unexpected non-numeric consumer offset file: '{}', skipping.",
name
);
continue;
};
let path = dir_entry.path();
let Some(path) = path.to_str().map(str::to_owned) else {
error!("Invalid consumer ID path for file with name: '{}'.", name);
continue;
};
let Some(offset) = read_offset_file(&path, "consumer offset") else {
continue;
};
consumer_offsets.push(ConsumerOffset {
kind: ConsumerKind::Consumer,
consumer_id,
offset,
path,
});
}
consumer_offsets.sort_by_key(|consumer_offset| consumer_offset.consumer_id);
Ok(consumer_offsets)
}
pub fn load_consumer_group_offsets(
path: &str,
) -> Result<Vec<(ConsumerGroupId, ConsumerOffset)>, IggyError> {
trace!("Loading consumer group offsets from path: {path}...");
let Ok(dir_entries) = std::fs::read_dir(path) else {
return Err(IggyError::CannotReadConsumerOffsets(path.to_owned()));
};
let mut consumer_group_offsets = Vec::new();
for dir_entry in dir_entries {
let dir_entry = match dir_entry {
Ok(entry) => entry,
Err(e) => {
warn!(
"Failed to read directory entry in consumer group offsets path: {path}, \
error: {e}, skipping."
);
continue;
}
};
let metadata = match dir_entry.metadata() {
Ok(m) => m,
Err(e) => {
warn!(
"Failed to read metadata for entry in consumer group offsets path: {path}, \
error: {e}, skipping."
);
continue;
}
};
if metadata.is_dir() {
continue;
}
let name = dir_entry.file_name().to_string_lossy().to_string();
let Ok(raw_consumer_group_id) = name.parse::<u32>() else {
warn!(
"Unexpected non-numeric consumer group offset file: '{}', skipping.",
name
);
continue;
};
let consumer_group_id = ConsumerGroupId(raw_consumer_group_id as usize);
let path = dir_entry.path();
let Some(path) = path.to_str().map(str::to_owned) else {
error!(
"Invalid consumer group offset path for file with name: '{}'.",
name
);
continue;
};
let Some(offset) = read_offset_file(&path, "consumer group offset") else {
continue;
};
let consumer_offset = ConsumerOffset {
kind: ConsumerKind::ConsumerGroup,
consumer_id: raw_consumer_group_id,
offset,
path,
};
consumer_group_offsets.push((consumer_group_id, consumer_offset));
}
Ok(consumer_group_offsets)
}
fn read_offset_file(path: &str, offset_kind: &'static str) -> Option<AtomicU64> {
let bytes = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(e) => {
warn!(
"{COMPONENT} (error: {e}) - failed to read offset file, \
path: {path}, skipping."
);
return None;
}
};
match decode_offset_record(&bytes) {
OffsetRecord::Value { offset, .. } => Some(AtomicU64::new(offset)),
OffsetRecord::Torn => {
warn!(
"{COMPONENT} - failed to read {offset_kind} from file (truncated), \
path: {path}, skipping."
);
None
}
// Skipped rather than loaded: resuming from a cursor provably not the one
// written reads as ordinary redelivery or a gap, never as corruption.
//
// And unlinked, not just skipped: the offset map starts cold every boot, so a
// file left behind is re-read by the first auto-commit and trips the commit
// path again.
OffsetRecord::Corrupt {
offset,
expected,
found,
} => {
error!(
"{COMPONENT} - {offset_kind} file failed its checksum \
(offset: {offset}, expected: {expected}, found: {found}), \
path: {path}, removing it and resuming this consumer from the start."
);
if let Err(e) = std::fs::remove_file(path) {
error!(
"{COMPONENT} (error: {e}) - could not remove the corrupt \
{offset_kind} file, path: {path}; remove it manually."
);
}
None
}
}
}