blob: b1bd98f6ecd1ef6fadd2748a70c0a0764f31cd83 [file]
use crate::state::system::TopicState;
use crate::streaming::partitions::partition::Partition;
use crate::streaming::storage::TopicStorage;
use crate::streaming::topics::consumer_group::ConsumerGroup;
use crate::streaming::topics::topic::Topic;
use crate::streaming::topics::COMPONENT;
use ahash::AHashSet;
use anyhow::Context;
use error_set::ErrContext;
use futures::future::join_all;
use iggy::error::IggyError;
use iggy::locking::IggySharedMut;
use iggy::locking::IggySharedMutFn;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use tokio::fs;
use tokio::fs::create_dir_all;
use tokio::sync::{Mutex, RwLock};
use tracing::{error, info, warn};
#[derive(Debug)]
pub struct FileTopicStorage;
#[derive(Debug, Serialize, Deserialize)]
struct ConsumerGroupData {
id: u32,
name: String,
}
impl TopicStorage for FileTopicStorage {
async fn load(&self, topic: &mut Topic, mut state: TopicState) -> Result<(), IggyError> {
info!("Loading topic {} from disk...", topic);
if !Path::new(&topic.path).exists() {
return Err(IggyError::TopicIdNotFound(topic.topic_id, topic.stream_id));
}
let message_expiry = Topic::get_message_expiry(state.message_expiry, &topic.config);
let max_topic_size = Topic::get_max_topic_size(state.max_topic_size, &topic.config)?;
topic.created_at = state.created_at;
topic.message_expiry = message_expiry;
topic.max_topic_size = max_topic_size;
topic.compression_algorithm = state.compression_algorithm;
topic.replication_factor = state.replication_factor.unwrap_or(1);
let mut dir_entries = fs::read_dir(&topic.partitions_path).await
.with_context(|| format!("Failed to read partition with ID: {} for stream with ID: {} for topic with ID: {} and path: {}",
topic.topic_id, topic.stream_id, topic.topic_id, &topic.partitions_path))
.map_err(|_| IggyError::CannotReadPartitions)?;
let mut unloaded_partitions = Vec::new();
while let Some(dir_entry) = dir_entries.next_entry().await.unwrap_or(None) {
let metadata = dir_entry.metadata().await;
if metadata.is_err() || metadata.unwrap().is_file() {
continue;
}
let name = dir_entry.file_name().into_string().unwrap();
let partition_id = name.parse::<u32>();
if partition_id.is_err() {
error!("Invalid partition ID file with name: '{}'.", name);
continue;
}
let partition_id = partition_id.unwrap();
let partition_state = state.partitions.get(&partition_id);
if partition_state.is_none() {
let stream_id = topic.stream_id;
let topic_id = topic.topic_id;
error!("Partition with ID: '{partition_id}' for stream with ID: '{stream_id}' and topic with ID: '{topic_id}' was not found in state, but exists on disk and will be removed.");
if let Err(error) = fs::remove_dir_all(&dir_entry.path()).await {
error!("Cannot remove partition directory: {error}");
} else {
warn!("Partition with ID: '{partition_id}' for stream with ID: '{stream_id}' and topic with ID: '{topic_id}' was removed.");
}
continue;
}
let partition_state = partition_state.unwrap();
let partition = Partition::create(
topic.stream_id,
topic.topic_id,
partition_id,
false,
topic.config.clone(),
topic.storage.clone(),
message_expiry,
topic.messages_count_of_parent_stream.clone(),
topic.messages_count.clone(),
topic.size_of_parent_stream.clone(),
topic.size_bytes.clone(),
topic.segments_count_of_parent_stream.clone(),
partition_state.created_at,
)
.await;
unloaded_partitions.push(partition);
}
let state_partition_ids = state.partitions.keys().copied().collect::<AHashSet<u32>>();
let unloaded_partition_ids = unloaded_partitions
.iter()
.map(|partition| partition.partition_id)
.collect::<AHashSet<u32>>();
let missing_ids = state_partition_ids
.difference(&unloaded_partition_ids)
.copied()
.collect::<AHashSet<u32>>();
if missing_ids.is_empty() {
info!(
"All partitions for topic with ID: '{}' for stream with ID: '{}' found on disk were found in state.",
topic.topic_id, topic.stream_id
);
} else {
error!(
"Partitions with IDs: '{missing_ids:?}' for topic with ID: '{topic_id}' for stream with ID: '{stream_id}' were not found on disk.",
topic_id = topic.topic_id, stream_id = topic.stream_id
);
if !topic.config.recovery.recreate_missing_state {
warn!(
"Recreating missing state in recovery config is disabled, missing partitions will not be created for topic with ID: '{}' for stream with ID: '{}'.", topic.topic_id, topic.stream_id);
return Err(IggyError::MissingPartitions(
topic.topic_id,
topic.stream_id,
));
}
info!(
"Recreating missing state in recovery config is enabled, missing partitions will be created for topic with ID: '{}' for stream with ID: '{}'.",
topic.topic_id, topic.stream_id
);
for partition_id in missing_ids {
let partition_state = state.partitions.get(&partition_id).unwrap();
let mut partition = Partition::create(
topic.stream_id,
topic.topic_id,
partition_id,
true,
topic.config.clone(),
topic.storage.clone(),
message_expiry,
topic.messages_count_of_parent_stream.clone(),
topic.messages_count.clone(),
topic.size_of_parent_stream.clone(),
topic.size_bytes.clone(),
topic.segments_count_of_parent_stream.clone(),
partition_state.created_at,
)
.await;
partition.persist().await.with_error_context(|error| {
format!(
"{COMPONENT} (error: {error}) - failed to persist partition: {partition}"
)
})?;
partition.segments.clear();
unloaded_partitions.push(partition);
info!(
"Created missing partition with ID: '{partition_id}', for topic with ID: '{}' for stream with ID: '{}'.",
topic.topic_id, topic.stream_id
);
}
}
let stream_id = topic.stream_id;
let topic_id = topic.topic_id;
let loaded_partitions = Arc::new(Mutex::new(Vec::new()));
let mut load_partitions = Vec::new();
for mut partition in unloaded_partitions {
let loaded_partitions = loaded_partitions.clone();
let partition_state = state.partitions.remove(&partition.partition_id).unwrap();
let load_partition = tokio::spawn(async move {
match partition.load(partition_state).await {
Ok(_) => {
loaded_partitions.lock().await.push(partition);
}
Err(error) => {
error!(
"Failed to load partition with ID: {} for stream with ID: {stream_id} and topic with ID: {topic_id}. Error: {error}",
partition.partition_id);
}
}
});
load_partitions.push(load_partition);
}
join_all(load_partitions).await;
for partition in loaded_partitions.lock().await.drain(..) {
topic
.partitions
.insert(partition.partition_id, IggySharedMut::new(partition));
}
for consumer_group in state.consumer_groups.into_values() {
let consumer_group = ConsumerGroup::new(
topic.topic_id,
consumer_group.id,
&consumer_group.name,
topic.get_partitions_count(),
);
topic
.consumer_groups_ids
.insert(consumer_group.name.to_owned(), consumer_group.group_id);
topic
.consumer_groups
.insert(consumer_group.group_id, RwLock::new(consumer_group));
}
topic
.load_messages_from_disk_to_cache()
.await
.with_error_context(|error| {
format!("{COMPONENT} (error: {error}) - failed to load messages from disk to cache, topic: {topic}")
})?;
info!("Loaded topic {topic}");
Ok(())
}
async fn save(&self, topic: &Topic) -> Result<(), IggyError> {
if !Path::new(&topic.path).exists() && create_dir_all(&topic.path).await.is_err() {
return Err(IggyError::CannotCreateTopicDirectory(
topic.topic_id,
topic.stream_id,
topic.path.clone(),
));
}
if !Path::new(&topic.partitions_path).exists()
&& create_dir_all(&topic.partitions_path).await.is_err()
{
return Err(IggyError::CannotCreatePartitionsDirectory(
topic.stream_id,
topic.topic_id,
));
}
info!(
"Saving {} partition(s) for topic {topic}...",
topic.partitions.len()
);
for (_, partition) in topic.partitions.iter() {
let mut partition = partition.write().await;
partition.persist().await.with_error_context(|error| {
format!(
"{COMPONENT} (error: {error}) - failed to persist partition, topic: {topic}"
)
})?;
}
info!("Saved topic {topic}");
Ok(())
}
async fn delete(&self, topic: &Topic) -> Result<(), IggyError> {
info!("Deleting topic {topic}...");
if fs::remove_dir_all(&topic.path).await.is_err() {
return Err(IggyError::CannotDeleteTopicDirectory(
topic.topic_id,
topic.stream_id,
topic.path.clone(),
));
}
info!(
"Deleted topic with ID: {} for stream with ID: {}.",
topic.topic_id, topic.stream_id
);
Ok(())
}
}