use crate::consumer::Consumer;
use crate::error::{KafkoError, Result};
use crate::log::LogConfig;
use crate::offset_store::OffsetStore;
use crate::producer::Producer;
use crate::topic::Topic;
use fs4::fs_std::FileExt;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
const LOCK_FILENAME: &str = "LOCK";
pub struct Kafko {
dir: PathBuf,
topics: RwLock<HashMap<String, Arc<Topic>>>,
default_log_config: LogConfig,
_dir_lock: File,
}
impl Kafko {
pub async fn open(dir: impl AsRef<Path>) -> Result<Self> {
Self::open_with_config(dir, LogConfig::default()).await
}
pub async fn open_with_config(
dir: impl AsRef<Path>,
default_log_config: LogConfig,
) -> Result<Self> {
let dir = dir.as_ref().to_path_buf();
tokio::fs::create_dir_all(&dir).await?;
let dir_lock = acquire_dir_lock(&dir)?;
let mut topics = HashMap::new();
let mut entries = tokio::fs::read_dir(&dir).await?;
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
if !file_type.is_dir() {
continue;
}
let name = match entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
let topic = Topic::open(&entry.path(), &name, default_log_config).await?;
topics.insert(name, Arc::new(topic));
}
Ok(Self {
dir,
topics: RwLock::new(topics),
default_log_config,
_dir_lock: dir_lock,
})
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn default_log_config(&self) -> &LogConfig {
&self.default_log_config
}
pub async fn create_topic(&self, name: &str) -> Result<()> {
self.create_topic_inner(name, self.default_log_config, 1)
.await
}
pub async fn create_topic_with_config(&self, name: &str, log_config: LogConfig) -> Result<()> {
self.create_topic_inner(name, log_config, 1).await
}
pub async fn create_topic_with_partitions(&self, name: &str, partitions: u32) -> Result<()> {
self.create_topic_inner(name, self.default_log_config, partitions)
.await
}
pub async fn create_topic_with_config_and_partitions(
&self,
name: &str,
log_config: LogConfig,
partitions: u32,
) -> Result<()> {
self.create_topic_inner(name, log_config, partitions).await
}
async fn create_topic_inner(
&self,
name: &str,
log_config: LogConfig,
partitions: u32,
) -> Result<()> {
if partitions == 0 {
return Err(KafkoError::InvalidPartitionCount(0));
}
{
let topics = self.topics.read().expect("topics RwLock poisoned");
if topics.contains_key(name) {
return Err(KafkoError::TopicAlreadyExists(name.to_string()));
}
}
let topic_dir = self.dir.join(name);
let topic = Topic::create(&topic_dir, name, partitions, log_config).await?;
let mut topics = self.topics.write().expect("topics RwLock poisoned");
if topics.contains_key(name) {
return Err(KafkoError::TopicAlreadyExists(name.to_string()));
}
topics.insert(name.to_string(), Arc::new(topic));
Ok(())
}
pub async fn delete_topic(&self, name: &str) -> Result<()> {
let topic = {
let mut topics = self.topics.write().expect("topics RwLock poisoned");
match topics.remove(name) {
Some(t) => t,
None => return Err(KafkoError::TopicNotFound(name.to_string())),
}
};
match Arc::try_unwrap(topic) {
Ok(owned) => {
let topic_dir = self.dir.join(name);
owned.shutdown().await;
tokio::fs::remove_dir_all(&topic_dir).await?;
Ok(())
}
Err(arc) => {
self.topics
.write()
.expect("topics RwLock poisoned")
.insert(name.to_string(), arc);
Err(KafkoError::TopicInUse(name.to_string()))
}
}
}
pub async fn list_topics(&self) -> Vec<String> {
let mut names: Vec<String> = self
.topics
.read()
.expect("topics RwLock poisoned")
.keys()
.cloned()
.collect();
names.sort();
names
}
pub async fn has_topic(&self, name: &str) -> bool {
self.topics
.read()
.expect("topics RwLock poisoned")
.contains_key(name)
}
pub async fn topic(&self, name: &str) -> Option<Arc<Topic>> {
self.topics
.read()
.expect("topics RwLock poisoned")
.get(name)
.cloned()
}
pub async fn partition_count(&self, name: &str) -> Option<u32> {
self.topics
.read()
.expect("topics RwLock poisoned")
.get(name)
.map(|t| t.partition_count())
}
pub async fn producer_for(&self, name: &str) -> Result<Producer> {
let topic = self
.topic(name)
.await
.ok_or_else(|| KafkoError::TopicNotFound(name.to_string()))?;
Ok(Producer::new(topic))
}
pub async fn consumer_for(&self, name: &str) -> Result<Consumer> {
let topic = self
.topic(name)
.await
.ok_or_else(|| KafkoError::TopicNotFound(name.to_string()))?;
Ok(Consumer::from_topic(topic))
}
pub async fn consumer_for_group(&self, name: &str, group: &str) -> Result<Consumer> {
let topic = self
.topic(name)
.await
.ok_or_else(|| KafkoError::TopicNotFound(name.to_string()))?;
let offsets_dir = self.dir.join(name).join("offsets");
let store = OffsetStore::open(&offsets_dir, group, topic.partition_count()).await?;
Ok(Consumer::from_topic_with_group(topic, store))
}
pub async fn shutdown(mut self) -> Result<()> {
let topics = std::mem::take(self.topics.get_mut().expect("topics RwLock poisoned"));
shutdown_topics(topics).await;
Ok(())
}
}
impl Drop for Kafko {
fn drop(&mut self) {
let topics = std::mem::take(self.topics.get_mut().expect("topics RwLock poisoned"));
if topics.is_empty() {
return;
}
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return;
};
let cleanup = shutdown_topics(topics);
match handle.runtime_flavor() {
tokio::runtime::RuntimeFlavor::CurrentThread => {
handle.spawn(cleanup);
}
_ => {
tokio::task::block_in_place(|| handle.block_on(cleanup));
}
}
}
}
async fn shutdown_topics(topics: HashMap<String, Arc<Topic>>) {
for (_, topic) in topics {
if let Ok(owned) = Arc::try_unwrap(topic) {
owned.shutdown().await;
}
}
}
fn acquire_dir_lock(dir: &Path) -> Result<File> {
let lock_path = dir.join(LOCK_FILENAME);
let lock_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)?;
match FileExt::try_lock_exclusive(&lock_file) {
Ok(true) => Ok(lock_file),
Ok(false) => Err(KafkoError::AlreadyOpen {
path: dir.to_path_buf(),
}),
Err(e) => Err(e.into()),
}
}