blob: 983aeaabd66490ab6b5f3490fcbfb755d1c7dbe8 [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.
use crate::metadata::{
ConsumerGroupId, ConsumerGroupMemberMeta, ConsumerGroupMeta, InnerMetadata, PartitionId,
PartitionMeta, StreamId, StreamMeta, TopicId, TopicMeta, UserId, UserMeta,
};
use crate::streaming::partitions::partition::{ConsumerGroupOffsets, ConsumerOffsets};
use crate::streaming::stats::{PartitionStats, StreamStats, TopicStats};
use arc_swap::{ArcSwap, Guard};
use iggy_common::collections::SegmentedSlab;
use iggy_common::{
CompressionAlgorithm, IdKind, Identifier, IggyError, IggyExpiry, IggyTimestamp, MaxTopicSize,
PersonalAccessToken, UserStatus,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
/// Thread-safe wrapper for GlobalMetadata using ArcSwap for lock-free reads.
/// Uses hierarchical structure: streams contain topics, topics contain partitions and consumer groups.
/// IDs are assigned by SegmentedSlab::insert() at each level.
pub struct Metadata {
inner: ArcSwap<InnerMetadata>,
}
impl Default for Metadata {
fn default() -> Self {
Self::new(InnerMetadata::new())
}
}
impl Metadata {
pub fn new(initial: InnerMetadata) -> Self {
Self {
inner: ArcSwap::from_pointee(initial),
}
}
#[inline]
pub fn load(&self) -> Guard<Arc<InnerMetadata>> {
self.inner.load()
}
#[inline]
pub fn load_full(&self) -> Arc<InnerMetadata> {
self.inner.load_full()
}
/// Add a stream with a specific ID (for bootstrap/recovery).
pub fn add_stream_with_id(&self, id: StreamId, meta: StreamMeta) {
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
let entries: Vec<_> = new
.streams
.iter()
.map(|(k, v)| (k, v.clone()))
.chain(std::iter::once((id, meta.clone())))
.collect();
new.streams = SegmentedSlab::from_entries(entries);
new.stream_index = new.stream_index.update(meta.name.clone(), id);
new.revision += 1;
Arc::new(new)
});
}
/// Add a new stream with slab-assigned ID. Returns the assigned ID.
pub fn add_stream(&self, meta: StreamMeta) -> StreamId {
let assigned_id = Arc::new(AtomicUsize::new(0));
let assigned_id_clone = assigned_id.clone();
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
let (streams, id) = new.streams.insert(meta.clone());
assigned_id_clone.store(id, Ordering::Release);
new.streams = streams;
new.stream_index = new.stream_index.update(meta.name.clone(), id);
new.revision += 1;
Arc::new(new)
});
assigned_id.load(Ordering::Acquire)
}
/// Atomically validates name uniqueness and updates stream name.
/// Returns Ok(()) if update succeeded, or appropriate error.
pub fn try_update_stream(&self, id: StreamId, new_name: Arc<str>) -> Result<(), IggyError> {
let stream_not_found = Arc::new(AtomicBool::new(false));
let name_conflict = Arc::new(AtomicBool::new(false));
let unchanged = Arc::new(AtomicBool::new(false));
let stream_not_found_clone = stream_not_found.clone();
let name_conflict_clone = name_conflict.clone();
let unchanged_clone = unchanged.clone();
let new_name_clone = new_name.clone();
self.inner.rcu(move |current| {
let Some(old_meta) = current.streams.get(id) else {
stream_not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
if old_meta.name == new_name_clone {
unchanged_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
if let Some(&existing_id) = current.stream_index.get(&new_name_clone)
&& existing_id != id
{
name_conflict_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
stream_not_found_clone.store(false, Ordering::Release);
name_conflict_clone.store(false, Ordering::Release);
unchanged_clone.store(false, Ordering::Release);
let mut new = (**current).clone();
new.stream_index = new.stream_index.without(&old_meta.name);
let mut updated = old_meta.clone();
updated.name = new_name_clone.clone();
let (streams, _) = new.streams.update(id, updated);
new.streams = streams;
new.stream_index = new.stream_index.update(new_name_clone.clone(), id);
new.revision += 1;
Arc::new(new)
});
if stream_not_found.load(Ordering::Acquire) {
Err(IggyError::StreamIdNotFound(
Identifier::numeric(id as u32).unwrap(),
))
} else if name_conflict.load(Ordering::Acquire) {
Err(IggyError::StreamNameAlreadyExists(new_name.to_string()))
} else {
Ok(())
}
}
/// Delete a stream and all its nested topics/partitions/consumer groups.
pub fn delete_stream(&self, id: StreamId) {
self.inner.rcu(|current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(id) {
new.stream_index = new.stream_index.without(&stream.name);
}
let (streams, _) = new.streams.remove(id);
new.streams = streams;
new.revision += 1;
Arc::new(new)
});
}
/// Add a topic with a specific ID (for bootstrap/recovery).
pub fn add_topic_with_id(&self, stream_id: StreamId, topic_id: TopicId, meta: TopicMeta) {
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
let entries: Vec<_> = updated_stream
.topics
.iter()
.map(|(k, v)| (k, v.clone()))
.chain(std::iter::once((topic_id, meta.clone())))
.collect();
updated_stream.topics = SegmentedSlab::from_entries(entries);
updated_stream.topic_index = updated_stream
.topic_index
.update(meta.name.clone(), topic_id);
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
/// Add a new topic with slab-assigned ID. Returns the assigned ID.
pub fn add_topic(&self, stream_id: StreamId, meta: TopicMeta) -> Option<TopicId> {
let assigned_id = Arc::new(AtomicUsize::new(usize::MAX));
let assigned_id_clone = assigned_id.clone();
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
let (topics, id) = updated_stream.topics.insert(meta.clone());
assigned_id_clone.store(id, Ordering::Release);
updated_stream.topics = topics;
updated_stream.topic_index =
updated_stream.topic_index.update(meta.name.clone(), id);
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
let id = assigned_id.load(Ordering::Acquire);
if id == usize::MAX { None } else { Some(id) }
}
/// Atomically validates name uniqueness and updates topic.
/// Returns Ok(()) if update succeeded, or appropriate error.
#[allow(clippy::too_many_arguments)]
pub fn try_update_topic(
&self,
stream_id: StreamId,
topic_id: TopicId,
new_name: Arc<str>,
message_expiry: IggyExpiry,
compression_algorithm: CompressionAlgorithm,
max_topic_size: MaxTopicSize,
replication_factor: u8,
) -> Result<(), IggyError> {
let stream_not_found = Arc::new(AtomicBool::new(false));
let topic_not_found = Arc::new(AtomicBool::new(false));
let name_conflict = Arc::new(AtomicBool::new(false));
let stream_not_found_clone = stream_not_found.clone();
let topic_not_found_clone = topic_not_found.clone();
let name_conflict_clone = name_conflict.clone();
let new_name_clone = new_name.clone();
self.inner.rcu(move |current| {
let Some(stream) = current.streams.get(stream_id) else {
stream_not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
let Some(old_meta) = stream.topics.get(topic_id) else {
topic_not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
if old_meta.name != new_name_clone
&& let Some(&existing_id) = stream.topic_index.get(&new_name_clone)
&& existing_id != topic_id
{
name_conflict_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
stream_not_found_clone.store(false, Ordering::Release);
topic_not_found_clone.store(false, Ordering::Release);
name_conflict_clone.store(false, Ordering::Release);
let mut new = (**current).clone();
let mut updated_stream = stream.clone();
if old_meta.name != new_name_clone {
updated_stream.topic_index = updated_stream.topic_index.without(&old_meta.name);
updated_stream.topic_index = updated_stream
.topic_index
.update(new_name_clone.clone(), topic_id);
}
let updated_meta = TopicMeta {
id: topic_id,
name: new_name_clone.clone(),
created_at: old_meta.created_at,
message_expiry,
compression_algorithm,
max_topic_size,
replication_factor,
partitions_count: old_meta.partitions_count,
stats: old_meta.stats.clone(),
partitions: old_meta.partitions.clone(),
consumer_groups: old_meta.consumer_groups.clone(),
consumer_group_index: old_meta.consumer_group_index.clone(),
partition_counter: old_meta.partition_counter.clone(),
};
let (topics, _) = updated_stream.topics.update(topic_id, updated_meta);
updated_stream.topics = topics;
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
new.revision += 1;
Arc::new(new)
});
if stream_not_found.load(Ordering::Acquire) {
Err(IggyError::StreamIdNotFound(
Identifier::numeric(stream_id as u32).unwrap(),
))
} else if topic_not_found.load(Ordering::Acquire) {
Err(IggyError::TopicIdNotFound(
Identifier::numeric(topic_id as u32).unwrap(),
Identifier::numeric(stream_id as u32).unwrap(),
))
} else if name_conflict.load(Ordering::Acquire) {
Err(IggyError::TopicNameAlreadyExists(
new_name.to_string(),
Identifier::numeric(stream_id as u32).unwrap(),
))
} else {
Ok(())
}
}
/// Delete a topic and all its nested partitions/consumer groups.
pub fn delete_topic(&self, stream_id: StreamId, topic_id: TopicId) {
self.inner.rcu(|current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
updated_stream.topic_index = updated_stream.topic_index.without(&topic.name);
}
let (topics, _) = updated_stream.topics.remove(topic_id);
updated_stream.topics = topics;
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
/// Add partitions with specific IDs (for bootstrap/recovery).
pub fn add_partitions_with_ids(
&self,
stream_id: StreamId,
topic_id: TopicId,
partitions: Vec<(PartitionId, PartitionMeta)>,
) {
if partitions.is_empty() {
return;
}
self.inner.rcu(move |current| {
let partitions = partitions.clone();
let mut new = (**current).clone();
let new_version = new.revision + 1;
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
let entries: Vec<_> = updated_topic
.partitions
.iter()
.map(|(k, v)| (k, v.clone()))
.chain(partitions.into_iter().map(|(id, mut meta)| {
meta.revision_id = new_version;
(id, meta)
}))
.collect();
updated_topic.partitions = SegmentedSlab::from_entries(entries);
updated_topic.partitions_count = updated_topic.partitions.len() as u32;
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision = new_version;
Arc::new(new)
});
}
/// Add new partitions with slab-assigned IDs. Returns the assigned IDs.
pub fn add_partitions(
&self,
stream_id: StreamId,
topic_id: TopicId,
partitions: Vec<PartitionMeta>,
) -> Vec<PartitionId> {
if partitions.is_empty() {
return Vec::new();
}
let assigned_ids = Arc::new(std::sync::Mutex::new(Vec::new()));
let assigned_ids_clone = assigned_ids.clone();
self.inner.rcu(move |current| {
let partitions = partitions.clone();
let mut new = (**current).clone();
let new_version = new.revision + 1;
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
let mut ids = Vec::new();
for mut meta in partitions {
meta.revision_id = new_version;
let (parts, id) = updated_topic.partitions.insert(meta.clone());
meta.id = id;
let (parts, _) = parts.update(id, meta);
updated_topic.partitions = parts;
ids.push(id);
}
updated_topic.partitions_count = updated_topic.partitions.len() as u32;
*assigned_ids_clone.lock().unwrap() = ids;
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision = new_version;
Arc::new(new)
});
Arc::try_unwrap(assigned_ids).unwrap().into_inner().unwrap()
}
pub fn delete_partitions(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_ids: &[PartitionId],
) {
if partition_ids.is_empty() {
return;
}
let partition_ids = partition_ids.to_vec();
self.inner.rcu(move |current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
for &partition_id in &partition_ids {
let (parts, _) = updated_topic.partitions.remove(partition_id);
updated_topic.partitions = parts;
}
updated_topic.partitions_count = updated_topic.partitions.len() as u32;
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
/// Add a user with a specific ID (for bootstrap/recovery).
pub fn add_user_with_id(&self, id: UserId, meta: UserMeta) {
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
let entries: Vec<_> = new
.users
.iter()
.map(|(k, v)| (k, v.clone()))
.chain(std::iter::once((id as usize, meta.clone())))
.collect();
new.users = SegmentedSlab::from_entries(entries);
new.user_index = new.user_index.update(meta.username.clone(), id);
new.revision += 1;
Arc::new(new)
});
}
/// Add a new user with slab-assigned ID. Returns the assigned ID.
pub fn add_user(&self, meta: UserMeta) -> UserId {
let assigned_id = Arc::new(AtomicUsize::new(0));
let assigned_id_clone = assigned_id.clone();
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
let (users, id) = new.users.insert(meta.clone());
assigned_id_clone.store(id, Ordering::Release);
new.users = users;
new.user_index = new.user_index.update(meta.username.clone(), id as UserId);
new.revision += 1;
Arc::new(new)
});
assigned_id.load(Ordering::Acquire) as UserId
}
/// Atomically validates username uniqueness and updates user.
/// Returns the updated UserMeta if successful.
pub fn try_update_user(
&self,
id: UserId,
new_username: Option<Arc<str>>,
new_status: Option<UserStatus>,
) -> Result<UserMeta, IggyError> {
let user_not_found = Arc::new(AtomicBool::new(false));
let name_conflict = Arc::new(AtomicBool::new(false));
let updated_meta: Arc<std::sync::Mutex<Option<UserMeta>>> =
Arc::new(std::sync::Mutex::new(None));
let user_not_found_clone = user_not_found.clone();
let name_conflict_clone = name_conflict.clone();
let updated_meta_clone = updated_meta.clone();
let new_username_clone = new_username.clone();
self.inner.rcu(move |current| {
let Some(old_meta) = current.users.get(id as usize) else {
user_not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
let final_username = new_username_clone
.clone()
.unwrap_or_else(|| old_meta.username.clone());
if final_username != old_meta.username
&& let Some(&existing_id) = current.user_index.get(&final_username)
&& existing_id != id
{
name_conflict_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
user_not_found_clone.store(false, Ordering::Release);
name_conflict_clone.store(false, Ordering::Release);
let mut new = (**current).clone();
if final_username != old_meta.username {
new.user_index = new.user_index.without(&old_meta.username);
new.user_index = new.user_index.update(final_username.clone(), id);
}
let meta = UserMeta {
id: old_meta.id,
username: final_username,
password_hash: old_meta.password_hash.clone(),
status: new_status.unwrap_or(old_meta.status),
permissions: old_meta.permissions.clone(),
created_at: old_meta.created_at,
};
*updated_meta_clone.lock().unwrap() = Some(meta.clone());
let (users, _) = new.users.update(id as usize, meta);
new.users = users;
new.revision += 1;
Arc::new(new)
});
if user_not_found.load(Ordering::Acquire) {
Err(IggyError::ResourceNotFound(format!("User {}", id)))
} else if name_conflict.load(Ordering::Acquire) {
Err(IggyError::UserAlreadyExists)
} else {
Ok(updated_meta.lock().unwrap().take().unwrap())
}
}
/// Updates user metadata directly. Use only when username is not changing
/// or when caller has already verified username uniqueness.
pub fn update_user_meta(&self, id: UserId, meta: UserMeta) {
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
if let Some(old_meta) = new.users.get(id as usize)
&& old_meta.username != meta.username
{
new.user_index = new.user_index.without(&old_meta.username);
new.user_index = new.user_index.update(meta.username.clone(), id);
}
let (users, _) = new.users.update(id as usize, meta);
new.users = users;
new.revision += 1;
Arc::new(new)
});
}
pub fn delete_user(&self, id: UserId) {
self.inner.rcu(|current| {
let mut new = (**current).clone();
if let Some(user) = new.users.get(id as usize) {
new.user_index = new.user_index.without(&user.username);
}
let (users, _) = new.users.remove(id as usize);
new.users = users;
new.personal_access_tokens = new.personal_access_tokens.without(&id);
new.revision += 1;
Arc::new(new)
});
}
pub fn add_personal_access_token(&self, user_id: UserId, pat: PersonalAccessToken) {
self.inner.rcu(move |current| {
let pat = pat.clone();
let mut new = (**current).clone();
let user_pats = new
.personal_access_tokens
.get(&user_id)
.cloned()
.unwrap_or_default();
new.personal_access_tokens = new
.personal_access_tokens
.update(user_id, user_pats.update(pat.token.clone(), pat));
new.revision += 1;
Arc::new(new)
});
}
pub fn delete_personal_access_token(&self, user_id: UserId, token_hash: &Arc<str>) {
let token_hash = token_hash.clone();
self.inner.rcu(move |current| {
let mut new = (**current).clone();
if let Some(user_pats) = new.personal_access_tokens.get(&user_id) {
new.personal_access_tokens = new
.personal_access_tokens
.update(user_id, user_pats.without(&token_hash));
}
new.revision += 1;
Arc::new(new)
});
}
pub fn get_user_personal_access_tokens(&self, user_id: UserId) -> Vec<PersonalAccessToken> {
self.load()
.personal_access_tokens
.get(&user_id)
.map(|pats| pats.values().cloned().collect())
.unwrap_or_default()
}
pub fn get_personal_access_token_by_hash(
&self,
token_hash: &str,
) -> Option<PersonalAccessToken> {
let token_hash_arc: Arc<str> = Arc::from(token_hash);
let metadata = self.load();
for (_, user_pats) in metadata.personal_access_tokens.iter() {
if let Some(pat) = user_pats.get(&token_hash_arc) {
return Some(pat.clone());
}
}
None
}
pub fn user_pat_count(&self, user_id: UserId) -> usize {
self.load()
.personal_access_tokens
.get(&user_id)
.map(|pats| pats.len())
.unwrap_or(0)
}
pub fn user_has_pat_with_name(&self, user_id: UserId, name: &str) -> bool {
self.load()
.personal_access_tokens
.get(&user_id)
.map(|pats| pats.values().any(|pat| &*pat.name == name))
.unwrap_or(false)
}
pub fn find_pat_token_hash_by_name(&self, user_id: UserId, name: &str) -> Option<Arc<str>> {
self.load()
.personal_access_tokens
.get(&user_id)
.and_then(|pats| {
pats.iter()
.find(|(_, pat)| &*pat.name == name)
.map(|(hash, _)| hash.clone())
})
}
/// Add a consumer group with a specific ID (for bootstrap/recovery).
pub fn add_consumer_group_with_id(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
meta: ConsumerGroupMeta,
) {
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
let entries: Vec<_> = updated_topic
.consumer_groups
.iter()
.map(|(k, v)| (k, v.clone()))
.chain(std::iter::once((group_id, meta.clone())))
.collect();
updated_topic.consumer_groups = SegmentedSlab::from_entries(entries);
updated_topic.consumer_group_index = updated_topic
.consumer_group_index
.update(meta.name.clone(), group_id);
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
/// Add a new consumer group with slab-assigned ID. Returns the assigned ID.
pub fn add_consumer_group(
&self,
stream_id: StreamId,
topic_id: TopicId,
meta: ConsumerGroupMeta,
) -> Option<ConsumerGroupId> {
let assigned_id = Arc::new(AtomicUsize::new(usize::MAX));
let assigned_id_clone = assigned_id.clone();
self.inner.rcu(move |current| {
let meta = meta.clone();
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
let (groups, id) = updated_topic.consumer_groups.insert(meta.clone());
assigned_id_clone.store(id, Ordering::Release);
updated_topic.consumer_groups = groups;
updated_topic.consumer_group_index = updated_topic
.consumer_group_index
.update(meta.name.clone(), id);
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
let id = assigned_id.load(Ordering::Acquire);
if id == usize::MAX { None } else { Some(id) }
}
pub fn delete_consumer_group(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
) {
self.inner.rcu(|current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
if let Some(group) = updated_topic.consumer_groups.get(group_id) {
updated_topic.consumer_group_index =
updated_topic.consumer_group_index.without(&group.name);
}
let (groups, _) = updated_topic.consumer_groups.remove(group_id);
updated_topic.consumer_groups = groups;
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
pub fn join_consumer_group(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
client_id: u32,
) -> Option<usize> {
let member_id = Arc::new(AtomicUsize::new(usize::MAX));
let member_id_clone = member_id.clone();
self.inner.rcu(|current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
if let Some(group) = updated_topic.consumer_groups.get(group_id) {
let mut updated_group = group.clone();
let next_id = updated_group
.members
.iter()
.map(|(_, m)| m.id)
.max()
.map(|m| m + 1)
.unwrap_or(0);
let new_member = ConsumerGroupMemberMeta::new(next_id, client_id);
let (members, _) = updated_group.members.insert(new_member);
updated_group.members = members;
updated_group.rebalance_members();
member_id_clone.store(next_id, Ordering::Release);
let (groups, _) = updated_topic
.consumer_groups
.update(group_id, updated_group);
updated_topic.consumer_groups = groups;
}
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
let id = member_id.load(Ordering::Acquire);
if id == usize::MAX { None } else { Some(id) }
}
pub fn leave_consumer_group(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
client_id: u32,
) -> Option<usize> {
let member_id = Arc::new(AtomicUsize::new(usize::MAX));
let member_id_clone = member_id.clone();
self.inner.rcu(|current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
if let Some(group) = updated_topic.consumer_groups.get(group_id) {
let mut updated_group = group.clone();
let member_to_remove: Option<usize> = updated_group
.members
.iter()
.find(|(_, m)| m.client_id == client_id)
.map(|(id, _)| id);
if let Some(mid) = member_to_remove {
member_id_clone.store(mid, Ordering::Release);
let (members, _) = updated_group.members.remove(mid);
updated_group.members = members;
updated_group.rebalance_members();
let (groups, _) = updated_topic
.consumer_groups
.update(group_id, updated_group);
updated_topic.consumer_groups = groups;
}
}
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
let id = member_id.load(Ordering::Acquire);
if id == usize::MAX { None } else { Some(id) }
}
pub fn is_consumer_group_member(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
client_id: u32,
) -> bool {
let metadata = self.load();
metadata
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.consumer_groups.get(group_id))
.map(|g| g.members.iter().any(|(_, m)| m.client_id == client_id))
.unwrap_or(false)
}
pub fn rebalance_consumer_groups_for_topic(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_ids: &[PartitionId],
) {
let partition_ids = partition_ids.to_vec();
self.inner.rcu(move |current| {
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
let group_ids: Vec<_> = updated_topic
.consumer_groups
.iter()
.map(|(id, _)| id)
.collect();
for gid in group_ids {
if let Some(group) = updated_topic.consumer_groups.get(gid) {
let mut updated_group = group.clone();
updated_group.partitions = partition_ids.clone();
updated_group.rebalance_members();
let (groups, _) =
updated_topic.consumer_groups.update(gid, updated_group);
updated_topic.consumer_groups = groups;
}
}
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
pub fn get_stream_id(&self, identifier: &Identifier) -> Option<StreamId> {
let metadata = self.load();
match identifier.kind {
IdKind::Numeric => {
let stream_id = identifier.get_u32_value().ok()? as StreamId;
if metadata.streams.get(stream_id).is_some() {
Some(stream_id)
} else {
None
}
}
IdKind::String => {
let name = identifier.get_cow_str_value().ok()?;
metadata.stream_index.get(name.as_ref()).copied()
}
}
}
pub fn stream_name_exists(&self, name: &str) -> bool {
self.load().stream_index.contains_key(name)
}
pub fn get_topic_id(&self, stream_id: StreamId, identifier: &Identifier) -> Option<TopicId> {
let metadata = self.load();
let stream = metadata.streams.get(stream_id)?;
match identifier.kind {
IdKind::Numeric => {
let topic_id = identifier.get_u32_value().ok()? as TopicId;
if stream.topics.get(topic_id).is_some() {
Some(topic_id)
} else {
None
}
}
IdKind::String => {
let name = identifier.get_cow_str_value().ok()?;
stream.topic_index.get(&Arc::from(name.as_ref())).copied()
}
}
}
pub fn get_user_id(&self, identifier: &Identifier) -> Option<UserId> {
let metadata = self.load();
match identifier.kind {
IdKind::Numeric => Some(identifier.get_u32_value().ok()? as UserId),
IdKind::String => {
let name = identifier.get_cow_str_value().ok()?;
metadata.user_index.get(name.as_ref()).copied()
}
}
}
pub fn get_consumer_group_id(
&self,
stream_id: StreamId,
topic_id: TopicId,
identifier: &Identifier,
) -> Option<ConsumerGroupId> {
let metadata = self.load();
let stream = metadata.streams.get(stream_id)?;
let topic = stream.topics.get(topic_id)?;
match identifier.kind {
IdKind::Numeric => {
let group_id = identifier.get_u32_value().ok()? as ConsumerGroupId;
if topic.consumer_groups.get(group_id).is_some() {
Some(group_id)
} else {
None
}
}
IdKind::String => {
let name = identifier.get_cow_str_value().ok()?;
topic
.consumer_group_index
.get(&Arc::from(name.as_ref()))
.copied()
}
}
}
pub fn stream_exists(&self, id: StreamId) -> bool {
self.load().streams.get(id).is_some()
}
pub fn topic_exists(&self, stream_id: StreamId, topic_id: TopicId) -> bool {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.is_some()
}
pub fn partition_exists(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
) -> bool {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.partitions.get(partition_id))
.is_some()
}
pub fn user_exists(&self, id: UserId) -> bool {
self.load().users.get(id as usize).is_some()
}
pub fn consumer_group_exists(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
) -> bool {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.consumer_groups.get(group_id))
.is_some()
}
pub fn consumer_group_exists_by_name(
&self,
stream_id: StreamId,
topic_id: TopicId,
name: &str,
) -> bool {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.map(|t| t.consumer_group_index.contains_key(name))
.unwrap_or(false)
}
pub fn streams_count(&self) -> usize {
self.load().streams.len()
}
pub fn topics_count(&self, stream_id: StreamId) -> usize {
self.load()
.streams
.get(stream_id)
.map(|s| s.topics.len())
.unwrap_or(0)
}
pub fn partitions_count(&self, stream_id: StreamId, topic_id: TopicId) -> usize {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.map(|t| t.partitions.len())
.unwrap_or(0)
}
pub fn get_next_partition_id(&self, stream_id: StreamId, topic_id: TopicId) -> Option<usize> {
let metadata = self.load();
let topic = metadata.streams.get(stream_id)?.topics.get(topic_id)?;
let partitions_count = topic.partitions.len();
if partitions_count == 0 {
return None;
}
let counter = &topic.partition_counter;
let mut partition_id = counter.fetch_add(1, Ordering::AcqRel);
if partition_id >= partitions_count {
partition_id %= partitions_count;
counter.store(partition_id + 1, Ordering::Relaxed);
}
Some(partition_id)
}
pub fn get_next_member_partition_id(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
member_id: usize,
calculate: bool,
) -> Option<PartitionId> {
let metadata = self.load();
let member = metadata
.streams
.get(stream_id)?
.topics
.get(topic_id)?
.consumer_groups
.get(group_id)?
.members
.get(member_id)?;
let assigned_partitions = &member.partitions;
if assigned_partitions.is_empty() {
return None;
}
let partitions_count = assigned_partitions.len();
let counter = &member.partition_index;
if calculate {
let current = counter
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
Some((current + 1) % partitions_count)
})
.unwrap();
Some(assigned_partitions[current % partitions_count])
} else {
let current = counter.load(Ordering::Relaxed);
Some(assigned_partitions[current % partitions_count])
}
}
pub fn users_count(&self) -> usize {
self.load().users.len()
}
pub fn username_exists(&self, username: &str) -> bool {
self.load().user_index.contains_key(username)
}
pub fn consumer_groups_count(&self, stream_id: StreamId, topic_id: TopicId) -> usize {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.map(|t| t.consumer_groups.len())
.unwrap_or(0)
}
pub fn get_stream_stats(&self, id: StreamId) -> Option<Arc<StreamStats>> {
self.load().streams.get(id).map(|s| s.stats.clone())
}
pub fn get_topic_stats(
&self,
stream_id: StreamId,
topic_id: TopicId,
) -> Option<Arc<TopicStats>> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.map(|t| t.stats.clone())
}
pub fn get_partition_stats(
&self,
ns: &crate::shard::namespace::IggyNamespace,
) -> Option<Arc<PartitionStats>> {
self.load()
.streams
.get(ns.stream_id())
.and_then(|s| s.topics.get(ns.topic_id()))
.and_then(|t| t.partitions.get(ns.partition_id()))
.map(|p| p.stats.clone())
}
pub fn get_partition_stats_by_ids(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
) -> Option<Arc<PartitionStats>> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.partitions.get(partition_id))
.map(|p| p.stats.clone())
}
/// Set consumer offsets for a partition
pub fn set_partition_offsets(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
consumer_offsets: Arc<ConsumerOffsets>,
consumer_group_offsets: Arc<ConsumerGroupOffsets>,
) {
self.inner.rcu(move |current| {
let consumer_offsets = consumer_offsets.clone();
let consumer_group_offsets = consumer_group_offsets.clone();
let mut new = (**current).clone();
if let Some(stream) = new.streams.get(stream_id) {
let mut updated_stream = stream.clone();
if let Some(topic) = updated_stream.topics.get(topic_id) {
let mut updated_topic = topic.clone();
if let Some(partition) = updated_topic.partitions.get(partition_id) {
let mut updated_partition = partition.clone();
updated_partition.consumer_offsets = Some(consumer_offsets);
updated_partition.consumer_group_offsets = Some(consumer_group_offsets);
let (partitions, _) = updated_topic
.partitions
.update(partition_id, updated_partition);
updated_topic.partitions = partitions;
}
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
}
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
}
new.revision += 1;
Arc::new(new)
});
}
pub fn get_partition_consumer_offsets(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
) -> Option<Arc<crate::streaming::partitions::partition::ConsumerOffsets>> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.partitions.get(partition_id))
.and_then(|p| p.consumer_offsets.clone())
}
pub fn get_partition_consumer_group_offsets(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
) -> Option<Arc<crate::streaming::partitions::partition::ConsumerGroupOffsets>> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.partitions.get(partition_id))
.and_then(|p| p.consumer_group_offsets.clone())
}
/// Register a stream with a specific ID (for bootstrap/recovery).
/// Unlike try_register_stream, this uses a pre-determined stream_id.
pub fn register_stream(
&self,
stream_id: StreamId,
name: Arc<str>,
created_at: IggyTimestamp,
) -> Arc<StreamStats> {
let stats = Arc::new(StreamStats::default());
let meta = StreamMeta::with_stats(stream_id, name, created_at, stats.clone());
self.add_stream_with_id(stream_id, meta);
stats
}
/// Atomically validates name uniqueness and registers stream.
pub fn try_register_stream(
&self,
name: Arc<str>,
created_at: IggyTimestamp,
) -> Result<(StreamId, Arc<StreamStats>), IggyError> {
let stats = Arc::new(StreamStats::default());
let name_existed = Arc::new(AtomicBool::new(false));
let assigned_id = Arc::new(AtomicUsize::new(0));
let name_existed_clone = name_existed.clone();
let assigned_id_clone = assigned_id.clone();
let name_clone = name.clone();
let stats_clone = stats.clone();
self.inner.rcu(move |current| {
if current.stream_index.contains_key(&name_clone) {
name_existed_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
name_existed_clone.store(false, Ordering::Release);
let mut meta =
StreamMeta::with_stats(0, name_clone.clone(), created_at, stats_clone.clone());
let mut new = (**current).clone();
let (streams, id) = new.streams.insert(meta.clone());
meta.id = id;
let (streams, _) = streams.update(id, meta);
assigned_id_clone.store(id, Ordering::Release);
new.streams = streams;
new.stream_index = new.stream_index.update(name_clone.clone(), id);
new.revision += 1;
Arc::new(new)
});
if name_existed.load(Ordering::Acquire) {
Err(IggyError::StreamNameAlreadyExists(name.to_string()))
} else {
Ok((assigned_id.load(Ordering::Acquire), stats))
}
}
/// Atomically validates name uniqueness and registers topic.
#[allow(clippy::too_many_arguments)]
pub fn try_register_topic(
&self,
stream_id: StreamId,
name: Arc<str>,
created_at: IggyTimestamp,
message_expiry: IggyExpiry,
compression_algorithm: CompressionAlgorithm,
max_topic_size: MaxTopicSize,
replication_factor: u8,
partitions_count: u32,
) -> Result<(TopicId, Arc<TopicStats>), IggyError> {
let parent_stats = self.get_stream_stats(stream_id).ok_or_else(|| {
IggyError::StreamIdNotFound(Identifier::numeric(stream_id as u32).unwrap())
})?;
let stats = Arc::new(TopicStats::new(parent_stats));
let name_existed = Arc::new(AtomicBool::new(false));
let stream_not_found = Arc::new(AtomicBool::new(false));
let assigned_id = Arc::new(AtomicUsize::new(0));
let name_existed_clone = name_existed.clone();
let stream_not_found_clone = stream_not_found.clone();
let assigned_id_clone = assigned_id.clone();
let name_clone = name.clone();
let stats_clone = stats.clone();
self.inner.rcu(move |current| {
let Some(stream) = current.streams.get(stream_id) else {
stream_not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
if stream.topic_index.contains_key(&name_clone) {
name_existed_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
name_existed_clone.store(false, Ordering::Release);
stream_not_found_clone.store(false, Ordering::Release);
let mut meta = TopicMeta {
id: 0,
name: name_clone.clone(),
created_at,
message_expiry,
compression_algorithm,
max_topic_size,
replication_factor,
partitions_count,
stats: stats_clone.clone(),
partitions: SegmentedSlab::new(),
consumer_groups: SegmentedSlab::new(),
consumer_group_index: imbl::HashMap::new(),
partition_counter: Arc::new(AtomicUsize::new(0)),
};
let mut new = (**current).clone();
let mut updated_stream = stream.clone();
let (topics, id) = updated_stream.topics.insert(meta.clone());
meta.id = id;
let (topics, _) = topics.update(id, meta);
assigned_id_clone.store(id, Ordering::Release);
updated_stream.topics = topics;
updated_stream.topic_index = updated_stream.topic_index.update(name_clone.clone(), id);
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
new.revision += 1;
Arc::new(new)
});
if stream_not_found.load(Ordering::Acquire) {
Err(IggyError::StreamIdNotFound(
Identifier::numeric(stream_id as u32).unwrap(),
))
} else if name_existed.load(Ordering::Acquire) {
Err(IggyError::TopicNameAlreadyExists(
name.to_string(),
Identifier::numeric(stream_id as u32).unwrap(),
))
} else {
Ok((assigned_id.load(Ordering::Acquire), stats))
}
}
/// Register a topic with a specific ID (for bootstrap/recovery).
/// Unlike try_register_topic, this uses a pre-determined topic_id.
#[allow(clippy::too_many_arguments)]
pub fn register_topic(
&self,
stream_id: StreamId,
topic_id: TopicId,
name: Arc<str>,
created_at: IggyTimestamp,
message_expiry: IggyExpiry,
compression_algorithm: CompressionAlgorithm,
max_topic_size: MaxTopicSize,
replication_factor: u8,
partitions_count: u32,
) -> Arc<TopicStats> {
let parent_stats = self
.get_stream_stats(stream_id)
.expect("Stream must exist when registering topic");
let stats = Arc::new(TopicStats::new(parent_stats));
let stats_clone = stats.clone();
let name_clone = name.clone();
self.inner.rcu(move |current| {
let Some(stream) = current.streams.get(stream_id) else {
return Arc::clone(current);
};
let meta = TopicMeta {
id: topic_id,
name: name_clone.clone(),
created_at,
message_expiry,
compression_algorithm,
max_topic_size,
replication_factor,
partitions_count,
stats: stats_clone.clone(),
partitions: SegmentedSlab::new(),
consumer_groups: SegmentedSlab::new(),
consumer_group_index: imbl::HashMap::new(),
partition_counter: Arc::new(AtomicUsize::new(0)),
};
let mut new = (**current).clone();
let mut updated_stream = stream.clone();
let entries: Vec<_> = updated_stream
.topics
.iter()
.map(|(k, v)| (k, v.clone()))
.chain(std::iter::once((topic_id, meta)))
.collect();
updated_stream.topics = SegmentedSlab::from_entries(entries);
updated_stream.topic_index = updated_stream
.topic_index
.update(name_clone.clone(), topic_id);
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
new.revision += 1;
Arc::new(new)
});
stats
}
/// Atomically validates username uniqueness and registers user.
pub fn try_register_user(
&self,
username: Arc<str>,
password_hash: Arc<str>,
status: iggy_common::UserStatus,
permissions: Option<Arc<iggy_common::Permissions>>,
max_users: usize,
) -> Result<UserId, IggyError> {
let name_existed = Arc::new(AtomicBool::new(false));
let limit_reached = Arc::new(AtomicBool::new(false));
let assigned_id = Arc::new(AtomicUsize::new(0));
let name_existed_clone = name_existed.clone();
let limit_reached_clone = limit_reached.clone();
let assigned_id_clone = assigned_id.clone();
let username_clone = username.clone();
self.inner.rcu(move |current| {
if current.user_index.contains_key(&username_clone) {
name_existed_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
if current.users.len() >= max_users {
limit_reached_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
name_existed_clone.store(false, Ordering::Release);
limit_reached_clone.store(false, Ordering::Release);
let mut meta = UserMeta {
id: 0,
username: username_clone.clone(),
password_hash: password_hash.clone(),
status,
permissions: permissions.clone(),
created_at: IggyTimestamp::now(),
};
let mut new = (**current).clone();
let (users, id) = new.users.insert(meta.clone());
meta.id = id as UserId;
let (users, _) = users.update(id, meta);
assigned_id_clone.store(id, Ordering::Release);
new.users = users;
new.user_index = new.user_index.update(username_clone.clone(), id as UserId);
new.revision += 1;
Arc::new(new)
});
if name_existed.load(Ordering::Acquire) {
Err(IggyError::UserAlreadyExists)
} else if limit_reached.load(Ordering::Acquire) {
Err(IggyError::UsersLimitReached)
} else {
Ok(assigned_id.load(Ordering::Acquire) as UserId)
}
}
/// Atomically validates consumer group name uniqueness and registers the group.
pub fn try_register_consumer_group(
&self,
stream_id: StreamId,
topic_id: TopicId,
name: Arc<str>,
partitions: Vec<PartitionId>,
) -> Result<ConsumerGroupId, IggyError> {
let name_existed = Arc::new(AtomicBool::new(false));
let not_found = Arc::new(AtomicBool::new(false));
let assigned_id = Arc::new(AtomicUsize::new(0));
let name_existed_clone = name_existed.clone();
let not_found_clone = not_found.clone();
let assigned_id_clone = assigned_id.clone();
let name_clone = name.clone();
self.inner.rcu(move |current| {
let Some(stream) = current.streams.get(stream_id) else {
not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
let Some(topic) = stream.topics.get(topic_id) else {
not_found_clone.store(true, Ordering::Release);
return Arc::clone(current);
};
if topic.consumer_group_index.contains_key(&name_clone) {
name_existed_clone.store(true, Ordering::Release);
return Arc::clone(current);
}
name_existed_clone.store(false, Ordering::Release);
not_found_clone.store(false, Ordering::Release);
let mut meta = ConsumerGroupMeta {
id: 0,
name: name_clone.clone(),
partitions: partitions.clone(),
members: SegmentedSlab::new(),
};
let mut new = (**current).clone();
let mut updated_stream = stream.clone();
let mut updated_topic = topic.clone();
let (groups, id) = updated_topic.consumer_groups.insert(meta.clone());
meta.id = id;
let (groups, _) = groups.update(id, meta);
assigned_id_clone.store(id, Ordering::Release);
updated_topic.consumer_groups = groups;
updated_topic.consumer_group_index = updated_topic
.consumer_group_index
.update(name_clone.clone(), id);
let (topics, _) = updated_stream.topics.update(topic_id, updated_topic);
updated_stream.topics = topics;
let (streams, _) = new.streams.update(stream_id, updated_stream);
new.streams = streams;
new.revision += 1;
Arc::new(new)
});
if not_found.load(Ordering::Acquire) {
Err(IggyError::TopicIdNotFound(
Identifier::numeric(topic_id as u32).unwrap(),
Identifier::numeric(stream_id as u32).unwrap(),
))
} else if name_existed.load(Ordering::Acquire) {
Err(IggyError::ConsumerGroupNameAlreadyExists(
name.to_string(),
Identifier::numeric(topic_id as u32).unwrap(),
))
} else {
Ok(assigned_id.load(Ordering::Acquire))
}
}
pub fn register_partitions(
&self,
stream_id: StreamId,
topic_id: TopicId,
count: usize,
created_at: IggyTimestamp,
) -> Vec<Arc<PartitionStats>> {
if count == 0 {
return Vec::new();
}
let parent_stats = self
.get_topic_stats(stream_id, topic_id)
.expect("Parent topic stats must exist before registering partitions");
let mut stats_list = Vec::with_capacity(count);
let mut metas = Vec::with_capacity(count);
for _ in 0..count {
let stats = Arc::new(PartitionStats::new(parent_stats.clone()));
metas.push(PartitionMeta {
id: 0,
created_at,
revision_id: 0,
stats: stats.clone(),
consumer_offsets: None,
consumer_group_offsets: None,
});
stats_list.push(stats);
}
self.add_partitions(stream_id, topic_id, metas);
stats_list
}
/// Register a single partition with a specific ID (for bootstrap/recovery).
pub fn register_partition(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
created_at: IggyTimestamp,
) -> Arc<PartitionStats> {
let parent_stats = self
.get_topic_stats(stream_id, topic_id)
.expect("Parent topic stats must exist before registering partition");
let stats = Arc::new(PartitionStats::new(parent_stats));
let meta = PartitionMeta {
id: partition_id,
created_at,
revision_id: 0,
stats: stats.clone(),
consumer_offsets: None,
consumer_group_offsets: None,
};
self.add_partitions_with_ids(stream_id, topic_id, vec![(partition_id, meta)]);
stats
}
pub fn get_user(&self, id: UserId) -> Option<UserMeta> {
self.load().users.get(id as usize).cloned()
}
pub fn get_all_users(&self) -> Vec<UserMeta> {
self.load().users.iter().map(|(_, u)| u.clone()).collect()
}
pub fn get_stream(&self, id: StreamId) -> Option<StreamMeta> {
self.load().streams.get(id).cloned()
}
pub fn get_topic(&self, stream_id: StreamId, topic_id: TopicId) -> Option<TopicMeta> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id).cloned())
}
pub fn get_partition(
&self,
stream_id: StreamId,
topic_id: TopicId,
partition_id: PartitionId,
) -> Option<PartitionMeta> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.partitions.get(partition_id).cloned())
}
pub fn get_consumer_group(
&self,
stream_id: StreamId,
topic_id: TopicId,
group_id: ConsumerGroupId,
) -> Option<ConsumerGroupMeta> {
self.load()
.streams
.get(stream_id)
.and_then(|s| s.topics.get(topic_id))
.and_then(|t| t.consumer_groups.get(group_id).cloned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_crud() {
let metadata = Metadata::default();
let stream_meta = StreamMeta::with_stats(
0,
Arc::from("test-stream"),
IggyTimestamp::now(),
Arc::new(StreamStats::default()),
);
let id = metadata.add_stream(stream_meta);
assert!(metadata.stream_exists(id));
assert_eq!(metadata.streams_count(), 1);
metadata
.try_update_stream(id, Arc::from("renamed-stream"))
.unwrap();
let loaded = metadata.load();
assert_eq!(
loaded.streams.get(id).unwrap().name.as_ref(),
"renamed-stream"
);
assert!(loaded.stream_index.contains_key("renamed-stream"));
assert!(!loaded.stream_index.contains_key("test-stream"));
metadata.delete_stream(id);
assert!(!metadata.stream_exists(id));
assert_eq!(metadata.streams_count(), 0);
}
#[test]
fn test_cascade_delete() {
let metadata = Metadata::default();
let stream_stats = Arc::new(StreamStats::default());
let stream_meta = StreamMeta::with_stats(
0,
Arc::from("stream-1"),
IggyTimestamp::now(),
stream_stats.clone(),
);
let stream_id = metadata.add_stream(stream_meta);
let topic_stats = Arc::new(TopicStats::new(stream_stats));
let topic_meta = TopicMeta {
id: 0,
name: Arc::from("topic-1"),
created_at: IggyTimestamp::now(),
message_expiry: IggyExpiry::NeverExpire,
compression_algorithm: CompressionAlgorithm::None,
max_topic_size: MaxTopicSize::Unlimited,
replication_factor: 1,
partitions_count: 0,
stats: topic_stats.clone(),
partitions: SegmentedSlab::new(),
consumer_groups: SegmentedSlab::new(),
consumer_group_index: imbl::HashMap::new(),
partition_counter: Arc::new(AtomicUsize::new(0)),
};
let topic_id = metadata.add_topic(stream_id, topic_meta).unwrap();
let partitions = vec![
PartitionMeta {
id: 0,
created_at: IggyTimestamp::now(),
revision_id: 0,
stats: Arc::new(PartitionStats::new(topic_stats.clone())),
consumer_offsets: None,
consumer_group_offsets: None,
},
PartitionMeta {
id: 0,
created_at: IggyTimestamp::now(),
revision_id: 0,
stats: Arc::new(PartitionStats::new(topic_stats)),
consumer_offsets: None,
consumer_group_offsets: None,
},
];
let partition_ids = metadata.add_partitions(stream_id, topic_id, partitions);
assert!(metadata.stream_exists(stream_id));
assert!(metadata.topic_exists(stream_id, topic_id));
assert!(metadata.partition_exists(stream_id, topic_id, partition_ids[0]));
assert!(metadata.partition_exists(stream_id, topic_id, partition_ids[1]));
metadata.delete_stream(stream_id);
assert!(!metadata.stream_exists(stream_id));
assert!(!metadata.topic_exists(stream_id, topic_id));
assert!(!metadata.partition_exists(stream_id, topic_id, partition_ids[0]));
assert!(!metadata.partition_exists(stream_id, topic_id, partition_ids[1]));
}
}