use crate::error::{KafkoError, Result};
use crate::log::LogConfig;
use crate::partition::Partition;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use tokio::sync::Notify;
use tokio::sync::futures::Notified;
pub(crate) struct Wakeup {
notify: Notify,
parked: AtomicUsize,
}
impl Wakeup {
fn new() -> Self {
Self {
notify: Notify::new(),
parked: AtomicUsize::new(0),
}
}
pub(crate) fn signal(&self) {
if self.parked.load(Ordering::Relaxed) > 0 {
self.notify.notify_waiters();
}
}
pub(crate) fn mark_parked(&self) {
self.parked.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn unmark_parked(&self) {
self.parked.fetch_sub(1, Ordering::Relaxed);
}
pub(crate) fn notified(&self) -> Notified<'_> {
self.notify.notified()
}
}
pub struct Topic {
name: String,
partitions: Vec<Arc<Partition>>,
config: LogConfig,
round_robin: AtomicU64,
wake: Arc<Wakeup>,
}
impl Topic {
pub async fn create(
topic_dir: &Path,
name: &str,
partition_count: u32,
config: LogConfig,
) -> Result<Self> {
if partition_count == 0 {
return Err(KafkoError::InvalidPartitionCount(0));
}
let wake = Arc::new(Wakeup::new());
let mut partitions = Vec::with_capacity(partition_count as usize);
for i in 0..partition_count {
let dir = topic_dir.join(i.to_string());
let partition = Partition::open_with_wake(&dir, config, Some(wake.clone())).await?;
partitions.push(Arc::new(partition));
}
Ok(Self::from_parts(name, partitions, config, wake))
}
pub async fn open(topic_dir: &Path, name: &str, config: LogConfig) -> Result<Self> {
let mut indices = discover_partition_indices(topic_dir).await?;
indices.sort_unstable();
if indices.is_empty() {
return Err(KafkoError::InvalidTopicLayout {
topic: name.to_string(),
detail: "no partition subdirectories found (a data directory \
written by kafko <= 0.2 is not compatible with the \
partitioned 0.3 layout)"
.to_string(),
});
}
for (expected, &actual) in indices.iter().enumerate() {
if expected as u32 != actual {
return Err(KafkoError::InvalidTopicLayout {
topic: name.to_string(),
detail: format!(
"partition indices are not contiguous from 0 (expected {expected}, found {actual})"
),
});
}
}
let wake = Arc::new(Wakeup::new());
let mut partitions = Vec::with_capacity(indices.len());
for i in indices {
let dir = topic_dir.join(i.to_string());
let partition = Partition::open_with_wake(&dir, config, Some(wake.clone())).await?;
partitions.push(Arc::new(partition));
}
Ok(Self::from_parts(name, partitions, config, wake))
}
fn from_parts(
name: &str,
partitions: Vec<Arc<Partition>>,
config: LogConfig,
wake: Arc<Wakeup>,
) -> Self {
Self {
name: name.to_string(),
partitions,
config,
round_robin: AtomicU64::new(0),
wake,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn config(&self) -> &LogConfig {
&self.config
}
pub fn partition_count(&self) -> u32 {
self.partitions.len() as u32
}
pub fn partitions(&self) -> &[Arc<Partition>] {
&self.partitions
}
pub fn partition(&self, index: u32) -> Option<&Arc<Partition>> {
self.partitions.get(index as usize)
}
pub fn partition_for_key(&self, key: &[u8]) -> u32 {
partition_index(key, self.partition_count())
}
pub fn next_round_robin(&self) -> u32 {
let n = self.round_robin.fetch_add(1, Ordering::Relaxed);
(n % self.partition_count() as u64) as u32
}
pub(crate) fn wake_handle(&self) -> Arc<Wakeup> {
self.wake.clone()
}
pub async fn shutdown(self) {
for partition in self.partitions {
if let Ok(owned) = Arc::try_unwrap(partition) {
let _ = owned.shutdown().await;
}
}
}
}
fn fnv1a_64(bytes: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET_BASIS;
for &b in bytes {
hash ^= b as u64;
hash = hash.wrapping_mul(PRIME);
}
hash
}
fn partition_index(key: &[u8], partition_count: u32) -> u32 {
(fnv1a_64(key) % partition_count as u64) as u32
}
async fn discover_partition_indices(topic_dir: &Path) -> Result<Vec<u32>> {
let mut indices = Vec::new();
let mut entries = tokio::fs::read_dir(topic_dir).await?;
while let Some(entry) = entries.next_entry().await? {
if !entry.file_type().await?.is_dir() {
continue;
}
if let Some(name) = entry.file_name().to_str()
&& let Ok(index) = name.parse::<u32>()
{
indices.push(index);
}
}
Ok(indices)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partition_index_is_in_range_and_deterministic() {
for count in [1u32, 2, 3, 7, 16] {
for key in [b"".as_slice(), b"a", b"customer-42", b"\x00\xff\x10"] {
let p = partition_index(key, count);
assert!(p < count, "index {p} out of range for count {count}");
assert_eq!(p, partition_index(key, count));
}
}
}
#[test]
fn partition_index_spreads_keys_across_partitions() {
let count = 8u32;
let mut seen = std::collections::HashSet::new();
for i in 0..1000u32 {
let key = format!("key-{i}");
seen.insert(partition_index(key.as_bytes(), count));
}
assert_eq!(seen.len(), count as usize);
}
}