You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by hg...@apache.org on 2026/01/23 12:13:07 UTC
(iggy) branch global-metadata-leftright updated (2d20028a5 -> 0e81d8fb2)
This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a change to branch global-metadata-leftright
in repository https://gitbox.apache.org/repos/asf/iggy.git
omit 2d20028a5 refactor concurrent scenario, check messages order
omit c9f2d16ca refactor(server): replace slab ECS with LeftRight-based global metadata
add 871b18397 feat(metadata): impl states for streams,users, consumer_groups in metadata module (#2582)
new 26c65d5fe refactor(server): replace slab ECS with LeftRight-based global metadata
new 0e81d8fb2 refactor concurrent scenario, check messages order
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (2d20028a5)
\
N -- N -- N refs/heads/global-metadata-leftright (0e81d8fb2)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
Cargo.lock | 3 +-
Cargo.toml | 1 +
.../consumer_groups/create_consumer_group.rs | 2 +-
.../consumer_groups/delete_consumer_group.rs | 2 +-
.../src/commands/partitions/create_partitions.rs | 2 +-
.../src/commands/partitions/delete_partitions.rs | 2 +-
.../create_personal_access_token.rs | 2 +-
.../delete_personal_access_token.rs | 2 +-
.../src/commands/segments/delete_segments.rs | 2 +-
core/common/src/commands/streams/create_stream.rs | 2 +-
core/common/src/commands/streams/delete_stream.rs | 2 +-
core/common/src/commands/streams/purge_stream.rs | 2 +-
core/common/src/commands/streams/update_stream.rs | 2 +-
core/common/src/commands/topics/create_topic.rs | 2 +-
core/common/src/commands/topics/delete_topic.rs | 2 +-
core/common/src/commands/topics/purge_topic.rs | 2 +-
core/common/src/commands/topics/update_topic.rs | 2 +-
core/common/src/commands/users/change_password.rs | 2 +-
core/common/src/commands/users/create_user.rs | 2 +-
core/common/src/commands/users/delete_user.rs | 2 +-
.../src/commands/users/update_permissions.rs | 2 +-
core/common/src/commands/users/update_user.rs | 2 +-
core/common/src/types/consensus/message.rs | 16 +
core/metadata/Cargo.toml | 3 +-
core/metadata/src/impls/metadata.rs | 220 ++++---
core/metadata/src/stm/consumer_group.rs | 321 +++++-----
core/metadata/src/stm/mod.rs | 304 +++++++---
core/metadata/src/stm/mux.rs | 12 +-
core/metadata/src/stm/stream.rs | 672 +++++++++------------
core/metadata/src/stm/user.rs | 401 +++++-------
core/server/Cargo.toml | 2 +-
31 files changed, 1020 insertions(+), 975 deletions(-)
(iggy) 02/02: refactor concurrent scenario, check messages order
Posted by hg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch global-metadata-leftright
in repository https://gitbox.apache.org/repos/asf/iggy.git
commit 0e81d8fb2a713995affe71ed5396c389701146e3
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Fri Jan 23 10:58:13 2026 +0100
refactor concurrent scenario, check messages order
---
.../tests/server/concurrent_addition.rs | 15 +-
.../tests/server/scenarios/concurrent_scenario.rs | 560 ++++++++++++---------
...h_multiple_clients_polling_messages_scenario.rs | 60 ++-
...with_single_client_polling_messages_scenario.rs | 39 +-
4 files changed, 426 insertions(+), 248 deletions(-)
diff --git a/core/integration/tests/server/concurrent_addition.rs b/core/integration/tests/server/concurrent_addition.rs
index c63681340..a1618f6b8 100644
--- a/core/integration/tests/server/concurrent_addition.rs
+++ b/core/integration/tests/server/concurrent_addition.rs
@@ -30,16 +30,17 @@ use test_case::test_matrix;
// Test matrix for race condition scenarios
// Tests all combinations of:
// - Transport: TCP, HTTP, QUIC, WebSocket (4)
-// - Resource: User, Stream, Topic (3)
+// - Resource: User, Stream, Topic, Partition, ConsumerGroup (5)
// - Path: Hot (unique names), Cold (duplicate names) (2)
// - Barrier: On (synchronized), Off (unsynchronized) (2)
-// Total: 4 × 3 × 2 × 2 = 48 test cases
+// Total: 4 × 5 × 2 × 2 = 80 test cases
+// Note: Partition + Cold is skipped (partitions don't have names)
// TODO: Websocket fails for the `cold` type, cold means that we are creating resources with the same name.
// It fails with the error assertion, instead of `AlreadyExist`, we get generic `Error`.
#[test_matrix(
[tcp(), http(), quic(), websocket()],
- [user(), stream(), topic()],
+ [user(), stream(), topic(), partition(), consumer_group()],
[hot(), cold()],
[barrier_on(), barrier_off()]
)]
@@ -119,6 +120,14 @@ fn topic() -> ResourceType {
ResourceType::Topic
}
+fn partition() -> ResourceType {
+ ResourceType::Partition
+}
+
+fn consumer_group() -> ResourceType {
+ ResourceType::ConsumerGroup
+}
+
fn hot() -> ScenarioType {
ScenarioType::Hot
}
diff --git a/core/integration/tests/server/scenarios/concurrent_scenario.rs b/core/integration/tests/server/scenarios/concurrent_scenario.rs
index 5140b42b6..d42296ba2 100644
--- a/core/integration/tests/server/scenarios/concurrent_scenario.rs
+++ b/core/integration/tests/server/scenarios/concurrent_scenario.rs
@@ -19,19 +19,16 @@
use crate::server::scenarios::create_client;
use futures::future::join_all;
use iggy::prelude::*;
-use iggy_common::UserInfo;
+use iggy_common::{ConsumerGroup, UserInfo};
use integration::test_server::{ClientFactory, login_root};
use std::sync::Arc;
use tokio::sync::Barrier;
-const OPERATIONS_COUNT: usize = 40;
-const MULTIPLE_CLIENT_COUNT: usize = 10;
-const OPERATIONS_PER_CLIENT: usize = OPERATIONS_COUNT / MULTIPLE_CLIENT_COUNT;
+const CONCURRENT_CLIENTS: usize = 20;
const USER_PASSWORD: &str = "secret";
const TEST_STREAM_NAME: &str = "race-test-stream";
const TEST_TOPIC_NAME: &str = "race-test-topic";
const PARTITIONS_COUNT: u32 = 1;
-const PARTITIONS_TO_ADD: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceType {
@@ -39,7 +36,7 @@ pub enum ResourceType {
Stream,
Topic,
Partition,
- // TODO(hubcio): add ConsumerGroup
+ ConsumerGroup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -72,8 +69,8 @@ pub async fn run(
root_client.create_stream(TEST_STREAM_NAME).await.unwrap();
}
- // For partition tests, create parent stream and topic first
- if resource_type == ResourceType::Partition {
+ // For partition and consumer group tests, create parent stream and topic first
+ if resource_type == ResourceType::Partition || resource_type == ResourceType::ConsumerGroup {
root_client.create_stream(TEST_STREAM_NAME).await.unwrap();
let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
root_client
@@ -91,29 +88,35 @@ pub async fn run(
}
let results = match (resource_type, scenario_type) {
- (ResourceType::User, ScenarioType::Cold) => {
- execute_multiple_clients_users_cold(client_factory, use_barrier).await
- }
(ResourceType::User, ScenarioType::Hot) => {
- execute_multiple_clients_users_hot(client_factory, use_barrier).await
+ execute_users_hot(client_factory, use_barrier).await
+ }
+ (ResourceType::User, ScenarioType::Cold) => {
+ execute_users_cold(client_factory, use_barrier).await
}
(ResourceType::Stream, ScenarioType::Hot) => {
- execute_multiple_clients_streams_hot(client_factory, use_barrier).await
+ execute_streams_hot(client_factory, use_barrier).await
}
(ResourceType::Stream, ScenarioType::Cold) => {
- execute_multiple_clients_streams_cold(client_factory, use_barrier).await
+ execute_streams_cold(client_factory, use_barrier).await
}
(ResourceType::Topic, ScenarioType::Hot) => {
- execute_multiple_clients_topics_hot(client_factory, use_barrier).await
+ execute_topics_hot(client_factory, use_barrier).await
}
(ResourceType::Topic, ScenarioType::Cold) => {
- execute_multiple_clients_topics_cold(client_factory, use_barrier).await
+ execute_topics_cold(client_factory, use_barrier).await
}
(ResourceType::Partition, ScenarioType::Hot) => {
- execute_multiple_clients_partitions_hot(client_factory, use_barrier).await
+ execute_partitions_hot(client_factory, use_barrier).await
}
// Partitions don't have names, so Cold scenario doesn't apply
(ResourceType::Partition, ScenarioType::Cold) => vec![],
+ (ResourceType::ConsumerGroup, ScenarioType::Hot) => {
+ execute_consumer_groups_hot(client_factory, use_barrier).await
+ }
+ (ResourceType::ConsumerGroup, ScenarioType::Cold) => {
+ execute_consumer_groups_cold(client_factory, use_barrier).await
+ }
};
if !results.is_empty() {
@@ -123,320 +126,299 @@ pub async fn run(
}
}
-async fn execute_multiple_clients_users_hot(
+async fn execute_users_hot(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for client_id in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for client_id in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
- for i in 0..OPERATIONS_PER_CLIENT {
- let username = format!("race-user-{}-{}", client_id, i);
- let result = client
- .create_user(&username, USER_PASSWORD, UserStatus::Active, None)
- .await
- .map(|_| ());
- results.push(result);
- }
- results
- });
-
- handles.push(handle);
+ let username = format!("race-user-{}", client_id);
+ client
+ .create_user(&username, USER_PASSWORD, UserStatus::Active, None)
+ .await
+ .map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
-async fn execute_multiple_clients_users_cold(
+async fn execute_users_cold(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
const DUPLICATE_USER: &str = "race-user-duplicate";
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for _ in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for _ in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
- for _ in 0..OPERATIONS_PER_CLIENT {
- let result = client
- .create_user(DUPLICATE_USER, USER_PASSWORD, UserStatus::Active, None)
- .await
- .map(|_| ());
- results.push(result);
- }
- results
- });
-
- handles.push(handle);
+ client
+ .create_user(DUPLICATE_USER, USER_PASSWORD, UserStatus::Active, None)
+ .await
+ .map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
-async fn execute_multiple_clients_streams_hot(
+async fn execute_streams_hot(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for client_id in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for client_id in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
- for i in 0..OPERATIONS_PER_CLIENT {
- let stream_name = format!("race-stream-{}-{}", client_id, i);
- let result = client.create_stream(&stream_name).await.map(|_| ());
- results.push(result);
- }
- results
- });
-
- handles.push(handle);
+ let stream_name = format!("race-stream-{}", client_id);
+ client.create_stream(&stream_name).await.map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
-async fn execute_multiple_clients_streams_cold(
+async fn execute_streams_cold(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
const DUPLICATE_STREAM: &str = "race-stream-duplicate";
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for _ in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for _ in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
- for _ in 0..OPERATIONS_PER_CLIENT {
- let result = client.create_stream(DUPLICATE_STREAM).await.map(|_| ());
- results.push(result);
- }
- results
- });
-
- handles.push(handle);
+ client.create_stream(DUPLICATE_STREAM).await.map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
-async fn execute_multiple_clients_topics_hot(
+async fn execute_topics_hot(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for client_id in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for client_id in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
-
- for i in 0..OPERATIONS_PER_CLIENT {
- let topic_name = format!("race-topic-{}-{}", client_id, i);
- let result = client
- .create_topic(
- &stream_id,
- &topic_name,
- PARTITIONS_COUNT,
- CompressionAlgorithm::default(),
- None,
- IggyExpiry::NeverExpire,
- MaxTopicSize::ServerDefault,
- )
- .await
- .map(|_| ());
- results.push(result);
- }
- results
- });
-
- handles.push(handle);
+ let topic_name = format!("race-topic-{}", client_id);
+ client
+ .create_topic(
+ &stream_id,
+ &topic_name,
+ PARTITIONS_COUNT,
+ CompressionAlgorithm::default(),
+ None,
+ IggyExpiry::NeverExpire,
+ MaxTopicSize::ServerDefault,
+ )
+ .await
+ .map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
-async fn execute_multiple_clients_topics_cold(
+async fn execute_topics_cold(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
const DUPLICATE_TOPIC: &str = "race-topic-duplicate";
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for _ in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for _ in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ client
+ .create_topic(
+ &stream_id,
+ DUPLICATE_TOPIC,
+ PARTITIONS_COUNT,
+ CompressionAlgorithm::default(),
+ None,
+ IggyExpiry::NeverExpire,
+ MaxTopicSize::ServerDefault,
+ )
+ .await
+ .map(|_| ())
+ }));
+ }
- for _ in 0..OPERATIONS_PER_CLIENT {
- let result = client
- .create_topic(
- &stream_id,
- DUPLICATE_TOPIC,
- PARTITIONS_COUNT,
- CompressionAlgorithm::default(),
- None,
- IggyExpiry::NeverExpire,
- MaxTopicSize::ServerDefault,
- )
- .await
- .map(|_| ());
- results.push(result);
- }
- results
- });
+ join_all(handles)
+ .await
+ .into_iter()
+ .map(|r| r.expect("task panicked"))
+ .collect()
+}
- handles.push(handle);
+async fn execute_partitions_hot(
+ client_factory: &dyn ClientFactory,
+ use_barrier: bool,
+) -> Vec<OperationResult> {
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
+
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for _ in 0..CONCURRENT_CLIENTS {
+ let client = create_client(client_factory).await;
+ login_root(&client).await;
+ let barrier = barrier.clone();
+
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
+ b.wait().await;
+ }
+ let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ let topic_id = Identifier::named(TEST_TOPIC_NAME).unwrap();
+ client
+ .create_partitions(&stream_id, &topic_id, 1)
+ .await
+ .map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
-async fn execute_multiple_clients_partitions_hot(
+async fn execute_consumer_groups_hot(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
- let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
- let barrier = if use_barrier {
- Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
- } else {
- None
- };
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- for _ in 0..MULTIPLE_CLIENT_COUNT {
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for client_id in 0..CONCURRENT_CLIENTS {
let client = create_client(client_factory).await;
login_root(&client).await;
+ let barrier = barrier.clone();
- let barrier_clone = barrier.clone();
- let handle = tokio::spawn(async move {
- if let Some(b) = barrier_clone {
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
b.wait().await;
}
-
- let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
let topic_id = Identifier::named(TEST_TOPIC_NAME).unwrap();
+ let group_name = format!("race-consumer-group-{}", client_id);
+ client
+ .create_consumer_group(&stream_id, &topic_id, &group_name)
+ .await
+ .map(|_| ())
+ }));
+ }
- for _ in 0..OPERATIONS_PER_CLIENT {
- let result = client
- .create_partitions(&stream_id, &topic_id, PARTITIONS_TO_ADD)
- .await
- .map(|_| ());
- results.push(result);
- }
- results
- });
+ join_all(handles)
+ .await
+ .into_iter()
+ .map(|r| r.expect("task panicked"))
+ .collect()
+}
+
+async fn execute_consumer_groups_cold(
+ client_factory: &dyn ClientFactory,
+ use_barrier: bool,
+) -> Vec<OperationResult> {
+ const DUPLICATE_CONSUMER_GROUP: &str = "race-consumer-group-duplicate";
+ let barrier = use_barrier.then(|| Arc::new(Barrier::new(CONCURRENT_CLIENTS)));
- handles.push(handle);
+ let mut handles = Vec::with_capacity(CONCURRENT_CLIENTS);
+ for _ in 0..CONCURRENT_CLIENTS {
+ let client = create_client(client_factory).await;
+ login_root(&client).await;
+ let barrier = barrier.clone();
+
+ handles.push(tokio::spawn(async move {
+ if let Some(b) = barrier {
+ b.wait().await;
+ }
+ let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ let topic_id = Identifier::named(TEST_TOPIC_NAME).unwrap();
+ client
+ .create_consumer_group(&stream_id, &topic_id, DUPLICATE_CONSUMER_GROUP)
+ .await
+ .map(|_| ())
+ }));
}
- let all_results = join_all(handles).await;
- all_results
+ join_all(handles)
+ .await
.into_iter()
- .flat_map(|r| r.expect("Tokio task panicked"))
+ .map(|r| r.expect("task panicked"))
.collect()
}
@@ -449,9 +431,9 @@ fn validate_results(results: &[OperationResult], scenario_type: ScenarioType) {
// Hot path: all operations should succeed
assert_eq!(
success_count,
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
"Hot path: Expected all {} operations to succeed, but only {} succeeded. Errors: {:?}",
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
success_count,
results.iter().filter(|r| r.is_err()).collect::<Vec<_>>()
);
@@ -470,9 +452,9 @@ fn validate_results(results: &[OperationResult], scenario_type: ScenarioType) {
);
assert_eq!(
error_count,
- OPERATIONS_COUNT - 1,
+ CONCURRENT_CLIENTS - 1,
"Cold path: Expected {} errors, but got {}",
- OPERATIONS_COUNT - 1,
+ CONCURRENT_CLIENTS - 1,
error_count
);
@@ -485,6 +467,7 @@ fn validate_results(results: &[OperationResult], scenario_type: ScenarioType) {
IggyError::UserAlreadyExists
| IggyError::StreamNameAlreadyExists(_)
| IggyError::TopicNameAlreadyExists(_, _)
+ | IggyError::ConsumerGroupNameAlreadyExists(_, _)
| IggyError::HttpResponseError(400, _)
),
"Expected 'already exists' error, got: {:?}",
@@ -624,6 +607,36 @@ async fn validate_server_state(
);
}
}
+ ResourceType::ConsumerGroup => {
+ let mut group_states = Vec::new();
+ for client in &clients {
+ let state = validate_consumer_groups_state(client, scenario_type).await;
+ group_states.push(state);
+ }
+
+ let first_state = &group_states[0];
+ for (i, state) in group_states.iter().enumerate().skip(1) {
+ assert_eq!(
+ state.len(),
+ first_state.len(),
+ "Client {} sees different number of consumer groups ({}) than client 0 ({})",
+ i,
+ state.len(),
+ first_state.len()
+ );
+
+ let mut first_names: Vec<_> = first_state.iter().map(|g| &g.name).collect();
+ let mut current_names: Vec<_> = state.iter().map(|g| &g.name).collect();
+ first_names.sort();
+ current_names.sort();
+
+ assert_eq!(
+ current_names, first_names,
+ "Client {} sees different consumer groups than client 0",
+ i
+ );
+ }
+ }
}
}
@@ -636,12 +649,12 @@ async fn validate_users_state(client: &IggyClient, scenario_type: ScenarioType)
match scenario_type {
ScenarioType::Hot => {
- // Hot path: should have OPERATIONS_COUNT unique users
+ // Hot path: should have CONCURRENT_CLIENTS unique users
assert_eq!(
test_users.len(),
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
"Hot path: Expected {} users, but found {}. Users: {:?}",
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
test_users.len(),
test_users.iter().map(|u| &u.username).collect::<Vec<_>>()
);
@@ -697,12 +710,12 @@ async fn validate_streams_state(client: &IggyClient, scenario_type: ScenarioType
match scenario_type {
ScenarioType::Hot => {
- // Hot path: should have OPERATIONS_COUNT unique streams
+ // Hot path: should have CONCURRENT_CLIENTS unique streams
assert_eq!(
test_streams.len(),
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
"Hot path: Expected {} streams, but found {}. Streams: {:?}",
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
test_streams.len(),
test_streams.iter().map(|s| &s.name).collect::<Vec<_>>()
);
@@ -765,12 +778,12 @@ async fn validate_topics_state(client: &IggyClient, scenario_type: ScenarioType)
match scenario_type {
ScenarioType::Hot => {
- // Hot path: should have OPERATIONS_COUNT unique topics
+ // Hot path: should have CONCURRENT_CLIENTS unique topics
assert_eq!(
test_topics.len(),
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
"Hot path: Expected {} topics, but found {}. Topics: {:?}",
- OPERATIONS_COUNT,
+ CONCURRENT_CLIENTS,
test_topics.len(),
test_topics.iter().map(|t| &t.name).collect::<Vec<_>>()
);
@@ -826,17 +839,82 @@ async fn validate_partitions_state(client: &IggyClient) -> u32 {
.expect("Failed to get test topic")
.expect("Test topic not found");
- // Expected: initial partition + OPERATIONS_COUNT added partitions
- let expected_partitions = PARTITIONS_COUNT + (OPERATIONS_COUNT as u32 * PARTITIONS_TO_ADD);
+ // Expected: initial partition + CONCURRENT_CLIENTS added partitions (1 each)
+ let expected_partitions = PARTITIONS_COUNT + CONCURRENT_CLIENTS as u32;
assert_eq!(
topic.partitions_count, expected_partitions,
- "Hot path: Expected {} partitions (1 initial + {} added), but found {}",
- expected_partitions, OPERATIONS_COUNT, topic.partitions_count
+ "Expected {} partitions (1 initial + {} added), but found {}",
+ expected_partitions, CONCURRENT_CLIENTS, topic.partitions_count
);
topic.partitions_count
}
+async fn validate_consumer_groups_state(
+ client: &IggyClient,
+ scenario_type: ScenarioType,
+) -> Vec<ConsumerGroup> {
+ let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ let topic_id = Identifier::named(TEST_TOPIC_NAME).unwrap();
+ let groups = client
+ .get_consumer_groups(&stream_id, &topic_id)
+ .await
+ .expect("Failed to get consumer groups");
+ let test_groups: Vec<_> = groups
+ .into_iter()
+ .filter(|g| g.name.starts_with("race-consumer-group-"))
+ .collect();
+
+ match scenario_type {
+ ScenarioType::Hot => {
+ assert_eq!(
+ test_groups.len(),
+ CONCURRENT_CLIENTS,
+ "Hot path: Expected {} consumer groups, but found {}. Groups: {:?}",
+ CONCURRENT_CLIENTS,
+ test_groups.len(),
+ test_groups.iter().map(|g| &g.name).collect::<Vec<_>>()
+ );
+
+ let mut ids: Vec<u32> = test_groups.iter().map(|g| g.id).collect();
+ ids.sort_unstable();
+ let unique_ids: std::collections::HashSet<u32> = ids.iter().copied().collect();
+ assert_eq!(
+ unique_ids.len(),
+ test_groups.len(),
+ "Hot path: Found duplicate consumer group IDs: {:?}",
+ ids
+ );
+
+ let names: std::collections::HashSet<&str> =
+ test_groups.iter().map(|g| g.name.as_str()).collect();
+ assert_eq!(
+ names.len(),
+ test_groups.len(),
+ "Hot path: Found duplicate consumer group names"
+ );
+ }
+ ScenarioType::Cold => {
+ assert_eq!(
+ test_groups.len(),
+ 1,
+ "Cold path: Expected exactly 1 consumer group, but found {}. Groups: {:?}",
+ test_groups.len(),
+ test_groups.iter().map(|g| &g.name).collect::<Vec<_>>()
+ );
+
+ let group = &test_groups[0];
+ assert_eq!(
+ group.name, "race-consumer-group-duplicate",
+ "Cold path: Expected consumer group named 'race-consumer-group-duplicate', found '{}'",
+ group.name
+ );
+ }
+ }
+
+ test_groups
+}
+
async fn cleanup_resources(client: &IggyClient, resource_type: ResourceType) {
match resource_type {
ResourceType::User => {
@@ -852,13 +930,15 @@ async fn cleanup_resources(client: &IggyClient, resource_type: ResourceType) {
ResourceType::Stream => {
let streams = client.get_streams().await.unwrap();
for stream in streams {
- let _ = client
- .delete_stream(&Identifier::numeric(stream.id).unwrap())
- .await;
+ if stream.name.starts_with("race-stream-") {
+ let _ = client
+ .delete_stream(&Identifier::numeric(stream.id).unwrap())
+ .await;
+ }
}
}
- // Just delete test stream (also cleans up topics and partitions)
- ResourceType::Topic | ResourceType::Partition => {
+ // Just delete test stream (also cleans up topics, partitions, and consumer groups)
+ ResourceType::Topic | ResourceType::Partition | ResourceType::ConsumerGroup => {
let _ = client
.delete_stream(&Identifier::named(TEST_STREAM_NAME).unwrap())
.await;
diff --git a/core/integration/tests/server/scenarios/consumer_group_with_multiple_clients_polling_messages_scenario.rs b/core/integration/tests/server/scenarios/consumer_group_with_multiple_clients_polling_messages_scenario.rs
index 6345061d2..076ecae25 100644
--- a/core/integration/tests/server/scenarios/consumer_group_with_multiple_clients_polling_messages_scenario.rs
+++ b/core/integration/tests/server/scenarios/consumer_group_with_multiple_clients_polling_messages_scenario.rs
@@ -24,7 +24,7 @@ use iggy::prelude::*;
use integration::test_server::{
ClientFactory, assert_clean_system, create_user, login_root, login_user,
};
-use std::str::FromStr;
+use std::str::{FromStr, from_utf8};
pub async fn run(client_factory: &dyn ClientFactory) {
let system_client = create_client(client_factory).await;
@@ -132,6 +132,7 @@ async fn execute_using_messages_key_key(
async fn poll_messages(client: &IggyClient) -> u32 {
let consumer = Consumer::group(Identifier::named(CONSUMER_GROUP_NAME).unwrap());
let mut total_read_messages_count = 0;
+ let mut last_entity_id: Option<u32> = None;
const MAX_RETRIES: u32 = 5;
let mut total_retries = 0;
@@ -163,12 +164,42 @@ async fn poll_messages(client: &IggyClient) -> u32 {
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
};
- total_read_messages_count += polled_messages.messages.len() as u32;
+ if polled_messages.messages.is_empty() {
+ if total_retries >= MAX_RETRIES {
+ break;
+ }
+ continue;
+ }
+
+ let message = &polled_messages.messages[0];
+ let payload = from_utf8(&message.payload).unwrap();
+ let entity_id = parse_entity_id_from_payload(payload);
+
+ // Each client polls from a single partition; messages should arrive in send order
+ if let Some(last) = last_entity_id {
+ assert!(
+ entity_id > last,
+ "Messages out of order: {} should be > {}",
+ entity_id,
+ last
+ );
+ }
+ last_entity_id = Some(entity_id);
+
+ total_read_messages_count += 1;
}
total_read_messages_count
}
+fn parse_entity_id_from_payload(payload: &str) -> u32 {
+ payload
+ .strip_prefix("message-")
+ .expect("payload should start with 'message-'")
+ .parse()
+ .expect("entity_id should be a valid u32")
+}
+
fn create_message_payload(entity_id: u32) -> String {
format!("message-{entity_id}")
}
@@ -214,6 +245,7 @@ async fn execute_using_none_key(
async fn validate_message_polling(client: &IggyClient) {
let consumer = Consumer::group(Identifier::named(CONSUMER_GROUP_NAME).unwrap());
+ let mut partition_id: Option<u32> = None;
const MAX_RETRIES: u32 = 5;
let mut total_retries = 0;
@@ -256,6 +288,23 @@ async fn validate_message_polling(client: &IggyClient) {
let message = &polled_messages.messages[0];
let offset = (i - 1) as u64;
assert_eq!(message.header.offset, offset);
+
+ let payload = from_utf8(&message.payload).unwrap();
+
+ if partition_id.is_none() {
+ partition_id = Some(parse_partition_id_from_extended_payload(payload));
+ }
+
+ // For balanced partitioning: entity_id = partition_id + offset * PARTITIONS_COUNT
+ let p = partition_id.unwrap();
+ let expected_entity_id = p + (offset as u32) * PARTITIONS_COUNT;
+ let expected_payload = create_extended_message_payload(p, expected_entity_id);
+
+ assert_eq!(
+ payload, expected_payload,
+ "Payload mismatch at offset {}: expected '{}', got '{}'",
+ offset, expected_payload, payload
+ );
}
let polled_messages = client
@@ -273,6 +322,13 @@ async fn validate_message_polling(client: &IggyClient) {
assert!(polled_messages.messages.is_empty())
}
+fn parse_partition_id_from_extended_payload(payload: &str) -> u32 {
+ let parts: Vec<&str> = payload.split('-').collect();
+ parts[1]
+ .parse()
+ .expect("partition_id should be a valid u32")
+}
+
fn create_extended_message_payload(partition_id: u32, entity_id: u32) -> String {
format!("message-{partition_id}-{entity_id}")
}
diff --git a/core/integration/tests/server/scenarios/consumer_group_with_single_client_polling_messages_scenario.rs b/core/integration/tests/server/scenarios/consumer_group_with_single_client_polling_messages_scenario.rs
index 5eece2dcb..166267390 100644
--- a/core/integration/tests/server/scenarios/consumer_group_with_single_client_polling_messages_scenario.rs
+++ b/core/integration/tests/server/scenarios/consumer_group_with_single_client_polling_messages_scenario.rs
@@ -22,6 +22,7 @@ use crate::server::scenarios::{
};
use iggy::prelude::*;
use integration::test_server::{ClientFactory, assert_clean_system, login_root};
+use std::collections::HashSet;
use std::str::{FromStr, from_utf8};
pub async fn run(client_factory: &dyn ClientFactory) {
@@ -99,8 +100,11 @@ async fn execute_using_messages_key_key(client: &IggyClient) {
}
// 2. Poll the messages for the single client which has assigned all partitions in the consumer group
+ // Hash-based partitioning distributes messages unpredictably across partitions, and polling
+ // round-robins across partitions, so we verify completeness rather than order.
let consumer = Consumer::group(Identifier::named(CONSUMER_GROUP_NAME).unwrap());
- let mut total_read_messages_count = 0;
+ let mut received_entity_ids = HashSet::new();
+
for _ in 1..=PARTITIONS_COUNT * MESSAGES_COUNT {
let polled_messages = client
.poll_messages(
@@ -115,10 +119,39 @@ async fn execute_using_messages_key_key(client: &IggyClient) {
.await
.unwrap();
- total_read_messages_count += polled_messages.messages.len() as u32;
+ for message in &polled_messages.messages {
+ let payload = from_utf8(&message.payload).unwrap();
+ let entity_id = parse_entity_id_from_payload(payload);
+ assert!(
+ (1..=MESSAGES_COUNT).contains(&entity_id),
+ "entity_id {} out of expected range 1..={}",
+ entity_id,
+ MESSAGES_COUNT
+ );
+ received_entity_ids.insert(entity_id);
+ }
}
- assert_eq!(total_read_messages_count, MESSAGES_COUNT);
+ assert_eq!(
+ received_entity_ids.len() as u32,
+ MESSAGES_COUNT,
+ "Expected {} unique messages, got {}",
+ MESSAGES_COUNT,
+ received_entity_ids.len()
+ );
+ let expected: HashSet<u32> = (1..=MESSAGES_COUNT).collect();
+ assert_eq!(
+ received_entity_ids, expected,
+ "Missing or duplicate messages"
+ );
+}
+
+fn parse_entity_id_from_payload(payload: &str) -> u32 {
+ payload
+ .strip_prefix("message-")
+ .expect("payload should start with 'message-'")
+ .parse()
+ .expect("entity_id should be a valid u32")
}
fn create_message_payload(entity_id: u32) -> String {
(iggy) 01/02: refactor(server): replace slab ECS with LeftRight-based global metadata
Posted by hg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch global-metadata-leftright
in repository https://gitbox.apache.org/repos/asf/iggy.git
commit 26c65d5fe46b69aa5e28e47a0e16a53021ef0680
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Mon Jan 5 14:14:16 2026 +0100
refactor(server): replace slab ECS with LeftRight-based global metadata
Centralizing metadata into a single LeftRight store provides lock-free
reads across shards and eliminates coordination complexity inherent in
the distributed slab approach. Shards now read from a shared snapshot
while shard 0 exclusively handles writes, ensuring consistency without
per-read synchronization.
---
Cargo.lock | 244 +--
Cargo.toml | 3 +-
DEPENDENCIES.md | 52 +-
.../data_integrity/verify_after_server_restart.rs | 205 ++-
.../tests/server/scenarios/concurrent_scenario.rs | 122 +-
.../scenarios/segment_rotation_race_scenario.rs | 2 +-
core/metadata/Cargo.toml | 2 +-
core/server/Cargo.toml | 3 +-
.../cluster/get_cluster_metadata_handler.rs | 1 -
.../create_consumer_group_handler.rs | 123 +-
.../delete_consumer_group_handler.rs | 95 +-
.../consumer_groups/get_consumer_group_handler.rs | 30 +-
.../consumer_groups/get_consumer_groups_handler.rs | 41 +-
.../consumer_groups/join_consumer_group_handler.rs | 74 +-
.../leave_consumer_group_handler.rs | 74 +-
.../delete_consumer_offset_handler.rs | 9 +-
.../get_consumer_offset_handler.rs | 6 +-
.../store_consumer_offset_handler.rs | 9 +-
.../messages/flush_unsaved_buffer_handler.rs | 1 -
.../handlers/messages/poll_messages_handler.rs | 1 -
.../handlers/messages/send_messages_handler.rs | 59 +-
.../partitions/create_partitions_handler.rs | 127 +-
.../partitions/delete_partitions_handler.rs | 126 +-
.../create_personal_access_token_handler.rs | 109 +-
.../delete_personal_access_token_handler.rs | 85 +-
.../login_with_personal_access_token_handler.rs | 1 -
.../handlers/segments/delete_segments_handler.rs | 49 +-
.../handlers/streams/create_stream_handler.rs | 44 +-
.../handlers/streams/delete_stream_handler.rs | 29 +-
.../binary/handlers/streams/get_stream_handler.rs | 31 +-
.../binary/handlers/streams/get_streams_handler.rs | 13 +-
.../handlers/streams/purge_stream_handler.rs | 88 +-
.../handlers/streams/update_stream_handler.rs | 82 +-
.../binary/handlers/system/get_client_handler.rs | 5 +-
.../binary/handlers/system/get_clients_handler.rs | 5 +-
.../binary/handlers/system/get_stats_handler.rs | 5 +-
.../src/binary/handlers/system/ping_handler.rs | 1 -
.../binary/handlers/topics/create_topic_handler.rs | 72 +-
.../binary/handlers/topics/delete_topic_handler.rs | 51 +-
.../binary/handlers/topics/get_topic_handler.rs | 32 +-
.../binary/handlers/topics/get_topics_handler.rs | 21 +-
.../binary/handlers/topics/purge_topic_handler.rs | 105 +-
.../binary/handlers/topics/update_topic_handler.rs | 64 +-
.../handlers/users/change_password_handler.rs | 124 +-
.../binary/handlers/users/create_user_handler.rs | 40 +-
.../binary/handlers/users/delete_user_handler.rs | 26 +-
.../src/binary/handlers/users/get_user_handler.rs | 5 +-
.../src/binary/handlers/users/get_users_handler.rs | 5 +-
.../binary/handlers/users/login_user_handler.rs | 1 -
.../binary/handlers/users/logout_user_handler.rs | 1 -
.../handlers/users/update_permissions_handler.rs | 99 +-
.../binary/handlers/users/update_user_handler.rs | 111 +-
core/server/src/binary/macros.rs | 2 +-
core/server/src/binary/mapper.rs | 152 +-
core/server/src/bootstrap.rs | 441 +++---
core/server/src/configs/sharding.rs | 11 -
core/server/src/configs/system.rs | 12 +-
core/server/src/http/consumer_groups.rs | 182 +--
core/server/src/http/consumer_offsets.rs | 42 +-
core/server/src/http/http_shard_wrapper.rs | 97 +-
core/server/src/http/mapper.rs | 383 ++---
core/server/src/http/messages.rs | 3 +-
core/server/src/http/partitions.rs | 26 +-
core/server/src/http/personal_access_tokens.rs | 37 +-
core/server/src/http/streams.rs | 151 +-
core/server/src/http/system.rs | 15 +-
core/server/src/http/topics.rs | 200 +--
core/server/src/http/users.rs | 124 +-
core/server/src/lib.rs | 1 -
core/server/src/main.rs | 108 +-
core/server/src/metadata/absorb.rs | 101 +-
core/server/src/metadata/inner.rs | 14 +-
core/server/src/metadata/mod.rs | 6 +-
core/server/src/metadata/ops.rs | 3 +-
core/server/src/metadata/partition.rs | 11 +-
core/server/src/metadata/reader.rs | 699 ++++++++-
core/server/src/metadata/writer.rs | 7 +-
core/server/src/quic/listener.rs | 2 +-
core/server/src/shard/builder.rs | 47 +-
core/server/src/shard/handlers.rs | 903 ++++++++---
core/server/src/shard/mod.rs | 125 +-
core/server/src/shard/namespace.rs | 53 -
core/server/src/shard/system/clients.rs | 52 +-
core/server/src/shard/system/consumer_groups.rs | 280 ++--
core/server/src/shard/system/consumer_offsets.rs | 343 +++--
core/server/src/shard/system/messages.rs | 570 +++++--
core/server/src/shard/system/partitions.rs | 473 +++---
.../src/shard/system/personal_access_tokens.rs | 153 +-
core/server/src/shard/system/segments.rs | 220 ++-
core/server/src/shard/system/stats.rs | 61 +-
core/server/src/shard/system/streams.rs | 320 +++-
core/server/src/shard/system/topics.rs | 460 ++++--
core/server/src/shard/system/users.rs | 252 ++-
core/server/src/shard/system/utils.rs | 159 +-
.../src/shard/tasks/periodic/heartbeat_verifier.rs | 2 +-
.../src/shard/tasks/periodic/message_cleaner.rs | 177 +--
.../src/shard/tasks/periodic/message_saver.rs | 80 +-
.../periodic/personal_access_token_cleaner.rs | 40 +-
core/server/src/shard/transmission/event.rs | 115 +-
core/server/src/shard/transmission/frame.rs | 28 +-
core/server/src/shard/transmission/message.rs | 88 +-
core/server/src/slab/consumer_groups.rs | 200 ---
core/server/src/slab/helpers.rs | 70 -
core/server/src/slab/mod.rs | 43 -
core/server/src/slab/partitions.rs | 281 ----
core/server/src/slab/streams.rs | 1612 --------------------
core/server/src/slab/topics.rs | 307 ----
core/server/src/slab/traits_ext.rs | 117 --
core/server/src/slab/users.rs | 207 ---
core/server/src/streaming/mod.rs | 1 -
.../streaming/partitions/consumer_group_offsets.rs | 51 +
.../src/streaming/partitions/consumer_offset.rs | 20 +-
.../{traits.rs => partitions/consumer_offsets.rs} | 55 +-
core/server/src/streaming/partitions/helpers.rs | 512 +------
.../src/streaming/partitions/local_partition.rs | 102 ++
.../src/streaming/partitions/local_partitions.rs | 213 +++
core/server/src/streaming/partitions/mod.rs | 6 +-
core/server/src/streaming/partitions/ops.rs | 700 +++++++++
core/server/src/streaming/partitions/partition.rs | 454 ------
.../streaming/segments/messages/messages_reader.rs | 7 +-
.../streaming/segments/messages/messages_writer.rs | 3 -
.../streaming/segments/types/messages_batch_mut.rs | 28 +-
.../streaming/segments/types/messages_batch_set.rs | 3 +-
core/server/src/streaming/stats/mod.rs | 15 +
core/server/src/streaming/streams/helpers.rs | 107 --
core/server/src/streaming/streams/mod.rs | 2 -
core/server/src/streaming/streams/storage.rs | 27 +-
core/server/src/streaming/streams/stream.rs | 225 ---
core/server/src/streaming/topics/consumer_group.rs | 243 ---
core/server/src/streaming/topics/helpers.rs | 305 +---
core/server/src/streaming/topics/mod.rs | 2 -
core/server/src/streaming/topics/storage.rs | 43 +-
core/server/src/streaming/topics/topic.rs | 379 -----
core/server/src/tcp/tcp_listener.rs | 2 +-
core/server/src/tcp/tcp_tls_listener.rs | 2 +-
core/server/src/websocket/websocket_listener.rs | 2 +-
.../server/src/websocket/websocket_tls_listener.rs | 2 +-
137 files changed, 7499 insertions(+), 9393 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 5f7fceb1a..e25a50414 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -415,15 +415,6 @@ dependencies = [
"uuid",
]
-[[package]]
-name = "arbitrary"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
-dependencies = [
- "derive_arbitrary",
-]
-
[[package]]
name = "arc-swap"
version = "1.8.0"
@@ -433,12 +424,6 @@ dependencies = [
"rustversion",
]
-[[package]]
-name = "arcshift"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809ce7e2c99d17c161c0ca1673665bab3a3e1aa277b0f793abffc66b57c32d9f"
-
[[package]]
name = "argon2"
version = "0.5.3"
@@ -729,20 +714,15 @@ dependencies = [
[[package]]
name = "async-compression"
-version = "0.4.19"
+version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c"
+checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40"
dependencies = [
- "bzip2 0.5.2",
- "flate2",
- "futures-core",
+ "compression-codecs",
+ "compression-core",
"futures-io",
- "memchr",
"pin-project-lite",
"tokio",
- "xz2",
- "zstd",
- "zstd-safe",
]
[[package]]
@@ -910,9 +890,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
-version = "1.15.3"
+version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86"
+checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -920,9 +900,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.36.0"
+version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8"
+checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1"
dependencies = [
"cc",
"cmake",
@@ -1219,7 +1199,7 @@ dependencies = [
"arrayvec",
"cc",
"cfg-if",
- "constant_time_eq 0.4.2",
+ "constant_time_eq",
"cpufeatures",
]
@@ -1471,15 +1451,6 @@ dependencies = [
"bytes",
]
-[[package]]
-name = "bzip2"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
-dependencies = [
- "bzip2-sys",
-]
-
[[package]]
name = "bzip2"
version = "0.6.1"
@@ -1489,16 +1460,6 @@ dependencies = [
"libbz2-rs-sys",
]
-[[package]]
-name = "bzip2-sys"
-version = "0.1.13+1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
-dependencies = [
- "cc",
- "pkg-config",
-]
-
[[package]]
name = "camino"
version = "1.2.2"
@@ -1534,9 +1495,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.53"
+version = "1.2.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
+checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1920,6 +1881,27 @@ dependencies = [
"tungstenite",
]
+[[package]]
+name = "compression-codecs"
+version = "0.4.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a"
+dependencies = [
+ "bzip2",
+ "compression-core",
+ "flate2",
+ "liblzma",
+ "memchr",
+ "zstd",
+ "zstd-safe",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -1989,12 +1971,6 @@ dependencies = [
"tiny-keccak",
]
-[[package]]
-name = "constant_time_eq"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
-
[[package]]
name = "constant_time_eq"
version = "0.4.2"
@@ -2077,9 +2053,9 @@ dependencies = [
[[package]]
name = "crc"
-version = "3.3.0"
+version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
@@ -2554,12 +2530,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "deflate64"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
-
[[package]]
name = "der"
version = "0.7.10"
@@ -2606,17 +2576,6 @@ dependencies = [
"syn 2.0.114",
]
-[[package]]
-name = "derive_arbitrary"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.114",
-]
-
[[package]]
name = "derive_builder"
version = "0.20.2"
@@ -3225,9 +3184,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
-version = "0.1.8"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
+checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]]
name = "flatbuffers"
@@ -5180,9 +5139,9 @@ version = "0.1.0"
[[package]]
name = "js-sys"
-version = "0.3.85"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -5428,6 +5387,26 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "liblzma"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73c36d08cad03a3fbe2c4e7bb3a9e84c57e4ee4135ed0b065cade3d98480c648"
+dependencies = [
+ "liblzma-sys",
+]
+
+[[package]]
+name = "liblzma-sys"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "libm"
version = "0.2.15"
@@ -5595,27 +5574,6 @@ dependencies = [
"twox-hash",
]
-[[package]]
-name = "lzma-rust2"
-version = "0.15.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48172246aa7c3ea28e423295dd1ca2589a24617cc4e588bb8cfe177cb2c54d95"
-dependencies = [
- "crc",
- "sha2",
-]
-
-[[package]]
-name = "lzma-sys"
-version = "0.1.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
-]
-
[[package]]
name = "macro_rules_attribute"
version = "0.1.3"
@@ -6559,16 +6517,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
-[[package]]
-name = "pbkdf2"
-version = "0.12.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
-dependencies = [
- "digest",
- "hmac",
-]
-
[[package]]
name = "pear"
version = "0.2.9"
@@ -6832,12 +6780,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-[[package]]
-name = "ppmd-rust"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4"
-
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -7832,9 +7774,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.14.0"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
dependencies = [
"web-time",
"zeroize",
@@ -7869,9 +7811,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
-version = "0.103.9"
+version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"aws-lc-rs",
"ring",
@@ -8275,7 +8217,6 @@ version = "0.6.1-edge.5"
dependencies = [
"ahash 0.8.12",
"anyhow",
- "arcshift",
"argon2",
"async-channel",
"async_zip",
@@ -10073,9 +10014,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
-version = "1.0.2+wasi-0.2.9"
+version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
@@ -10088,9 +10029,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.108"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
@@ -10101,12 +10042,11 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.58"
+version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if",
- "futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -10115,9 +10055,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.108"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -10125,9 +10065,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.108"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -10138,9 +10078,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.108"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
@@ -10174,9 +10114,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.85"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -10732,9 +10672,9 @@ dependencies = [
[[package]]
name = "wit-bindgen"
-version = "0.51.0"
+version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
@@ -10779,15 +10719,6 @@ dependencies = [
"rustix",
]
-[[package]]
-name = "xz2"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
-dependencies = [
- "lzma-sys",
-]
-
[[package]]
name = "yansi"
version = "1.0.1"
@@ -10992,30 +10923,15 @@ dependencies = [
[[package]]
name = "zip"
-version = "7.0.0"
+version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdd8a47718a4ee5fe78e07667cd36f3de80e7c2bfe727c7074245ffc7303c037"
+checksum = "9013f1222db8a6d680f13a7ccdc60a781199cd09c2fa4eff58e728bb181757fc"
dependencies = [
- "aes",
- "arbitrary",
- "bzip2 0.6.1",
- "constant_time_eq 0.3.1",
"crc32fast",
- "deflate64",
"flate2",
- "generic-array",
- "getrandom 0.3.4",
- "hmac",
"indexmap 2.13.0",
- "lzma-rust2",
"memchr",
- "pbkdf2",
- "ppmd-rust",
- "sha1",
- "time",
- "zeroize",
"zopfli",
- "zstd",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 802dde2d8..1a3cbdb4f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -178,6 +178,7 @@ serde_yaml_ng = "0.10.0"
serial_test = "3.3.1"
server = { path = "core/server" }
simd-json = { version = "0.17.0", features = ["serde_impl"] }
+slab = "0.4.1"
strum = { version = "0.27.2", features = ["derive"] }
strum_macros = "0.27.2"
sysinfo = "0.37.2"
@@ -212,7 +213,7 @@ uuid = { version = "1.19.0", features = [
"zerocopy",
] }
webpki-roots = "1.0.5"
-zip = "=7.0.0"
+zip = { version = "7.1.0", default-features = false, features = ["deflate"] }
[profile.release]
lto = true
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 814a96a36..4edd99ca1 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -31,9 +31,7 @@ anstyle-query: 1.1.5, "Apache-2.0 OR MIT",
anstyle-wincon: 3.0.11, "Apache-2.0 OR MIT",
anyhow: 1.0.100, "Apache-2.0 OR MIT",
apache-avro: 0.17.0, "Apache-2.0",
-arbitrary: 1.4.2, "Apache-2.0 OR MIT",
arc-swap: 1.8.0, "Apache-2.0 OR MIT",
-arcshift: 0.4.2, "Apache-2.0 OR MIT",
argon2: 0.5.3, "Apache-2.0 OR MIT",
array-init: 2.1.0, "Apache-2.0 OR MIT",
arrayref: 0.3.9, "BSD-2-Clause",
@@ -57,7 +55,7 @@ assert_cmd: 2.1.2, "Apache-2.0 OR MIT",
astral-tokio-tar: 0.5.6, "Apache-2.0 OR MIT",
async-broadcast: 0.7.2, "Apache-2.0 OR MIT",
async-channel: 2.5.0, "Apache-2.0 OR MIT",
-async-compression: 0.4.19, "Apache-2.0 OR MIT",
+async-compression: 0.4.37, "Apache-2.0 OR MIT",
async-dropper: 0.3.1, "MIT",
async-dropper-derive: 0.3.1, "MIT",
async-dropper-simple: 0.2.6, "MIT",
@@ -74,8 +72,8 @@ atomic-polyfill: 1.0.3, "Apache-2.0 OR MIT",
atomic-waker: 1.1.2, "Apache-2.0 OR MIT",
autocfg: 1.5.0, "Apache-2.0 OR MIT",
autotools: 0.2.7, "MIT",
-aws-lc-rs: 1.15.3, "(Apache-2.0 OR ISC) AND ISC",
-aws-lc-sys: 0.36.0, "(Apache-2.0 OR ISC) AND ISC AND OpenSSL",
+aws-lc-rs: 1.15.2, "(Apache-2.0 OR ISC) AND ISC",
+aws-lc-sys: 0.35.0, "(Apache-2.0 OR ISC) AND ISC AND OpenSSL",
axum: 0.8.8, "MIT",
axum-core: 0.5.6, "MIT",
axum-macros: 0.5.0, "MIT",
@@ -122,13 +120,11 @@ bytemuck: 1.24.0, "Apache-2.0 OR MIT OR Zlib",
byteorder: 1.5.0, "MIT OR Unlicense",
bytes: 1.11.0, "MIT",
bytestring: 1.5.0, "Apache-2.0 OR MIT",
-bzip2: 0.5.2, "Apache-2.0 OR MIT",
bzip2: 0.6.1, "Apache-2.0 OR MIT",
-bzip2-sys: 0.1.13+1.0.8, "Apache-2.0 OR MIT",
camino: 1.2.2, "Apache-2.0 OR MIT",
cargo-platform: 0.3.2, "Apache-2.0 OR MIT",
cargo_metadata: 0.23.1, "MIT",
-cc: 1.2.53, "Apache-2.0 OR MIT",
+cc: 1.2.52, "Apache-2.0 OR MIT",
cesu8: 1.1.0, "Apache-2.0 OR MIT",
cfg-if: 1.0.4, "Apache-2.0 OR MIT",
cfg_aliases: 0.2.1, "MIT",
@@ -160,6 +156,8 @@ compio-quic: 0.6.0, "MIT",
compio-runtime: 0.10.1, "MIT",
compio-tls: 0.8.0, "MIT",
compio-ws: 0.2.0, "MIT",
+compression-codecs: 0.4.36, "Apache-2.0 OR MIT",
+compression-core: 0.4.31, "Apache-2.0 OR MIT",
concurrent-queue: 2.5.0, "Apache-2.0 OR MIT",
consensus: 0.1.0, "Apache-2.0",
console: 0.16.2, "MIT",
@@ -167,7 +165,6 @@ console_error_panic_hook: 0.1.7, "Apache-2.0 OR MIT",
const-oid: 0.9.6, "Apache-2.0 OR MIT",
const-random: 0.1.18, "Apache-2.0 OR MIT",
const-random-macro: 0.1.16, "Apache-2.0 OR MIT",
-constant_time_eq: 0.3.1, "Apache-2.0 OR CC0-1.0 OR MIT-0",
constant_time_eq: 0.4.2, "Apache-2.0 OR CC0-1.0 OR MIT-0",
convert_case: 0.6.0, "MIT",
convert_case: 0.10.0, "MIT",
@@ -177,7 +174,7 @@ core-foundation-sys: 0.8.7, "Apache-2.0 OR MIT",
core2: 0.4.0, "Apache-2.0 OR MIT",
core_affinity: 0.8.3, "Apache-2.0 OR MIT",
cpufeatures: 0.2.17, "Apache-2.0 OR MIT",
-crc: 3.3.0, "Apache-2.0 OR MIT",
+crc: 3.4.0, "Apache-2.0 OR MIT",
crc-catalog: 2.4.0, "Apache-2.0 OR MIT",
crc32c: 0.6.8, "Apache-2.0 OR MIT",
crc32fast: 1.5.0, "Apache-2.0 OR MIT",
@@ -219,12 +216,10 @@ dashmap: 6.1.0, "MIT",
data-encoding: 2.10.0, "MIT",
dbus: 0.9.10, "Apache-2.0 OR MIT",
dbus-secret-service: 4.1.0, "Apache-2.0 OR MIT",
-deflate64: 0.1.10, "MIT",
der: 0.7.10, "Apache-2.0 OR MIT",
der-parser: 10.0.0, "Apache-2.0 OR MIT",
deranged: 0.5.5, "Apache-2.0 OR MIT",
derive-new: 0.7.0, "MIT",
-derive_arbitrary: 1.4.2, "Apache-2.0 OR MIT",
derive_builder: 0.20.2, "Apache-2.0 OR MIT",
derive_builder_core: 0.20.2, "Apache-2.0 OR MIT",
derive_builder_macro: 0.20.2, "Apache-2.0 OR MIT",
@@ -286,7 +281,7 @@ figlet-rs: 0.1.5, "Apache-2.0",
figment: 0.10.19, "Apache-2.0 OR MIT",
file-operation: 0.8.11, "MIT",
filetime: 0.2.26, "Apache-2.0 OR MIT",
-find-msvc-tools: 0.1.8, "Apache-2.0 OR MIT",
+find-msvc-tools: 0.1.7, "Apache-2.0 OR MIT",
flatbuffers: 25.12.19, "Apache-2.0",
flate2: 1.1.8, "Apache-2.0 OR MIT",
float-cmp: 0.10.0, "MIT",
@@ -437,7 +432,7 @@ jni: 0.21.1, "Apache-2.0 OR MIT",
jni-sys: 0.3.0, "Apache-2.0 OR MIT",
jobserver: 0.1.34, "Apache-2.0 OR MIT",
journal: 0.1.0, "Apache-2.0",
-js-sys: 0.3.85, "Apache-2.0 OR MIT",
+js-sys: 0.3.83, "Apache-2.0 OR MIT",
jsonwebtoken: 10.2.0, "MIT",
jwalk: 0.8.1, "MIT",
keccak: 0.1.5, "Apache-2.0 OR MIT",
@@ -461,6 +456,8 @@ libdbus-sys: 0.2.7, "Apache-2.0 OR MIT",
libflate: 2.2.1, "MIT",
libflate_lz77: 2.2.0, "MIT",
libgit2-sys: 0.18.3+1.9.2, "Apache-2.0 OR MIT",
+liblzma: 0.4.5, "Apache-2.0 OR MIT",
+liblzma-sys: 0.4.4, "Apache-2.0 OR MIT",
libm: 0.2.15, "MIT",
libmimalloc-sys: 0.1.44, "MIT",
libredox: 0.1.12, "MIT",
@@ -480,8 +477,6 @@ logos-derive: 0.15.1, "Apache-2.0 OR MIT",
loom: 0.7.2, "MIT",
lru-slab: 0.1.2, "Apache-2.0 OR MIT OR Zlib",
lz4_flex: 0.11.5, "MIT",
-lzma-rust2: 0.15.4, "Apache-2.0",
-lzma-sys: 0.1.20, "Apache-2.0 OR MIT",
macro_rules_attribute: 0.1.3, "MIT",
macro_rules_attribute-proc_macro: 0.1.3, "MIT",
matchers: 0.2.0, "MIT",
@@ -570,7 +565,6 @@ passterm: 2.0.1, "BSD-3-Clause",
password-hash: 0.5.0, "Apache-2.0 OR MIT",
paste: 1.0.15, "Apache-2.0 OR MIT",
pastey: 0.2.1, "Apache-2.0 OR MIT",
-pbkdf2: 0.12.2, "Apache-2.0 OR MIT",
pear: 0.2.9, "Apache-2.0 OR MIT",
pear_codegen: 0.2.9, "Apache-2.0 OR MIT",
peg: 0.6.3, "MIT",
@@ -599,7 +593,6 @@ portable-atomic-util: 0.2.4, "Apache-2.0 OR MIT",
postcard: 1.1.3, "Apache-2.0 OR MIT",
potential_utf: 0.1.4, "Unicode-3.0",
powerfmt: 0.2.0, "Apache-2.0 OR MIT",
-ppmd-rust: 1.3.0, "CC0-1.0",
ppv-lite86: 0.2.21, "Apache-2.0 OR MIT",
predicates: 3.1.3, "Apache-2.0 OR MIT",
predicates-core: 1.0.9, "Apache-2.0 OR MIT",
@@ -684,10 +677,10 @@ rustix: 1.1.3, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
rustls: 0.23.36, "Apache-2.0 OR ISC OR MIT",
rustls-native-certs: 0.8.3, "Apache-2.0 OR ISC OR MIT",
rustls-pemfile: 2.2.0, "Apache-2.0 OR ISC OR MIT",
-rustls-pki-types: 1.14.0, "Apache-2.0 OR MIT",
+rustls-pki-types: 1.13.2, "Apache-2.0 OR MIT",
rustls-platform-verifier: 0.6.2, "Apache-2.0 OR MIT",
rustls-platform-verifier-android: 0.1.1, "Apache-2.0 OR MIT",
-rustls-webpki: 0.103.9, "ISC",
+rustls-webpki: 0.103.8, "ISC",
rustversion: 1.0.22, "Apache-2.0 OR MIT",
ryu: 1.0.22, "Apache-2.0 OR BSL-1.0",
same-file: 1.0.6, "MIT OR Unlicense",
@@ -884,16 +877,16 @@ wait-timeout: 0.2.1, "Apache-2.0 OR MIT",
walkdir: 2.5.0, "MIT OR Unlicense",
want: 0.3.1, "MIT",
wasi: 0.11.1+wasi-snapshot-preview1, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
-wasip2: 1.0.2+wasi-0.2.9, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
+wasip2: 1.0.1+wasi-0.2.4, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wasite: 0.1.0, "Apache-2.0 OR BSL-1.0 OR MIT",
-wasm-bindgen: 0.2.108, "Apache-2.0 OR MIT",
-wasm-bindgen-futures: 0.4.58, "Apache-2.0 OR MIT",
-wasm-bindgen-macro: 0.2.108, "Apache-2.0 OR MIT",
-wasm-bindgen-macro-support: 0.2.108, "Apache-2.0 OR MIT",
-wasm-bindgen-shared: 0.2.108, "Apache-2.0 OR MIT",
+wasm-bindgen: 0.2.106, "Apache-2.0 OR MIT",
+wasm-bindgen-futures: 0.4.56, "Apache-2.0 OR MIT",
+wasm-bindgen-macro: 0.2.106, "Apache-2.0 OR MIT",
+wasm-bindgen-macro-support: 0.2.106, "Apache-2.0 OR MIT",
+wasm-bindgen-shared: 0.2.106, "Apache-2.0 OR MIT",
wasm-streams: 0.4.2, "Apache-2.0 OR MIT",
wasmtimer: 0.4.3, "MIT",
-web-sys: 0.3.85, "Apache-2.0 OR MIT",
+web-sys: 0.3.83, "Apache-2.0 OR MIT",
web-time: 1.1.0, "Apache-2.0 OR MIT",
webpki-root-certs: 1.0.5, "CDLA-Permissive-2.0",
webpki-roots: 0.26.11, "CDLA-Permissive-2.0",
@@ -961,12 +954,11 @@ windows_x86_64_msvc: 0.52.6, "Apache-2.0 OR MIT",
windows_x86_64_msvc: 0.53.1, "Apache-2.0 OR MIT",
winnow: 0.5.40, "MIT",
winnow: 0.7.14, "MIT",
-wit-bindgen: 0.51.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
+wit-bindgen: 0.46.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
writeable: 0.6.2, "Unicode-3.0",
wyz: 0.5.1, "MIT",
x509-parser: 0.18.0, "Apache-2.0 OR MIT",
xattr: 1.6.1, "Apache-2.0 OR MIT",
-xz2: 0.1.7, "Apache-2.0 OR MIT",
yansi: 1.0.1, "Apache-2.0 OR MIT",
yasna: 0.5.2, "Apache-2.0 OR MIT",
yew: 0.22.0, "Apache-2.0 OR MIT",
@@ -984,7 +976,7 @@ zeroize_derive: 1.4.3, "Apache-2.0 OR MIT",
zerotrie: 0.2.3, "Unicode-3.0",
zerovec: 0.11.5, "Unicode-3.0",
zerovec-derive: 0.11.2, "Unicode-3.0",
-zip: 7.0.0, "MIT",
+zip: 7.1.0, "MIT",
zlib-rs: 0.5.5, "Zlib",
zmij: 1.0.14, "MIT",
zopfli: 0.8.3, "Apache-2.0",
diff --git a/core/integration/tests/data_integrity/verify_after_server_restart.rs b/core/integration/tests/data_integrity/verify_after_server_restart.rs
index f632abd92..ac63d6e71 100644
--- a/core/integration/tests/data_integrity/verify_after_server_restart.rs
+++ b/core/integration/tests/data_integrity/verify_after_server_restart.rs
@@ -326,6 +326,10 @@ async fn should_fill_data_and_verify_after_restart(cache_setting: &'static str)
/// - Topics per stream: 0, 2 (1 deleted)
/// - Partitions per topic: 0, 2 (1 deleted)
/// - Consumer groups per topic: 0, 2 (1 deleted)
+///
+/// Also tests ID reuse: after deletion, creating new resources should reuse the freed IDs.
+/// This catches the bug where `streams_count()`/`topics_count()` (slab length) was used
+/// instead of `vacant_key()` to predict assigned IDs, causing mismatch when IDs are reused.
#[tokio::test]
#[parallel]
async fn should_handle_resource_deletion_and_restart() {
@@ -424,11 +428,55 @@ async fn should_handle_resource_deletion_and_restart() {
.await
.unwrap();
+ // TEST ID REUSE: Create a new stream after deletion - it should reuse ID 1
+ // This is the critical test that catches the bug where streams_count() (slab.len())
+ // was used instead of vacant_key() to predict the assigned ID.
+ // After deletion: streams are [0, 2], len=2, but vacant_key=1
+ // Bug: code used len (2) to create directory, but metadata assigned ID 1
+ let stream_reused = client.create_stream("stream-reused").await.unwrap();
+ assert_eq!(
+ stream_reused.id, 1,
+ "New stream should reuse deleted ID 1, got {}",
+ stream_reused.id
+ );
+
+ // Also test topic ID reuse within a stream
+ // Delete topic 0 in stream 0, then create new topic - should reuse ID 0... wait, topic 1 was already deleted
+ // Actually let's delete topic 0 and create new one, it should get ID 0 or 1 depending on vacant_key
+ let stream_0_ident = Identifier::numeric(0).unwrap();
+ client
+ .delete_topic(&stream_0_ident, &Identifier::numeric(0).unwrap())
+ .await
+ .unwrap();
+
+ // Now stream 0 has only topic 2. Creating new topic should reuse ID 0 (first vacant slot)
+ let topic_reused = client
+ .create_topic(
+ &stream_0_ident,
+ "topic-reused",
+ 1,
+ CompressionAlgorithm::None,
+ None,
+ IggyExpiry::NeverExpire,
+ MaxTopicSize::Unlimited,
+ )
+ .await
+ .unwrap();
+ assert_eq!(
+ topic_reused.id, 0,
+ "New topic should reuse deleted ID 0, got {}",
+ topic_reused.id
+ );
+
// Verify state before restart
let streams = client.get_streams().await.unwrap();
- assert_eq!(streams.len(), 2);
+ assert_eq!(streams.len(), 3); // 0, 1 (reused), 2
let stream_ids: Vec<u32> = streams.iter().map(|s| s.id).collect();
- assert!(stream_ids.contains(&0) && stream_ids.contains(&2));
+ assert!(
+ stream_ids.contains(&0) && stream_ids.contains(&1) && stream_ids.contains(&2),
+ "Expected streams 0, 1, 2 after ID reuse, got: {:?}",
+ stream_ids
+ );
drop(client);
test_server.stop();
@@ -451,72 +499,117 @@ async fn should_handle_resource_deletion_and_restart() {
);
login_root(&client).await;
- // Verify streams after restart
+ // Verify streams after restart - should have 3 streams: 0, 1 (reused), 2
let streams = client.get_streams().await.unwrap();
- assert_eq!(streams.len(), 2, "Expected 2 streams after restart");
+ assert_eq!(streams.len(), 3, "Expected 3 streams after restart");
let stream_ids: Vec<u32> = streams.iter().map(|s| s.id).collect();
assert!(
- stream_ids.contains(&0) && stream_ids.contains(&2),
- "Expected streams 0 and 2, got: {:?}",
+ stream_ids.contains(&0) && stream_ids.contains(&1) && stream_ids.contains(&2),
+ "Expected streams 0, 1, 2 after restart, got: {:?}",
stream_ids
);
- // Verify topics, partitions, and consumer groups for each stream
- for stream_id in [0u32, 2u32] {
- let stream_ident = Identifier::numeric(stream_id).unwrap();
- let stream = client.get_stream(&stream_ident).await.unwrap().unwrap();
+ // Verify the reused stream 1 has the correct name
+ let stream_1 = client
+ .get_stream(&Identifier::numeric(1).unwrap())
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ stream_1.name, "stream-reused",
+ "Stream 1 should be the reused stream"
+ );
+
+ // Verify stream 0 topics after restart
+ // Stream 0 should have: topic 0 (reused, "topic-reused") and topic 2
+ let stream_0_ident = Identifier::numeric(0).unwrap();
+ let topics_stream_0 = client.get_topics(&stream_0_ident).await.unwrap();
+ let topic_ids_0: Vec<u32> = topics_stream_0.iter().map(|t| t.id).collect();
+ assert!(
+ topic_ids_0.contains(&0) && topic_ids_0.contains(&2),
+ "Stream 0 should have topics 0 and 2, got: {:?}",
+ topic_ids_0
+ );
+
+ // Verify the reused topic has correct name
+ let topic_0 = client
+ .get_topic(&stream_0_ident, &Identifier::numeric(0).unwrap())
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ topic_0.name, "topic-reused",
+ "Topic 0 in stream 0 should be the reused topic"
+ );
+
+ // Verify topic 2 in stream 0 still has correct structure
+ let topic_2_ident = Identifier::numeric(2).unwrap();
+ let topic_2 = client
+ .get_topic(&stream_0_ident, &topic_2_ident)
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ topic_2.partitions_count, 2,
+ "Topic 2 in stream 0 should have 2 partitions"
+ );
+ let cgs_topic_2 = client
+ .get_consumer_groups(&stream_0_ident, &topic_2_ident)
+ .await
+ .unwrap();
+ assert_eq!(
+ cgs_topic_2.len(),
+ 2,
+ "Topic 2 in stream 0 should have 2 consumer groups"
+ );
+ let cg_ids: Vec<u32> = cgs_topic_2.iter().map(|c| c.id).collect();
+ assert!(
+ cg_ids.contains(&0) && cg_ids.contains(&2),
+ "Topic 2 in stream 0 should have consumer groups 0 and 2, got: {:?}",
+ cg_ids
+ );
+
+ // Verify stream 2 structure (unchanged from original test)
+ let stream_2_ident = Identifier::numeric(2).unwrap();
+ let topics_stream_2 = client.get_topics(&stream_2_ident).await.unwrap();
+ let topic_ids_2: Vec<u32> = topics_stream_2.iter().map(|t| t.id).collect();
+ assert!(
+ topic_ids_2.contains(&0) && topic_ids_2.contains(&2),
+ "Stream 2 should have topics 0 and 2, got: {:?}",
+ topic_ids_2
+ );
+
+ for topic_id in [0u32, 2u32] {
+ let topic_ident = Identifier::numeric(topic_id).unwrap();
+ let topic = client
+ .get_topic(&stream_2_ident, &topic_ident)
+ .await
+ .unwrap()
+ .unwrap();
assert_eq!(
- stream.topics_count, 2,
- "Stream {} should have 2 topics",
- stream_id
+ topic.partitions_count, 2,
+ "Topic {} in stream 2 should have 2 partitions",
+ topic_id
);
- // Verify topics have correct IDs (0 and 2, not 0 and 1)
- let topics = client.get_topics(&stream_ident).await.unwrap();
- let topic_ids: Vec<u32> = topics.iter().map(|t| t.id).collect();
+ let cgs = client
+ .get_consumer_groups(&stream_2_ident, &topic_ident)
+ .await
+ .unwrap();
+ assert_eq!(
+ cgs.len(),
+ 2,
+ "Topic {} in stream 2 should have 2 consumer groups",
+ topic_id
+ );
+ let cg_ids: Vec<u32> = cgs.iter().map(|c| c.id).collect();
assert!(
- topic_ids.contains(&0) && topic_ids.contains(&2),
- "Stream {} should have topics 0 and 2, got: {:?}",
- stream_id,
- topic_ids
+ cg_ids.contains(&0) && cg_ids.contains(&2),
+ "Topic {} in stream 2 should have consumer groups 0 and 2, got: {:?}",
+ topic_id,
+ cg_ids
);
-
- for topic_id in [0u32, 2u32] {
- let topic_ident = Identifier::numeric(topic_id).unwrap();
- let topic = client
- .get_topic(&stream_ident, &topic_ident)
- .await
- .unwrap()
- .unwrap();
-
- assert_eq!(
- topic.partitions_count, 2,
- "Topic {} in stream {} should have 2 partitions",
- topic_id, stream_id
- );
-
- // Verify consumer groups have correct IDs (0 and 2)
- let cgs = client
- .get_consumer_groups(&stream_ident, &topic_ident)
- .await
- .unwrap();
- assert_eq!(
- cgs.len(),
- 2,
- "Topic {} in stream {} should have 2 consumer groups",
- topic_id,
- stream_id
- );
- let cg_ids: Vec<u32> = cgs.iter().map(|c| c.id).collect();
- assert!(
- cg_ids.contains(&0) && cg_ids.contains(&2),
- "Topic {} in stream {} should have consumer groups 0 and 2, got: {:?}",
- topic_id,
- stream_id,
- cg_ids
- );
- }
}
drop(client);
diff --git a/core/integration/tests/server/scenarios/concurrent_scenario.rs b/core/integration/tests/server/scenarios/concurrent_scenario.rs
index 535d6ac76..5140b42b6 100644
--- a/core/integration/tests/server/scenarios/concurrent_scenario.rs
+++ b/core/integration/tests/server/scenarios/concurrent_scenario.rs
@@ -29,15 +29,17 @@ const MULTIPLE_CLIENT_COUNT: usize = 10;
const OPERATIONS_PER_CLIENT: usize = OPERATIONS_COUNT / MULTIPLE_CLIENT_COUNT;
const USER_PASSWORD: &str = "secret";
const TEST_STREAM_NAME: &str = "race-test-stream";
+const TEST_TOPIC_NAME: &str = "race-test-topic";
const PARTITIONS_COUNT: u32 = 1;
+const PARTITIONS_TO_ADD: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceType {
User,
Stream,
Topic,
+ Partition,
// TODO(hubcio): add ConsumerGroup
- // TODO(hubcio): add Partition
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -70,10 +72,31 @@ pub async fn run(
root_client.create_stream(TEST_STREAM_NAME).await.unwrap();
}
+ // For partition tests, create parent stream and topic first
+ if resource_type == ResourceType::Partition {
+ root_client.create_stream(TEST_STREAM_NAME).await.unwrap();
+ let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ root_client
+ .create_topic(
+ &stream_id,
+ TEST_TOPIC_NAME,
+ PARTITIONS_COUNT,
+ CompressionAlgorithm::default(),
+ None,
+ IggyExpiry::NeverExpire,
+ MaxTopicSize::ServerDefault,
+ )
+ .await
+ .unwrap();
+ }
+
let results = match (resource_type, scenario_type) {
(ResourceType::User, ScenarioType::Cold) => {
execute_multiple_clients_users_cold(client_factory, use_barrier).await
}
+ (ResourceType::User, ScenarioType::Hot) => {
+ execute_multiple_clients_users_hot(client_factory, use_barrier).await
+ }
(ResourceType::Stream, ScenarioType::Hot) => {
execute_multiple_clients_streams_hot(client_factory, use_barrier).await
}
@@ -86,13 +109,11 @@ pub async fn run(
(ResourceType::Topic, ScenarioType::Cold) => {
execute_multiple_clients_topics_cold(client_factory, use_barrier).await
}
- _ => vec![],
- // TODO: Figure out why those tests timeout in CI.
- /*
- (ResourceType::User, ScenarioType::Hot) => {
- execute_multiple_clients_users_hot(client_factory, use_barrier).await
+ (ResourceType::Partition, ScenarioType::Hot) => {
+ execute_multiple_clients_partitions_hot(client_factory, use_barrier).await
}
- */
+ // Partitions don't have names, so Cold scenario doesn't apply
+ (ResourceType::Partition, ScenarioType::Cold) => vec![],
};
if !results.is_empty() {
@@ -102,7 +123,7 @@ pub async fn run(
}
}
-async fn _execute_multiple_clients_users_hot(
+async fn execute_multiple_clients_users_hot(
client_factory: &dyn ClientFactory,
use_barrier: bool,
) -> Vec<OperationResult> {
@@ -374,6 +395,51 @@ async fn execute_multiple_clients_topics_cold(
.collect()
}
+async fn execute_multiple_clients_partitions_hot(
+ client_factory: &dyn ClientFactory,
+ use_barrier: bool,
+) -> Vec<OperationResult> {
+ let mut handles = Vec::with_capacity(MULTIPLE_CLIENT_COUNT);
+ let barrier = if use_barrier {
+ Some(Arc::new(Barrier::new(MULTIPLE_CLIENT_COUNT)))
+ } else {
+ None
+ };
+
+ for _ in 0..MULTIPLE_CLIENT_COUNT {
+ let client = create_client(client_factory).await;
+ login_root(&client).await;
+
+ let barrier_clone = barrier.clone();
+ let handle = tokio::spawn(async move {
+ if let Some(b) = barrier_clone {
+ b.wait().await;
+ }
+
+ let mut results = Vec::with_capacity(OPERATIONS_PER_CLIENT);
+ let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ let topic_id = Identifier::named(TEST_TOPIC_NAME).unwrap();
+
+ for _ in 0..OPERATIONS_PER_CLIENT {
+ let result = client
+ .create_partitions(&stream_id, &topic_id, PARTITIONS_TO_ADD)
+ .await
+ .map(|_| ());
+ results.push(result);
+ }
+ results
+ });
+
+ handles.push(handle);
+ }
+
+ let all_results = join_all(handles).await;
+ all_results
+ .into_iter()
+ .flat_map(|r| r.expect("Tokio task panicked"))
+ .collect()
+}
+
fn validate_results(results: &[OperationResult], scenario_type: ScenarioType) {
let success_count = results.iter().filter(|r| r.is_ok()).count();
let error_count = results.iter().filter(|r| r.is_err()).count();
@@ -542,6 +608,22 @@ async fn validate_server_state(
);
}
}
+ ResourceType::Partition => {
+ let mut partition_counts = Vec::new();
+ for client in &clients {
+ let count = validate_partitions_state(client).await;
+ partition_counts.push(count);
+ }
+
+ let first_count = partition_counts[0];
+ for (i, count) in partition_counts.iter().enumerate().skip(1) {
+ assert_eq!(
+ *count, first_count,
+ "Client {} sees different partition count ({}) than client 0 ({})",
+ i, count, first_count
+ );
+ }
+ }
}
}
@@ -735,6 +817,26 @@ async fn validate_topics_state(client: &IggyClient, scenario_type: ScenarioType)
test_topics
}
+async fn validate_partitions_state(client: &IggyClient) -> u32 {
+ let stream_id = Identifier::named(TEST_STREAM_NAME).unwrap();
+ let topic_id = Identifier::named(TEST_TOPIC_NAME).unwrap();
+ let topic = client
+ .get_topic(&stream_id, &topic_id)
+ .await
+ .expect("Failed to get test topic")
+ .expect("Test topic not found");
+
+ // Expected: initial partition + OPERATIONS_COUNT added partitions
+ let expected_partitions = PARTITIONS_COUNT + (OPERATIONS_COUNT as u32 * PARTITIONS_TO_ADD);
+ assert_eq!(
+ topic.partitions_count, expected_partitions,
+ "Hot path: Expected {} partitions (1 initial + {} added), but found {}",
+ expected_partitions, OPERATIONS_COUNT, topic.partitions_count
+ );
+
+ topic.partitions_count
+}
+
async fn cleanup_resources(client: &IggyClient, resource_type: ResourceType) {
match resource_type {
ResourceType::User => {
@@ -755,8 +857,8 @@ async fn cleanup_resources(client: &IggyClient, resource_type: ResourceType) {
.await;
}
}
- // Just delete test stream
- ResourceType::Topic => {
+ // Just delete test stream (also cleans up topics and partitions)
+ ResourceType::Topic | ResourceType::Partition => {
let _ = client
.delete_stream(&Identifier::named(TEST_STREAM_NAME).unwrap())
.await;
diff --git a/core/integration/tests/server/scenarios/segment_rotation_race_scenario.rs b/core/integration/tests/server/scenarios/segment_rotation_race_scenario.rs
index 9856777b2..de6274402 100644
--- a/core/integration/tests/server/scenarios/segment_rotation_race_scenario.rs
+++ b/core/integration/tests/server/scenarios/segment_rotation_race_scenario.rs
@@ -177,7 +177,7 @@ async fn run_producer(
batch_num += 1;
}
Err(e) => {
- eprintln!("Producer {} send error: {}", producer_name, e);
+ panic!("Producer {} send error: {}", producer_name, e);
}
}
}
diff --git a/core/metadata/Cargo.toml b/core/metadata/Cargo.toml
index 76a2f64ea..6dda4bdeb 100644
--- a/core/metadata/Cargo.toml
+++ b/core/metadata/Cargo.toml
@@ -35,5 +35,5 @@ journal = { path = "../journal" }
left-right = { workspace = true }
message_bus = { path = "../message_bus" }
paste = "1.0"
-slab = "0.4.11"
+slab = { workspace = true }
tracing = { workspace = true }
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index 3ba2cecb5..d9bd6ac46 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -40,7 +40,6 @@ iggy-web = ["dep:rust-embed", "dep:mime_guess"]
[dependencies]
ahash = { workspace = true }
anyhow = { workspace = true }
-arcshift = "0.4.2"
argon2 = { workspace = true }
async-channel = { workspace = true }
async_zip = { workspace = true }
@@ -108,7 +107,7 @@ rustls-pemfile = "2.2.0"
send_wrapper = "0.6.0"
serde = { workspace = true }
serde_with = { workspace = true }
-slab = "0.4.11"
+slab = { workspace = true }
socket2 = "0.6.1"
static-toml = "1.3.0"
strum = { workspace = true }
diff --git a/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs b/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs
index 395ed81f2..aa29f8512 100644
--- a/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs
+++ b/core/server/src/binary/handlers/cluster/get_cluster_metadata_handler.rs
@@ -22,7 +22,6 @@ use crate::binary::command::{
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::get_cluster_metadata::GetClusterMetadata;
use iggy_common::{BytesSerializable, IggyError, SenderKind};
use std::rc::Rc;
diff --git a/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs
index a56c80551..7eef62841 100644
--- a/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/create_consumer_group_handler.rs
@@ -23,12 +23,13 @@ use crate::binary::handlers::consumer_groups::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
-use crate::slab::traits_ext::EntityMarker;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::state::models::CreateConsumerGroupWithId;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::create_consumer_group::CreateConsumerGroup;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -51,45 +52,93 @@ impl ServerCommandHandler for CreateConsumerGroup {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().create_consumer_group(
- session.get_user_id(),
- stream_id,
- topic_id,
- )?;
- let cg = shard.create_consumer_group(&self.stream_id, &self.topic_id, self.name.clone())?;
- let cg_id = cg.id();
+ shard
+ .metadata
+ .perm_create_consumer_group(session.get_user_id(), stream_id, topic_id)?;
- let event = ShardEvent::CreatedConsumerGroup {
+ let request = ShardRequest {
stream_id: self.stream_id.clone(),
topic_id: self.topic_id.clone(),
- cg,
+ partition_id: 0,
+ payload: ShardRequestPayload::CreateConsumerGroup {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ name: self.name.clone(),
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- let stream_id = self.stream_id.clone();
- let topic_id = self.topic_id.clone();
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::CreateConsumerGroup(CreateConsumerGroupWithId {
- group_id: cg_id as u32,
- command: self
- }),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply create consumer group for stream_id: {stream_id}, topic_id: {topic_id}, group_id: {cg_id}, session: {session}"
- )
- })?;
- let response = shard.streams.with_consumer_group_by_id(
- &stream_id,
- &topic_id,
- &Identifier::numeric(cg_id as u32).unwrap(),
- |(root, members)| mapper::map_consumer_group(root, members),
- );
- sender.send_ok_response(&response).await?;
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::CreateConsumerGroup {
+ stream_id: recoil_stream_id,
+ topic_id: recoil_topic_id,
+ name,
+ ..
+ } = payload
+ {
+ let cg_id =
+ shard.create_consumer_group(&recoil_stream_id, &recoil_topic_id, name)?;
+
+ let stream_id = self.stream_id.clone();
+ let topic_id = self.topic_id.clone();
+ shard
+ .state
+ .apply(
+ session.get_user_id(),
+ &EntryCommand::CreateConsumerGroup(CreateConsumerGroupWithId {
+ group_id: cg_id as u32,
+ command: self,
+ }),
+ )
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply create consumer group for stream_id: {stream_id}, topic_id: {topic_id}, group_id: {cg_id}, session: {session}"
+ )
+ })?;
+
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+
+ let cg_identifier = Identifier::numeric(cg_id as u32).unwrap();
+ let response = shard
+ .metadata
+ .get_consumer_group(numeric_stream_id, numeric_topic_id, cg_id)
+ .map(|cg| mapper::map_consumer_group_from_meta(&cg))
+ .ok_or_else(|| {
+ IggyError::ConsumerGroupIdNotFound(cg_identifier, topic_id.clone())
+ })?;
+ sender.send_ok_response(&response).await?;
+ } else {
+ unreachable!(
+ "Expected a CreateConsumerGroup request inside of CreateConsumerGroup handler"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::CreateConsumerGroupResponse(cg_id) => {
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
+
+ let cg_identifier = Identifier::numeric(cg_id as u32).unwrap();
+ let response = shard
+ .metadata
+ .get_consumer_group(numeric_stream_id, numeric_topic_id, cg_id)
+ .map(|cg| mapper::map_consumer_group_from_meta(&cg))
+ .ok_or_else(|| {
+ IggyError::ConsumerGroupIdNotFound(cg_identifier, self.topic_id.clone())
+ })?;
+ sender.send_ok_response(&response).await?;
+ }
+ ShardResponse::ErrorResponse(err) => return Err(err),
+ _ => unreachable!(
+ "Expected a CreateConsumerGroupResponse inside of CreateConsumerGroup handler"
+ ),
+ },
+ };
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs
index 6e372719d..8bc03be43 100644
--- a/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/delete_consumer_group_handler.rs
@@ -21,14 +21,15 @@ use crate::binary::command::{
};
use crate::binary::handlers::consumer_groups::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
+use crate::metadata::ConsumerGroupMeta;
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
-use crate::slab::traits_ext::EntityMarker;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::polling_consumer::ConsumerGroupId;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_consumer_group::DeleteConsumerGroup;
use iggy_common::{IggyError, SenderKind};
@@ -50,31 +51,71 @@ impl ServerCommandHandler for DeleteConsumerGroup {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- let (stream_id_numeric, topic_id_numeric) =
+ let (numeric_stream_id, numeric_topic_id) =
shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().delete_consumer_group(
+ shard.metadata.perm_delete_consumer_group(
session.get_user_id(),
- stream_id_numeric,
- topic_id_numeric,
+ numeric_stream_id,
+ numeric_topic_id,
)?;
- shard.ensure_consumer_group_exists(&self.stream_id, &self.topic_id, &self.group_id)?;
- let cg = shard.delete_consumer_group(&self.stream_id, &self.topic_id, &self.group_id).error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to delete consumer group with ID: {} for topic with ID: {} in stream with ID: {} for session: {}",
- self.group_id, self.topic_id, self.stream_id, session
- )
- })?;
- let cg_id = cg.id();
- let partition_ids = cg.partitions();
+ let request = ShardRequest {
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ partition_id: 0,
+ payload: ShardRequestPayload::DeleteConsumerGroup {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ group_id: self.group_id.clone(),
+ },
+ };
- // Get members from the deleted consumer group and make them leave
- let slab = cg.members().inner().shared_get();
- for (_, member) in slab.iter() {
+ let message = ShardMessage::Request(request);
+ let cg_meta: ConsumerGroupMeta = match shard
+ .send_request_to_shard_or_recoil(None, message)
+ .await?
+ {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::DeleteConsumerGroup {
+ stream_id,
+ topic_id,
+ group_id,
+ ..
+ } = payload
+ {
+ shard.delete_consumer_group(&stream_id, &topic_id, &group_id).error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to delete consumer group with ID: {} for topic with ID: {} in stream with ID: {} for session: {}",
+ group_id, topic_id, stream_id, session
+ )
+ })?
+ } else {
+ unreachable!(
+ "Expected a DeleteConsumerGroup request inside of DeleteConsumerGroup handler"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::DeleteConsumerGroupResponse => {
+ sender.send_empty_ok_response().await?;
+ return Ok(HandlerResult::Finished);
+ }
+ ShardResponse::ErrorResponse(err) => return Err(err),
+ _ => unreachable!(
+ "Expected a DeleteConsumerGroupResponse inside of DeleteConsumerGroup handler"
+ ),
+ },
+ };
+
+ let cg_id = cg_meta.id;
+
+ for (_, member) in cg_meta.members.iter() {
if let Err(err) = shard.client_manager.leave_consumer_group(
member.client_id,
- stream_id_numeric,
- topic_id_numeric,
+ numeric_stream_id,
+ numeric_topic_id,
cg_id,
) {
tracing::warn!(
@@ -86,12 +127,11 @@ impl ServerCommandHandler for DeleteConsumerGroup {
}
let cg_id_spez = ConsumerGroupId(cg_id);
- // Delete all consumer group offsets for this group using the specialized method
shard.delete_consumer_group_offsets(
cg_id_spez,
&self.stream_id,
&self.topic_id,
- partition_ids,
+ &cg_meta.partitions,
).await.error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - failed to delete consumer group offsets for group ID: {} in stream: {}, topic: {}",
@@ -101,13 +141,6 @@ impl ServerCommandHandler for DeleteConsumerGroup {
)
})?;
- let event = ShardEvent::DeletedConsumerGroup {
- id: cg_id,
- stream_id: self.stream_id.clone(),
- topic_id: self.topic_id.clone(),
- group_id: self.group_id.clone(),
- };
- shard.broadcast_event_to_all_shards(event).await?;
let stream_id = self.stream_id.clone();
let topic_id = self.topic_id.clone();
shard
diff --git a/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs
index 0f684368f..ac716aabc 100644
--- a/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/get_consumer_group_handler.rs
@@ -23,7 +23,6 @@ use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::IggyError;
use iggy_common::SenderKind;
use iggy_common::get_consumer_group::GetConsumerGroup;
@@ -44,33 +43,28 @@ impl ServerCommandHandler for GetConsumerGroup {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- let Ok((stream_id, topic_id)) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)
+ let Ok((stream_id, topic_id, numeric_group_id)) =
+ shard.resolve_consumer_group_id(&self.stream_id, &self.topic_id, &self.group_id)
else {
sender.send_empty_ok_response().await?;
return Ok(HandlerResult::Finished);
};
if shard
- .ensure_consumer_group_exists(&self.stream_id, &self.topic_id, &self.group_id)
+ .metadata
+ .perm_get_consumer_group(session.get_user_id(), stream_id, topic_id)
.is_err()
{
sender.send_empty_ok_response().await?;
return Ok(HandlerResult::Finished);
}
- if shard
- .permissioner
- .borrow()
- .get_consumer_group(session.get_user_id(), stream_id, topic_id)
- .is_err()
- {
- sender.send_empty_ok_response().await?;
- return Ok(HandlerResult::Finished);
- }
- let consumer_group = shard.streams.with_consumer_group_by_id(
- &self.stream_id,
- &self.topic_id,
- &self.group_id,
- |(root, members)| mapper::map_consumer_group(root, members),
- );
+
+ let consumer_group = shard
+ .metadata
+ .get_consumer_group(stream_id, topic_id, numeric_group_id)
+ .map(|cg| mapper::map_consumer_group_from_meta(&cg))
+ .ok_or_else(|| {
+ IggyError::ConsumerGroupIdNotFound(self.group_id.clone(), self.topic_id.clone())
+ })?;
sender.send_ok_response(&consumer_group).await?;
Ok(HandlerResult::Finished)
}
diff --git a/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs b/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs
index 8b3cab821..16b606092 100644
--- a/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/get_consumer_groups_handler.rs
@@ -20,14 +20,11 @@ use crate::binary::command::{
BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
};
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
use crate::shard::IggyShard;
-use crate::slab::traits_ext::{EntityComponentSystem, IntoComponents};
use crate::streaming::session::Session;
-use anyhow::Result;
-use iggy_common::IggyError;
-use iggy_common::SenderKind;
+use bytes::{BufMut, BytesMut};
use iggy_common::get_consumer_groups::GetConsumerGroups;
+use iggy_common::{IggyError, SenderKind};
use std::rc::Rc;
use tracing::debug;
@@ -46,21 +43,27 @@ impl ServerCommandHandler for GetConsumerGroups {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().get_consumer_groups(
- session.get_user_id(),
- stream_id,
- topic_id,
- )?;
+ shard
+ .metadata
+ .perm_get_consumer_groups(session.get_user_id(), stream_id, topic_id)?;
- let consumer_groups =
- shard
- .streams
- .with_consumer_groups(&self.stream_id, &self.topic_id, |cgs| {
- cgs.with_components(|cgs| {
- let (roots, members) = cgs.into_components();
- mapper::map_consumer_groups(roots, members)
- })
- });
+ let consumer_groups = shard.metadata.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .and_then(|s| s.topics.get(topic_id))
+ .map(|topic| {
+ let mut bytes = BytesMut::new();
+ for (_, cg_meta) in topic.consumer_groups.iter() {
+ bytes.put_u32_le(cg_meta.id as u32);
+ bytes.put_u32_le(cg_meta.partitions.len() as u32);
+ bytes.put_u32_le(cg_meta.members.len() as u32);
+ bytes.put_u8(cg_meta.name.len() as u8);
+ bytes.put_slice(cg_meta.name.as_bytes());
+ }
+ bytes.freeze()
+ })
+ .unwrap_or_default()
+ });
sender.send_ok_response(&consumer_groups).await?;
Ok(HandlerResult::Finished)
}
diff --git a/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs
index 17722303c..6a593dd4d 100644
--- a/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/join_consumer_group_handler.rs
@@ -22,8 +22,11 @@ use crate::binary::command::{
use crate::binary::handlers::consumer_groups::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::join_consumer_group::JoinConsumerGroup;
use iggy_common::{IggyError, SenderKind};
@@ -46,25 +49,58 @@ impl ServerCommandHandler for JoinConsumerGroup {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().join_consumer_group(
- session.get_user_id(),
- stream_id,
- topic_id,
- )?;
- shard.ensure_consumer_group_exists(&self.stream_id, &self.topic_id, &self.group_id)?;
shard
- .join_consumer_group(
- session.client_id,
- &self.stream_id,
- &self.topic_id,
- &self.group_id,
- )
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to join consumer group for stream_id: {}, topic_id: {}, group_id: {}, session: {}",
- self.stream_id, self.topic_id, self.group_id, session
- )
- })?;
+ .metadata
+ .perm_join_consumer_group(session.get_user_id(), stream_id, topic_id)?;
+ shard.ensure_consumer_group_exists(&self.stream_id, &self.topic_id, &self.group_id)?;
+
+ let request = ShardRequest {
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ partition_id: 0,
+ payload: ShardRequestPayload::JoinConsumerGroup {
+ user_id: session.get_user_id(),
+ client_id: session.client_id,
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ group_id: self.group_id.clone(),
+ },
+ };
+
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::JoinConsumerGroup {
+ client_id,
+ stream_id,
+ topic_id,
+ group_id,
+ ..
+ } = payload
+ {
+ shard
+ .join_consumer_group(client_id, &stream_id, &topic_id, &group_id)
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to join consumer group for stream_id: {}, topic_id: {}, group_id: {}, session: {}",
+ stream_id, topic_id, group_id, session
+ )
+ })?;
+ } else {
+ unreachable!(
+ "Expected a JoinConsumerGroup request inside of JoinConsumerGroup handler"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::JoinConsumerGroupResponse => {}
+ ShardResponse::ErrorResponse(err) => return Err(err),
+ _ => unreachable!(
+ "Expected a JoinConsumerGroupResponse inside of JoinConsumerGroup handler"
+ ),
+ },
+ }
sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
diff --git a/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs b/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs
index 4a94fd99e..12eb07b45 100644
--- a/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs
+++ b/core/server/src/binary/handlers/consumer_groups/leave_consumer_group_handler.rs
@@ -22,8 +22,11 @@ use crate::binary::command::{
};
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::IggyError;
use iggy_common::SenderKind;
@@ -47,25 +50,58 @@ impl ServerCommandHandler for LeaveConsumerGroup {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().leave_consumer_group(
- session.get_user_id(),
- stream_id,
- topic_id,
- )?;
- shard.ensure_consumer_group_exists(&self.stream_id, &self.topic_id, &self.group_id)?;
shard
- .leave_consumer_group(
- session.client_id,
- &self.stream_id,
- &self.topic_id,
- &self.group_id,
- )
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to leave consumer group for stream_id: {}, topic_id: {}, group_id: {}, session: {}",
- self.stream_id, self.topic_id, self.group_id, session
- )
- })?;
+ .metadata
+ .perm_leave_consumer_group(session.get_user_id(), stream_id, topic_id)?;
+ shard.ensure_consumer_group_exists(&self.stream_id, &self.topic_id, &self.group_id)?;
+
+ let request = ShardRequest {
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ partition_id: 0,
+ payload: ShardRequestPayload::LeaveConsumerGroup {
+ user_id: session.get_user_id(),
+ client_id: session.client_id,
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ group_id: self.group_id.clone(),
+ },
+ };
+
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::LeaveConsumerGroup {
+ client_id,
+ stream_id,
+ topic_id,
+ group_id,
+ ..
+ } = payload
+ {
+ shard
+ .leave_consumer_group(client_id, &stream_id, &topic_id, &group_id)
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to leave consumer group for stream_id: {}, topic_id: {}, group_id: {}, session: {}",
+ stream_id, topic_id, group_id, session
+ )
+ })?;
+ } else {
+ unreachable!(
+ "Expected a LeaveConsumerGroup request inside of LeaveConsumerGroup handler"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::LeaveConsumerGroupResponse => {}
+ ShardResponse::ErrorResponse(err) => return Err(err),
+ _ => unreachable!(
+ "Expected a LeaveConsumerGroupResponse inside of LeaveConsumerGroup handler"
+ ),
+ },
+ }
sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
diff --git a/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs b/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs
index e3436d2e2..f8f024915 100644
--- a/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs
+++ b/core/server/src/binary/handlers/consumer_offsets/delete_consumer_offset_handler.rs
@@ -23,7 +23,6 @@ use crate::binary::handlers::consumer_offsets::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_consumer_offset::DeleteConsumerOffset;
use iggy_common::{IggyError, SenderKind};
@@ -45,11 +44,9 @@ impl ServerCommandHandler for DeleteConsumerOffset {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().delete_consumer_offset(
- session.get_user_id(),
- stream_id,
- topic_id,
- )?;
+ shard
+ .metadata
+ .perm_delete_consumer_offset(session.get_user_id(), stream_id, topic_id)?;
shard
.delete_consumer_offset(
session.client_id,
diff --git a/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs b/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs
index 80890c087..070b54b5e 100644
--- a/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs
+++ b/core/server/src/binary/handlers/consumer_offsets/get_consumer_offset_handler.rs
@@ -23,7 +23,6 @@ use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::IggyError;
use iggy_common::SenderKind;
use iggy_common::get_consumer_offset::GetConsumerOffset;
@@ -50,9 +49,8 @@ impl ServerCommandHandler for GetConsumerOffset {
return Ok(HandlerResult::Finished);
};
if shard
- .permissioner
- .borrow()
- .get_consumer_offset(session.get_user_id(), stream_id, topic_id)
+ .metadata
+ .perm_get_consumer_offset(session.get_user_id(), stream_id, topic_id)
.is_err()
{
sender.send_empty_ok_response().await?;
diff --git a/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs b/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs
index 16b57b217..f28f2113f 100644
--- a/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs
+++ b/core/server/src/binary/handlers/consumer_offsets/store_consumer_offset_handler.rs
@@ -25,7 +25,6 @@ use crate::binary::handlers::consumer_offsets::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::IggyError;
use iggy_common::SenderKind;
@@ -47,11 +46,9 @@ impl ServerCommandHandler for StoreConsumerOffset {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().store_consumer_offset(
- session.get_user_id(),
- stream_id,
- topic_id,
- )?;
+ shard
+ .metadata
+ .perm_store_consumer_offset(session.get_user_id(), stream_id, topic_id)?;
shard
.store_consumer_offset(
session.client_id,
diff --git a/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs b/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs
index 008d1c600..28100048f 100644
--- a/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs
+++ b/core/server/src/binary/handlers/messages/flush_unsaved_buffer_handler.rs
@@ -23,7 +23,6 @@ use crate::binary::handlers::messages::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::{FlushUnsavedBuffer, IggyError, SenderKind};
use std::rc::Rc;
diff --git a/core/server/src/binary/handlers/messages/poll_messages_handler.rs b/core/server/src/binary/handlers/messages/poll_messages_handler.rs
index 354b554b0..e37bbe284 100644
--- a/core/server/src/binary/handlers/messages/poll_messages_handler.rs
+++ b/core/server/src/binary/handlers/messages/poll_messages_handler.rs
@@ -23,7 +23,6 @@ use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
use crate::shard::system::messages::PollingArgs;
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::SenderKind;
use iggy_common::{IggyError, PollMessages, PooledBuffer};
use std::rc::Rc;
diff --git a/core/server/src/binary/handlers/messages/send_messages_handler.rs b/core/server/src/binary/handlers/messages/send_messages_handler.rs
index 2181a9502..15bad58b9 100644
--- a/core/server/src/binary/handlers/messages/send_messages_handler.rs
+++ b/core/server/src/binary/handlers/messages/send_messages_handler.rs
@@ -22,7 +22,6 @@ use crate::shard::transmission::message::{ShardMessage, ShardRequest, ShardReque
use crate::streaming::segments::{IggyIndexesMut, IggyMessagesBatchMut};
use crate::streaming::session::Session;
use crate::streaming::topics;
-use anyhow::Result;
use compio::buf::{IntoInner as _, IoBuf};
use iggy_common::Identifier;
use iggy_common::PooledBuffer;
@@ -50,6 +49,7 @@ impl ServerCommandHandler for SendMessages {
mut self,
sender: &mut SenderKind,
length: u32,
+
session: &Session,
shard: &Rc<IggyShard>,
) -> Result<HandlerResult, IggyError> {
@@ -104,47 +104,40 @@ impl ServerCommandHandler for SendMessages {
let messages_buffer = messages_buffer.into_inner();
let indexes = IggyIndexesMut::from_bytes(indexes_buffer, 0);
- let batch = IggyMessagesBatchMut::from_indexes_and_messages(
- messages_count,
- indexes,
- messages_buffer,
- );
+ let batch = IggyMessagesBatchMut::from_indexes_and_messages(indexes, messages_buffer);
batch.validate()?;
let (numeric_stream_id, numeric_topic_id) =
shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().append_messages(
+ shard.metadata.perm_append_messages(
session.get_user_id(),
numeric_stream_id,
numeric_topic_id,
)?;
- // TODO(tungtose): dry this code && get partition_id below have a side effect
- let partition_id = shard.streams.with_topic_by_id(
- &self.stream_id,
- &self.topic_id,
- |(root, auxilary, ..)| match self.partitioning.kind {
- PartitioningKind::Balanced => {
- let upperbound = root.partitions().len();
- let pid = auxilary.get_next_partition_id(upperbound);
- Ok(pid)
- }
- PartitioningKind::PartitionId => Ok(u32::from_le_bytes(
- self.partitioning.value[..self.partitioning.length as usize]
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- ) as usize),
- PartitioningKind::MessagesKey => {
- let upperbound = root.partitions().len();
- Ok(
- topics::helpers::calculate_partition_id_by_messages_key_hash(
- upperbound,
- &self.partitioning.value,
- ),
- )
- }
- },
- )?;
+ let partition_id = match self.partitioning.kind {
+ PartitioningKind::Balanced => shard
+ .metadata
+ .get_next_partition_id(numeric_stream_id, numeric_topic_id)
+ .ok_or(IggyError::TopicIdNotFound(
+ self.stream_id.clone(),
+ self.topic_id.clone(),
+ ))?,
+ PartitioningKind::PartitionId => u32::from_le_bytes(
+ self.partitioning.value[..self.partitioning.length as usize]
+ .try_into()
+ .map_err(|_| IggyError::InvalidNumberEncoding)?,
+ ) as usize,
+ PartitioningKind::MessagesKey => {
+ let partitions_count = shard
+ .metadata
+ .partitions_count(numeric_stream_id, numeric_topic_id);
+ topics::helpers::calculate_partition_id_by_messages_key_hash(
+ partitions_count,
+ &self.partitioning.value,
+ )
+ }
+ };
let namespace = IggyNamespace::new(numeric_stream_id, numeric_topic_id, partition_id);
let user_id = session.get_user_id();
diff --git a/core/server/src/binary/handlers/partitions/create_partitions_handler.rs b/core/server/src/binary/handlers/partitions/create_partitions_handler.rs
index df9cfee22..f775edf2f 100644
--- a/core/server/src/binary/handlers/partitions/create_partitions_handler.rs
+++ b/core/server/src/binary/handlers/partitions/create_partitions_handler.rs
@@ -21,17 +21,17 @@ use crate::binary::command::{
};
use crate::binary::handlers::partitions::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
use crate::shard::transmission::event::ShardEvent;
-use crate::slab::traits_ext::EntityMarker;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use crate::streaming::{streams, topics};
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::create_partitions::CreatePartitions;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -52,53 +52,92 @@ impl ServerCommandHandler for CreatePartitions {
shard.ensure_authenticated(session)?;
let (stream_id_numeric, topic_id_numeric) =
shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().create_partitions(
+ shard.metadata.perm_create_partitions(
session.get_user_id(),
stream_id_numeric,
topic_id_numeric,
)?;
- // Acquire partition lock to serialize filesystem operations
- let _partition_guard = shard.fs_locks.partition_lock.lock().await;
-
- let partitions = shard
- .create_partitions(&self.stream_id, &self.topic_id, self.partitions_count)
- .await?;
- let partition_ids = partitions.iter().map(|p| p.id()).collect::<Vec<_>>();
- let event = ShardEvent::CreatedPartitions {
- stream_id: self.stream_id.clone(),
- topic_id: self.topic_id.clone(),
- partitions,
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::CreatePartitions {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ partitions_count: self.partitions_count,
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard.streams.with_topic_by_id_mut(
- &self.stream_id,
- &self.topic_id,
- topics::helpers::rebalance_consumer_groups(&partition_ids),
- );
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::CreatePartitions {
+ stream_id,
+ topic_id,
+ partitions_count,
+ ..
+ } = payload
+ {
+ let _partition_guard = shard.fs_locks.partition_lock.lock().await;
+
+ // Get numeric IDs BEFORE create (for rebalance operation)
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+
+ let partition_infos = shard
+ .create_partitions(&stream_id, &topic_id, partitions_count)
+ .await?;
+
+ let event = ShardEvent::CreatedPartitions {
+ stream_id: stream_id.clone(),
+ topic_id: topic_id.clone(),
+ partitions: partition_infos,
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ let total_partition_count = shard
+ .metadata
+ .partitions_count(numeric_stream_id, numeric_topic_id)
+ as u32;
+ shard.writer().rebalance_consumer_groups_for_topic(
+ numeric_stream_id,
+ numeric_topic_id,
+ total_partition_count,
+ );
+
+ shard
+ .state
+ .apply(session.get_user_id(), &EntryCommand::CreatePartitions(self))
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply create partitions for stream_id: {numeric_stream_id}, topic_id: {numeric_topic_id}, session: {session}"
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected a CreatePartitions request inside of CreatePartitions handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::CreatePartitionsResponse(_partitions) => {
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected a CreatePartitionsResponse inside of CreatePartitions handler, impossible state"
+ ),
+ },
+ }
- let stream_id = shard
- .streams
- .with_stream_by_id(&self.stream_id, streams::helpers::get_stream_id());
- let topic_id = shard.streams.with_topic_by_id(
- &self.stream_id,
- &self.topic_id,
- topics::helpers::get_topic_id(),
- );
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::CreatePartitions(self),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply create partitions for stream_id: {stream_id}, topic_id: {topic_id}, session: {session}"
- )
- })?;
- sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs b/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs
index 4ce2566ad..5b7fd0b7a 100644
--- a/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs
+++ b/core/server/src/binary/handlers/partitions/delete_partitions_handler.rs
@@ -21,15 +21,17 @@ use crate::binary::command::{
};
use crate::binary::handlers::partitions::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_partitions::DeletePartitions;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -50,52 +52,94 @@ impl ServerCommandHandler for DeletePartitions {
shard.ensure_authenticated(session)?;
let (stream_id_numeric, topic_id_numeric) =
shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
- shard.permissioner.borrow().delete_partitions(
+ shard.metadata.perm_delete_partitions(
session.get_user_id(),
stream_id_numeric,
topic_id_numeric,
)?;
- let stream_id = self.stream_id.clone();
- let topic_id = self.topic_id.clone();
-
- // Acquire partition lock to serialize filesystem operations
- let _partition_guard = shard.fs_locks.partition_lock.lock().await;
- let deleted_partition_ids = shard
- .delete_partitions(&self.stream_id, &self.topic_id, self.partitions_count)
- .await?;
- let event = ShardEvent::DeletedPartitions {
- stream_id: self.stream_id.clone(),
- topic_id: self.topic_id.clone(),
- partitions_count: self.partitions_count,
- partition_ids: deleted_partition_ids,
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::DeletePartitions {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ partitions_count: self.partitions_count,
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- let remaining_partition_ids = shard.streams.with_topic_by_id(
- &self.stream_id,
- &self.topic_id,
- crate::streaming::topics::helpers::get_partition_ids(),
- );
- shard.streams.with_topic_by_id_mut(
- &self.stream_id,
- &self.topic_id,
- crate::streaming::topics::helpers::rebalance_consumer_groups(&remaining_partition_ids),
- );
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::DeletePartitions {
+ stream_id,
+ topic_id,
+ partitions_count,
+ ..
+ } = payload
+ {
+ let _partition_guard = shard.fs_locks.partition_lock.lock().await;
+
+ // Get numeric IDs BEFORE delete
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+
+ let deleted_partition_ids = shard
+ .delete_partitions(&stream_id, &topic_id, partitions_count)
+ .await?;
+
+ let event = ShardEvent::DeletedPartitions {
+ stream_id: stream_id.clone(),
+ topic_id: topic_id.clone(),
+ partitions_count,
+ partition_ids: deleted_partition_ids,
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ let remaining_partition_count = shard
+ .metadata
+ .partitions_count(numeric_stream_id, numeric_topic_id)
+ as u32;
+ shard.writer().rebalance_consumer_groups_for_topic(
+ numeric_stream_id,
+ numeric_topic_id,
+ remaining_partition_count,
+ );
+
+ shard
+ .state
+ .apply(session.get_user_id(), &EntryCommand::DeletePartitions(self))
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply delete partitions for stream_id: {}, topic_id: {}, session: {session}",
+ stream_id, topic_id
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected a DeletePartitions request inside of DeletePartitions handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::DeletePartitionsResponse(_partition_ids) => {
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected a DeletePartitionsResponse inside of DeletePartitions handler, impossible state"
+ ),
+ },
+ }
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::DeletePartitions(self),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply delete partitions for stream_id: {stream_id}, topic_id: {topic_id}, session: {session}"
- )
- })?;
- sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs
index 8f2e766dd..e89ac1dff 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs
@@ -22,16 +22,17 @@ use crate::binary::command::{
use crate::binary::handlers::personal_access_tokens::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
-
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::state::models::CreatePersonalAccessTokenWithHash;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::create_personal_access_token::CreatePersonalAccessToken;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -50,43 +51,75 @@ impl ServerCommandHandler for CreatePersonalAccessToken {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
-
let user_id = session.get_user_id();
- let (personal_access_token, token) = shard
- .create_personal_access_token(user_id, &self.name, self.expiry)
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to create personal access token with name: {}, user: {user_id}",
- self.name
- )
- })?;
- let bytes = mapper::map_raw_pat(&token);
- let hash = personal_access_token.token.to_string();
- let event = ShardEvent::CreatedPersonalAccessToken {
- personal_access_token: personal_access_token.clone(),
+
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::CreatePersonalAccessToken {
+ user_id,
+ name: self.name.clone(),
+ expiry: self.expiry,
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::CreatePersonalAccessToken(CreatePersonalAccessTokenWithHash {
- command: CreatePersonalAccessToken {
- name: self.name.to_owned(),
- expiry: self.expiry,
- },
- hash,
- }),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to create personal access token with name: {}, session: {session}",
- self.name
- )
- })?;
- sender.send_ok_response(&bytes).await?;
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::CreatePersonalAccessToken { name, expiry, .. } =
+ payload
+ {
+ let (personal_access_token, token) = shard
+ .create_personal_access_token(user_id, &name, expiry)
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to create personal access token with name: {name}, user: {user_id}"
+ )
+ })?;
+
+ let bytes = mapper::map_raw_pat(&token);
+ let hash = personal_access_token.token.to_string();
+
+ shard
+ .state
+ .apply(
+ session.get_user_id(),
+ &EntryCommand::CreatePersonalAccessToken(CreatePersonalAccessTokenWithHash {
+ command: CreatePersonalAccessToken {
+ name: self.name.to_owned(),
+ expiry: self.expiry,
+ },
+ hash,
+ }),
+ )
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply create personal access token with name: {}, session: {session}",
+ self.name
+ )
+ })?;
+
+ sender.send_ok_response(&bytes).await?;
+ } else {
+ unreachable!(
+ "Expected a CreatePersonalAccessToken request inside of CreatePersonalAccessToken handler"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::CreatePersonalAccessTokenResponse(_pat, token) => {
+ let bytes = mapper::map_raw_pat(&token);
+ sender.send_ok_response(&bytes).await?;
+ }
+ ShardResponse::ErrorResponse(err) => return Err(err),
+ _ => unreachable!(
+ "Expected a CreatePersonalAccessTokenResponse inside of CreatePersonalAccessToken handler"
+ ),
+ },
+ };
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs
index 3e80f1dac..6c7ebb9a3 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs
@@ -22,12 +22,15 @@ use crate::binary::command::{
use crate::binary::handlers::personal_access_tokens::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_personal_access_token::DeletePersonalAccessToken;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -49,32 +52,62 @@ impl ServerCommandHandler for DeletePersonalAccessToken {
let token_name = self.name.clone();
let user_id = session.get_user_id();
- shard
- .delete_personal_access_token(user_id, &self.name)
- .error(|e: &IggyError| {format!(
- "{COMPONENT} (error: {e}) - failed to delete personal access token with name: {token_name}, user: {user_id}"
- )})?;
-
- // Broadcast the event to other shards
- let event = crate::shard::transmission::event::ShardEvent::DeletedPersonalAccessToken {
- user_id: session.get_user_id(),
- name: self.name.clone(),
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::DeletePersonalAccessToken {
+ user_id,
+ name: self.name.clone(),
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::DeletePersonalAccessToken(DeletePersonalAccessToken {
- name: self.name,
- }),
- )
- .await
- .error(|e: &IggyError| {format!(
- "{COMPONENT} (error: {e}) - failed to apply delete personal access token with name: {token_name}, session: {session}"
- )})?;
- sender.send_empty_ok_response().await?;
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::DeletePersonalAccessToken { name, .. } = payload
+ {
+ shard.delete_personal_access_token(user_id, &name).error(
+ |e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to delete personal access token with name: {name}, user: {user_id}"
+ )
+ },
+ )?;
+
+ shard
+ .state
+ .apply(
+ session.get_user_id(),
+ &EntryCommand::DeletePersonalAccessToken(DeletePersonalAccessToken {
+ name: self.name,
+ }),
+ )
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply delete personal access token with name: {token_name}, session: {session}"
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected a DeletePersonalAccessToken request inside of DeletePersonalAccessToken handler"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::DeletePersonalAccessTokenResponse => {
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => return Err(err),
+ _ => unreachable!(
+ "Expected a DeletePersonalAccessTokenResponse inside of DeletePersonalAccessToken handler"
+ ),
+ },
+ }
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs b/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs
index 1603aaa6a..db8a94f51 100644
--- a/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs
+++ b/core/server/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs
@@ -25,7 +25,6 @@ use crate::binary::handlers::personal_access_tokens::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::login_with_personal_access_token::LoginWithPersonalAccessToken;
use iggy_common::{IggyError, SenderKind};
diff --git a/core/server/src/binary/handlers/segments/delete_segments_handler.rs b/core/server/src/binary/handlers/segments/delete_segments_handler.rs
index b437af0bb..f22117844 100644
--- a/core/server/src/binary/handlers/segments/delete_segments_handler.rs
+++ b/core/server/src/binary/handlers/segments/delete_segments_handler.rs
@@ -28,7 +28,6 @@ use crate::shard::transmission::message::{
};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_segments::DeleteSegments;
use iggy_common::sharding::IggyNamespace;
@@ -50,22 +49,21 @@ impl ServerCommandHandler for DeleteSegments {
shard: &Rc<IggyShard>,
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
+ shard.ensure_authenticated(session)?;
let stream_id = self.stream_id.clone();
let topic_id = self.topic_id.clone();
let partition_id = self.partition_id as usize;
let segments_count = self.segments_count;
- shard.ensure_authenticated(session)?;
let (numeric_stream_id, numeric_topic_id, _) =
shard.resolve_partition_id(&stream_id, &topic_id, partition_id)?;
- shard.permissioner.borrow().delete_segments(
+ shard.metadata.perm_delete_segments(
session.get_user_id(),
numeric_stream_id,
numeric_topic_id,
)?;
- // Route request to the correct shard
let namespace = IggyNamespace::new(numeric_stream_id, numeric_topic_id, partition_id);
let payload = ShardRequestPayload::DeleteSegments { segments_count };
let request = ShardRequest::new(stream_id.clone(), topic_id.clone(), partition_id, payload);
@@ -77,21 +75,38 @@ impl ServerCommandHandler for DeleteSegments {
{
ShardSendRequestResult::Recoil(message) => {
if let ShardMessage::Request(crate::shard::transmission::message::ShardRequest {
- stream_id,
- topic_id,
- partition_id,
+ stream_id: recoil_stream_id,
+ topic_id: recoil_topic_id,
+ partition_id: recoil_partition_id,
payload,
}) = message
&& let ShardRequestPayload::DeleteSegments { segments_count } = payload
{
+ let (stream, topic) =
+ shard.resolve_topic_id(&recoil_stream_id, &recoil_topic_id)?;
+ shard
+ .delete_segments_base(stream, topic, recoil_partition_id, segments_count)
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to delete segments for topic with ID: {recoil_topic_id} in stream with ID: {recoil_stream_id}, session: {session}",
+ )
+ })?;
+
shard
- .delete_segments_base(&stream_id, &topic_id, partition_id, segments_count)
+ .state
+ .apply(
+ session.get_user_id(),
+ &EntryCommand::DeleteSegments(self),
+ )
.await
.error(|e: &IggyError| {
format!(
- "{COMPONENT} (error: {e}) - failed to delete segments for topic with ID: {topic_id} in stream with ID: {stream_id}, session: {session}",
+ "{COMPONENT} (error: {e}) - failed to apply 'delete segments' command for partition with ID: {partition_id} in topic with ID: {topic_id} in stream with ID: {stream_id}, session: {session}",
)
})?;
+
+ sender.send_empty_ok_response().await?;
} else {
return Err(IggyError::InvalidCommand);
}
@@ -100,23 +115,9 @@ impl ServerCommandHandler for DeleteSegments {
if !matches!(response, ShardResponse::DeleteSegments) {
return Err(IggyError::InvalidCommand);
}
+ sender.send_empty_ok_response().await?;
}
}
-
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::DeleteSegments(self),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply 'delete segments' command for partition with ID: {partition_id} in topic with ID: {topic_id} in stream with ID: {stream_id}, session: {session}",
- )
- })?;
-
- sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/streams/create_stream_handler.rs b/core/server/src/binary/handlers/streams/create_stream_handler.rs
index 4ff493517..4feacdca6 100644
--- a/core/server/src/binary/handlers/streams/create_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/create_stream_handler.rs
@@ -21,19 +21,14 @@ use crate::binary::command::{
};
use crate::binary::handlers::streams::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
-
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
};
-use crate::slab::traits_ext::{EntityComponentSystem, EntityMarker};
use crate::state::command::EntryCommand;
use crate::state::models::CreateStreamWithId;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::create_stream::CreateStream;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -55,10 +50,7 @@ impl ServerCommandHandler for CreateStream {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .create_stream(session.get_user_id())?;
+ shard.metadata.perm_create_stream(session.get_user_id())?;
let request = ShardRequest {
stream_id: Identifier::default(),
@@ -79,20 +71,9 @@ impl ServerCommandHandler for CreateStream {
// Acquire stream lock to serialize filesystem operations
let _stream_guard = shard.fs_locks.stream_lock.lock().await;
- let stream = shard.create_stream(name).await?;
- let created_stream_id = stream.id();
-
- let event = ShardEvent::CreatedStream {
- id: created_stream_id,
- stream: stream.clone(),
- };
- shard.broadcast_event_to_all_shards(event).await?;
+ let created_stream_id = shard.create_stream(name).await?;
- let response = shard
- .streams
- .with_components_by_id(created_stream_id, |(root, stats)| {
- mapper::map_stream(&root, &stats)
- });
+ let response = shard.get_stream_from_metadata(created_stream_id);
shard
.state
@@ -115,23 +96,8 @@ impl ServerCommandHandler for CreateStream {
}
}
ShardSendRequestResult::Response(response) => match response {
- ShardResponse::CreateStreamResponse(stream) => {
- let created_stream_id = stream.id();
- let response = mapper::map_stream(stream.root(), stream.stats());
-
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::CreateStream(CreateStreamWithId {
- stream_id: created_stream_id as u32,
- command: self
- }))
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply create stream for id: {created_stream_id}, session: {session}"
- )
- })?;
-
+ ShardResponse::CreateStreamResponse(created_stream_id) => {
+ let response = shard.get_stream_from_metadata(created_stream_id);
sender.send_ok_response(&response).await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/streams/delete_stream_handler.rs b/core/server/src/binary/handlers/streams/delete_stream_handler.rs
index b4af2893c..c41eeca14 100644
--- a/core/server/src/binary/handlers/streams/delete_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/delete_stream_handler.rs
@@ -22,15 +22,12 @@ use crate::binary::command::{
use crate::binary::handlers::streams::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
};
-use crate::slab::traits_ext::EntityMarker;
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_stream::DeleteStream;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -55,10 +52,8 @@ impl ServerCommandHandler for DeleteStream {
shard.ensure_authenticated(session)?;
let stream_id = shard.resolve_stream_id(&self.stream_id)?;
shard
- .permissioner
- .borrow()
- .delete_stream(session.get_user_id(), stream_id)?;
- let stream_id = self.stream_id.clone();
+ .metadata
+ .perm_delete_stream(session.get_user_id(), stream_id)?;
let request = ShardRequest {
stream_id: Identifier::default(),
topic_id: Identifier::default(),
@@ -77,7 +72,7 @@ impl ServerCommandHandler for DeleteStream {
{
// Acquire stream lock to serialize filesystem operations
let _stream_guard = shard.fs_locks.stream_lock.lock().await;
- let stream = shard
+ let stream_info = shard
.delete_stream(&stream_id)
.await
.error(|e: &IggyError| {
@@ -85,16 +80,9 @@ impl ServerCommandHandler for DeleteStream {
})?;
info!(
"Deleted stream with name: {}, ID: {}",
- stream.root().name(),
- stream.id()
+ stream_info.name, stream_info.id
);
- let event = ShardEvent::DeletedStream {
- id: stream.id(),
- stream_id: stream_id.clone(),
- };
- shard.broadcast_event_to_all_shards(event).await?;
-
shard
.state
.apply(session.get_user_id(), &EntryCommand::DeleteStream(DeleteStream { stream_id: stream_id.clone() }))
@@ -106,14 +94,7 @@ impl ServerCommandHandler for DeleteStream {
}
}
ShardSendRequestResult::Response(response) => match response {
- ShardResponse::DeleteStreamResponse(stream) => {
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::DeleteStream(DeleteStream { stream_id: (stream.id() as u32).try_into().unwrap() }))
- .await
- .error(|e: &IggyError| {
- format!("{COMPONENT} (error: {e}) - failed to apply delete stream with ID: {stream_id}, session: {session}")
- })?;
+ ShardResponse::DeleteStreamResponse(_stream_id_num) => {
sender.send_empty_ok_response().await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/streams/get_stream_handler.rs b/core/server/src/binary/handlers/streams/get_stream_handler.rs
index be51c5efc..cadaf05b7 100644
--- a/core/server/src/binary/handlers/streams/get_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/get_stream_handler.rs
@@ -20,11 +20,9 @@ use crate::binary::command::{
BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
};
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
use crate::shard::IggyShard;
-use crate::slab::traits_ext::EntityComponentSystem;
use crate::streaming::session::Session;
-use anyhow::Result;
+use err_trail::ErrContext;
use iggy_common::IggyError;
use iggy_common::SenderKind;
use iggy_common::get_stream::GetStream;
@@ -45,22 +43,29 @@ impl ServerCommandHandler for GetStream {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- let Ok(stream_id) = shard.resolve_stream_id(&self.stream_id) else {
+
+ let Some(numeric_stream_id) = shard.metadata.get_stream_id(&self.stream_id) else {
sender.send_empty_ok_response().await?;
return Ok(HandlerResult::Finished);
};
- if shard
- .permissioner
- .borrow()
- .get_stream(session.get_user_id(), stream_id)
- .is_err()
- {
+
+ let has_permission = shard
+ .metadata
+ .perm_get_stream(session.get_user_id(), numeric_stream_id)
+ .error(|e: &IggyError| {
+ format!(
+ "permission denied to get stream with ID: {} for user with ID: {}, error: {e}",
+ self.stream_id,
+ session.get_user_id(),
+ )
+ })
+ .is_ok();
+ if !has_permission {
sender.send_empty_ok_response().await?;
return Ok(HandlerResult::Finished);
}
- let response = shard
- .streams
- .with_components_by_id(stream_id, |(root, stats)| mapper::map_stream(&root, &stats));
+
+ let response = shard.get_stream_from_metadata(numeric_stream_id);
sender.send_ok_response(&response).await?;
Ok(HandlerResult::Finished)
}
diff --git a/core/server/src/binary/handlers/streams/get_streams_handler.rs b/core/server/src/binary/handlers/streams/get_streams_handler.rs
index f7bf06f2d..3e2ff4404 100644
--- a/core/server/src/binary/handlers/streams/get_streams_handler.rs
+++ b/core/server/src/binary/handlers/streams/get_streams_handler.rs
@@ -21,11 +21,8 @@ use crate::binary::command::{
};
use crate::binary::handlers::streams::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
use crate::shard::IggyShard;
-use crate::slab::traits_ext::{EntityComponentSystem, IntoComponents};
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::IggyError;
use iggy_common::SenderKind;
@@ -48,9 +45,8 @@ impl ServerCommandHandler for GetStreams {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
shard
- .permissioner
- .borrow()
- .get_streams(session.get_user_id())
+ .metadata
+ .perm_get_streams(session.get_user_id())
.error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - permission denied to get streams for user {}",
@@ -58,10 +54,7 @@ impl ServerCommandHandler for GetStreams {
)
})?;
- let response = shard.streams.with_components(|stream_ref| {
- let (roots, stats) = stream_ref.into_components();
- mapper::map_streams(&roots, &stats)
- });
+ let response = shard.get_streams_from_metadata();
sender.send_ok_response(&response).await?;
Ok(HandlerResult::Finished)
}
diff --git a/core/server/src/binary/handlers/streams/purge_stream_handler.rs b/core/server/src/binary/handlers/streams/purge_stream_handler.rs
index a352010dc..c904bb774 100644
--- a/core/server/src/binary/handlers/streams/purge_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/purge_stream_handler.rs
@@ -21,15 +21,17 @@ use crate::binary::command::{
};
use crate::binary::handlers::streams::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::purge_stream::PurgeStream;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -50,29 +52,67 @@ impl ServerCommandHandler for PurgeStream {
shard.ensure_authenticated(session)?;
let stream_id = shard.resolve_stream_id(&self.stream_id)?;
shard
- .permissioner
- .borrow()
- .purge_stream(session.get_user_id(), stream_id)?;
- let stream_id = self.stream_id.clone();
+ .metadata
+ .perm_purge_stream(session.get_user_id(), stream_id)?;
- shard
- .purge_stream(&self.stream_id)
- .await
- .error(|e: &IggyError| {
- format!("{COMPONENT} (error: {e}) - failed to purge stream with id: {stream_id}, session: {session}")
- })?;
- let event = ShardEvent::PurgedStream {
- stream_id: self.stream_id.clone(),
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::PurgeStream {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::PurgeStream(self))
- .await
- .error(|e: &IggyError| {
- format!("{COMPONENT} (error: {e}) - failed to apply purge stream with id: {stream_id}, session: {session}")
- })?;
- sender.send_empty_ok_response().await?;
+
+ let stream_id_for_log = self.stream_id.clone();
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::PurgeStream { stream_id, .. } = payload
+ {
+ shard.purge_stream(&stream_id).await.error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to purge stream with id: {stream_id_for_log}, session: {session}"
+ )
+ })?;
+
+ let event = ShardEvent::PurgedStream {
+ stream_id: stream_id.clone(),
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ shard
+ .state
+ .apply(session.get_user_id(), &EntryCommand::PurgeStream(self))
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply purge stream with id: {stream_id_for_log}, session: {session}"
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected a PurgeStream request inside of PurgeStream handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::PurgeStreamResponse => {
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected a PurgeStreamResponse inside of PurgeStream handler, impossible state"
+ ),
+ },
+ }
+
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/streams/update_stream_handler.rs b/core/server/src/binary/handlers/streams/update_stream_handler.rs
index eb9986666..332045ebf 100644
--- a/core/server/src/binary/handlers/streams/update_stream_handler.rs
+++ b/core/server/src/binary/handlers/streams/update_stream_handler.rs
@@ -21,15 +21,16 @@ use crate::binary::command::{
};
use crate::binary::handlers::streams::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::update_stream::UpdateStream;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -50,29 +51,60 @@ impl ServerCommandHandler for UpdateStream {
shard.ensure_authenticated(session)?;
let stream_id = shard.resolve_stream_id(&self.stream_id)?;
shard
- .permissioner
- .borrow()
- .update_stream(session.get_user_id(), stream_id)?;
- let stream_id = self.stream_id.clone();
- shard
- .update_stream(&self.stream_id, self.name.clone())
- .error(|e: &IggyError| {
- format!("{COMPONENT} (error: {e}) - failed to update stream with id: {stream_id}, session: {session}")
- })?;
+ .metadata
+ .perm_update_stream(session.get_user_id(), stream_id)?;
- let event = ShardEvent::UpdatedStream {
- stream_id: self.stream_id.clone(),
- name: self.name.clone(),
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::UpdateStream {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ name: self.name.clone(),
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::UpdateStream(self))
- .await
- .error(|e: &IggyError| {
- format!("{COMPONENT} (error: {e}) - failed to apply update stream with id: {stream_id}, session: {session}")
- })?;
- sender.send_empty_ok_response().await?;
+
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::UpdateStream {
+ stream_id, name, ..
+ } = payload
+ {
+ shard.update_stream(&stream_id, name.clone())?;
+
+ shard
+ .state
+ .apply(session.get_user_id(), &EntryCommand::UpdateStream(self))
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply update stream with id: {stream_id}, session: {session}"
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected an UpdateStream request inside of UpdateStream handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::UpdateStreamResponse => {
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected an UpdateStreamResponse inside of UpdateStream handler, impossible state"
+ ),
+ },
+ }
+
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/system/get_client_handler.rs b/core/server/src/binary/handlers/system/get_client_handler.rs
index 40122d7b8..51bee9e2f 100644
--- a/core/server/src/binary/handlers/system/get_client_handler.rs
+++ b/core/server/src/binary/handlers/system/get_client_handler.rs
@@ -43,10 +43,7 @@ impl ServerCommandHandler for GetClient {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .get_client(session.get_user_id())?;
+ shard.metadata.perm_get_client(session.get_user_id())?;
let Some(client) = shard.get_client(self.client_id) else {
sender.send_empty_ok_response().await?;
diff --git a/core/server/src/binary/handlers/system/get_clients_handler.rs b/core/server/src/binary/handlers/system/get_clients_handler.rs
index 4d1f1d779..ad5ccdc2d 100644
--- a/core/server/src/binary/handlers/system/get_clients_handler.rs
+++ b/core/server/src/binary/handlers/system/get_clients_handler.rs
@@ -43,10 +43,7 @@ impl ServerCommandHandler for GetClients {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .get_clients(session.get_user_id())?;
+ shard.metadata.perm_get_clients(session.get_user_id())?;
let clients = shard.get_clients();
let clients = mapper::map_clients(clients).await;
diff --git a/core/server/src/binary/handlers/system/get_stats_handler.rs b/core/server/src/binary/handlers/system/get_stats_handler.rs
index b0e9ddb1b..40e6b2024 100644
--- a/core/server/src/binary/handlers/system/get_stats_handler.rs
+++ b/core/server/src/binary/handlers/system/get_stats_handler.rs
@@ -49,10 +49,7 @@ impl ServerCommandHandler for GetStats {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .get_stats(session.get_user_id())?;
+ shard.metadata.perm_get_stats(session.get_user_id())?;
// Route GetStats to shard0 only
let request = ShardRequest {
diff --git a/core/server/src/binary/handlers/system/ping_handler.rs b/core/server/src/binary/handlers/system/ping_handler.rs
index e22fa7ba8..746de588f 100644
--- a/core/server/src/binary/handlers/system/ping_handler.rs
+++ b/core/server/src/binary/handlers/system/ping_handler.rs
@@ -19,7 +19,6 @@
use crate::binary::command::{BinaryServerCommand, HandlerResult, ServerCommandHandler};
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::IggyError;
use iggy_common::IggyTimestamp;
use iggy_common::SenderKind;
diff --git a/core/server/src/binary/handlers/topics/create_topic_handler.rs b/core/server/src/binary/handlers/topics/create_topic_handler.rs
index 7f66f2292..14f9a3ad8 100644
--- a/core/server/src/binary/handlers/topics/create_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/create_topic_handler.rs
@@ -21,20 +21,16 @@ use crate::binary::command::{
};
use crate::binary::handlers::topics::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
-
use crate::shard::IggyShard;
use crate::shard::transmission::event::ShardEvent;
use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
};
-use crate::slab::traits_ext::EntityMarker;
+
use crate::state::command::EntryCommand;
use crate::state::models::CreateTopicWithId;
use crate::streaming::session::Session;
-use crate::streaming::streams;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::create_topic::CreateTopic;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -58,9 +54,8 @@ impl ServerCommandHandler for CreateTopic {
shard.ensure_authenticated(session)?;
let stream_id = shard.resolve_stream_id(&self.stream_id)?;
shard
- .permissioner
- .borrow()
- .create_topic(session.get_user_id(), stream_id)?;
+ .metadata
+ .perm_create_topic(session.get_user_id(), stream_id)?;
let request = ShardRequest {
stream_id: Identifier::default(),
@@ -96,7 +91,7 @@ impl ServerCommandHandler for CreateTopic {
// Acquire topic lock to serialize filesystem operations
let _topic_guard = shard.fs_locks.topic_lock.lock().await;
- let topic = shard
+ let topic_id = shard
.create_topic(
&stream_id,
name,
@@ -106,21 +101,17 @@ impl ServerCommandHandler for CreateTopic {
replication_factor,
)
.await?;
- self.message_expiry = topic.root().message_expiry();
- self.max_topic_size = topic.root().max_topic_size();
- let stream_id_num = shard
- .streams
- .with_stream_by_id(&stream_id, streams::helpers::get_stream_id());
- let topic_id = topic.id();
+ let stream_id_num = shard.resolve_stream_id(&stream_id)?;
- let event = ShardEvent::CreatedTopic {
- stream_id: stream_id.clone(),
- topic,
- };
+ if let Some((expiry, max_size)) =
+ shard.metadata.get_topic_config(stream_id_num, topic_id)
+ {
+ self.message_expiry = expiry;
+ self.max_topic_size = max_size;
+ }
- shard.broadcast_event_to_all_shards(event).await?;
- let partitions = shard
+ let partition_infos = shard
.create_partitions(
&stream_id,
&Identifier::numeric(topic_id as u32).unwrap(),
@@ -130,15 +121,11 @@ impl ServerCommandHandler for CreateTopic {
let event = ShardEvent::CreatedPartitions {
stream_id: stream_id.clone(),
topic_id: Identifier::numeric(topic_id as u32).unwrap(),
- partitions,
+ partitions: partition_infos.clone(),
};
shard.broadcast_event_to_all_shards(event).await?;
- let response = shard.streams.with_topic_by_id(
- &stream_id,
- &Identifier::numeric(topic_id as u32).unwrap(),
- |(root, _, stats)| mapper::map_topic(&root, &stats),
- );
+ let response = shard.get_topic_from_metadata(stream_id_num, topic_id);
shard
.state
.apply(session.get_user_id(), &EntryCommand::CreateTopic(CreateTopicWithId {
@@ -159,34 +146,9 @@ impl ServerCommandHandler for CreateTopic {
}
}
ShardSendRequestResult::Response(response) => match response {
- ShardResponse::CreateTopicResponse(topic) => {
- let topic_id = topic.id();
- self.message_expiry = topic.root().message_expiry();
- self.max_topic_size = topic.root().max_topic_size();
-
- let stream_id = shard
- .streams
- .with_stream_by_id(&self.stream_id, streams::helpers::get_stream_id());
-
- let response = shard.streams.with_topic_by_id(
- &self.stream_id,
- &Identifier::numeric(topic_id as u32).unwrap(),
- |(root, _, stats)| mapper::map_topic(&root, &stats),
- );
-
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::CreateTopic(CreateTopicWithId {
- topic_id: topic_id as u32,
- command: self
- }))
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply create topic for stream_id: {stream_id}, topic_id: {topic_id:?}"
- )
- })?;
-
+ ShardResponse::CreateTopicResponse(topic_id) => {
+ let stream_id_num = shard.resolve_stream_id(&self.stream_id)?;
+ let response = shard.get_topic_from_metadata(stream_id_num, topic_id);
sender.send_ok_response(&response).await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/topics/delete_topic_handler.rs b/core/server/src/binary/handlers/topics/delete_topic_handler.rs
index 8b5d16ff8..b81f1ad9d 100644
--- a/core/server/src/binary/handlers/topics/delete_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/delete_topic_handler.rs
@@ -28,11 +28,9 @@ use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
};
-use crate::slab::traits_ext::EntityMarker;
+
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use crate::streaming::streams;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_topic::DeleteTopic;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -57,9 +55,8 @@ impl ServerCommandHandler for DeleteTopic {
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
shard
- .permissioner
- .borrow()
- .delete_topic(session.get_user_id(), stream_id, topic_id)?;
+ .metadata
+ .perm_delete_topic(session.get_user_id(), stream_id, topic_id)?;
let request = ShardRequest {
stream_id: Identifier::default(),
@@ -82,25 +79,32 @@ impl ServerCommandHandler for DeleteTopic {
..
} = payload
{
+ // Capture numeric IDs and partition_ids BEFORE deletion for broadcast.
+ // After deletion, the topic won't exist in metadata.
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+ let partition_ids = shard
+ .metadata
+ .get_partition_ids(numeric_stream_id, numeric_topic_id);
+
// Acquire topic lock to serialize filesystem operations
let _topic_guard = shard.fs_locks.topic_lock.lock().await;
- let topic = shard.delete_topic(&stream_id, &topic_id).await?;
- let stream_id_num = shard
- .streams
- .with_stream_by_id(&stream_id, streams::helpers::get_stream_id());
- let topic_id_num = topic.root().id();
+ let topic_info = shard.delete_topic(&stream_id, &topic_id).await?;
+ let topic_id_num = topic_info.id;
+ let stream_id_num = topic_info.stream_id;
info!(
"Deleted topic with name: {}, ID: {} in stream with ID: {}",
- topic.root().name(),
- topic_id_num,
- stream_id_num
+ topic_info.name, topic_id_num, stream_id_num
);
- let event = ShardEvent::DeletedTopic {
- id: topic_id_num,
- stream_id: stream_id.clone(),
- topic_id: topic_id.clone(),
+ // Broadcast to all shards to clean up their local_partitions entries.
+ // Use numeric Identifiers since the topic is already deleted from metadata.
+ let event = ShardEvent::DeletedPartitions {
+ stream_id: Identifier::numeric(numeric_stream_id as u32).unwrap(),
+ topic_id: Identifier::numeric(numeric_topic_id as u32).unwrap(),
+ partitions_count: partition_ids.len() as u32,
+ partition_ids,
};
shard.broadcast_event_to_all_shards(event).await?;
@@ -119,16 +123,7 @@ impl ServerCommandHandler for DeleteTopic {
}
}
ShardSendRequestResult::Response(response) => match response {
- ShardResponse::DeleteTopicResponse(topic) => {
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::DeleteTopic(self))
- .await
- .error(|e: &IggyError| format!(
- "{COMPONENT} (error: {e}) - failed to apply delete topic with ID: {}, session: {session}",
- topic.id()
- ))?;
-
+ ShardResponse::DeleteTopicResponse(_topic_id_num) => {
sender.send_empty_ok_response().await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/topics/get_topic_handler.rs b/core/server/src/binary/handlers/topics/get_topic_handler.rs
index 58837ca62..f79a30e37 100644
--- a/core/server/src/binary/handlers/topics/get_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/get_topic_handler.rs
@@ -20,10 +20,8 @@ use crate::binary::command::{
BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
};
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::IggyError;
use iggy_common::SenderKind;
use iggy_common::get_topic::GetTopic;
@@ -44,26 +42,30 @@ impl ServerCommandHandler for GetTopic {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- let Ok((stream_id, topic_id)) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)
+
+ let Some(numeric_stream_id) = shard.metadata.get_stream_id(&self.stream_id) else {
+ sender.send_empty_ok_response().await?;
+ return Ok(HandlerResult::Finished);
+ };
+
+ let Some(numeric_topic_id) = shard
+ .metadata
+ .get_topic_id(numeric_stream_id, &self.topic_id)
else {
sender.send_empty_ok_response().await?;
return Ok(HandlerResult::Finished);
};
- if shard
- .permissioner
- .borrow()
- .get_topic(session.get_user_id(), stream_id, topic_id)
- .is_err()
- {
+
+ let has_permission = shard
+ .metadata
+ .perm_get_topic(session.get_user_id(), numeric_stream_id, numeric_topic_id)
+ .is_ok();
+ if !has_permission {
sender.send_empty_ok_response().await?;
return Ok(HandlerResult::Finished);
}
- let response =
- shard
- .streams
- .with_topic_by_id(&self.stream_id, &self.topic_id, |(root, _, stats)| {
- mapper::map_topic(&root, &stats)
- });
+
+ let response = shard.get_topic_from_metadata(numeric_stream_id, numeric_topic_id);
sender.send_ok_response(&response).await?;
Ok(HandlerResult::Finished)
}
diff --git a/core/server/src/binary/handlers/topics/get_topics_handler.rs b/core/server/src/binary/handlers/topics/get_topics_handler.rs
index 572edeb62..9e569e06a 100644
--- a/core/server/src/binary/handlers/topics/get_topics_handler.rs
+++ b/core/server/src/binary/handlers/topics/get_topics_handler.rs
@@ -20,11 +20,8 @@ use crate::binary::command::{
BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
};
use crate::binary::handlers::utils::receive_and_validate;
-use crate::binary::mapper;
use crate::shard::IggyShard;
-use crate::slab::traits_ext::{EntityComponentSystem, IntoComponents};
use crate::streaming::session::Session;
-use anyhow::Result;
use iggy_common::IggyError;
use iggy_common::SenderKind;
use iggy_common::get_topics::GetTopics;
@@ -45,19 +42,17 @@ impl ServerCommandHandler for GetTopics {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- let stream_id = shard.resolve_stream_id(&self.stream_id)?;
+
+ let Some(numeric_stream_id) = shard.metadata.get_stream_id(&self.stream_id) else {
+ sender.send_empty_ok_response().await?;
+ return Ok(HandlerResult::Finished);
+ };
shard
- .permissioner
- .borrow()
- .get_topics(session.get_user_id(), stream_id)?;
+ .metadata
+ .perm_get_topics(session.get_user_id(), numeric_stream_id)?;
- let response = shard.streams.with_topics(&self.stream_id, |topics| {
- topics.with_components(|topics| {
- let (roots, _, stats) = topics.into_components();
- mapper::map_topics(&roots, &stats)
- })
- });
+ let response = shard.get_topics_from_metadata(numeric_stream_id);
sender.send_ok_response(&response).await?;
Ok(HandlerResult::Finished)
}
diff --git a/core/server/src/binary/handlers/topics/purge_topic_handler.rs b/core/server/src/binary/handlers/topics/purge_topic_handler.rs
index 6673a5ac3..e497ff4d9 100644
--- a/core/server/src/binary/handlers/topics/purge_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/purge_topic_handler.rs
@@ -22,12 +22,16 @@ use crate::binary::command::{
use crate::binary::handlers::topics::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
+use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::purge_topic::PurgeTopic;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::{debug, instrument};
@@ -48,38 +52,77 @@ impl ServerCommandHandler for PurgeTopic {
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
shard
- .permissioner
- .borrow()
- .purge_topic(session.get_user_id(), stream_id, topic_id)?;
- let topic_id = self.topic_id.clone();
- let stream_id = self.stream_id.clone();
-
- shard
- .purge_topic(&self.stream_id, &self.topic_id)
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to purge topic with id: {}, stream_id: {}",
- self.topic_id, self.stream_id
- )
- })?;
+ .metadata
+ .perm_purge_topic(session.get_user_id(), stream_id, topic_id)?;
- let event = crate::shard::transmission::event::ShardEvent::PurgedTopic {
- stream_id: self.stream_id.clone(),
- topic_id: self.topic_id.clone(),
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::PurgeTopic {
+ user_id: session.get_user_id(),
+ stream_id: self.stream_id.clone(),
+ topic_id: self.topic_id.clone(),
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::PurgeTopic(self))
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply purge topic with id: {topic_id}, stream_id: {stream_id}",
- )
- })?;
- sender.send_empty_ok_response().await?;
+ let stream_id_for_log = self.stream_id.clone();
+ let topic_id_for_log = self.topic_id.clone();
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::PurgeTopic {
+ stream_id,
+ topic_id,
+ ..
+ } = payload
+ {
+ shard
+ .purge_topic(&stream_id, &topic_id)
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to purge topic with id: {topic_id_for_log}, stream_id: {stream_id_for_log}"
+ )
+ })?;
+
+ let event = ShardEvent::PurgedTopic {
+ stream_id: stream_id.clone(),
+ topic_id: topic_id.clone(),
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ shard
+ .state
+ .apply(session.get_user_id(), &EntryCommand::PurgeTopic(self))
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply purge topic with id: {topic_id_for_log}, stream_id: {stream_id_for_log}"
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected a PurgeTopic request inside of PurgeTopic handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::PurgeTopicResponse => {
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected a PurgeTopicResponse inside of PurgeTopic handler, impossible state"
+ ),
+ },
+ }
+
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/topics/update_topic_handler.rs b/core/server/src/binary/handlers/topics/update_topic_handler.rs
index 41ba447ef..a57380a39 100644
--- a/core/server/src/binary/handlers/topics/update_topic_handler.rs
+++ b/core/server/src/binary/handlers/topics/update_topic_handler.rs
@@ -23,15 +23,12 @@ use crate::binary::handlers::topics::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use crate::streaming::{streams, topics};
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::update_topic::UpdateTopic;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -55,9 +52,8 @@ impl ServerCommandHandler for UpdateTopic {
shard.ensure_authenticated(session)?;
let (stream_id, topic_id) = shard.resolve_topic_id(&self.stream_id, &self.topic_id)?;
shard
- .permissioner
- .borrow()
- .update_topic(session.get_user_id(), stream_id, topic_id)?;
+ .metadata
+ .perm_update_topic(session.get_user_id(), stream_id, topic_id)?;
let request = ShardRequest {
stream_id: Identifier::default(),
@@ -90,6 +86,10 @@ impl ServerCommandHandler for UpdateTopic {
..
} = payload
{
+ // Get numeric IDs BEFORE update (name might change during update)
+ let (stream_id_num, topic_id_num) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+
shard.update_topic(
&stream_id,
&topic_id,
@@ -100,38 +100,14 @@ impl ServerCommandHandler for UpdateTopic {
replication_factor,
)?;
- let name_changed = !name.is_empty();
- let lookup_topic_id = if name_changed {
- Identifier::named(&name).unwrap()
- } else {
- topic_id.clone()
- };
-
- self.message_expiry = shard.streams.with_topic_by_id(
- &stream_id,
- &lookup_topic_id,
- topics::helpers::get_message_expiry(),
- );
- self.max_topic_size = shard.streams.with_topic_by_id(
- &stream_id,
- &lookup_topic_id,
- topics::helpers::get_max_topic_size(),
- );
-
- let stream_id_num = shard
- .streams
- .with_stream_by_id(&stream_id, streams::helpers::get_stream_id());
-
- let event = ShardEvent::UpdatedTopic {
- stream_id: stream_id.clone(),
- topic_id: topic_id.clone(),
- name: name.clone(),
- message_expiry: self.message_expiry,
- compression_algorithm: self.compression_algorithm,
- max_topic_size: self.max_topic_size,
- replication_factor: self.replication_factor,
- };
- shard.broadcast_event_to_all_shards(event).await?;
+ let (expiry, max_size) = shard
+ .metadata
+ .get_topic_config(stream_id_num, topic_id_num)
+ .ok_or_else(|| {
+ IggyError::TopicIdNotFound(topic_id.clone(), stream_id.clone())
+ })?;
+ self.message_expiry = expiry;
+ self.max_topic_size = max_size;
shard
.state
@@ -149,18 +125,6 @@ impl ServerCommandHandler for UpdateTopic {
}
ShardSendRequestResult::Response(response) => match response {
ShardResponse::UpdateTopicResponse => {
- let stream_id = shard
- .streams
- .with_stream_by_id(&self.stream_id, streams::helpers::get_stream_id());
-
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::UpdateTopic(self))
- .await
- .error(|e: &IggyError| format!(
- "{COMPONENT} (error: {e}) - failed to apply update topic in stream with ID: {stream_id}, session: {session}"
- ))?;
-
sender.send_empty_ok_response().await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/users/change_password_handler.rs b/core/server/src/binary/handlers/users/change_password_handler.rs
index ec8c80ece..a7657cbf0 100644
--- a/core/server/src/binary/handlers/users/change_password_handler.rs
+++ b/core/server/src/binary/handlers/users/change_password_handler.rs
@@ -21,16 +21,17 @@ use crate::binary::command::{
};
use crate::binary::handlers::users::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
use crate::streaming::utils::crypto;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::change_password::ChangePassword;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use std::rc::Rc;
use tracing::info;
use tracing::{debug, instrument};
@@ -54,50 +55,87 @@ impl ServerCommandHandler for ChangePassword {
// Check if user is changing someone else's password
let target_user = shard.get_user(&self.user_id)?;
if target_user.id != session.get_user_id() {
- shard
- .permissioner
- .borrow()
- .change_password(session.get_user_id())?;
+ shard.metadata.perm_change_password(session.get_user_id())?;
}
- info!("Changing password for user with ID: {}...", self.user_id);
- shard
- .change_password(&self.user_id, &self.current_password, &self.new_password)
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to change password for user_id: {}, session: {session}",
- self.user_id
- )
- })?;
+ let user_id_for_log = self.user_id.clone();
+ let new_password_hash = crypto::hash_password(&self.new_password);
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::ChangePassword {
+ session_user_id: session.get_user_id(),
+ user_id: self.user_id.clone(),
+ current_password: self.current_password.clone(),
+ new_password: self.new_password.clone(),
+ },
+ };
- info!("Changed password for user with ID: {}.", self.user_id);
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::ChangePassword {
+ user_id,
+ current_password,
+ new_password,
+ ..
+ } = payload
+ {
+ info!("Changing password for user with ID: {}...", user_id);
- let event = ShardEvent::ChangedPassword {
- user_id: self.user_id.clone(),
- current_password: self.current_password.clone(),
- new_password: self.new_password.clone(),
- };
- shard.broadcast_event_to_all_shards(event).await?;
+ let _user_guard = shard.fs_locks.user_lock.lock().await;
+ shard
+ .change_password(&user_id, ¤t_password, &new_password)
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to change password for user_id: {}, session: {session}",
+ user_id
+ )
+ })?;
+
+ info!("Changed password for user with ID: {}.", user_id_for_log);
+
+ shard
+ .state
+ .apply(
+ session.get_user_id(),
+ &EntryCommand::ChangePassword(ChangePassword {
+ user_id: user_id_for_log.clone(),
+ current_password: "".into(),
+ new_password: new_password_hash.clone(),
+ }),
+ )
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply change password for user_id: {}, session: {session}",
+ user_id_for_log
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected a ChangePassword request inside of ChangePassword handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::ChangePasswordResponse => {
+ info!("Changed password for user with ID: {}.", user_id_for_log);
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected a ChangePasswordResponse inside of ChangePassword handler, impossible state"
+ ),
+ },
+ }
- // For the security of the system, we hash the password before storing it in metadata.
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::ChangePassword(ChangePassword {
- user_id: self.user_id.to_owned(),
- current_password: "".into(),
- new_password: crypto::hash_password(&self.new_password),
- }),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply change password for user_id: {}, session: {session}",
- self.user_id
- )
- })?;
- sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/users/create_user_handler.rs b/core/server/src/binary/handlers/users/create_user_handler.rs
index c49180b4b..abd973029 100644
--- a/core/server/src/binary/handlers/users/create_user_handler.rs
+++ b/core/server/src/binary/handlers/users/create_user_handler.rs
@@ -23,7 +23,6 @@ use crate::binary::handlers::users::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
@@ -32,7 +31,6 @@ use crate::state::command::EntryCommand;
use crate::state::models::CreateUserWithId;
use crate::streaming::session::Session;
use crate::streaming::utils::crypto;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::create_user::CreateUser;
use iggy_common::{Identifier, IggyError, SenderKind};
@@ -55,10 +53,7 @@ impl ServerCommandHandler for CreateUser {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .create_user(session.get_user_id())?;
+ shard.metadata.perm_create_user(session.get_user_id())?;
let request = ShardRequest {
stream_id: Identifier::default(),
@@ -96,16 +91,6 @@ impl ServerCommandHandler for CreateUser {
})?;
let user_id = user.id;
-
- let event = ShardEvent::CreatedUser {
- user_id,
- username: username.clone(),
- password: password.clone(),
- status,
- permissions: permissions.clone(),
- };
- shard.broadcast_event_to_all_shards(event).await?;
-
let response = mapper::map_user(&user);
shard
@@ -139,30 +124,7 @@ impl ServerCommandHandler for CreateUser {
}
ShardSendRequestResult::Response(response) => match response {
ShardResponse::CreateUserResponse(user) => {
- let user_id = user.id;
let response = mapper::map_user(&user);
-
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::CreateUser(CreateUserWithId {
- user_id,
- command: CreateUser {
- username: self.username.to_owned(),
- password: crypto::hash_password(&self.password),
- status: self.status,
- permissions: self.permissions.clone(),
- },
- }),
- )
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply create user for user_id: {user_id}, session: {session}"
- )
- })?;
-
sender.send_ok_response(&response).await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/users/delete_user_handler.rs b/core/server/src/binary/handlers/users/delete_user_handler.rs
index a1d5c3538..375aa9f06 100644
--- a/core/server/src/binary/handlers/users/delete_user_handler.rs
+++ b/core/server/src/binary/handlers/users/delete_user_handler.rs
@@ -16,26 +16,22 @@
* under the License.
*/
-use std::rc::Rc;
-
use crate::binary::command::{
BinaryServerCommand, HandlerResult, ServerCommand, ServerCommandHandler,
};
use crate::binary::handlers::users::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
use crate::shard::transmission::frame::ShardResponse;
use crate::shard::transmission::message::{
ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::delete_user::DeleteUser;
use iggy_common::{Identifier, IggyError, SenderKind};
+use std::rc::Rc;
use tracing::info;
use tracing::{debug, instrument};
@@ -54,10 +50,7 @@ impl ServerCommandHandler for DeleteUser {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .delete_user(session.get_user_id())?;
+ shard.metadata.perm_delete_user(session.get_user_id())?;
let request = ShardRequest {
stream_id: Identifier::default(),
@@ -86,8 +79,6 @@ impl ServerCommandHandler for DeleteUser {
})?;
info!("Deleted user: {} with ID: {}.", user.username, user.id);
- let event = ShardEvent::DeletedUser { user_id };
- shard.broadcast_event_to_all_shards(event).await?;
shard
.state
@@ -104,18 +95,7 @@ impl ServerCommandHandler for DeleteUser {
}
}
ShardSendRequestResult::Response(response) => match response {
- ShardResponse::DeletedUser(user) => {
- shard
- .state
- .apply(user.id, &EntryCommand::DeleteUser(DeleteUser { user_id: user.id.try_into().unwrap() }))
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply delete user with ID: {user_id}, session: {session}",
- user_id = user.id,
- session = session
- )
- })?;
+ ShardResponse::DeletedUser(_user) => {
sender.send_empty_ok_response().await?;
}
ShardResponse::ErrorResponse(err) => {
diff --git a/core/server/src/binary/handlers/users/get_user_handler.rs b/core/server/src/binary/handlers/users/get_user_handler.rs
index 3dc2e6bf1..c9d9c62d3 100644
--- a/core/server/src/binary/handlers/users/get_user_handler.rs
+++ b/core/server/src/binary/handlers/users/get_user_handler.rs
@@ -55,10 +55,7 @@ impl ServerCommandHandler for GetUser {
// Permission check: only required if user is looking for someone else
if user.id != session.get_user_id() {
- shard
- .permissioner
- .borrow()
- .get_user(session.get_user_id())?;
+ shard.metadata.perm_get_user(session.get_user_id())?;
}
let bytes = mapper::map_user(&user);
diff --git a/core/server/src/binary/handlers/users/get_users_handler.rs b/core/server/src/binary/handlers/users/get_users_handler.rs
index d783656e9..32ed6039d 100644
--- a/core/server/src/binary/handlers/users/get_users_handler.rs
+++ b/core/server/src/binary/handlers/users/get_users_handler.rs
@@ -44,10 +44,7 @@ impl ServerCommandHandler for GetUsers {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .get_users(session.get_user_id())?;
+ shard.metadata.perm_get_users(session.get_user_id())?;
let users = shard.get_users();
let users = mapper::map_users(users);
sender.send_ok_response(&users).await?;
diff --git a/core/server/src/binary/handlers/users/login_user_handler.rs b/core/server/src/binary/handlers/users/login_user_handler.rs
index 550be02b7..89a981c2a 100644
--- a/core/server/src/binary/handlers/users/login_user_handler.rs
+++ b/core/server/src/binary/handlers/users/login_user_handler.rs
@@ -24,7 +24,6 @@ use crate::binary::handlers::utils::receive_and_validate;
use crate::binary::mapper;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::login_user::LoginUser;
use iggy_common::{IggyError, SenderKind};
diff --git a/core/server/src/binary/handlers/users/logout_user_handler.rs b/core/server/src/binary/handlers/users/logout_user_handler.rs
index f09a20b36..39689cfa6 100644
--- a/core/server/src/binary/handlers/users/logout_user_handler.rs
+++ b/core/server/src/binary/handlers/users/logout_user_handler.rs
@@ -26,7 +26,6 @@ use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::logout_user::LogoutUser;
use iggy_common::{IggyError, SenderKind};
diff --git a/core/server/src/binary/handlers/users/update_permissions_handler.rs b/core/server/src/binary/handlers/users/update_permissions_handler.rs
index 5e6ee02f9..92093f158 100644
--- a/core/server/src/binary/handlers/users/update_permissions_handler.rs
+++ b/core/server/src/binary/handlers/users/update_permissions_handler.rs
@@ -23,15 +23,16 @@ use crate::binary::command::{
};
use crate::binary::handlers::users::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
-
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::update_permissions::UpdatePermissions;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use tracing::info;
use tracing::{debug, instrument};
@@ -51,9 +52,8 @@ impl ServerCommandHandler for UpdatePermissions {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
shard
- .permissioner
- .borrow()
- .update_permissions(session.get_user_id())?;
+ .metadata
+ .perm_update_permissions(session.get_user_id())?;
// Check if target user is root - cannot change root user permissions
let target_user = shard.get_user(&self.user_id)?;
@@ -61,29 +61,72 @@ impl ServerCommandHandler for UpdatePermissions {
return Err(IggyError::CannotChangePermissions(target_user.id));
}
- shard
- .update_permissions(&self.user_id, self.permissions.clone())
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to update permissions for user_id: {}, session: {session}",
- self.user_id
- )
- })?;
- info!("Updated permissions for user with ID: {}.", self.user_id);
- let event = ShardEvent::UpdatedPermissions {
- user_id: self.user_id.clone(),
- permissions: self.permissions.clone(),
+ let user_id_for_log = self.user_id.clone();
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::UpdatePermissions {
+ session_user_id: session.get_user_id(),
+ user_id: self.user_id.clone(),
+ permissions: self.permissions.clone(),
+ },
};
- shard.broadcast_event_to_all_shards(event).await?;
- shard
- .state
- .apply(
- session.get_user_id(),
- &EntryCommand::UpdatePermissions(self),
- )
- .await?;
- sender.send_empty_ok_response().await?;
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::UpdatePermissions {
+ user_id,
+ permissions,
+ ..
+ } = payload
+ {
+ let _user_guard = shard.fs_locks.user_lock.lock().await;
+ shard
+ .update_permissions(&user_id, permissions)
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to update permissions for user_id: {}, session: {session}",
+ user_id
+ )
+ })?;
+
+ info!("Updated permissions for user with ID: {}.", user_id_for_log);
+
+ shard
+ .state
+ .apply(session.get_user_id(), &EntryCommand::UpdatePermissions(self))
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply update permissions for user_id: {}, session: {session}",
+ user_id_for_log
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected an UpdatePermissions request inside of UpdatePermissions handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::UpdatePermissionsResponse => {
+ info!("Updated permissions for user with ID: {}.", user_id_for_log);
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected an UpdatePermissionsResponse inside of UpdatePermissions handler, impossible state"
+ ),
+ },
+ }
+
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/handlers/users/update_user_handler.rs b/core/server/src/binary/handlers/users/update_user_handler.rs
index 349d2bfe0..2e457e8a8 100644
--- a/core/server/src/binary/handlers/users/update_user_handler.rs
+++ b/core/server/src/binary/handlers/users/update_user_handler.rs
@@ -25,13 +25,15 @@ use crate::binary::handlers::users::COMPONENT;
use crate::binary::handlers::utils::receive_and_validate;
use crate::shard::IggyShard;
-use crate::shard::transmission::event::ShardEvent;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::state::command::EntryCommand;
use crate::streaming::session::Session;
-use anyhow::Result;
use err_trail::ErrContext;
use iggy_common::update_user::UpdateUser;
-use iggy_common::{IggyError, SenderKind};
+use iggy_common::{Identifier, IggyError, SenderKind};
use tracing::info;
use tracing::{debug, instrument};
@@ -50,40 +52,83 @@ impl ServerCommandHandler for UpdateUser {
) -> Result<HandlerResult, IggyError> {
debug!("session: {session}, command: {self}");
shard.ensure_authenticated(session)?;
- shard
- .permissioner
- .borrow()
- .update_user(session.get_user_id())?;
+ shard.metadata.perm_update_user(session.get_user_id())?;
- let user = shard
- .update_user(&self.user_id, self.username.clone(), self.status)
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to update user with user_id: {}, session: {session}",
- self.user_id
- )
- })?;
+ let user_id_for_log = self.user_id.clone();
+ let request = ShardRequest {
+ stream_id: Identifier::default(),
+ topic_id: Identifier::default(),
+ partition_id: 0,
+ payload: ShardRequestPayload::UpdateUser {
+ session_user_id: session.get_user_id(),
+ user_id: self.user_id.clone(),
+ username: self.username.clone(),
+ status: self.status,
+ },
+ };
- info!("Updated user: {} with ID: {}.", user.username, user.id);
+ let message = ShardMessage::Request(request);
+ match shard.send_request_to_shard_or_recoil(None, message).await? {
+ ShardSendRequestResult::Recoil(message) => {
+ if let ShardMessage::Request(ShardRequest { payload, .. }) = message
+ && let ShardRequestPayload::UpdateUser {
+ user_id,
+ username,
+ status,
+ ..
+ } = payload
+ {
+ let _user_guard = shard.fs_locks.user_lock.lock().await;
+ let user = shard
+ .update_user(&user_id, username.clone(), status)
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to update user with user_id: {}, session: {session}",
+ user_id
+ )
+ })?;
- let event = ShardEvent::UpdatedUser {
- user_id: self.user_id.clone(),
- username: self.username.clone(),
- status: self.status,
- };
- shard.broadcast_event_to_all_shards(event).await?;
+ info!("Updated user: {} with ID: {}.", user.username, user.id);
+
+ shard
+ .state
+ .apply(
+ session.get_user_id(),
+ &EntryCommand::UpdateUser(UpdateUser {
+ user_id: user_id_for_log.clone(),
+ username,
+ status,
+ }),
+ )
+ .await
+ .error(|e: &IggyError| {
+ format!(
+ "{COMPONENT} (error: {e}) - failed to apply update user with user_id: {}, session: {session}",
+ user_id_for_log
+ )
+ })?;
+
+ sender.send_empty_ok_response().await?;
+ } else {
+ unreachable!(
+ "Expected an UpdateUser request inside of UpdateUser handler, impossible state"
+ );
+ }
+ }
+ ShardSendRequestResult::Response(response) => match response {
+ ShardResponse::UpdateUserResponse(user) => {
+ info!("Updated user: {} with ID: {}.", user.username, user.id);
+ sender.send_empty_ok_response().await?;
+ }
+ ShardResponse::ErrorResponse(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(
+ "Expected an UpdateUserResponse inside of UpdateUser handler, impossible state"
+ ),
+ },
+ }
- let user_id = self.user_id.clone();
- shard
- .state
- .apply(session.get_user_id(), &EntryCommand::UpdateUser(self))
- .await
- .error(|e: &IggyError| {
- format!(
- "{COMPONENT} (error: {e}) - failed to apply update user with user_id: {user_id}, session: {session}"
- )
- })?;
- sender.send_empty_ok_response().await?;
Ok(HandlerResult::Finished)
}
}
diff --git a/core/server/src/binary/macros.rs b/core/server/src/binary/macros.rs
index f69632c4c..22e5a6677 100644
--- a/core/server/src/binary/macros.rs
+++ b/core/server/src/binary/macros.rs
@@ -81,7 +81,7 @@ macro_rules! define_server_command_enum {
}
}
- /// Validate the command by delegating to the inner command’s implementation.
+ /// Validate the command by delegating to the inner command's implementation.
pub fn validate(&self) -> Result<(), IggyError> {
match self {
$(
diff --git a/core/server/src/binary/mapper.rs b/core/server/src/binary/mapper.rs
index 6e98081d6..89dac029b 100644
--- a/core/server/src/binary/mapper.rs
+++ b/core/server/src/binary/mapper.rs
@@ -16,23 +16,13 @@
* under the License.
*/
-use std::sync::{Arc, atomic::AtomicU64};
-
-use crate::slab::Keyed;
-use crate::slab::traits_ext::{EntityComponentSystem, IntoComponents};
+use crate::metadata::ConsumerGroupMeta;
use crate::streaming::clients::client_manager::Client;
-use crate::streaming::partitions::partition::PartitionRoot;
-use crate::streaming::stats::{PartitionStats, StreamStats, TopicStats};
-use crate::streaming::streams::stream;
-use crate::streaming::topics::consumer_group::{ConsumerGroupMembers, ConsumerGroupRoot, Member};
-use crate::streaming::topics::topic::{self, TopicRoot};
use crate::streaming::users::user::User;
-use arcshift::SharedGetGuard;
use bytes::{BufMut, Bytes, BytesMut};
use iggy_common::{
BytesSerializable, ConsumerOffsetInfo, PersonalAccessToken, Stats, TransportProtocol, UserId,
};
-use slab::Slab;
pub fn map_stats(stats: &Stats) -> Bytes {
let mut bytes = BytesMut::with_capacity(104);
@@ -153,145 +143,29 @@ pub fn map_personal_access_tokens(personal_access_tokens: Vec<PersonalAccessToke
bytes.freeze()
}
-pub fn map_streams(roots: &Slab<stream::StreamRoot>, stats: &Slab<Arc<StreamStats>>) -> Bytes {
+/// Map consumer group from SharedMetadata format.
+pub fn map_consumer_group_from_meta(meta: &ConsumerGroupMeta) -> Bytes {
let mut bytes = BytesMut::new();
- for (root, stat) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(stats.iter().map(|(_, val)| val))
- {
- extend_stream(root, stat, &mut bytes);
- }
- bytes.freeze()
-}
-pub fn map_stream(root: &stream::StreamRoot, stats: &StreamStats) -> Bytes {
- let mut bytes = BytesMut::new();
- extend_stream(root, stats, &mut bytes);
- root.topics().with_components(|topics| {
- let (roots, _, stats, ..) = topics.into_components();
- for (root, stat) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(stats.iter().map(|(_, val)| val))
- {
- extend_topic(root, stat, &mut bytes);
- }
- });
- bytes.freeze()
-}
+ // Header: id, partitions_count, members_count, name_len, name
+ bytes.put_u32_le(meta.id as u32);
+ bytes.put_u32_le(meta.partitions.len() as u32);
+ bytes.put_u32_le(meta.members.len() as u32);
+ bytes.put_u8(meta.name.len() as u8);
+ bytes.put_slice(meta.name.as_bytes());
-pub fn map_topics(roots: &Slab<TopicRoot>, stats: &Slab<Arc<TopicStats>>) -> Bytes {
- let mut bytes = BytesMut::new();
- for (root, stat) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(stats.iter().map(|(_, val)| val))
- {
- extend_topic(root, stat, &mut bytes);
- }
- bytes.freeze()
-}
-
-pub fn map_topic(root: &topic::TopicRoot, stats: &TopicStats) -> Bytes {
- let mut bytes = BytesMut::new();
- extend_topic(root, stats, &mut bytes);
- root.partitions().with_components(|partitions| {
- let (roots, stats, _, offsets, _, _, _) = partitions.into_components();
- for (root, stat, offset) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(stats.iter().map(|(_, val)| val))
- .zip(offsets.iter().map(|(_, val)| val))
- .map(|((root, stat), offset)| (root, stat, offset))
- {
- extend_partition(root, stat, offset, &mut bytes);
- }
- });
-
- bytes.freeze()
-}
-
-pub fn map_consumer_group(root: &ConsumerGroupRoot, members: &ConsumerGroupMembers) -> Bytes {
- let mut bytes = BytesMut::new();
- let members = members.inner().shared_get();
- extend_consumer_group(root, &members, &mut bytes);
-
- for (_, member) in members.iter() {
+ // Members
+ for (_, member) in meta.members.iter() {
bytes.put_u32_le(member.id as u32);
bytes.put_u32_le(member.partitions.len() as u32);
- for partition in &member.partitions {
- bytes.put_u32_le(*partition as u32);
+ for &partition_id in &member.partitions {
+ bytes.put_u32_le(partition_id as u32);
}
}
- bytes.freeze()
-}
-pub fn map_consumer_groups(
- roots: &Slab<ConsumerGroupRoot>,
- members: &Slab<ConsumerGroupMembers>,
-) -> Bytes {
- let mut bytes = BytesMut::new();
- for (root, member) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(members.iter().map(|(_, val)| val.inner().shared_get()))
- {
- extend_consumer_group(root, &member, &mut bytes);
- }
bytes.freeze()
}
-fn extend_stream(root: &stream::StreamRoot, stats: &StreamStats, bytes: &mut BytesMut) {
- bytes.put_u32_le(root.id() as u32);
- bytes.put_u64_le(root.created_at().into());
- bytes.put_u32_le(root.topics_count() as u32);
- bytes.put_u64_le(stats.size_bytes_inconsistent());
- bytes.put_u64_le(stats.messages_count_inconsistent());
- bytes.put_u8(root.name().len() as u8);
- bytes.put_slice(root.name().as_bytes());
-}
-
-fn extend_topic(root: &TopicRoot, stats: &TopicStats, bytes: &mut BytesMut) {
- bytes.put_u32_le(root.id() as u32);
- bytes.put_u64_le(root.created_at().into());
- bytes.put_u32_le(root.partitions().len() as u32);
- bytes.put_u64_le(root.message_expiry().into());
- bytes.put_u8(root.compression_algorithm().as_code());
- bytes.put_u64_le(root.max_topic_size().into());
- bytes.put_u8(root.replication_factor());
- bytes.put_u64_le(stats.size_bytes_inconsistent());
- bytes.put_u64_le(stats.messages_count_inconsistent());
- bytes.put_u8(root.name().len() as u8);
- bytes.put_slice(root.name().as_bytes());
-}
-
-fn extend_partition(
- root: &PartitionRoot,
- stats: &PartitionStats,
- offset: &Arc<AtomicU64>,
- bytes: &mut BytesMut,
-) {
- bytes.put_u32_le(root.id() as u32);
- bytes.put_u64_le(root.created_at().into());
- bytes.put_u32_le(stats.segments_count_inconsistent());
- bytes.put_u64_le(offset.load(std::sync::atomic::Ordering::Relaxed));
- bytes.put_u64_le(stats.size_bytes_inconsistent());
- bytes.put_u64_le(stats.messages_count_inconsistent());
-}
-
-fn extend_consumer_group(
- root: &ConsumerGroupRoot,
- members: &SharedGetGuard<'_, Slab<Member>>,
- bytes: &mut BytesMut,
-) {
- bytes.put_u32_le(root.id() as u32);
- bytes.put_u32_le(root.partitions().len() as u32);
- bytes.put_u32_le(members.len() as u32);
- bytes.put_u8(root.key().len() as u8);
- bytes.put_slice(root.key().as_bytes());
-}
-
fn extend_client(client: &Client, bytes: &mut BytesMut) {
bytes.put_u32_le(client.session.client_id);
bytes.put_u32_le(client.user_id.unwrap_or(u32::MAX));
diff --git a/core/server/src/bootstrap.rs b/core/server/src/bootstrap.rs
index 13690a813..12174ed4b 100644
--- a/core/server/src/bootstrap.rs
+++ b/core/server/src/bootstrap.rs
@@ -27,6 +27,7 @@ use crate::{
system::{INDEX_EXTENSION, LOG_EXTENSION, SystemConfig},
},
io::fs_utils::{self, DirEntry},
+ metadata::{ConsumerGroupMeta, MetadataWriter, PartitionMeta, StreamMeta, TopicMeta, UserMeta},
server_error::ServerError,
shard::{
system::info::SystemInfo,
@@ -35,33 +36,21 @@ use crate::{
frame::ShardFrame,
},
},
- slab::{
- consumer_groups::ConsumerGroups, partitions::Partitions, streams::Streams, topics::Topics,
- traits_ext::IntoComponents, users::Users,
- },
- state::system::{StreamState, UserState},
+ state::system::{StreamState, TopicState, UserState},
streaming::{
partitions::{
- consumer_offset::ConsumerOffset,
- helpers::create_message_deduplicator,
- journal::MemoryMessageJournal,
- log::SegmentedLog,
- partition,
- storage::{load_consumer_group_offsets, load_consumer_offsets},
+ consumer_group_offsets::ConsumerGroupOffsets, consumer_offsets::ConsumerOffsets,
+ journal::MemoryMessageJournal, log::SegmentedLog,
},
persistence::persister::{FilePersister, FileWithSyncPersister, PersisterKind},
- polling_consumer::ConsumerGroupId,
segments::{Segment, storage::Storage},
stats::{PartitionStats, StreamStats, TopicStats},
storage::SystemStorage,
- streams::stream,
- topics::{consumer_group, topic},
users::user::User,
utils::{crypto, file::overwrite},
},
versioning::SemanticVersion,
};
-use ahash::HashMap;
use compio::{fs::create_dir_all, runtime::Runtime};
use err_trail::ErrContext;
use iggy_common::{
@@ -71,157 +60,10 @@ use iggy_common::{
MIN_USERNAME_LENGTH,
},
};
+use slab::Slab;
use std::{env, path::Path, sync::Arc};
use tracing::{info, warn};
-pub async fn load_streams(
- state: impl IntoIterator<Item = StreamState>,
- config: &SystemConfig,
-) -> Result<Streams, IggyError> {
- let state: Vec<StreamState> = state.into_iter().collect();
- let mut stream_entries = Vec::with_capacity(state.len());
-
- for stream_state in state {
- let stream_id = stream_state.id as usize;
- info!(
- "Loading stream with ID: {}, name: {} from state...",
- stream_state.id, stream_state.name
- );
-
- let stream_stats = Arc::new(StreamStats::default());
- let mut topic_entries = Vec::new();
-
- for topic_state in stream_state.topics.into_values() {
- let topic_id = topic_state.id as usize;
- info!(
- "Loading topic with ID: {}, name: {} from state...",
- topic_state.id, topic_state.name
- );
-
- let topic_stats = Arc::new(TopicStats::new(stream_stats.clone()));
-
- // Build partitions
- let mut partition_entries = Vec::new();
- for partition_state in topic_state.partitions.into_values() {
- let partition_id = partition_state.id as usize;
- info!(
- "Loading partition with ID: {}, for topic with ID: {} from state...",
- partition_id, topic_id
- );
-
- let partition = load_partition(
- config,
- stream_id,
- topic_id,
- partition_state,
- topic_stats.clone(),
- )
- .await?;
- partition_entries.push((partition_id, partition));
-
- info!(
- "Loaded partition with ID: {}, for topic with ID: {} from state...",
- partition_id, topic_id
- );
- }
-
- // Build consumer groups
- let partition_ids: Vec<_> = partition_entries.iter().map(|(id, _)| *id).collect();
- let cg_entries: Vec<_> = topic_state
- .consumer_groups
- .into_values()
- .map(|cg_state| {
- info!(
- "Loading consumer group with ID: {}, name: {} for topic with ID: {} from state...",
- cg_state.id, cg_state.name, topic_id
- );
- let cg = consumer_group::ConsumerGroup::new(
- cg_state.name.clone(),
- Default::default(),
- partition_ids.clone(),
- );
- info!(
- "Loaded consumer group with ID: {}, name: {} for topic with ID: {} from state...",
- cg_state.id, cg_state.name, topic_id
- );
- (cg_state.id as usize, cg)
- })
- .collect();
-
- // Build topic with pre-built partitions and consumer groups
- let mut topic = topic::Topic::new(
- topic_state.name.clone(),
- topic_stats,
- topic_state.created_at,
- topic_state.replication_factor.unwrap_or(1),
- topic_state.message_expiry,
- topic_state.compression_algorithm,
- topic_state.max_topic_size,
- );
-
- // Decompose, set nested containers, recompose
- let (mut root, auxilary, stats) = topic.into_components();
- root.set_partitions(Partitions::from_entries(partition_entries));
- root.set_consumer_groups(ConsumerGroups::from_entries(cg_entries));
- topic = topic::Topic::new_with_components(root, auxilary, stats);
-
- topic_entries.push((topic_id, topic));
- info!(
- "Loaded topic with ID: {}, name: {} from state...",
- topic_state.id, topic_state.name
- );
- }
-
- // Build stream with pre-built topics
- let mut stream = stream::Stream::new(
- stream_state.name.clone(),
- stream_stats,
- stream_state.created_at,
- );
-
- // Decompose, set nested containers, recompose
- let (mut root, stats) = stream.into_components();
- root.set_topics(Topics::from_entries(topic_entries));
- stream = stream::Stream::new_with_components(root, stats);
-
- stream_entries.push((stream_id, stream));
- info!(
- "Loaded stream with ID: {}, name: {} from state...",
- stream_state.id, stream_state.name
- );
- }
-
- Ok(Streams::from_entries(stream_entries))
-}
-
-pub fn load_users(state: impl IntoIterator<Item = UserState>) -> Users {
- let users = Users::new();
- for user_state in state {
- let UserState {
- id,
- username,
- password_hash,
- status,
- created_at,
- permissions,
- personal_access_tokens,
- } = user_state;
- let mut user = User::with_password(id, &username, password_hash, status, permissions);
- user.created_at = created_at;
- user.personal_access_tokens = personal_access_tokens
- .into_values()
- .map(|token| {
- (
- Arc::from(token.token_hash.as_str()),
- PersonalAccessToken::raw(id, &token.name, &token.token_hash, token.expiry_at),
- )
- })
- .collect();
- users.insert(user);
- }
- users
-}
-
pub fn create_shard_connections(
shard_assignment: &[ShardInfo],
) -> (Vec<ShardConnector<ShardFrame>>, Vec<(u16, StopSender)>) {
@@ -620,86 +462,215 @@ pub async fn load_segments(
Ok(log)
}
-async fn load_partition(
- config: &SystemConfig,
- stream_id: usize,
- topic_id: usize,
- partition_state: crate::state::system::PartitionState,
- parent_stats: Arc<TopicStats>,
-) -> Result<partition::Partition, IggyError> {
- let stats = Arc::new(PartitionStats::new(parent_stats));
- let partition_id = partition_state.id;
-
- let partition_path = config.get_partition_path(stream_id, topic_id, partition_id as usize);
- let log_files = collect_log_files(&partition_path).await?;
- let should_increment_offset = !log_files.is_empty()
- && log_files
- .first()
- .map(|entry| {
- let log_file_name = entry
- .path
- .file_stem()
- .unwrap()
- .to_string_lossy()
- .to_string();
-
- let start_offset = log_file_name.parse::<u64>().unwrap();
-
- let messages_file_path = config.get_messages_file_path(
- stream_id,
- topic_id,
- partition_id as usize,
- start_offset,
- );
- let metadata = std::fs::metadata(&messages_file_path)
- .expect("failed to get metadata for first segment in log");
- metadata.len() > 0
- })
- .unwrap_or_else(|| false);
+/// Builds `InnerMetadata` from persisted user and stream state.
+pub fn build_inner_metadata(
+ users_state: impl IntoIterator<Item = UserState>,
+ streams_state: impl IntoIterator<Item = StreamState>,
+) -> crate::metadata::InnerMetadata {
+ use crate::metadata::InnerMetadata;
+ use std::sync::atomic::AtomicUsize;
+
+ let mut user_entries = Vec::new();
+ let mut user_index = ahash::AHashMap::default();
+ let mut personal_access_tokens: ahash::AHashMap<
+ u32,
+ ahash::AHashMap<Arc<str>, PersonalAccessToken>,
+ > = ahash::AHashMap::default();
+ let mut users_count = 0;
+ let mut pats_count = 0;
+
+ for UserState {
+ id,
+ username,
+ password_hash,
+ status,
+ created_at,
+ permissions,
+ personal_access_tokens: user_pats,
+ } in users_state
+ {
+ let username_arc: Arc<str> = Arc::from(username.as_str());
+ let user_meta = UserMeta {
+ id,
+ username: username_arc.clone(),
+ password_hash: Arc::from(password_hash.as_str()),
+ status,
+ permissions: permissions.map(Arc::new),
+ created_at,
+ };
+ user_entries.push((id as usize, user_meta));
+ user_index.insert(username_arc, id);
+
+ if !user_pats.is_empty() {
+ let user_pat_map: ahash::AHashMap<Arc<str>, PersonalAccessToken> = user_pats
+ .into_values()
+ .map(|token| {
+ let pat = PersonalAccessToken::raw(
+ id,
+ &token.name,
+ &token.token_hash,
+ token.expiry_at,
+ );
+ (Arc::from(token.token_hash.as_str()), pat)
+ })
+ .collect();
+ pats_count += user_pat_map.len();
+ personal_access_tokens.insert(id, user_pat_map);
+ }
+ users_count += 1;
+ }
info!(
- "Loading partition with ID: {} for stream with ID: {} and topic with ID: {}, for path: {} from disk...",
- partition_id, stream_id, topic_id, partition_path
+ "Building metadata: {} users, {} personal access tokens",
+ users_count, pats_count
);
- // Load consumer offsets
- let message_deduplicator = create_message_deduplicator(config);
- let consumer_offset_path =
- config.get_consumer_offsets_path(stream_id, topic_id, partition_id as usize);
- let consumer_group_offsets_path =
- config.get_consumer_group_offsets_path(stream_id, topic_id, partition_id as usize);
-
- let consumer_offset = Arc::new(
- load_consumer_offsets(&consumer_offset_path)?
- .into_iter()
- .map(|offset| (offset.consumer_id as usize, offset))
- .collect::<HashMap<usize, ConsumerOffset>>()
- .into(),
- );
+ let mut stream_entries = Vec::new();
+ let mut stream_index = ahash::AHashMap::default();
+ let mut streams_count = 0;
+ let mut topics_count = 0;
+ let mut partitions_count = 0;
+ let mut consumer_groups_count = 0;
+
+ for StreamState {
+ name,
+ created_at,
+ id,
+ topics,
+ } in streams_state
+ {
+ info!(
+ "Building stream with ID: {}, name: {} metadata...",
+ id, name
+ );
+ let stream_id = id as usize;
+ let stream_name: Arc<str> = Arc::from(name.as_str());
- let consumer_group_offset = Arc::new(
- load_consumer_group_offsets(&consumer_group_offsets_path)?
- .into_iter()
- .collect::<HashMap<ConsumerGroupId, ConsumerOffset>>()
- .into(),
- );
+ let stream_stats = Arc::new(StreamStats::default());
- let log = Default::default();
- let partition = partition::Partition::new(
- partition_state.created_at,
- should_increment_offset,
- stats,
- message_deduplicator,
- Arc::new(Default::default()),
- consumer_offset,
- consumer_group_offset,
- log,
- );
+ let mut topic_entries = Vec::new();
+ let mut topic_index = ahash::AHashMap::default();
+
+ for TopicState {
+ id,
+ name,
+ created_at,
+ compression_algorithm,
+ message_expiry,
+ max_topic_size,
+ replication_factor,
+ consumer_groups,
+ partitions,
+ } in topics.into_values()
+ {
+ info!("Building topic with ID: {}, name: {} metadata...", id, name);
+ let topic_id = id as usize;
+ let topic_name: Arc<str> = Arc::from(name.as_str());
+
+ let topic_stats = Arc::new(TopicStats::new(stream_stats.clone()));
+
+ let mut partition_entries = Vec::new();
+ let mut partition_ids = Vec::new();
+
+ for partition_state in partitions.into_values() {
+ let partition_id = partition_state.id as usize;
+ partition_ids.push(partition_id);
+
+ let partition_stats = Arc::new(PartitionStats::new(topic_stats.clone()));
+ let partition_meta = PartitionMeta {
+ id: partition_id,
+ created_at: partition_state.created_at,
+ revision_id: 0,
+ stats: partition_stats,
+ consumer_offsets: Arc::new(ConsumerOffsets::with_capacity(0)),
+ consumer_group_offsets: Arc::new(ConsumerGroupOffsets::with_capacity(0)),
+ };
+ partition_entries.push((partition_id, partition_meta));
+ partitions_count += 1;
+ }
+
+ partition_ids.sort_unstable();
+
+ let mut cg_entries = Vec::new();
+ let mut cg_index = ahash::AHashMap::default();
+
+ for cg_state in consumer_groups.into_values() {
+ info!(
+ "Building consumer group with ID: {}, name: {} for topic with ID: {} metadata...",
+ cg_state.id, cg_state.name, topic_id
+ );
+ let group_id = cg_state.id as usize;
+ let group_name: Arc<str> = Arc::from(cg_state.name.as_str());
+ let cg_meta = ConsumerGroupMeta {
+ id: group_id,
+ name: group_name.clone(),
+ partitions: partition_ids.clone(),
+ members: Slab::new(),
+ };
+ cg_entries.push((group_id, cg_meta));
+ cg_index.insert(group_name, group_id);
+ consumer_groups_count += 1;
+ }
+
+ let topic_meta = TopicMeta {
+ id: topic_id,
+ name: topic_name.clone(),
+ created_at,
+ message_expiry,
+ compression_algorithm,
+ max_topic_size,
+ replication_factor: replication_factor.unwrap_or(1),
+ stats: topic_stats,
+ partitions: partition_entries.into_iter().map(|(_, p)| p).collect(),
+ consumer_groups: cg_entries.into_iter().collect(),
+ consumer_group_index: cg_index,
+ round_robin_counter: Arc::new(AtomicUsize::new(0)),
+ };
+ topic_entries.push((topic_id, topic_meta));
+ topic_index.insert(topic_name, topic_id);
+ topics_count += 1;
+ }
+
+ let stream_meta = StreamMeta {
+ id: stream_id,
+ name: stream_name.clone(),
+ created_at,
+ stats: stream_stats,
+ topics: topic_entries.into_iter().collect(),
+ topic_index,
+ };
+ stream_entries.push((stream_id, stream_meta));
+ stream_index.insert(stream_name, stream_id);
+ streams_count += 1;
+ }
info!(
- "Loaded partition with ID: {} for stream with ID: {} and topic with ID: {}",
- partition_id, stream_id, topic_id
+ "Built metadata: {} streams, {} topics, {} partitions, {} consumer groups",
+ streams_count, topics_count, partitions_count, consumer_groups_count
);
- Ok(partition)
+ InnerMetadata {
+ streams: stream_entries.into_iter().collect(),
+ users: user_entries.into_iter().collect(),
+ stream_index,
+ user_index,
+ personal_access_tokens,
+ users_global_permissions: Default::default(),
+ users_stream_permissions: Default::default(),
+ users_can_poll_all_streams: Default::default(),
+ users_can_send_all_streams: Default::default(),
+ users_can_poll_stream: Default::default(),
+ users_can_send_stream: Default::default(),
+ }
+}
+
+/// Loads all metadata from persisted state into metadata writer.
+/// Single atomic initialization via `Initialize` operation.
+pub fn load_metadata(
+ users_state: impl IntoIterator<Item = UserState>,
+ streams_state: impl IntoIterator<Item = StreamState>,
+ writer: &mut MetadataWriter,
+) {
+ let inner = build_inner_metadata(users_state, streams_state);
+ writer.initialize(inner);
}
diff --git a/core/server/src/configs/sharding.rs b/core/server/src/configs/sharding.rs
index c8f289e4f..7891b8161 100644
--- a/core/server/src/configs/sharding.rs
+++ b/core/server/src/configs/sharding.rs
@@ -101,17 +101,6 @@ impl NumaConfig {
Ok(())
}
-
- // pub fn or_fallback(&self, topology: &NumaTopology) -> CpuAllocation {
- // if topology.node_count < 2 {
- // tracing::warn!(
- // "NUMA requested but only {} node detected, falling back to count",
- // topology.node_count
- // );
- // }
- //
- // todo!()
- // }
}
impl CpuAllocation {
diff --git a/core/server/src/configs/system.rs b/core/server/src/configs/system.rs
index c1b089a79..e3e39f317 100644
--- a/core/server/src/configs/system.rs
+++ b/core/server/src/configs/system.rs
@@ -16,13 +16,9 @@
* under the License.
*/
-use crate::configs::server::MemoryPoolConfig;
-use crate::slab::partitions;
-use crate::slab::streams;
-use crate::slab::topics;
-
use super::cache_indexes::CacheIndexesConfig;
use super::sharding::ShardingConfig;
+use crate::configs::server::MemoryPoolConfig;
use iggy_common::IggyByteSize;
use iggy_common::IggyError;
use iggy_common::IggyExpiry;
@@ -218,9 +214,9 @@ impl SystemConfig {
pub fn get_partition_path(
&self,
- stream_id: streams::ContainerId,
- topic_id: topics::ContainerId,
- partition_id: partitions::ContainerId,
+ stream_id: usize,
+ topic_id: usize,
+ partition_id: usize,
) -> String {
format!(
"{}/{}",
diff --git a/core/server/src/http/consumer_groups.rs b/core/server/src/http/consumer_groups.rs
index 11f381921..c595de22a 100644
--- a/core/server/src/http/consumer_groups.rs
+++ b/core/server/src/http/consumer_groups.rs
@@ -21,7 +21,6 @@ use crate::http::error::CustomError;
use crate::http::jwt::json_web_token::Identity;
use crate::http::mapper;
use crate::http::shared::AppState;
-use crate::slab::traits_ext::{EntityComponentSystem, EntityMarker, IntoComponents};
use crate::state::command::EntryCommand;
use crate::state::models::CreateConsumerGroupWithId;
use crate::streaming::polling_consumer::ConsumerGroupId;
@@ -62,36 +61,25 @@ async fn get_consumer_group(
let identifier_topic_id = Identifier::from_str_value(&topic_id)?;
let identifier_group_id = Identifier::from_str_value(&group_id)?;
- let (numeric_stream_id, numeric_topic_id) = state
- .shard
- .shard()
- .resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_consumer_group(identity.user_id, numeric_stream_id, numeric_topic_id)?;
-
- let exists = state
- .shard
- .shard()
- .ensure_consumer_group_exists(
- &identifier_stream_id,
- &identifier_topic_id,
- &identifier_group_id,
- )
- .is_ok();
- if !exists {
- return Err(CustomError::ResourceNotFound);
- }
-
- let consumer_group = state.shard.shard().streams.with_consumer_group_by_id(
+ let shard = state.shard.shard();
+ let (numeric_stream_id, numeric_topic_id, numeric_group_id) = shard.resolve_consumer_group_id(
&identifier_stream_id,
&identifier_topic_id,
&identifier_group_id,
- |(root, members)| mapper::map_consumer_group(root, members),
- );
+ )?;
+
+ shard.metadata.perm_get_consumer_group(
+ identity.user_id,
+ numeric_stream_id,
+ numeric_topic_id,
+ )?;
+
+ let cg_meta = shard
+ .metadata
+ .get_consumer_group(numeric_stream_id, numeric_topic_id, numeric_group_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+
+ let consumer_group = mapper::map_consumer_group_details_from_metadata(&cg_meta);
Ok(Json(consumer_group))
}
@@ -104,27 +92,21 @@ async fn get_consumer_groups(
let identifier_stream_id = Identifier::from_str_value(&stream_id)?;
let identifier_topic_id = Identifier::from_str_value(&topic_id)?;
- let (numeric_stream_id, numeric_topic_id) = state
- .shard
- .shard()
- .resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_consumer_groups(identity.user_id, numeric_stream_id, numeric_topic_id)?;
+ let shard = state.shard.shard();
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
- let consumer_groups = state.shard.shard().streams.with_consumer_groups(
- &identifier_stream_id,
- &identifier_topic_id,
- |cgs| {
- cgs.with_components(|cgs| {
- let (roots, members) = cgs.into_components();
- mapper::map_consumer_groups(roots, members)
- })
- },
- );
+ shard.metadata.perm_get_consumer_groups(
+ identity.user_id,
+ numeric_stream_id,
+ numeric_topic_id,
+ )?;
+
+ let topic_meta = shard
+ .metadata
+ .get_topic(numeric_stream_id, numeric_topic_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+ let consumer_groups = mapper::map_consumer_groups_from_metadata(&topic_meta);
Ok(Json(consumer_groups))
}
@@ -145,50 +127,28 @@ async fn create_consumer_group(
.shard
.shard()
.resolve_topic_id(&command.stream_id, &command.topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .create_consumer_group(identity.user_id, numeric_stream_id, numeric_topic_id)?;
+ state.shard.shard().metadata.perm_create_consumer_group(
+ identity.user_id,
+ numeric_stream_id,
+ numeric_topic_id,
+ )?;
- let consumer_group = state.shard.shard().create_consumer_group(
+ let group_id = state.shard.shard().create_consumer_group(
&command.stream_id,
&command.topic_id,
command.name.clone(),
)
.error(|e: &IggyError| format!("{COMPONENT} (error: {e}) - failed to create consumer group, stream ID: {}, topic ID: {}, name: {}", stream_id, topic_id, command.name))?;
- let group_id = consumer_group.id();
-
- // Send event for consumer group creation
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::CreatedConsumerGroup {
- stream_id: command.stream_id.clone(),
- topic_id: command.topic_id.clone(),
- cg: consumer_group.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
+ let shard = state.shard.shard();
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&command.stream_id, &command.topic_id)?;
+ let cg_meta = shard
+ .metadata
+ .get_consumer_group(numeric_stream_id, numeric_topic_id, group_id)
+ .expect("Consumer group must exist after creation");
+ let consumer_group_details = mapper::map_consumer_group_details_from_metadata(&cg_meta);
- // Get the created consumer group details
- let group_id_identifier = Identifier::numeric(group_id as u32).unwrap();
- let consumer_group_details = state.shard.shard().streams.with_consumer_group_by_id(
- &command.stream_id,
- &command.topic_id,
- &group_id_identifier,
- |(root, members)| mapper::map_consumer_group(root, members),
- );
-
- // Apply state change
let entry_command = EntryCommand::CreateConsumerGroup(CreateConsumerGroupWithId {
group_id: group_id as u32,
command,
@@ -222,12 +182,11 @@ async fn delete_consumer_group(
.shard
.shard()
.resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .delete_consumer_group(identity.user_id, stream_id_numeric, topic_id_numeric)?;
+ state.shard.shard().metadata.perm_delete_consumer_group(
+ identity.user_id,
+ stream_id_numeric,
+ topic_id_numeric,
+ )?;
// Now check if consumer group exists
state.shard.shard().ensure_consumer_group_exists(
@@ -236,23 +195,24 @@ async fn delete_consumer_group(
&identifier_group_id,
)?;
- let consumer_group = state.shard.shard().delete_consumer_group(
+ let shard = state.shard.shard();
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
+
+ let cg_meta = shard.delete_consumer_group(
&identifier_stream_id,
&identifier_topic_id,
&identifier_group_id
)
.error(|e: &IggyError| format!("{COMPONENT} (error: {e}) - failed to delete consumer group with ID: {group_id} for topic with ID: {topic_id} in stream with ID: {stream_id}"))?;
- let cg_id = consumer_group.id();
+ let cg_id = cg_meta.id;
- // TODO: Tech debt, repeated code from `delete_consumer_group_handler.rs`
- // Get members from the deleted consumer group and make them leave
- let slab = consumer_group.members().inner().shared_get();
- for (_, member) in slab.iter() {
- if let Err(err) = state.shard.shard().client_manager.leave_consumer_group(
+ for (_, member) in cg_meta.members.iter() {
+ if let Err(err) = shard.client_manager.leave_consumer_group(
member.client_id,
- stream_id_numeric,
- topic_id_numeric,
+ numeric_stream_id,
+ numeric_topic_id,
cg_id,
) {
tracing::warn!(
@@ -264,13 +224,11 @@ async fn delete_consumer_group(
}
let cg_id_spez = ConsumerGroupId(cg_id);
- // Clean up consumer group offsets from all partitions using the specialized method
- let partition_ids = consumer_group.partitions();
state.shard.shard().delete_consumer_group_offsets(
cg_id_spez,
&identifier_stream_id,
&identifier_topic_id,
- partition_ids,
+ &cg_meta.partitions,
).await.error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - failed to delete consumer group offsets for group ID: {} in stream: {}, topic: {}",
@@ -280,26 +238,6 @@ async fn delete_consumer_group(
)
})?;
- // Send event for consumer group deletion
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::DeletedConsumerGroup {
- id: cg_id,
- stream_id: identifier_stream_id.clone(),
- topic_id: identifier_topic_id.clone(),
- group_id: identifier_group_id.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
- // Apply state change
let entry_command = EntryCommand::DeleteConsumerGroup(DeleteConsumerGroup {
stream_id: identifier_stream_id,
topic_id: identifier_topic_id,
diff --git a/core/server/src/http/consumer_offsets.rs b/core/server/src/http/consumer_offsets.rs
index 19fbfdb0a..ab7807d48 100644
--- a/core/server/src/http/consumer_offsets.rs
+++ b/core/server/src/http/consumer_offsets.rs
@@ -65,12 +65,11 @@ async fn get_consumer_offset(
.shard
.shard()
.resolve_topic_id(&query.stream_id, &query.topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_consumer_offset(identity.user_id, stream_id_numeric, topic_id_numeric)?;
+ state.shard.shard().metadata.perm_get_consumer_offset(
+ identity.user_id,
+ stream_id_numeric,
+ topic_id_numeric,
+ )?;
let consumer = Consumer::new(query.0.consumer.id);
let Ok(offset) = state
@@ -109,12 +108,11 @@ async fn store_consumer_offset(
.shard
.shard()
.resolve_topic_id(&command.stream_id, &command.topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .store_consumer_offset(identity.user_id, stream_id_numeric, topic_id_numeric)?;
+ state.shard.shard().metadata.perm_store_consumer_offset(
+ identity.user_id,
+ stream_id_numeric,
+ topic_id_numeric,
+ )?;
let consumer = Consumer::new(command.0.consumer.id);
state.shard
@@ -138,16 +136,18 @@ async fn delete_consumer_offset(
Path((stream_id, topic_id, consumer_id)): Path<(String, String, String)>,
query: Query<DeleteConsumerOffset>,
) -> Result<StatusCode, CustomError> {
+ let stream_id_ident = Identifier::from_str_value(&stream_id)?;
+ let topic_id_ident = Identifier::from_str_value(&topic_id)?;
+
let (stream_id_numeric, topic_id_numeric) = state
.shard
.shard()
- .resolve_topic_id(&query.stream_id, &query.topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .delete_consumer_offset(identity.user_id, stream_id_numeric, topic_id_numeric)?;
+ .resolve_topic_id(&stream_id_ident, &topic_id_ident)?;
+ state.shard.shard().metadata.perm_delete_consumer_offset(
+ identity.user_id,
+ stream_id_numeric,
+ topic_id_numeric,
+ )?;
let consumer = Consumer::new(consumer_id.try_into()?);
state
@@ -155,8 +155,8 @@ async fn delete_consumer_offset(
.delete_consumer_offset(
0, // HTTP uses client_id 0 as it doesn't have persistent sessions
consumer,
- &query.stream_id,
- &query.topic_id,
+ &stream_id_ident,
+ &topic_id_ident,
query.partition_id,
)
.await
diff --git a/core/server/src/http/http_shard_wrapper.rs b/core/server/src/http/http_shard_wrapper.rs
index e48abd6c4..b1f5bc754 100644
--- a/core/server/src/http/http_shard_wrapper.rs
+++ b/core/server/src/http/http_shard_wrapper.rs
@@ -136,10 +136,7 @@ impl HttpSafeShard {
future.await
}
- pub async fn create_stream(
- &self,
- name: String,
- ) -> Result<crate::streaming::streams::stream::Stream, IggyError> {
+ pub async fn create_stream(&self, name: String) -> Result<usize, IggyError> {
let future = SendWrapper::new(self.shard().create_stream(name));
future.await
}
@@ -279,31 +276,41 @@ impl HttpSafeShard {
) -> Result<(), IggyError> {
self.shard().ensure_topic_exists(&stream_id, &topic_id)?;
- let partition_id = self.shard().streams.with_topic_by_id(
- &stream_id,
- &topic_id,
- |(root, auxilary, ..)| match partitioning.kind {
- PartitioningKind::Balanced => {
- let upperbound = root.partitions().len();
- let pid = auxilary.get_next_partition_id(upperbound);
- Ok(pid)
- }
- PartitioningKind::PartitionId => Ok(u32::from_le_bytes(
- partitioning.value[..partitioning.length as usize]
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- ) as usize),
- PartitioningKind::MessagesKey => {
- let upperbound = root.partitions().len();
- Ok(
- topics::helpers::calculate_partition_id_by_messages_key_hash(
- upperbound,
- &partitioning.value,
- ),
- )
- }
- },
- )?;
+ let numeric_stream_id = self
+ .shard()
+ .metadata
+ .get_stream_id(&stream_id)
+ .expect("Stream existence already verified");
+ let numeric_topic_id = self
+ .shard()
+ .metadata
+ .get_topic_id(numeric_stream_id, &topic_id)
+ .expect("Topic existence already verified");
+ let partition_id = match partitioning.kind {
+ PartitioningKind::Balanced => self
+ .shard()
+ .metadata
+ .get_next_partition_id(numeric_stream_id, numeric_topic_id)
+ .ok_or(IggyError::TopicIdNotFound(
+ stream_id.clone(),
+ topic_id.clone(),
+ ))?,
+ PartitioningKind::PartitionId => u32::from_le_bytes(
+ partitioning.value[..partitioning.length as usize]
+ .try_into()
+ .map_err(|_| IggyError::InvalidNumberEncoding)?,
+ ) as usize,
+ PartitioningKind::MessagesKey => {
+ let partitions_count = self
+ .shard()
+ .metadata
+ .partitions_count(numeric_stream_id, numeric_topic_id);
+ topics::helpers::calculate_partition_id_by_messages_key_hash(
+ partitions_count,
+ &partitioning.value,
+ )
+ }
+ };
let future = SendWrapper::new(self.shard().append_messages(
user_id,
@@ -314,4 +321,36 @@ impl HttpSafeShard {
));
future.await
}
+
+ pub fn create_consumer_group(
+ &self,
+ stream_id: &Identifier,
+ topic_id: &Identifier,
+ name: String,
+ ) -> Result<usize, IggyError> {
+ self.shard()
+ .create_consumer_group(stream_id, topic_id, name)
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn update_topic(
+ &self,
+ stream_id: &Identifier,
+ topic_id: &Identifier,
+ name: String,
+ message_expiry: iggy_common::IggyExpiry,
+ compression_algorithm: iggy_common::CompressionAlgorithm,
+ max_topic_size: iggy_common::MaxTopicSize,
+ replication_factor: Option<u8>,
+ ) -> Result<(), IggyError> {
+ self.shard().update_topic(
+ stream_id,
+ topic_id,
+ name,
+ message_expiry,
+ compression_algorithm,
+ max_topic_size,
+ replication_factor,
+ )
+ }
}
diff --git a/core/server/src/http/mapper.rs b/core/server/src/http/mapper.rs
index d7ec542f2..879d98338 100644
--- a/core/server/src/http/mapper.rs
+++ b/core/server/src/http/mapper.rs
@@ -17,95 +17,13 @@
*/
use crate::http::jwt::json_web_token::GeneratedToken;
-use crate::slab::Keyed;
-use crate::slab::traits_ext::{EntityComponentSystem, IntoComponents};
+use crate::metadata::{ConsumerGroupMeta, InnerMetadata, PartitionMeta, StreamMeta, TopicMeta};
use crate::streaming::clients::client_manager::Client;
-use crate::streaming::stats::TopicStats;
-use crate::streaming::topics::consumer_group::{ConsumerGroupMembers, ConsumerGroupRoot};
-use crate::streaming::topics::topic::TopicRoot;
use crate::streaming::users::user::User;
use iggy_common::PersonalAccessToken;
use iggy_common::{ConsumerGroupDetails, ConsumerGroupInfo, ConsumerGroupMember, IggyByteSize};
use iggy_common::{IdentityInfo, PersonalAccessTokenInfo, TokenInfo, TopicDetails};
use iggy_common::{UserInfo, UserInfoDetails};
-use slab::Slab;
-use std::sync::Arc;
-
-/// Map TopicRoot with partitions to TopicDetails for HTTP responses
-pub fn map_topic_details(root: &TopicRoot, stats: &TopicStats) -> TopicDetails {
- let mut partitions = Vec::new();
-
- // Get partition details similar to binary mapper
- root.partitions().with_components(|partition_components| {
- let (partition_roots, partition_stats, _, offsets, _, _, _) =
- partition_components.into_components();
- for (partition_root, partition_stat, offset) in partition_roots
- .iter()
- .map(|(_, val)| val)
- .zip(partition_stats.iter().map(|(_, val)| val))
- .zip(offsets.iter().map(|(_, val)| val))
- .map(|((root, stat), offset)| (root, stat, offset))
- {
- partitions.push(iggy_common::Partition {
- id: partition_root.id() as u32,
- created_at: partition_root.created_at(),
- segments_count: partition_stat.segments_count_inconsistent(),
- current_offset: offset.load(std::sync::atomic::Ordering::Relaxed),
- size: IggyByteSize::from(partition_stat.size_bytes_inconsistent()),
- messages_count: partition_stat.messages_count_inconsistent(),
- });
- }
- });
-
- // Sort partitions by ID
- partitions.sort_by(|a, b| a.id.cmp(&b.id));
-
- TopicDetails {
- id: root.id() as u32,
- created_at: root.created_at(),
- name: root.name().clone(),
- size: stats.size_bytes_inconsistent().into(),
- messages_count: stats.messages_count_inconsistent(),
- partitions_count: partitions.len() as u32,
- partitions,
- message_expiry: root.message_expiry(),
- compression_algorithm: root.compression_algorithm(),
- max_topic_size: root.max_topic_size(),
- replication_factor: root.replication_factor(),
- }
-}
-
-/// Map TopicRoot and TopicStats to Topic for HTTP responses
-pub fn map_topic(root: &TopicRoot, stats: &TopicStats) -> iggy_common::Topic {
- iggy_common::Topic {
- id: root.id() as u32,
- created_at: root.created_at(),
- name: root.name().clone(),
- size: stats.size_bytes_inconsistent().into(),
- partitions_count: root.partitions().len() as u32,
- messages_count: stats.messages_count_inconsistent(),
- message_expiry: root.message_expiry(),
- compression_algorithm: root.compression_algorithm(),
- max_topic_size: root.max_topic_size(),
- replication_factor: root.replication_factor(),
- }
-}
-
-/// Map multiple topics from slab components to Vec<Topic> for HTTP responses
-pub fn map_topics_from_components(
- roots: &Slab<TopicRoot>,
- stats: &Slab<Arc<TopicStats>>,
-) -> Vec<iggy_common::Topic> {
- let mut topics = roots
- .iter()
- .map(|(_, root)| root)
- .zip(stats.iter().map(|(_, stat)| stat))
- .map(|(root, stat)| map_topic(root, stat))
- .collect::<Vec<_>>();
-
- topics.sort_by(|a, b| a.id.cmp(&b.id));
- topics
-}
pub fn map_user(user: &User) -> UserInfoDetails {
UserInfoDetails {
@@ -183,52 +101,6 @@ pub fn map_clients(clients: &[Client]) -> Vec<iggy_common::ClientInfo> {
all_clients
}
-pub fn map_consumer_groups(
- roots: &slab::Slab<ConsumerGroupRoot>,
- members: &slab::Slab<ConsumerGroupMembers>,
-) -> Vec<iggy_common::ConsumerGroup> {
- let mut groups = Vec::new();
- for (root, member) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(members.iter().map(|(_, val)| val))
- {
- let members_guard = member.inner().shared_get();
- let consumer_group = iggy_common::ConsumerGroup {
- id: root.id() as u32,
- name: root.key().clone(),
- partitions_count: root.partitions().len() as u32,
- members_count: members_guard.len() as u32,
- };
- groups.push(consumer_group);
- }
- groups.sort_by(|a, b| a.id.cmp(&b.id));
- groups
-}
-
-pub fn map_consumer_group(
- root: &ConsumerGroupRoot,
- members: &ConsumerGroupMembers,
-) -> ConsumerGroupDetails {
- let members_guard = members.inner().shared_get();
- let mut consumer_group_details = ConsumerGroupDetails {
- id: root.id() as u32,
- name: root.key().clone(),
- partitions_count: root.partitions().len() as u32,
- members_count: members_guard.len() as u32,
- members: Vec::new(),
- };
-
- for (_, member) in members_guard.iter() {
- consumer_group_details.members.push(ConsumerGroupMember {
- id: member.id as u32,
- partitions_count: member.partitions.len() as u32,
- partitions: member.partitions.iter().map(|p| *p as u32).collect(),
- });
- }
- consumer_group_details
-}
-
pub fn map_generated_access_token_to_identity_info(token: GeneratedToken) -> IdentityInfo {
IdentityInfo {
user_id: token.user_id,
@@ -239,69 +111,220 @@ pub fn map_generated_access_token_to_identity_info(token: GeneratedToken) -> Ide
}
}
-/// Map StreamRoot and StreamStats to StreamDetails for HTTP responses
-pub fn map_stream_details(
- root: &crate::streaming::streams::stream::StreamRoot,
- stats: &crate::streaming::stats::StreamStats,
-) -> iggy_common::StreamDetails {
- // Get topics using the new slab-based API
- let topics = root.topics().with_components(|topic_ref| {
- let (topic_roots, _topic_auxiliaries, topic_stats) = topic_ref.into_components();
- let mut topics_vec = Vec::new();
-
- // Iterate over topics in the stream
- for (topic_root, topic_stat) in topic_roots
- .iter()
- .map(|(_, root)| root)
- .zip(topic_stats.iter().map(|(_, stat)| stat))
- {
- topics_vec.push(map_topic(topic_root, topic_stat));
+/// Map a stream from SharedMetadata to StreamDetails (with topics)
+pub fn map_stream_details_from_metadata(stream_meta: &StreamMeta) -> iggy_common::StreamDetails {
+ // Get topic IDs sorted
+ let mut topic_ids: Vec<_> = stream_meta.topics.iter().map(|(k, _)| k).collect();
+ topic_ids.sort_unstable();
+
+ // Map topics
+ let mut topics = Vec::with_capacity(topic_ids.len());
+ for topic_id in topic_ids {
+ if let Some(topic_meta) = stream_meta.topics.get(topic_id) {
+ topics.push(map_topic_from_metadata(topic_meta));
}
+ }
- // Sort topics by ID for consistent ordering
- topics_vec.sort_by(|a, b| a.id.cmp(&b.id));
- topics_vec
- });
+ // Aggregate stats
+ let (total_size, total_messages) = aggregate_stream_stats(stream_meta);
iggy_common::StreamDetails {
- id: root.id() as u32,
- created_at: root.created_at(),
- name: root.name().clone(),
- topics_count: root.topics_count() as u32,
- size: stats.size_bytes_inconsistent().into(),
- messages_count: stats.messages_count_inconsistent(),
+ id: stream_meta.id as u32,
+ created_at: stream_meta.created_at,
+ name: stream_meta.name.to_string(),
+ topics_count: topics.len() as u32,
+ size: IggyByteSize::from(total_size),
+ messages_count: total_messages,
topics,
}
}
-/// Map StreamRoot and StreamStats to Stream for HTTP responses
-pub fn map_stream(
- root: &crate::streaming::streams::stream::StreamRoot,
- stats: &crate::streaming::stats::StreamStats,
-) -> iggy_common::Stream {
+/// Map a stream from SharedMetadata to Stream (without topics)
+pub fn map_stream_from_metadata(stream_meta: &StreamMeta) -> iggy_common::Stream {
+ let (total_size, total_messages) = aggregate_stream_stats(stream_meta);
+
iggy_common::Stream {
- id: root.id() as u32,
- created_at: root.created_at(),
- name: root.name().clone(),
- topics_count: root.topics_count() as u32,
- size: stats.size_bytes_inconsistent().into(),
- messages_count: stats.messages_count_inconsistent(),
+ id: stream_meta.id as u32,
+ created_at: stream_meta.created_at,
+ name: stream_meta.name.to_string(),
+ topics_count: stream_meta.topics.len() as u32,
+ size: IggyByteSize::from(total_size),
+ messages_count: total_messages,
}
}
-/// Map multiple streams from slabs
-pub fn map_streams_from_slabs(
- roots: &slab::Slab<crate::streaming::streams::stream::StreamRoot>,
- stats: &slab::Slab<Arc<crate::streaming::stats::StreamStats>>,
-) -> Vec<iggy_common::Stream> {
- let mut streams = Vec::new();
- for (root, stat) in roots
- .iter()
- .map(|(_, val)| val)
- .zip(stats.iter().map(|(_, val)| val))
- {
- streams.push(map_stream(root, stat));
+/// Map all streams from SharedMetadata
+pub fn map_streams_from_metadata(metadata: &InnerMetadata) -> Vec<iggy_common::Stream> {
+ let mut stream_ids: Vec<_> = metadata.streams.iter().map(|(k, _)| k).collect();
+ stream_ids.sort_unstable();
+
+ let mut streams = Vec::with_capacity(stream_ids.len());
+ for stream_id in stream_ids {
+ if let Some(stream_meta) = metadata.streams.get(stream_id) {
+ streams.push(map_stream_from_metadata(stream_meta));
+ }
}
- streams.sort_by(|a, b| a.id.cmp(&b.id));
streams
}
+
+/// Map a topic from SharedMetadata to Topic (without partitions)
+pub fn map_topic_from_metadata(topic_meta: &TopicMeta) -> iggy_common::Topic {
+ let (total_size, total_messages) = aggregate_topic_stats(topic_meta);
+
+ iggy_common::Topic {
+ id: topic_meta.id as u32,
+ created_at: topic_meta.created_at,
+ name: topic_meta.name.to_string(),
+ size: IggyByteSize::from(total_size),
+ partitions_count: topic_meta.partitions.len() as u32,
+ messages_count: total_messages,
+ message_expiry: topic_meta.message_expiry,
+ compression_algorithm: topic_meta.compression_algorithm,
+ max_topic_size: topic_meta.max_topic_size,
+ replication_factor: topic_meta.replication_factor,
+ }
+}
+
+/// Map all topics for a stream from SharedMetadata
+pub fn map_topics_from_metadata(stream_meta: &StreamMeta) -> Vec<iggy_common::Topic> {
+ let mut topic_ids: Vec<_> = stream_meta.topics.iter().map(|(k, _)| k).collect();
+ topic_ids.sort_unstable();
+
+ let mut topics = Vec::with_capacity(topic_ids.len());
+ for topic_id in topic_ids {
+ if let Some(topic_meta) = stream_meta.topics.get(topic_id) {
+ topics.push(map_topic_from_metadata(topic_meta));
+ }
+ }
+ topics
+}
+
+/// Map a topic from SharedMetadata to TopicDetails (with partitions)
+pub fn map_topic_details_from_metadata(topic_meta: &TopicMeta) -> TopicDetails {
+ // Get partition IDs sorted
+ let mut partition_ids: Vec<_> = topic_meta
+ .partitions
+ .iter()
+ .enumerate()
+ .map(|(k, _)| k)
+ .collect();
+ partition_ids.sort_unstable();
+
+ // Map partitions
+ let mut partitions = Vec::with_capacity(partition_ids.len());
+ for partition_id in partition_ids {
+ if let Some(partition_meta) = topic_meta.partitions.get(partition_id) {
+ partitions.push(map_partition_from_metadata(partition_meta));
+ }
+ }
+
+ // Aggregate stats
+ let (total_size, total_messages) = aggregate_topic_stats(topic_meta);
+
+ TopicDetails {
+ id: topic_meta.id as u32,
+ created_at: topic_meta.created_at,
+ name: topic_meta.name.to_string(),
+ size: IggyByteSize::from(total_size),
+ messages_count: total_messages,
+ partitions_count: partitions.len() as u32,
+ partitions,
+ message_expiry: topic_meta.message_expiry,
+ compression_algorithm: topic_meta.compression_algorithm,
+ max_topic_size: topic_meta.max_topic_size,
+ replication_factor: topic_meta.replication_factor,
+ }
+}
+
+/// Map a partition from SharedMetadata
+pub fn map_partition_from_metadata(partition_meta: &PartitionMeta) -> iggy_common::Partition {
+ let stats = &partition_meta.stats;
+ let segments_count = stats.segments_count_inconsistent();
+ let size_bytes = stats.size_bytes_inconsistent();
+ let messages_count = stats.messages_count_inconsistent();
+ let current_offset = stats.current_offset();
+
+ iggy_common::Partition {
+ id: partition_meta.id as u32,
+ created_at: partition_meta.created_at,
+ segments_count,
+ current_offset,
+ size: IggyByteSize::from(size_bytes),
+ messages_count,
+ }
+}
+
+/// Map a consumer group from SharedMetadata
+pub fn map_consumer_group_from_metadata(cg_meta: &ConsumerGroupMeta) -> iggy_common::ConsumerGroup {
+ iggy_common::ConsumerGroup {
+ id: cg_meta.id as u32,
+ name: cg_meta.name.to_string(),
+ partitions_count: cg_meta.partitions.len() as u32,
+ members_count: cg_meta.members.len() as u32,
+ }
+}
+
+/// Map a consumer group to ConsumerGroupDetails from SharedMetadata
+pub fn map_consumer_group_details_from_metadata(
+ cg_meta: &ConsumerGroupMeta,
+) -> ConsumerGroupDetails {
+ let members: Vec<ConsumerGroupMember> = cg_meta
+ .members
+ .iter()
+ .map(|(_, member)| ConsumerGroupMember {
+ id: member.id as u32,
+ partitions_count: member.partitions.len() as u32,
+ partitions: member.partitions.iter().map(|&p| p as u32).collect(),
+ })
+ .collect();
+
+ ConsumerGroupDetails {
+ id: cg_meta.id as u32,
+ name: cg_meta.name.to_string(),
+ partitions_count: cg_meta.partitions.len() as u32,
+ members_count: members.len() as u32,
+ members,
+ }
+}
+
+/// Map all consumer groups for a topic from SharedMetadata
+pub fn map_consumer_groups_from_metadata(
+ topic_meta: &TopicMeta,
+) -> Vec<iggy_common::ConsumerGroup> {
+ let mut group_ids: Vec<_> = topic_meta.consumer_groups.iter().map(|(k, _)| k).collect();
+ group_ids.sort_unstable();
+
+ let mut groups = Vec::with_capacity(group_ids.len());
+ for group_id in group_ids {
+ if let Some(cg_meta) = topic_meta.consumer_groups.get(group_id) {
+ groups.push(map_consumer_group_from_metadata(cg_meta));
+ }
+ }
+ groups
+}
+
+fn aggregate_stream_stats(stream_meta: &StreamMeta) -> (u64, u64) {
+ let mut total_size = 0u64;
+ let mut total_messages = 0u64;
+
+ for (_, topic_meta) in stream_meta.topics.iter() {
+ for partition_meta in topic_meta.partitions.iter() {
+ total_size += partition_meta.stats.size_bytes_inconsistent();
+ total_messages += partition_meta.stats.messages_count_inconsistent();
+ }
+ }
+
+ (total_size, total_messages)
+}
+
+fn aggregate_topic_stats(topic_meta: &TopicMeta) -> (u64, u64) {
+ let mut total_size = 0u64;
+ let mut total_messages = 0u64;
+
+ for partition_meta in topic_meta.partitions.iter() {
+ total_size += partition_meta.stats.size_bytes_inconsistent();
+ total_messages += partition_meta.stats.messages_count_inconsistent();
+ }
+
+ (total_size, total_messages)
+}
diff --git a/core/server/src/http/messages.rs b/core/server/src/http/messages.rs
index 6d73ff5bc..008175eab 100644
--- a/core/server/src/http/messages.rs
+++ b/core/server/src/http/messages.rs
@@ -151,7 +151,6 @@ fn make_mutable(batch: IggyMessagesBatch) -> IggyMessagesBatchMut {
let (_, indexes_buffer) = indexes.decompose();
let indexes_buffer_mut = PooledBuffer::from_existing(indexes_buffer.into());
let indexes_mut = IggyIndexesMut::from_bytes(indexes_buffer_mut, 0);
- let count = indexes_mut.count();
let messages_buffer_mut = PooledBuffer::from_existing(messages.into());
- IggyMessagesBatchMut::from_indexes_and_messages(count, indexes_mut, messages_buffer_mut)
+ IggyMessagesBatchMut::from_indexes_and_messages(indexes_mut, messages_buffer_mut)
}
diff --git a/core/server/src/http/partitions.rs b/core/server/src/http/partitions.rs
index e2ec4942c..2c5a2abf5 100644
--- a/core/server/src/http/partitions.rs
+++ b/core/server/src/http/partitions.rs
@@ -61,15 +61,14 @@ async fn create_partitions(
.shard
.shard()
.resolve_topic_id(&command.stream_id, &command.topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .create_partitions(identity.user_id, numeric_stream_id, numeric_topic_id)?;
+ state.shard.shard().metadata.perm_create_partitions(
+ identity.user_id,
+ numeric_stream_id,
+ numeric_topic_id,
+ )?;
let _parititon_guard = state.shard.shard().fs_locks.partition_lock.lock().await;
- let partitions = SendWrapper::new(state.shard.shard().create_partitions(
+ let partition_infos = SendWrapper::new(state.shard.shard().create_partitions(
&command.stream_id,
&command.topic_id,
command.partitions_count,
@@ -82,7 +81,7 @@ async fn create_partitions(
let event = ShardEvent::CreatedPartitions {
stream_id: command.stream_id.clone(),
topic_id: command.topic_id.clone(),
- partitions,
+ partitions: partition_infos,
};
let _responses = shard.broadcast_event_to_all_shards(event).await;
Ok::<(), CustomError>(())
@@ -124,12 +123,11 @@ async fn delete_partitions(
.shard
.shard()
.resolve_topic_id(&query.stream_id, &query.topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .delete_partitions(identity.user_id, numeric_stream_id, numeric_topic_id)?;
+ state.shard.shard().metadata.perm_delete_partitions(
+ identity.user_id,
+ numeric_stream_id,
+ numeric_topic_id,
+ )?;
let deleted_partition_ids = {
let delete_future = SendWrapper::new(state.shard.shard().delete_partitions(
diff --git a/core/server/src/http/personal_access_tokens.rs b/core/server/src/http/personal_access_tokens.rs
index a8244d160..10f05f961 100644
--- a/core/server/src/http/personal_access_tokens.rs
+++ b/core/server/src/http/personal_access_tokens.rs
@@ -84,7 +84,7 @@ async fn create_personal_access_token(
Json(command): Json<CreatePersonalAccessToken>,
) -> Result<Json<RawPersonalAccessToken>, CustomError> {
command.validate()?;
- let (personal_access_token, token) = state
+ let (_, token) = state
.shard
.create_personal_access_token(identity.user_id, &command.name, command.expiry)
.error(|e: &IggyError| {
@@ -94,21 +94,6 @@ async fn create_personal_access_token(
)
})?;
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::CreatedPersonalAccessToken {
- personal_access_token: personal_access_token.clone(),
- };
- let _ = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
let token_hash = PersonalAccessToken::hash_token(&token);
let command = EntryCommand::CreatePersonalAccessToken(CreatePersonalAccessTokenWithHash {
command,
@@ -134,7 +119,6 @@ async fn delete_personal_access_token(
Extension(identity): Extension<Identity>,
Path(name): Path<String>,
) -> Result<StatusCode, CustomError> {
- let token_name = name.clone();
state
.shard
.delete_personal_access_token(identity.user_id, &name)
@@ -145,23 +129,8 @@ async fn delete_personal_access_token(
)
})?;
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::DeletedPersonalAccessToken {
- user_id: identity.user_id,
- name: token_name,
- };
- let _ = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
- let command = EntryCommand::DeletePersonalAccessToken(DeletePersonalAccessToken { name });
+ let command =
+ EntryCommand::DeletePersonalAccessToken(DeletePersonalAccessToken { name: name.clone() });
let state_future =
SendWrapper::new(state.shard.shard().state.apply(identity.user_id, &command));
diff --git a/core/server/src/http/streams.rs b/core/server/src/http/streams.rs
index 79b09bd7e..48de93e63 100644
--- a/core/server/src/http/streams.rs
+++ b/core/server/src/http/streams.rs
@@ -20,7 +20,6 @@ use crate::http::COMPONENT;
use crate::http::error::CustomError;
use crate::http::jwt::json_web_token::Identity;
use crate::http::shared::AppState;
-use crate::slab::traits_ext::{EntityComponentSystem, IntoComponents};
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get};
@@ -59,24 +58,22 @@ async fn get_stream(
) -> Result<Json<StreamDetails>, CustomError> {
let stream_id = Identifier::from_str_value(&stream_id)?;
- let stream_id_numeric = state.shard.shard().resolve_stream_id(&stream_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_stream(identity.user_id, stream_id_numeric)?;
+ let shard = state.shard.shard();
+ let numeric_stream_id = shard
+ .metadata
+ .get_stream_id(&stream_id)
+ .ok_or(CustomError::ResourceNotFound)?;
- // Use direct slab access for thread-safe stream retrieval
- let stream_details = SendWrapper::new(|| {
- state
- .shard
- .shard()
- .streams
- .with_stream_by_id(&stream_id, |(root, stats)| {
- crate::http::mapper::map_stream_details(&root, &stats)
- })
- })();
+ shard
+ .metadata
+ .perm_get_stream(identity.user_id, numeric_stream_id)?;
+
+ let stream_meta = shard
+ .metadata
+ .get_stream(numeric_stream_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+
+ let stream_details = crate::http::mapper::map_stream_details_from_metadata(&stream_meta);
Ok(Json(stream_details))
}
@@ -86,20 +83,13 @@ async fn get_streams(
State(state): State<Arc<AppState>>,
Extension(identity): Extension<Identity>,
) -> Result<Json<Vec<Stream>>, CustomError> {
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_streams(identity.user_id)?;
-
- // Use direct slab access for thread-safe streams retrieval
- let streams = SendWrapper::new(|| {
- state.shard.shard().streams.with_components(|stream_ref| {
- let (roots, stats) = stream_ref.into_components();
- crate::http::mapper::map_streams_from_slabs(&roots, &stats)
- })
- })();
+ let shard = state.shard.shard();
+
+ shard.metadata.perm_get_streams(identity.user_id)?;
+
+ let streams = shard
+ .metadata
+ .with_metadata(crate::http::mapper::map_streams_from_metadata);
Ok(Json(streams))
}
@@ -115,14 +105,13 @@ async fn create_stream(
state
.shard
.shard()
- .permissioner
- .borrow()
- .create_stream(identity.user_id)?;
+ .metadata
+ .perm_create_stream(identity.user_id)?;
let result = SendWrapper::new(async move {
let _stream_guard = state.shard.shard().fs_locks.stream_lock.lock().await;
// Create stream using wrapper method
- let stream = state
+ let created_stream_id = state
.shard
.create_stream(command.name.clone())
.await
@@ -133,23 +122,6 @@ async fn create_stream(
)
})?;
- let created_stream_id = stream.root().id();
-
- // Send event for stream creation - inlined from wrapper
- {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::CreatedStream {
- id: created_stream_id,
- stream,
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- }
-
- // Apply state change using wrapper method
let entry_command = EntryCommand::CreateStream(CreateStreamWithId {
stream_id: created_stream_id as u32,
command,
@@ -166,16 +138,12 @@ async fn create_stream(
)
})?;
- // Get the created stream details using direct slab access
- let response = SendWrapper::new(|| {
- state
- .shard
- .shard()
- .streams
- .with_components_by_id(created_stream_id, |(root, stats)| {
- crate::http::mapper::map_stream_details(&root, &stats)
- })
- })();
+ let shard = state.shard.shard();
+ let stream_meta = shard
+ .metadata
+ .get_stream(created_stream_id)
+ .expect("Stream must exist after creation");
+ let response = crate::http::mapper::map_stream_details_from_metadata(&stream_meta);
Ok::<Json<StreamDetails>, CustomError>(Json(response))
});
@@ -198,9 +166,8 @@ async fn update_stream(
state
.shard
.shard()
- .permissioner
- .borrow()
- .update_stream(identity.user_id, stream_id_numeric)?;
+ .metadata
+ .perm_update_stream(identity.user_id, stream_id_numeric)?;
let result = SendWrapper::new(async move {
// Update stream using wrapper method
@@ -213,24 +180,6 @@ async fn update_stream(
)
})?;
- // Send event for stream update
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::UpdatedStream {
- stream_id: command.stream_id.clone(),
- name: command.name.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
- // Apply state change using wrapper method
let entry_command = EntryCommand::UpdateStream(command);
state.shard.apply_state(identity.user_id, &entry_command).await.error(|e: &IggyError| {
format!(
@@ -259,15 +208,13 @@ async fn delete_stream(
state
.shard
.shard()
- .permissioner
- .borrow()
- .delete_stream(identity.user_id, stream_id_numeric)?;
+ .metadata
+ .perm_delete_stream(identity.user_id, stream_id_numeric)?;
let result =
SendWrapper::new(async move {
let _stream_guard = state.shard.shard().fs_locks.stream_lock.lock().await;
- // Delete stream and get the stream entity
- let stream = {
+ {
let future =
SendWrapper::new(state.shard.shard().delete_stream(&identifier_stream_id));
future.await
@@ -276,26 +223,6 @@ async fn delete_stream(
format!("{COMPONENT} (error: {e}) - failed to delete stream with ID: {stream_id}",)
})?;
- let stream_id_numeric = stream.root().id();
-
- // Send event for stream deletion
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::DeletedStream {
- id: stream_id_numeric,
- stream_id: identifier_stream_id.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
- // Apply state change using wrapper method
let entry_command = EntryCommand::DeleteStream(DeleteStream {
stream_id: identifier_stream_id,
});
@@ -327,9 +254,8 @@ async fn purge_stream(
state
.shard
.shard()
- .permissioner
- .borrow()
- .purge_stream(identity.user_id, stream_id_numeric)?;
+ .metadata
+ .perm_purge_stream(identity.user_id, stream_id_numeric)?;
let result = SendWrapper::new(async move {
// Purge stream using wrapper method
@@ -357,7 +283,6 @@ async fn purge_stream(
broadcast_future.await;
}
- // Apply state change using wrapper method
let entry_command = EntryCommand::PurgeStream(PurgeStream {
stream_id: identifier_stream_id,
});
diff --git a/core/server/src/http/system.rs b/core/server/src/http/system.rs
index ead8b416b..b31de76d5 100644
--- a/core/server/src/http/system.rs
+++ b/core/server/src/http/system.rs
@@ -71,9 +71,8 @@ async fn get_stats(
state
.shard
.shard()
- .permissioner
- .borrow()
- .get_stats(identity.user_id)?;
+ .metadata
+ .perm_get_stats(identity.user_id)?;
let stats_future = SendWrapper::new(state.shard.shard().get_stats());
let stats = stats_future.await.error(|e: &IggyError| {
format!(
@@ -101,9 +100,8 @@ async fn get_client(
state
.shard
.shard()
- .permissioner
- .borrow()
- .get_client(identity.user_id)?;
+ .metadata
+ .perm_get_client(identity.user_id)?;
let Some(client) = state.shard.shard().get_client(client_id) else {
return Err(CustomError::ResourceNotFound);
};
@@ -120,9 +118,8 @@ async fn get_clients(
state
.shard
.shard()
- .permissioner
- .borrow()
- .get_clients(identity.user_id)?;
+ .metadata
+ .perm_get_clients(identity.user_id)?;
let clients = state.shard.shard().get_clients();
let clients = mapper::map_clients(&clients);
Ok(Json(clients))
diff --git a/core/server/src/http/topics.rs b/core/server/src/http/topics.rs
index 9cb62eb71..91a771eba 100644
--- a/core/server/src/http/topics.rs
+++ b/core/server/src/http/topics.rs
@@ -20,7 +20,6 @@ use crate::http::COMPONENT;
use crate::http::error::CustomError;
use crate::http::jwt::json_web_token::Identity;
use crate::http::shared::AppState;
-use crate::slab::traits_ext::{EntityComponentSystem, EntityMarker, IntoComponents};
use crate::state::command::EntryCommand;
use crate::state::models::CreateTopicWithId;
use axum::extract::{Path, State};
@@ -65,16 +64,21 @@ async fn get_topic(
let identity_stream_id = Identifier::from_str_value(&stream_id)?;
let identity_topic_id = Identifier::from_str_value(&topic_id)?;
- let (numeric_stream_id, numeric_topic_id) = state
- .shard
- .shard()
- .resolve_topic_id(&identity_stream_id, &identity_topic_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_topic(identity.user_id, numeric_stream_id, numeric_topic_id)
+ let shard = state.shard.shard();
+
+ let numeric_stream_id = shard
+ .metadata
+ .get_stream_id(&identity_stream_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+
+ let numeric_topic_id = shard
+ .metadata
+ .get_topic_id(numeric_stream_id, &identity_topic_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+
+ shard
+ .metadata
+ .perm_get_topic(identity.user_id, numeric_stream_id, numeric_topic_id)
.error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - permission denied to get topic with ID: {topic_id} in stream with ID: {stream_id} for user with ID: {}",
@@ -82,12 +86,12 @@ async fn get_topic(
)
})?;
- // Get topic details using the new API
- let topic_details = state.shard.shard().streams.with_topic_by_id(
- &identity_stream_id,
- &identity_topic_id,
- |(root, _, stats)| crate::http::mapper::map_topic_details(&root, &stats),
- );
+ let topic_meta = shard
+ .metadata
+ .get_topic(numeric_stream_id, numeric_topic_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+
+ let topic_details = crate::http::mapper::map_topic_details_from_metadata(&topic_meta);
Ok(Json(topic_details))
}
@@ -98,15 +102,17 @@ async fn get_topics(
Extension(identity): Extension<Identity>,
Path(stream_id): Path<String>,
) -> Result<Json<Vec<Topic>>, CustomError> {
- let stream_id = Identifier::from_str_value(&stream_id)?;
+ let stream_id_ident = Identifier::from_str_value(&stream_id)?;
+ let shard = state.shard.shard();
- let numeric_stream_id = state.shard.shard().resolve_stream_id(&stream_id)?;
- state
- .shard
- .shard()
- .permissioner
- .borrow()
- .get_topics(identity.user_id, numeric_stream_id)
+ let numeric_stream_id = shard
+ .metadata
+ .get_stream_id(&stream_id_ident)
+ .ok_or(CustomError::ResourceNotFound)?;
+
+ shard
+ .metadata
+ .perm_get_topics(identity.user_id, numeric_stream_id)
.error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - permission denied to get topics in stream with ID: {stream_id} for user with ID: {}",
@@ -114,17 +120,11 @@ async fn get_topics(
)
})?;
- // Get topics using the new API
- let topics = state
- .shard
- .shard()
- .streams
- .with_topics(&stream_id, |topics| {
- topics.with_components(|topics| {
- let (roots, _, stats) = topics.into_components();
- crate::http::mapper::map_topics_from_components(&roots, &stats)
- })
- });
+ let stream_meta = shard
+ .metadata
+ .get_stream(numeric_stream_id)
+ .ok_or(CustomError::ResourceNotFound)?;
+ let topics = crate::http::mapper::map_topics_from_metadata(&stream_meta);
Ok(Json(topics))
}
@@ -144,12 +144,11 @@ async fn create_topic(
state
.shard
.shard()
- .permissioner
- .borrow()
- .create_topic(identity.user_id, numeric_stream_id)?;
+ .metadata
+ .perm_create_topic(identity.user_id, numeric_stream_id)?;
let _topic_guard = state.shard.shard().fs_locks.topic_lock.lock().await;
- let topic = {
+ let topic_id = {
let future = SendWrapper::new(state.shard.shard().create_topic(
&command.stream_id,
command.name.clone(),
@@ -164,26 +163,27 @@ async fn create_topic(
format!("{COMPONENT} (error: {e}) - failed to create topic, stream ID: {stream_id}")
})?;
- // Update command with actual values from created topic
- command.message_expiry = topic.root().message_expiry();
- command.max_topic_size = topic.root().max_topic_size();
-
- let topic_id = topic.id();
+ {
+ let shard = state.shard.shard();
+ let numeric_stream_id = shard
+ .metadata
+ .get_stream_id(&command.stream_id)
+ .expect("Stream must exist");
+ let (expiry, max_size) = shard
+ .metadata
+ .get_topic_config(numeric_stream_id, topic_id)
+ .expect("Topic config must exist after creation");
+ command.message_expiry = expiry;
+ command.max_topic_size = max_size;
+ }
- // Send events for topic creation
let broadcast_future = SendWrapper::new(async {
use crate::shard::transmission::event::ShardEvent;
let shard = state.shard.shard();
- let event = ShardEvent::CreatedTopic {
- stream_id: command.stream_id.clone(),
- topic,
- };
- let _responses = shard.broadcast_event_to_all_shards(event).await;
-
// Create partitions
- let partitions = shard
+ let partition_infos = shard
.create_partitions(
&command.stream_id,
&Identifier::numeric(topic_id as u32).unwrap(),
@@ -194,7 +194,7 @@ async fn create_topic(
let event = ShardEvent::CreatedPartitions {
stream_id: command.stream_id.clone(),
topic_id: Identifier::numeric(topic_id as u32).unwrap(),
- partitions,
+ partitions: partition_infos,
};
let _responses = shard.broadcast_event_to_all_shards(event).await;
@@ -207,18 +207,20 @@ async fn create_topic(
)
})?;
- // Create response using the same approach as binary handler
let response = {
- let topic_identifier = Identifier::numeric(topic_id as u32).unwrap();
- let topic_response = state.shard.shard().streams.with_topic_by_id(
- &command.stream_id,
- &topic_identifier,
- |(root, _, stats)| crate::http::mapper::map_topic_details(&root, &stats),
- );
+ let shard = state.shard.shard();
+ let numeric_stream_id = shard
+ .metadata
+ .get_stream_id(&command.stream_id)
+ .expect("Stream must exist");
+ let topic_meta = shard
+ .metadata
+ .get_topic(numeric_stream_id, topic_id)
+ .expect("Topic must exist after creation");
+ let topic_response = crate::http::mapper::map_topic_details_from_metadata(&topic_meta);
Json(topic_response)
};
- // Apply state change like in binary handler
{
let entry_command = EntryCommand::CreateTopic(CreateTopicWithId {
topic_id: topic_id as u32,
@@ -256,7 +258,7 @@ async fn update_topic(
.shard
.shard()
.resolve_topic_id(&command.stream_id, &command.topic_id)?;
- state.shard.shard().permissioner.borrow().update_topic(
+ state.shard.shard().metadata.perm_update_topic(
identity.user_id,
numeric_stream_id,
numeric_topic_id,
@@ -277,42 +279,27 @@ async fn update_topic(
)
})?;
- // TODO: Tech debt.
- let topic_id = if name_changed {
+ let shard = state.shard.shard();
+ let numeric_stream_id = shard
+ .metadata
+ .get_stream_id(&command.stream_id)
+ .expect("Stream must exist");
+
+ let topic_id_for_lookup = if name_changed {
Identifier::named(&command.name.clone()).unwrap()
} else {
command.topic_id.clone()
};
- // Get the updated values from the topic
- let (message_expiry, max_topic_size) = state.shard.shard().streams.with_topic_by_id(
- &command.stream_id,
- &topic_id,
- |(root, _, _)| (root.message_expiry(), root.max_topic_size()),
- );
-
- // Send event for topic update
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::UpdatedTopic {
- stream_id: command.stream_id.clone(),
- topic_id: command.topic_id.clone(),
- name: command.name.clone(),
- message_expiry: command.message_expiry,
- compression_algorithm: command.compression_algorithm,
- max_topic_size: command.max_topic_size,
- replication_factor: command.replication_factor,
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
+ let numeric_topic_id = shard
+ .metadata
+ .get_topic_id(numeric_stream_id, &topic_id_for_lookup)
+ .expect("Topic must exist after update");
+ let (message_expiry, max_topic_size) = shard
+ .metadata
+ .get_topic_config(numeric_stream_id, numeric_topic_id)
+ .expect("Topic metadata must exist");
command.message_expiry = message_expiry;
command.max_topic_size = max_topic_size;
@@ -344,14 +331,14 @@ async fn delete_topic(
.shard
.shard()
.resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
- state.shard.shard().permissioner.borrow().delete_topic(
+ state.shard.shard().metadata.perm_delete_topic(
identity.user_id,
numeric_stream_id,
numeric_topic_id,
)?;
let _topic_guard = state.shard.shard().fs_locks.topic_lock.lock().await;
- let topic = {
+ {
let future = SendWrapper::new(state.shard.shard().delete_topic(
&identifier_stream_id,
&identifier_topic_id,
@@ -363,26 +350,6 @@ async fn delete_topic(
)
})?;
- let topic_id_numeric = topic.root().id();
-
- // Send event for topic deletion
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::DeletedTopic {
- id: topic_id_numeric,
- stream_id: identifier_stream_id.clone(),
- topic_id: identifier_topic_id.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
{
let entry_command = EntryCommand::DeleteTopic(DeleteTopic {
stream_id: identifier_stream_id,
@@ -417,7 +384,7 @@ async fn purge_topic(
.shard
.shard()
.resolve_topic_id(&identifier_stream_id, &identifier_topic_id)?;
- state.shard.shard().permissioner.borrow().purge_topic(
+ state.shard.shard().metadata.perm_purge_topic(
identity.user_id,
numeric_stream_id,
numeric_topic_id,
@@ -435,7 +402,6 @@ async fn purge_topic(
)
})?;
- // Send event for topic purge
{
let broadcast_future = SendWrapper::new(async {
use crate::shard::transmission::event::ShardEvent;
diff --git a/core/server/src/http/users.rs b/core/server/src/http/users.rs
index ae7475ec4..8e4dcd1b9 100644
--- a/core/server/src/http/users.rs
+++ b/core/server/src/http/users.rs
@@ -81,9 +81,8 @@ async fn get_user(
state
.shard
.shard()
- .permissioner
- .borrow()
- .get_user(identity.user_id)?;
+ .metadata
+ .perm_get_user(identity.user_id)?;
}
let user = mapper::map_user(&user);
@@ -98,9 +97,8 @@ async fn get_users(
state
.shard
.shard()
- .permissioner
- .borrow()
- .get_users(identity.user_id)?;
+ .metadata
+ .perm_get_users(identity.user_id)?;
let users = state.shard.shard().get_users();
let user_refs: Vec<&User> = users.iter().collect();
@@ -119,9 +117,8 @@ async fn create_user(
state
.shard
.shard()
- .permissioner
- .borrow()
- .create_user(identity.user_id)?;
+ .metadata
+ .perm_create_user(identity.user_id)?;
let user = state
.shard
@@ -142,26 +139,6 @@ async fn create_user(
let user_id = user.id;
let response = Json(mapper::map_user(&user));
- // Send event for user creation
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::CreatedUser {
- user_id,
- username: command.username.to_owned(),
- password: command.password.to_owned(),
- status: command.status,
- permissions: command.permissions.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
{
let username = command.username.clone();
let entry_command = EntryCommand::CreateUser(CreateUserWithId {
@@ -204,9 +181,8 @@ async fn update_user(
state
.shard
.shard()
- .permissioner
- .borrow()
- .update_user(identity.user_id)?;
+ .metadata
+ .perm_update_user(identity.user_id)?;
state
.shard
@@ -216,24 +192,6 @@ async fn update_user(
format!("{COMPONENT} (error: {e}) - failed to update user, user ID: {user_id}")
})?;
- // Send event for user update
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::UpdatedUser {
- user_id: command.user_id.clone(),
- username: command.username.clone(),
- status: command.status,
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
{
let username = command.username.clone();
let entry_command = EntryCommand::UpdateUser(command);
@@ -268,9 +226,8 @@ async fn update_permissions(
state
.shard
.shard()
- .permissioner
- .borrow()
- .update_permissions(identity.user_id)?;
+ .metadata
+ .perm_update_permissions(identity.user_id)?;
// Check if target user is root - cannot change root user permissions
let target_user = state.shard.shard().get_user(&command.user_id)?;
@@ -288,23 +245,6 @@ async fn update_permissions(
format!("{COMPONENT} (error: {e}) - failed to update permissions, user ID: {user_id}")
})?;
- // Send event for permissions update
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::UpdatedPermissions {
- user_id: command.user_id.clone(),
- permissions: command.permissions.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
{
let entry_command = EntryCommand::UpdatePermissions(command);
let future = SendWrapper::new(
@@ -341,9 +281,8 @@ async fn change_password(
state
.shard
.shard()
- .permissioner
- .borrow()
- .change_password(identity.user_id)?;
+ .metadata
+ .perm_change_password(identity.user_id)?;
}
state
@@ -358,24 +297,6 @@ async fn change_password(
format!("{COMPONENT} (error: {e}) - failed to change password, user ID: {user_id}")
})?;
- // Send event for password change
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::ChangedPassword {
- user_id: command.user_id.clone(),
- current_password: command.current_password.clone(),
- new_password: command.new_password.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
{
let entry_command = EntryCommand::ChangePassword(command);
let future = SendWrapper::new(
@@ -406,9 +327,8 @@ async fn delete_user(
state
.shard
.shard()
- .permissioner
- .borrow()
- .delete_user(identity.user_id)?;
+ .metadata
+ .perm_delete_user(identity.user_id)?;
state
.shard
@@ -418,22 +338,6 @@ async fn delete_user(
format!("{COMPONENT} (error: {e}) - failed to delete user with ID: {user_id}")
})?;
- // Send event for user deletion
- {
- let broadcast_future = SendWrapper::new(async {
- use crate::shard::transmission::event::ShardEvent;
- let event = ShardEvent::DeletedUser {
- user_id: identifier_user_id.clone(),
- };
- let _responses = state
- .shard
- .shard()
- .broadcast_event_to_all_shards(event)
- .await;
- });
- broadcast_future.await;
- }
-
{
let entry_command = EntryCommand::DeleteUser(DeleteUser {
user_id: identifier_user_id,
diff --git a/core/server/src/lib.rs b/core/server/src/lib.rs
index 41669486f..b6a758006 100644
--- a/core/server/src/lib.rs
+++ b/core/server/src/lib.rs
@@ -41,7 +41,6 @@ pub mod metadata;
pub mod quic;
pub mod server_error;
pub mod shard;
-pub mod slab;
pub mod state;
pub mod streaming;
pub mod tcp;
diff --git a/core/server/src/main.rs b/core/server/src/main.rs
index 3fe36e20d..35ccbffc0 100644
--- a/core/server/src/main.rs
+++ b/core/server/src/main.rs
@@ -28,19 +28,17 @@ use iggy_common::{Aes256GcmEncryptor, EncryptorKind, IggyError, MemoryPool};
use server::SEMANTIC_VERSION;
use server::args::Args;
use server::bootstrap::{
- create_directories, create_shard_connections, create_shard_executor, load_config, load_streams,
- load_users, resolve_persister, update_system_info,
+ create_directories, create_shard_connections, create_shard_executor, load_config,
+ load_metadata, resolve_persister, update_system_info,
};
use server::configs::sharding::ShardAllocator;
use server::diagnostics::{print_io_uring_permission_info, print_locked_memory_limit_info};
use server::io::fs_utils;
use server::log::logger::Logging;
+use server::metadata::{Metadata, create_metadata_handles};
use server::server_error::ServerError;
use server::shard::system::info::SystemInfo;
use server::shard::{IggyShard, calculate_shard_assignment};
-use server::slab::traits_ext::{
- EntityComponentSystem, EntityComponentSystemMutCell, IntoComponents,
-};
use server::state::file::FileState;
use server::state::system::SystemState;
use server::streaming::clients::client_manager::{Client, ClientManager};
@@ -278,8 +276,19 @@ fn main() -> Result<(), ServerError> {
);
let state = SystemState::load(state).await?;
let (streams_state, users_state) = state.decompose();
- let streams = load_streams(streams_state.into_values(), &config.system).await?;
- let users = load_users(users_state.into_values());
+
+ // Create left-right handles for metadata
+ let (mut metadata_writer, metadata_reader) = create_metadata_handles();
+
+ // Create shared metadata reader - each shard will get a clone
+ let metadata = Metadata::new(metadata_reader);
+
+ // Load initial metadata using the writer
+ load_metadata(
+ users_state.into_values(),
+ streams_state.into_values(),
+ &mut metadata_writer,
+ );
// ELEVENTH DISCRETE LOADING STEP.
let shard_allocator = ShardAllocator::new(&config.system.sharding.cpu_allocation)?;
@@ -320,41 +329,33 @@ fn main() -> Result<(), ServerError> {
let client_manager: EternalPtr<DashMap<u32, Client>> = client_manager.into();
let client_manager = ClientManager::new(client_manager);
- streams.with_components(|components| {
- let (root, ..) = components.into_components();
- for (_, stream) in root.iter() {
- stream.topics().with_components(|components| {
- let (root, ..) = components.into_components();
- for (_, topic) in root.iter() {
- topic.partitions().with_components(|components| {
- let (root, ..) = components.into_components();
- for (_, partition) in root.iter() {
- let stream_id = stream.id();
- let topic_id = topic.id();
- let partition_id = partition.id();
- let ns = IggyNamespace::new(stream_id, topic_id, partition_id);
- let shard_id = ShardId::new(calculate_shard_assignment(
- &ns,
- shard_assignment.len() as u32,
- ));
- // TODO(hubcio): LocalIdx is 0 until IggyPartitions is integratedds
- let location = PartitionLocation::new(shard_id, LocalIdx::new(0));
- shards_table.insert(ns, location);
- }
- });
+ // Populate shards_table from SharedMetadata partitions (hierarchical traversal)
+ metadata.with_metadata(|metadata| {
+ for (stream_id, stream_meta) in metadata.streams.iter() {
+ for (topic_id, topic_meta) in stream_meta.topics.iter() {
+ for (partition_id, _partition_meta) in topic_meta.partitions.iter().enumerate() {
+ let ns = IggyNamespace::new(stream_id, topic_id, partition_id);
+ let shard_id = ShardId::new(calculate_shard_assignment(
+ &ns,
+ shard_assignment.len() as u32,
+ ));
+ // TODO(hubcio): LocalIdx is 0 until IggyPartitions is integrated
+ let location = PartitionLocation::new(shard_id, LocalIdx::new(0));
+ shards_table.insert(ns, location);
}
- })
+ }
}
});
+ // Wrap metadata_writer in Option so we can take it for shard 0
+ let mut metadata_writer_opt = Some(metadata_writer);
+
for (id, assignment) in shard_assignment
.into_iter()
.enumerate()
.map(|(idx, assignment)| (idx as u16, assignment))
{
- let streams = streams.clone();
let shards_table = shards_table.clone();
- let users = users.clone();
let connections = connections.clone();
let config = config.clone();
let encryptor = encryptor.clone();
@@ -372,29 +373,14 @@ fn main() -> Result<(), ServerError> {
state_term.clone(),
);
let client_manager = client_manager.clone();
+ let shard_metadata = metadata.clone();
- // TODO: Explore decoupling the `Log` from `Partition` entity.
- // Ergh... I knew this will backfire to include `Log` as part of the `Partition` entity,
- // We have to initialize with a default log for every partition, once we `Clone` the Streams / Topics / Partitions,
- // because `Clone` impl for `Partition` does not clone the actual log, just creates an empty one.
- streams.with_components(|components| {
- let (root, ..) = components.into_components();
- for (_, stream) in root.iter() {
- stream.topics().with_components_mut(|components| {
- let (mut root, ..) = components.into_components();
- for (_, topic) in root.iter_mut() {
- let partitions_count = topic.partitions().len();
- for log_id in 0..partitions_count {
- let id = topic.partitions_mut().insert_default_log();
- assert_eq!(
- id, log_id,
- "main: partition_insert_default_log: id mismatch when creating default log"
- );
- }
- }
- })
- }
- });
+ // Take metadata_writer for shard 0 only
+ let shard_metadata_writer = if id == 0 {
+ metadata_writer_opt.take()
+ } else {
+ None
+ };
let shard_done_tx = shard_done_tx.clone();
let handle = std::thread::Builder::new()
@@ -411,12 +397,10 @@ fn main() -> Result<(), ServerError> {
let rt = create_shard_executor();
rt.block_on(async move {
- let builder = IggyShard::builder();
- let shard = builder
+ let mut builder = IggyShard::builder();
+ builder = builder
.id(id)
- .streams(streams)
.state(state)
- .users(users)
.shards_table(shards_table)
.connections(connections)
.clients_manager(client_manager)
@@ -425,7 +409,13 @@ fn main() -> Result<(), ServerError> {
.version(current_version)
.metrics(metrics)
.is_follower(is_follower)
- .build();
+ .metadata(shard_metadata);
+
+ if let Some(writer) = shard_metadata_writer {
+ builder = builder.metadata_writer(writer);
+ }
+
+ let shard = builder.build();
let shard = Rc::new(shard);
diff --git a/core/server/src/metadata/absorb.rs b/core/server/src/metadata/absorb.rs
index d36186a64..36159dfdc 100644
--- a/core/server/src/metadata/absorb.rs
+++ b/core/server/src/metadata/absorb.rs
@@ -18,6 +18,8 @@
use crate::metadata::ConsumerGroupMemberMeta;
use crate::metadata::inner::InnerMetadata;
use crate::metadata::ops::MetadataOp;
+use crate::metadata::{StreamId, UserId};
+use iggy_common::Permissions;
use left_right::Absorb;
use std::sync::atomic::Ordering;
@@ -39,6 +41,7 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
match op {
MetadataOp::Initialize(initial) => {
*metadata = (**initial).clone();
+ rebuild_all_permission_indexes(metadata);
}
MetadataOp::AddStream { meta, assigned_id } => {
@@ -67,6 +70,7 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
if metadata.streams.contains(*id) {
let stream = metadata.streams.remove(*id);
metadata.stream_index.remove(&stream.name);
+ clear_stream_permission_indexes(metadata, *id);
}
}
@@ -176,8 +180,8 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
&& let Some(topic) = stream.topics.get_mut(*topic_id)
&& let Some(partition) = topic.partitions.get_mut(*partition_id)
{
- partition.consumer_offsets = Some(consumer_offsets.clone());
- partition.consumer_group_offsets = Some(consumer_group_offsets.clone());
+ partition.consumer_offsets = consumer_offsets.clone();
+ partition.consumer_group_offsets = consumer_group_offsets.clone();
}
}
@@ -190,8 +194,10 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
let mut meta = meta.clone();
meta.id = id as u32;
let username = meta.username.clone();
+ let permissions = meta.permissions.clone();
entry.insert(meta);
metadata.user_index.insert(username, id as u32);
+ update_permission_indexes(metadata, id as u32, permissions.as_deref());
}
MetadataOp::UpdateUserMeta { id, meta } => {
@@ -203,7 +209,9 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
metadata.user_index.insert(meta.username.clone(), *id);
}
if metadata.users.contains(user_id) {
+ let permissions = meta.permissions.clone();
metadata.users[user_id] = meta.clone();
+ update_permission_indexes(metadata, *id, permissions.as_deref());
}
}
@@ -214,6 +222,7 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
metadata.user_index.remove(&user.username);
}
metadata.personal_access_tokens.remove(id);
+ clear_permission_indexes(metadata, *id);
}
MetadataOp::AddPersonalAccessToken { user_id, pat } => {
@@ -346,3 +355,91 @@ fn apply_op(metadata: &mut InnerMetadata, op: &MetadataOp, populate_ids: bool) {
}
}
}
+
+fn clear_permission_indexes(metadata: &mut InnerMetadata, user_id: UserId) {
+ metadata.users_global_permissions.remove(&user_id);
+ metadata.users_can_poll_all_streams.remove(&user_id);
+ metadata.users_can_send_all_streams.remove(&user_id);
+ metadata
+ .users_stream_permissions
+ .retain(|(uid, _), _| *uid != user_id);
+ metadata
+ .users_can_poll_stream
+ .retain(|(uid, _)| *uid != user_id);
+ metadata
+ .users_can_send_stream
+ .retain(|(uid, _)| *uid != user_id);
+}
+
+fn clear_stream_permission_indexes(metadata: &mut InnerMetadata, stream_id: StreamId) {
+ metadata
+ .users_stream_permissions
+ .retain(|(_, sid), _| *sid != stream_id);
+ metadata
+ .users_can_poll_stream
+ .retain(|(_, sid)| *sid != stream_id);
+ metadata
+ .users_can_send_stream
+ .retain(|(_, sid)| *sid != stream_id);
+}
+
+fn update_permission_indexes(
+ metadata: &mut InnerMetadata,
+ user_id: UserId,
+ permissions: Option<&Permissions>,
+) {
+ clear_permission_indexes(metadata, user_id);
+
+ let Some(permissions) = permissions else {
+ return;
+ };
+
+ if permissions.global.poll_messages {
+ metadata.users_can_poll_all_streams.insert(user_id);
+ }
+
+ if permissions.global.send_messages {
+ metadata.users_can_send_all_streams.insert(user_id);
+ }
+
+ metadata
+ .users_global_permissions
+ .insert(user_id, permissions.global.clone());
+
+ let Some(streams) = &permissions.streams else {
+ return;
+ };
+
+ for (stream_id, stream_perm) in streams {
+ if stream_perm.poll_messages {
+ metadata.users_can_poll_stream.insert((user_id, *stream_id));
+ }
+
+ if stream_perm.send_messages {
+ metadata.users_can_send_stream.insert((user_id, *stream_id));
+ }
+
+ metadata
+ .users_stream_permissions
+ .insert((user_id, *stream_id), stream_perm.clone());
+ }
+}
+
+fn rebuild_all_permission_indexes(metadata: &mut InnerMetadata) {
+ metadata.users_global_permissions.clear();
+ metadata.users_stream_permissions.clear();
+ metadata.users_can_poll_all_streams.clear();
+ metadata.users_can_send_all_streams.clear();
+ metadata.users_can_poll_stream.clear();
+ metadata.users_can_send_stream.clear();
+
+ let user_permissions: Vec<_> = metadata
+ .users
+ .iter()
+ .map(|(_, user)| (user.id, user.permissions.clone()))
+ .collect();
+
+ for (user_id, permissions) in user_permissions {
+ update_permission_indexes(metadata, user_id, permissions.as_deref());
+ }
+}
diff --git a/core/server/src/metadata/inner.rs b/core/server/src/metadata/inner.rs
index f826d3d8e..4bcc165f5 100644
--- a/core/server/src/metadata/inner.rs
+++ b/core/server/src/metadata/inner.rs
@@ -16,8 +16,8 @@
// under the License.
use crate::metadata::{StreamId, StreamMeta, UserId, UserMeta};
-use ahash::AHashMap;
-use iggy_common::PersonalAccessToken;
+use ahash::{AHashMap, AHashSet};
+use iggy_common::{GlobalPermissions, PersonalAccessToken, StreamPermissions};
use slab::Slab;
use std::sync::Arc;
@@ -35,6 +35,16 @@ pub struct InnerMetadata {
/// user_id -> (token_hash -> PAT)
pub personal_access_tokens: AHashMap<UserId, AHashMap<Arc<str>, PersonalAccessToken>>,
+
+ // Permission indexes (auto-maintained by absorb)
+ pub users_global_permissions: AHashMap<UserId, GlobalPermissions>,
+ pub users_stream_permissions: AHashMap<(UserId, StreamId), StreamPermissions>,
+
+ // Hot-path optimizations for message send/poll
+ pub users_can_poll_all_streams: AHashSet<UserId>,
+ pub users_can_send_all_streams: AHashSet<UserId>,
+ pub users_can_poll_stream: AHashSet<(UserId, StreamId)>,
+ pub users_can_send_stream: AHashSet<(UserId, StreamId)>,
}
impl InnerMetadata {
diff --git a/core/server/src/metadata/mod.rs b/core/server/src/metadata/mod.rs
index 12804095b..8b00c4f2a 100644
--- a/core/server/src/metadata/mod.rs
+++ b/core/server/src/metadata/mod.rs
@@ -17,8 +17,8 @@
//! Shared metadata module providing a single source of truth for all shards.
//!
-//! This module provides an `ArcSwap`-based approach where all shards read from
-//! a shared snapshot, and only shard 0 can write (swap in new snapshots).
+//! This module provides a `LeftRight`-based approach where all shards read from
+//! a shared snapshot, and only shard 0 can write.
//!
//! # Architecture
//!
@@ -44,7 +44,7 @@ pub use consumer_group_member::ConsumerGroupMemberMeta;
pub use inner::InnerMetadata;
pub use ops::MetadataOp;
pub use partition::PartitionMeta;
-pub use reader::Metadata;
+pub use reader::{Metadata, PartitionInitInfo};
pub use stream::StreamMeta;
pub use topic::TopicMeta;
pub use user::UserMeta;
diff --git a/core/server/src/metadata/ops.rs b/core/server/src/metadata/ops.rs
index 4d21c0d90..f489b7309 100644
--- a/core/server/src/metadata/ops.rs
+++ b/core/server/src/metadata/ops.rs
@@ -20,7 +20,8 @@ use crate::metadata::{
ConsumerGroupId, ConsumerGroupMeta, PartitionId, PartitionMeta, StreamId, StreamMeta, TopicId,
TopicMeta, UserId, UserMeta,
};
-use crate::streaming::partitions::partition::{ConsumerGroupOffsets, ConsumerOffsets};
+use crate::streaming::partitions::consumer_group_offsets::ConsumerGroupOffsets;
+use crate::streaming::partitions::consumer_offsets::ConsumerOffsets;
use iggy_common::{CompressionAlgorithm, IggyExpiry, MaxTopicSize, PersonalAccessToken};
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
diff --git a/core/server/src/metadata/partition.rs b/core/server/src/metadata/partition.rs
index b2490b96a..02c8cb912 100644
--- a/core/server/src/metadata/partition.rs
+++ b/core/server/src/metadata/partition.rs
@@ -16,7 +16,8 @@
// under the License.
use crate::metadata::PartitionId;
-use crate::streaming::partitions::partition::{ConsumerGroupOffsets, ConsumerOffsets};
+use crate::streaming::partitions::consumer_group_offsets::ConsumerGroupOffsets;
+use crate::streaming::partitions::consumer_offsets::ConsumerOffsets;
use crate::streaming::stats::PartitionStats;
use iggy_common::IggyTimestamp;
use std::sync::Arc;
@@ -25,12 +26,10 @@ use std::sync::Arc;
pub struct PartitionMeta {
pub id: PartitionId,
pub created_at: IggyTimestamp,
- /// Monotonically increasing version to detect stale partition_store entries.
+ /// Monotonically increasing version to detect stale local_partitions entries.
/// Set to the Metadata version when the partition was created.
pub revision_id: u64,
pub stats: Arc<PartitionStats>,
- /// Consumer offsets for this partition // TODO: move to partition store
- pub consumer_offsets: Option<Arc<ConsumerOffsets>>,
- /// Consumer group offsets for this partition // TODO: move to partition store
- pub consumer_group_offsets: Option<Arc<ConsumerGroupOffsets>>,
+ pub consumer_offsets: Arc<ConsumerOffsets>,
+ pub consumer_group_offsets: Arc<ConsumerGroupOffsets>,
}
diff --git a/core/server/src/metadata/reader.rs b/core/server/src/metadata/reader.rs
index ea85511a6..765711037 100644
--- a/core/server/src/metadata/reader.rs
+++ b/core/server/src/metadata/reader.rs
@@ -19,10 +19,13 @@ use crate::metadata::{
ConsumerGroupId, ConsumerGroupMeta, InnerMetadata, MetadataReadHandle, PartitionId,
PartitionMeta, StreamId, StreamMeta, TopicId, TopicMeta, UserId, UserMeta,
};
-use crate::streaming::partitions::partition::{ConsumerGroupOffsets, ConsumerOffsets};
+use crate::streaming::partitions::consumer_group_offsets::ConsumerGroupOffsets;
+use crate::streaming::partitions::consumer_offsets::ConsumerOffsets;
use crate::streaming::stats::{PartitionStats, StreamStats, TopicStats};
use iggy_common::sharding::IggyNamespace;
-use iggy_common::{IdKind, Identifier, PersonalAccessToken};
+use iggy_common::{
+ IdKind, Identifier, IggyError, IggyExpiry, IggyTimestamp, MaxTopicSize, PersonalAccessToken,
+};
use left_right::ReadGuard;
use std::sync::Arc;
use std::sync::atomic::Ordering;
@@ -44,7 +47,7 @@ impl Metadata {
}
#[inline]
- pub fn load(&self) -> ReadGuard<'_, InnerMetadata> {
+ pub(super) fn load(&self) -> ReadGuard<'_, InnerMetadata> {
self.inner
.enter()
.expect("metadata not initialized - writer must publish before reads")
@@ -240,12 +243,12 @@ impl Metadata {
}
let counter = &topic.round_robin_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)
+ let current = counter
+ .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |c| {
+ Some((c + 1) % partitions_count)
+ })
+ .unwrap();
+ Some(current % partitions_count)
}
pub fn get_next_member_partition_id(
@@ -355,7 +358,7 @@ impl Metadata {
.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())
+ .map(|p| p.consumer_offsets.clone())
}
pub fn get_partition_consumer_group_offsets(
@@ -369,7 +372,7 @@ impl Metadata {
.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())
+ .map(|p| p.consumer_group_offsets.clone())
}
pub fn get_user(&self, id: UserId) -> Option<UserMeta> {
@@ -482,4 +485,678 @@ impl Metadata {
.map(|g| g.members.iter().any(|(_, m)| m.client_id == client_id))
.unwrap_or(false)
}
+
+ /// Execute a closure with read access to the metadata snapshot.
+ /// This is the safe way to perform complex read operations that need
+ /// atomic access to multiple metadata fields.
+ ///
+ /// The closure receives an immutable reference to the metadata and must
+ /// return owned data (not references). This ensures the ReadGuard is
+ /// dropped before any async operations can occur.
+ #[inline]
+ pub fn with_metadata<F, R>(&self, f: F) -> R
+ where
+ F: FnOnce(&InnerMetadata) -> R,
+ {
+ let guard = self.load();
+ f(&guard)
+ }
+
+ /// Get all partition IDs for a topic, sorted.
+ pub fn get_partition_ids(&self, stream_id: StreamId, topic_id: TopicId) -> Vec<PartitionId> {
+ self.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .and_then(|s| s.topics.get(topic_id))
+ .map(|t| {
+ let mut ids: Vec<_> = t.partitions.iter().enumerate().map(|(k, _)| k).collect();
+ ids.sort_unstable();
+ ids
+ })
+ .unwrap_or_default()
+ })
+ }
+
+ /// Get all topic IDs for a stream, sorted.
+ pub fn get_topic_ids(&self, stream_id: StreamId) -> Vec<TopicId> {
+ self.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .map(|s| {
+ let mut ids: Vec<_> = s.topics.iter().map(|(k, _)| k).collect();
+ ids.sort_unstable();
+ ids
+ })
+ .unwrap_or_default()
+ })
+ }
+
+ /// Get all stream IDs, sorted.
+ pub fn get_stream_ids(&self) -> Vec<StreamId> {
+ self.with_metadata(|m| {
+ let mut ids: Vec<_> = m.streams.iter().map(|(k, _)| k).collect();
+ ids.sort_unstable();
+ ids
+ })
+ }
+
+ /// Get all namespaces (stream/topic/partition combinations).
+ pub fn get_all_namespaces(&self) -> Vec<IggyNamespace> {
+ self.with_metadata(|m| {
+ let mut namespaces = Vec::new();
+ for (stream_id, stream) in m.streams.iter() {
+ for (topic_id, topic) in stream.topics.iter() {
+ for (partition_id, _) in topic.partitions.iter().enumerate() {
+ namespaces.push(IggyNamespace::new(stream_id, topic_id, partition_id));
+ }
+ }
+ }
+ namespaces
+ })
+ }
+
+ /// Get topic configuration (message_expiry, max_topic_size).
+ pub fn get_topic_config(
+ &self,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Option<(IggyExpiry, MaxTopicSize)> {
+ self.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .and_then(|s| s.topics.get(topic_id))
+ .map(|t| (t.message_expiry, t.max_topic_size))
+ })
+ }
+
+ /// Get partition initialization info needed for LocalPartition setup.
+ pub fn get_partition_init_info(
+ &self,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ partition_id: PartitionId,
+ ) -> Option<PartitionInitInfo> {
+ self.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .and_then(|s| s.topics.get(topic_id))
+ .and_then(|t| t.partitions.get(partition_id))
+ .map(|p| PartitionInitInfo {
+ created_at: p.created_at,
+ revision_id: p.revision_id,
+ stats: p.stats.clone(),
+ consumer_offsets: p.consumer_offsets.clone(),
+ consumer_group_offsets: p.consumer_group_offsets.clone(),
+ })
+ })
+ }
+
+ /// Get consumer group member ID for a client.
+ pub fn get_consumer_group_member_id(
+ &self,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ group_id: ConsumerGroupId,
+ client_id: u32,
+ ) -> Option<usize> {
+ self.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .and_then(|s| s.topics.get(topic_id))
+ .and_then(|t| t.consumer_groups.get(group_id))
+ .and_then(|g| {
+ g.members
+ .iter()
+ .find(|(_, member)| member.client_id == client_id)
+ .map(|(id, _)| id)
+ })
+ })
+ }
+
+ /// Get all consumer groups for a topic.
+ pub fn get_all_consumer_groups(
+ &self,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Vec<ConsumerGroupMeta> {
+ self.with_metadata(|m| {
+ m.streams
+ .get(stream_id)
+ .and_then(|s| s.topics.get(topic_id))
+ .map(|t| t.consumer_groups.iter().map(|(_, cg)| cg.clone()).collect())
+ .unwrap_or_default()
+ })
+ }
+
+ // ==========================================================================
+ // Permission checking methods (perm_*)
+ // ==========================================================================
+
+ /// Inheritance: manage_streams → read_streams → read_topics → poll_messages
+ pub fn perm_poll_messages(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if metadata.users_can_poll_all_streams.contains(&user_id) {
+ return Ok(());
+ }
+
+ if let Some(global) = metadata.users_global_permissions.get(&user_id)
+ && (global.read_topics
+ || global.manage_topics
+ || global.read_streams
+ || global.manage_streams)
+ {
+ return Ok(());
+ }
+
+ if metadata
+ .users_can_poll_stream
+ .contains(&(user_id, stream_id))
+ {
+ return Ok(());
+ }
+
+ let Some(stream_permissions) = metadata.users_stream_permissions.get(&(user_id, stream_id))
+ else {
+ return Err(IggyError::Unauthorized);
+ };
+
+ if stream_permissions.manage_stream || stream_permissions.read_stream {
+ return Ok(());
+ }
+
+ if stream_permissions.manage_topics || stream_permissions.read_topics {
+ return Ok(());
+ }
+
+ if stream_permissions.poll_messages {
+ return Ok(());
+ }
+
+ if let Some(topics) = &stream_permissions.topics
+ && let Some(topic_permissions) = topics.get(&topic_id)
+ && (topic_permissions.manage_topic
+ || topic_permissions.read_topic
+ || topic_permissions.poll_messages)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ /// Inheritance: manage_streams → manage_topics → send_messages
+ pub fn perm_append_messages(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if metadata.users_can_send_all_streams.contains(&user_id) {
+ return Ok(());
+ }
+
+ if let Some(global) = metadata.users_global_permissions.get(&user_id)
+ && (global.manage_streams || global.manage_topics)
+ {
+ return Ok(());
+ }
+
+ if metadata
+ .users_can_send_stream
+ .contains(&(user_id, stream_id))
+ {
+ return Ok(());
+ }
+
+ let Some(stream_permissions) = metadata.users_stream_permissions.get(&(user_id, stream_id))
+ else {
+ return Err(IggyError::Unauthorized);
+ };
+
+ if stream_permissions.manage_stream || stream_permissions.manage_topics {
+ return Ok(());
+ }
+
+ if stream_permissions.send_messages {
+ return Ok(());
+ }
+
+ if let Some(topics) = &stream_permissions.topics
+ && let Some(topic_permissions) = topics.get(&topic_id)
+ && (topic_permissions.manage_topic || topic_permissions.send_messages)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_get_stream(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && (global_permissions.manage_streams || global_permissions.read_streams)
+ {
+ return Ok(());
+ }
+
+ if let Some(stream_permissions) =
+ metadata.users_stream_permissions.get(&(user_id, stream_id))
+ && (stream_permissions.manage_stream || stream_permissions.read_stream)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_get_streams(&self, user_id: u32) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && (global_permissions.manage_streams || global_permissions.read_streams)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_create_stream(&self, user_id: u32) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && global_permissions.manage_streams
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_update_stream(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ self.perm_manage_stream(user_id, stream_id)
+ }
+
+ pub fn perm_delete_stream(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ self.perm_manage_stream(user_id, stream_id)
+ }
+
+ pub fn perm_purge_stream(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ self.perm_manage_stream(user_id, stream_id)
+ }
+
+ fn perm_manage_stream(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && global_permissions.manage_streams
+ {
+ return Ok(());
+ }
+
+ if let Some(stream_permissions) =
+ metadata.users_stream_permissions.get(&(user_id, stream_id))
+ && stream_permissions.manage_stream
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ /// Inheritance: manage_streams → read_streams → read_topics
+ pub fn perm_get_topic(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global) = metadata.users_global_permissions.get(&user_id)
+ && (global.read_streams
+ || global.manage_streams
+ || global.manage_topics
+ || global.read_topics)
+ {
+ return Ok(());
+ }
+
+ if let Some(stream_permissions) =
+ metadata.users_stream_permissions.get(&(user_id, stream_id))
+ {
+ if stream_permissions.manage_stream
+ || stream_permissions.read_stream
+ || stream_permissions.manage_topics
+ || stream_permissions.read_topics
+ {
+ return Ok(());
+ }
+
+ if let Some(topics) = &stream_permissions.topics
+ && let Some(topic_permissions) = topics.get(&topic_id)
+ && (topic_permissions.manage_topic || topic_permissions.read_topic)
+ {
+ return Ok(());
+ }
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_get_topics(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global) = metadata.users_global_permissions.get(&user_id)
+ && (global.read_streams
+ || global.manage_streams
+ || global.manage_topics
+ || global.read_topics)
+ {
+ return Ok(());
+ }
+
+ if let Some(stream_permissions) =
+ metadata.users_stream_permissions.get(&(user_id, stream_id))
+ && (stream_permissions.manage_stream
+ || stream_permissions.read_stream
+ || stream_permissions.manage_topics
+ || stream_permissions.read_topics)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ /// Inheritance: manage_streams → manage_topics
+ pub fn perm_create_topic(&self, user_id: u32, stream_id: StreamId) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global) = metadata.users_global_permissions.get(&user_id)
+ && (global.manage_streams || global.manage_topics)
+ {
+ return Ok(());
+ }
+
+ if let Some(stream_permissions) =
+ metadata.users_stream_permissions.get(&(user_id, stream_id))
+ && (stream_permissions.manage_stream || stream_permissions.manage_topics)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_update_topic(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_manage_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_delete_topic(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_manage_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_purge_topic(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_manage_topic(user_id, stream_id, topic_id)
+ }
+
+ /// Inheritance: manage_streams → manage_topics
+ fn perm_manage_topic(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global) = metadata.users_global_permissions.get(&user_id)
+ && (global.manage_streams || global.manage_topics)
+ {
+ return Ok(());
+ }
+
+ if let Some(stream_permissions) =
+ metadata.users_stream_permissions.get(&(user_id, stream_id))
+ {
+ if stream_permissions.manage_stream || stream_permissions.manage_topics {
+ return Ok(());
+ }
+
+ if let Some(topics) = &stream_permissions.topics
+ && let Some(topic_permissions) = topics.get(&topic_id)
+ && topic_permissions.manage_topic
+ {
+ return Ok(());
+ }
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ pub fn perm_create_partitions(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_update_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_delete_partitions(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_update_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_delete_segments(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_update_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_create_consumer_group(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_get_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_delete_consumer_group(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_get_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_get_consumer_group(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_get_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_get_consumer_groups(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_get_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_join_consumer_group(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_get_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_leave_consumer_group(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_get_topic(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_get_consumer_offset(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_poll_messages(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_store_consumer_offset(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_poll_messages(user_id, stream_id, topic_id)
+ }
+
+ pub fn perm_delete_consumer_offset(
+ &self,
+ user_id: u32,
+ stream_id: StreamId,
+ topic_id: TopicId,
+ ) -> Result<(), IggyError> {
+ self.perm_poll_messages(user_id, stream_id, topic_id)
+ }
+
+ // ==========================================================================
+ // User permission methods
+ // ==========================================================================
+
+ pub fn perm_get_user(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_read_users(user_id)
+ }
+
+ pub fn perm_get_users(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_read_users(user_id)
+ }
+
+ pub fn perm_create_user(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_manage_users(user_id)
+ }
+
+ pub fn perm_delete_user(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_manage_users(user_id)
+ }
+
+ pub fn perm_update_user(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_manage_users(user_id)
+ }
+
+ pub fn perm_update_permissions(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_manage_users(user_id)
+ }
+
+ pub fn perm_change_password(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_manage_users(user_id)
+ }
+
+ fn perm_manage_users(&self, user_id: u32) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && global_permissions.manage_users
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ fn perm_read_users(&self, user_id: u32) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && (global_permissions.manage_users || global_permissions.read_users)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+
+ // ==========================================================================
+ // System permission methods
+ // ==========================================================================
+
+ pub fn perm_get_stats(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_get_server_info(user_id)
+ }
+
+ pub fn perm_get_clients(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_get_server_info(user_id)
+ }
+
+ pub fn perm_get_client(&self, user_id: u32) -> Result<(), IggyError> {
+ self.perm_get_server_info(user_id)
+ }
+
+ fn perm_get_server_info(&self, user_id: u32) -> Result<(), IggyError> {
+ let metadata = self.load();
+
+ if let Some(global_permissions) = metadata.users_global_permissions.get(&user_id)
+ && (global_permissions.manage_servers || global_permissions.read_servers)
+ {
+ return Ok(());
+ }
+
+ Err(IggyError::Unauthorized)
+ }
+}
+
+/// Information needed to initialize a LocalPartition.
+#[derive(Clone, Debug)]
+pub struct PartitionInitInfo {
+ pub created_at: IggyTimestamp,
+ pub revision_id: u64,
+ pub stats: Arc<PartitionStats>,
+ pub consumer_offsets: Arc<ConsumerOffsets>,
+ pub consumer_group_offsets: Arc<ConsumerGroupOffsets>,
}
diff --git a/core/server/src/metadata/writer.rs b/core/server/src/metadata/writer.rs
index daa1142bf..f76d35abc 100644
--- a/core/server/src/metadata/writer.rs
+++ b/core/server/src/metadata/writer.rs
@@ -22,7 +22,8 @@ use crate::metadata::{
ConsumerGroupId, ConsumerGroupMeta, PartitionId, PartitionMeta, StreamId, StreamMeta, TopicId,
TopicMeta, UserId, UserMeta,
};
-use crate::streaming::partitions::partition::{ConsumerGroupOffsets, ConsumerOffsets};
+use crate::streaming::partitions::consumer_group_offsets::ConsumerGroupOffsets;
+use crate::streaming::partitions::consumer_offsets::ConsumerOffsets;
use crate::streaming::stats::{PartitionStats, StreamStats, TopicStats};
use iggy_common::{
CompressionAlgorithm, Identifier, IggyError, IggyExpiry, IggyTimestamp, MaxTopicSize,
@@ -481,8 +482,8 @@ impl MetadataWriter {
created_at,
revision_id: 0,
stats: stats.clone(),
- consumer_offsets: None,
- consumer_group_offsets: None,
+ consumer_offsets: Arc::new(ConsumerOffsets::with_capacity(0)),
+ consumer_group_offsets: Arc::new(ConsumerGroupOffsets::with_capacity(0)),
});
stats_list.push(stats);
}
diff --git a/core/server/src/quic/listener.rs b/core/server/src/quic/listener.rs
index 67a2d4937..69df4dadf 100644
--- a/core/server/src/quic/listener.rs
+++ b/core/server/src/quic/listener.rs
@@ -131,7 +131,7 @@ async fn handle_connection(
}
}
- shard.delete_client(client_id);
+ shard.delete_client(client_id).await;
shard.task_registry.remove_connection(&client_id);
info!("QUIC connection {} closed", client_id);
Ok(())
diff --git a/core/server/src/shard/builder.rs b/core/server/src/shard/builder.rs
index 135a9b4bc..ee29bc6a8 100644
--- a/core/server/src/shard/builder.rs
+++ b/core/server/src/shard/builder.rs
@@ -20,9 +20,10 @@ use super::{
IggyShard, TaskRegistry, transmission::connector::ShardConnector,
transmission::frame::ShardFrame,
};
+use crate::metadata::{Metadata, MetadataWriter};
+use crate::streaming::partitions::local_partitions::LocalPartitions;
use crate::{
configs::server::ServerConfig,
- slab::{streams::Streams, users::Users},
state::file::FileState,
streaming::{
clients::client_manager::ClientManager, diagnostics::metrics::Metrics,
@@ -30,18 +31,21 @@ use crate::{
},
versioning::SemanticVersion,
};
+use ahash::AHashSet;
use dashmap::DashMap;
use iggy_common::EncryptorKind;
use iggy_common::sharding::{IggyNamespace, PartitionLocation};
-use std::{cell::Cell, rc::Rc, sync::atomic::AtomicBool};
+use std::{
+ cell::{Cell, RefCell},
+ rc::Rc,
+ sync::atomic::AtomicBool,
+};
#[derive(Default)]
pub struct IggyShardBuilder {
id: Option<u16>,
- streams: Option<Streams>,
shards_table: Option<EternalPtr<DashMap<IggyNamespace, PartitionLocation>>>,
state: Option<FileState>,
- users: Option<Users>,
client_manager: Option<ClientManager>,
connections: Option<Vec<ShardConnector<ShardFrame>>>,
config: Option<ServerConfig>,
@@ -49,6 +53,8 @@ pub struct IggyShardBuilder {
version: Option<SemanticVersion>,
metrics: Option<Metrics>,
is_follower: bool,
+ metadata: Option<Metadata>,
+ metadata_writer: Option<MetadataWriter>,
}
impl IggyShardBuilder {
@@ -90,21 +96,11 @@ impl IggyShardBuilder {
self
}
- pub fn streams(mut self, streams: Streams) -> Self {
- self.streams = Some(streams);
- self
- }
-
pub fn state(mut self, state: FileState) -> Self {
self.state = Some(state);
self
}
- pub fn users(mut self, users: Users) -> Self {
- self.users = Some(users);
- self
- }
-
pub fn metrics(mut self, metrics: Metrics) -> Self {
self.metrics = Some(metrics);
self
@@ -115,18 +111,27 @@ impl IggyShardBuilder {
self
}
+ pub fn metadata(mut self, metadata: Metadata) -> Self {
+ self.metadata = Some(metadata);
+ self
+ }
+
+ pub fn metadata_writer(mut self, metadata_writer: MetadataWriter) -> Self {
+ self.metadata_writer = Some(metadata_writer);
+ self
+ }
+
// TODO: Too much happens in there, some of those bootstrapping logic should be moved outside.
pub fn build(self) -> IggyShard {
let id = self.id.unwrap();
- let streams = self.streams.unwrap();
let shards_table = self.shards_table.unwrap();
let state = self.state.unwrap();
- let users = self.users.unwrap();
let config = self.config.unwrap();
let connections = self.connections.unwrap();
let encryptor = self.encryptor;
let client_manager = self.client_manager.unwrap();
let version = self.version.unwrap();
+ let metadata = self.metadata.expect("metadata is required");
let (stop_receiver, frame_receiver) = connections
.iter()
.filter(|c| c.id == id)
@@ -150,12 +155,17 @@ impl IggyShardBuilder {
// Trigger initial check in case servers bind before task starts
let _ = config_writer_notify.try_send(());
+ // Create per-shard stores (wrapped in RefCell for interior mutability)
+ let local_partitions = RefCell::new(LocalPartitions::new());
+
IggyShard {
id,
shards,
shards_table,
- streams, // TODO: Fixme
- users,
+ metadata,
+ metadata_writer: self.metadata_writer.map(RefCell::new),
+ local_partitions,
+ pending_partition_inits: RefCell::new(AHashSet::new()),
fs_locks: Default::default(),
encryptor,
config,
@@ -173,7 +183,6 @@ impl IggyShardBuilder {
config_writer_notify,
config_writer_receiver,
task_registry,
- permissioner: Default::default(),
client_manager,
}
}
diff --git a/core/server/src/shard/handlers.rs b/core/server/src/shard/handlers.rs
index 30ead4d62..53756d5f8 100644
--- a/core/server/src/shard/handlers.rs
+++ b/core/server/src/shard/handlers.rs
@@ -17,22 +17,22 @@
use super::*;
use crate::{
+ metadata::{PartitionId, TopicId},
shard::{
IggyShard,
- namespace::IggyFullNamespace,
transmission::{
event::ShardEvent,
frame::ShardResponse,
message::{ShardMessage, ShardRequest, ShardRequestPayload},
},
},
- streaming::traits::MainOps,
tcp::{
connection_handler::{ConnectionAction, handle_connection, handle_error},
tcp_listener::cleanup_connection,
},
};
use compio_net::TcpStream;
+use iggy_common::sharding::IggyNamespace;
use iggy_common::{Identifier, IggyError, SenderKind, TransportProtocol};
use nix::sys::stat::SFlag;
use std::os::fd::{FromRawFd, IntoRawFd};
@@ -63,71 +63,189 @@ async fn handle_request(
let partition_id = request.partition_id;
match request.payload {
ShardRequestPayload::SendMessages { batch } => {
- let ns = IggyFullNamespace::new(stream_id, topic_id, partition_id);
let batch = shard.maybe_encrypt_messages(batch)?;
let messages_count = batch.count();
+
+ let (stream, topic) = shard.resolve_topic_id(&stream_id, &topic_id)?;
+ let namespace = IggyNamespace::new(stream, topic, partition_id);
+
+ shard.ensure_partition(&namespace).await?;
+
shard
- .streams
- .append_messages(&shard.config.system, &shard.task_registry, &ns, batch)
+ .append_messages_to_local_partition(&namespace, batch, &shard.config.system)
.await?;
+
shard.metrics.increment_messages(messages_count as u64);
Ok(ShardResponse::SendMessages)
}
ShardRequestPayload::PollMessages { args, consumer } => {
let auto_commit = args.auto_commit;
- let ns = IggyFullNamespace::new(stream_id, topic_id, partition_id);
- let (metadata, batches) = shard.streams.poll_messages(&ns, consumer, args).await?;
+
+ let (stream, topic) = shard.resolve_topic_id(&stream_id, &topic_id)?;
+ let namespace = IggyNamespace::new(stream, topic, partition_id);
+
+ shard.ensure_partition(&namespace).await?;
+
+ let (poll_metadata, batches) = shard
+ .poll_messages_from_local_partition(&namespace, consumer, args)
+ .await?;
if auto_commit && !batches.is_empty() {
let offset = batches
.last_offset()
.expect("Batch set should have at least one batch");
shard
- .streams
- .auto_commit_consumer_offset(
- &shard.config.system,
- ns.stream_id(),
- ns.topic_id(),
- partition_id,
- consumer,
- offset,
- )
+ .auto_commit_consumer_offset_from_local_partition(&namespace, consumer, offset)
.await?;
}
- Ok(ShardResponse::PollMessages((metadata, batches)))
+ Ok(ShardResponse::PollMessages((poll_metadata, batches)))
}
ShardRequestPayload::FlushUnsavedBuffer { fsync } => {
+ let (stream, topic) = shard.resolve_topic_id(&stream_id, &topic_id)?;
shard
- .flush_unsaved_buffer_base(&stream_id, &topic_id, partition_id, fsync)
+ .flush_unsaved_buffer_base(stream, topic, partition_id, fsync)
.await?;
Ok(ShardResponse::FlushUnsavedBuffer)
}
ShardRequestPayload::DeleteSegments { segments_count } => {
+ let (stream, topic) = shard.resolve_topic_id(&stream_id, &topic_id)?;
shard
- .delete_segments_base(&stream_id, &topic_id, partition_id, segments_count)
+ .delete_segments_base(stream, topic, partition_id, segments_count)
.await?;
Ok(ShardResponse::DeleteSegments)
}
- ShardRequestPayload::CreateStream { user_id: _, name } => {
- assert_eq!(shard.id, 0, "CreateStream should only be handled by shard0");
+ ShardRequestPayload::CreatePartitions {
+ user_id,
+ stream_id,
+ topic_id,
+ partitions_count,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "CreatePartitions should only be handled by shard0"
+ );
- // Acquire stream lock to serialize filesystem operations
- let _stream_guard = shard.fs_locks.stream_lock.lock().await;
+ let _partition_guard = shard.fs_locks.partition_lock.lock().await;
- let stream = shard.create_stream(name.clone()).await?;
- let created_stream_id = stream.id();
+ let partition_infos = shard
+ .create_partitions(&stream_id, &topic_id, partitions_count)
+ .await?;
+ let partition_ids = partition_infos.iter().map(|p| p.id).collect::<Vec<_>>();
- let event = ShardEvent::CreatedStream {
- id: created_stream_id,
- stream: stream.clone(),
+ let event = ShardEvent::CreatedPartitions {
+ stream_id: stream_id.clone(),
+ topic_id: topic_id.clone(),
+ partitions: partition_infos,
};
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+ let total_partition_count = shard
+ .metadata
+ .partitions_count(numeric_stream_id, numeric_topic_id)
+ as u32;
+ shard.writer().rebalance_consumer_groups_for_topic(
+ numeric_stream_id,
+ numeric_topic_id,
+ total_partition_count,
+ );
+ let command = iggy_common::create_partitions::CreatePartitions {
+ stream_id,
+ topic_id,
+ partitions_count,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::CreatePartitions(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::CreatePartitionsResponse(partition_ids))
+ }
+ ShardRequestPayload::DeletePartitions {
+ user_id,
+ stream_id,
+ topic_id,
+ partitions_count,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "DeletePartitions should only be handled by shard0"
+ );
+
+ let _partition_guard = shard.fs_locks.partition_lock.lock().await;
+
+ let deleted_partition_ids = shard
+ .delete_partitions(&stream_id, &topic_id, partitions_count)
+ .await?;
+
+ let event = ShardEvent::DeletedPartitions {
+ stream_id: stream_id.clone(),
+ topic_id: topic_id.clone(),
+ partitions_count,
+ partition_ids: deleted_partition_ids.clone(),
+ };
shard.broadcast_event_to_all_shards(event).await?;
- Ok(ShardResponse::CreateStreamResponse(stream))
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+ let remaining_partition_count = shard
+ .metadata
+ .partitions_count(numeric_stream_id, numeric_topic_id)
+ as u32;
+ shard.writer().rebalance_consumer_groups_for_topic(
+ numeric_stream_id,
+ numeric_topic_id,
+ remaining_partition_count,
+ );
+
+ let command = iggy_common::delete_partitions::DeletePartitions {
+ stream_id,
+ topic_id,
+ partitions_count,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::DeletePartitions(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::DeletePartitionsResponse(
+ deleted_partition_ids,
+ ))
+ }
+ ShardRequestPayload::CreateStream { user_id, name } => {
+ assert_eq!(shard.id, 0, "CreateStream should only be handled by shard0");
+
+ // Acquire stream lock to serialize filesystem operations
+ let _stream_guard = shard.fs_locks.stream_lock.lock().await;
+
+ let created_stream_id = shard.create_stream(name.clone()).await?;
+
+ let command = iggy_common::create_stream::CreateStream { name };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::CreateStream(
+ crate::state::models::CreateStreamWithId {
+ stream_id: created_stream_id as u32,
+ command,
+ },
+ ),
+ )
+ .await?;
+
+ Ok(ShardResponse::CreateStreamResponse(created_stream_id))
}
ShardRequestPayload::CreateTopic {
- user_id: _,
+ user_id,
stream_id,
name,
partitions_count,
@@ -141,7 +259,7 @@ async fn handle_request(
// Acquire topic lock to serialize filesystem operations
let _topic_guard = shard.fs_locks.topic_lock.lock().await;
- let topic = shard
+ let topic_id_num = shard
.create_topic(
&stream_id,
name.clone(),
@@ -152,31 +270,47 @@ async fn handle_request(
)
.await?;
- let topic_id = topic.id();
- let event = ShardEvent::CreatedTopic {
- stream_id: stream_id.clone(),
- topic: topic.clone(),
- };
- shard.broadcast_event_to_all_shards(event).await?;
- let partitions = shard
+ let partition_infos = shard
.create_partitions(
&stream_id,
- &Identifier::numeric(topic_id as u32).unwrap(),
+ &Identifier::numeric(topic_id_num as u32).unwrap(),
partitions_count,
)
.await?;
let event = ShardEvent::CreatedPartitions {
stream_id: stream_id.clone(),
- topic_id: Identifier::numeric(topic_id as u32).unwrap(),
- partitions,
+ topic_id: Identifier::numeric(topic_id_num as u32).unwrap(),
+ partitions: partition_infos,
};
shard.broadcast_event_to_all_shards(event).await?;
- Ok(ShardResponse::CreateTopicResponse(topic))
+ let command = iggy_common::create_topic::CreateTopic {
+ stream_id,
+ partitions_count,
+ compression_algorithm,
+ message_expiry,
+ max_topic_size,
+ replication_factor,
+ name,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::CreateTopic(
+ crate::state::models::CreateTopicWithId {
+ topic_id: topic_id_num as u32,
+ command,
+ },
+ ),
+ )
+ .await?;
+
+ Ok(ShardResponse::CreateTopicResponse(topic_id_num))
}
ShardRequestPayload::UpdateTopic {
- user_id: _,
+ user_id,
stream_id,
topic_id,
name,
@@ -197,41 +331,69 @@ async fn handle_request(
replication_factor,
)?;
- let event = ShardEvent::UpdatedTopic {
- stream_id: stream_id.clone(),
- topic_id: topic_id.clone(),
- name,
- message_expiry,
+ let command = iggy_common::update_topic::UpdateTopic {
+ stream_id,
+ topic_id,
compression_algorithm,
+ message_expiry,
max_topic_size,
replication_factor,
+ name,
};
- shard.broadcast_event_to_all_shards(event).await?;
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::UpdateTopic(command),
+ )
+ .await?;
Ok(ShardResponse::UpdateTopicResponse)
}
ShardRequestPayload::DeleteTopic {
- user_id: _,
+ user_id,
stream_id,
topic_id,
} => {
assert_eq!(shard.id, 0, "DeleteTopic should only be handled by shard0");
+ // Capture numeric IDs and partition_ids BEFORE deletion for broadcast.
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+ let partition_ids = shard
+ .metadata
+ .get_partition_ids(numeric_stream_id, numeric_topic_id);
+
let _topic_guard = shard.fs_locks.topic_lock.lock().await;
- let topic = shard.delete_topic(&stream_id, &topic_id).await?;
- let topic_id_num = topic.root().id();
+ let topic_info = shard.delete_topic(&stream_id, &topic_id).await?;
+ let topic_id_num = topic_info.id;
- let event = ShardEvent::DeletedTopic {
- id: topic_id_num,
- stream_id: stream_id.clone(),
- topic_id: topic_id.clone(),
+ // Broadcast to all shards to clean up their local_partitions entries.
+ // Use numeric Identifiers since the topic is already deleted from metadata.
+ let event = ShardEvent::DeletedPartitions {
+ stream_id: Identifier::numeric(numeric_stream_id as u32).unwrap(),
+ topic_id: Identifier::numeric(numeric_topic_id as u32).unwrap(),
+ partitions_count: partition_ids.len() as u32,
+ partition_ids,
};
shard.broadcast_event_to_all_shards(event).await?;
- Ok(ShardResponse::DeleteTopicResponse(topic))
+ let command = iggy_common::delete_topic::DeleteTopic {
+ stream_id,
+ topic_id,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::DeleteTopic(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::DeleteTopicResponse(topic_id_num))
}
ShardRequestPayload::CreateUser {
- user_id: _,
+ user_id: session_user_id,
username,
password,
status,
@@ -242,16 +404,25 @@ async fn handle_request(
let _user_guard = shard.fs_locks.user_lock.lock().await;
let user = shard.create_user(&username, &password, status, permissions.clone())?;
- let created_user_id = user.id;
-
- let event = ShardEvent::CreatedUser {
- user_id: created_user_id,
- username: username.clone(),
- password: password.clone(),
+ let command = iggy_common::create_user::CreateUser {
+ username,
+ password,
status,
- permissions: permissions.clone(),
+ permissions,
};
- shard.broadcast_event_to_all_shards(event).await?;
+ shard
+ .state
+ .apply(
+ session_user_id,
+ &crate::state::command::EntryCommand::CreateUser(
+ crate::state::models::CreateUserWithId {
+ user_id: user.id,
+ command,
+ },
+ ),
+ )
+ .await?;
+
Ok(ShardResponse::CreateUserResponse(user))
}
ShardRequestPayload::GetStats { .. } => {
@@ -260,31 +431,343 @@ async fn handle_request(
Ok(ShardResponse::GetStatsResponse(stats))
}
ShardRequestPayload::DeleteUser {
- session_user_id: _,
+ session_user_id,
user_id,
} => {
assert_eq!(shard.id, 0, "DeleteUser should only be handled by shard0");
let _user_guard = shard.fs_locks.user_lock.lock().await;
let user = shard.delete_user(&user_id)?;
- let event = ShardEvent::DeletedUser { user_id };
- shard.broadcast_event_to_all_shards(event).await?;
+
+ let command = iggy_common::delete_user::DeleteUser { user_id };
+ shard
+ .state
+ .apply(
+ session_user_id,
+ &crate::state::command::EntryCommand::DeleteUser(command),
+ )
+ .await?;
+
Ok(ShardResponse::DeletedUser(user))
}
- ShardRequestPayload::DeleteStream {
- user_id: _,
+ ShardRequestPayload::UpdateStream {
+ user_id,
stream_id,
+ name,
} => {
+ assert_eq!(shard.id, 0, "UpdateStream should only be handled by shard0");
+
+ shard.update_stream(&stream_id, name.clone())?;
+
+ let command = iggy_common::update_stream::UpdateStream { stream_id, name };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::UpdateStream(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::UpdateStreamResponse)
+ }
+ ShardRequestPayload::DeleteStream { user_id, stream_id } => {
assert_eq!(shard.id, 0, "DeleteStream should only be handled by shard0");
+ // Capture numeric stream ID and all topic/partition info BEFORE deletion for broadcast.
+ let numeric_stream_id = shard.resolve_stream_id(&stream_id)?;
+ let topics_with_partitions: Vec<(TopicId, Vec<PartitionId>)> = shard
+ .metadata
+ .get_topic_ids(numeric_stream_id)
+ .into_iter()
+ .map(|topic_id| {
+ let partition_ids = shard
+ .metadata
+ .get_partition_ids(numeric_stream_id, topic_id);
+ (topic_id, partition_ids)
+ })
+ .collect();
+
let _stream_guard = shard.fs_locks.stream_lock.lock().await;
- let stream = shard.delete_stream(&stream_id).await?;
- let event = ShardEvent::DeletedStream {
- id: stream.id(),
+ let stream_info = shard.delete_stream(&stream_id).await?;
+ let stream_id_num = stream_info.id;
+
+ // Broadcast DeletedPartitions to all shards for each topic's partitions.
+ // Use numeric Identifiers since the stream is already deleted from metadata.
+ for (topic_id, partition_ids) in topics_with_partitions {
+ if partition_ids.is_empty() {
+ continue;
+ }
+ let event = ShardEvent::DeletedPartitions {
+ stream_id: Identifier::numeric(numeric_stream_id as u32).unwrap(),
+ topic_id: Identifier::numeric(topic_id as u32).unwrap(),
+ partitions_count: partition_ids.len() as u32,
+ partition_ids,
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+ }
+
+ let command = iggy_common::delete_stream::DeleteStream { stream_id };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::DeleteStream(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::DeleteStreamResponse(stream_id_num))
+ }
+ ShardRequestPayload::UpdatePermissions {
+ session_user_id,
+ user_id,
+ permissions,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "UpdatePermissions should only be handled by shard0"
+ );
+
+ let _user_guard = shard.fs_locks.user_lock.lock().await;
+ shard.update_permissions(&user_id, permissions.clone())?;
+
+ let command = iggy_common::update_permissions::UpdatePermissions {
+ user_id,
+ permissions,
+ };
+ shard
+ .state
+ .apply(
+ session_user_id,
+ &crate::state::command::EntryCommand::UpdatePermissions(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::UpdatePermissionsResponse)
+ }
+ ShardRequestPayload::ChangePassword {
+ session_user_id,
+ user_id,
+ current_password,
+ new_password,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "ChangePassword should only be handled by shard0"
+ );
+
+ let _user_guard = shard.fs_locks.user_lock.lock().await;
+ shard.change_password(&user_id, ¤t_password, &new_password)?;
+
+ let command = iggy_common::change_password::ChangePassword {
+ user_id,
+ current_password,
+ new_password,
+ };
+ shard
+ .state
+ .apply(
+ session_user_id,
+ &crate::state::command::EntryCommand::ChangePassword(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::ChangePasswordResponse)
+ }
+ ShardRequestPayload::UpdateUser {
+ session_user_id,
+ user_id,
+ username,
+ status,
+ } => {
+ assert_eq!(shard.id, 0, "UpdateUser should only be handled by shard0");
+
+ let _user_guard = shard.fs_locks.user_lock.lock().await;
+ let user = shard.update_user(&user_id, username.clone(), status)?;
+
+ let command = iggy_common::update_user::UpdateUser {
+ user_id,
+ username,
+ status,
+ };
+ shard
+ .state
+ .apply(
+ session_user_id,
+ &crate::state::command::EntryCommand::UpdateUser(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::UpdateUserResponse(user))
+ }
+ ShardRequestPayload::CreateConsumerGroup {
+ user_id,
+ stream_id,
+ topic_id,
+ name,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "CreateConsumerGroup should only be handled by shard0"
+ );
+
+ let cg_id = shard.create_consumer_group(&stream_id, &topic_id, name.clone())?;
+
+ let command = iggy_common::create_consumer_group::CreateConsumerGroup {
stream_id,
+ topic_id,
+ name,
};
- shard.broadcast_event_to_all_shards(event).await?;
- Ok(ShardResponse::DeleteStreamResponse(stream))
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::CreateConsumerGroup(
+ crate::state::models::CreateConsumerGroupWithId {
+ group_id: cg_id as u32,
+ command,
+ },
+ ),
+ )
+ .await?;
+
+ Ok(ShardResponse::CreateConsumerGroupResponse(cg_id))
+ }
+ ShardRequestPayload::JoinConsumerGroup {
+ user_id: _,
+ client_id,
+ stream_id,
+ topic_id,
+ group_id,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "JoinConsumerGroup should only be handled by shard0"
+ );
+
+ shard.join_consumer_group(client_id, &stream_id, &topic_id, &group_id)?;
+
+ Ok(ShardResponse::JoinConsumerGroupResponse)
+ }
+ ShardRequestPayload::LeaveConsumerGroup {
+ user_id: _,
+ client_id,
+ stream_id,
+ topic_id,
+ group_id,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "LeaveConsumerGroup should only be handled by shard0"
+ );
+
+ shard.leave_consumer_group(client_id, &stream_id, &topic_id, &group_id)?;
+
+ Ok(ShardResponse::LeaveConsumerGroupResponse)
+ }
+ ShardRequestPayload::DeleteConsumerGroup {
+ user_id,
+ stream_id,
+ topic_id,
+ group_id,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "DeleteConsumerGroup should only be handled by shard0"
+ );
+
+ let cg_meta = shard.delete_consumer_group(&stream_id, &topic_id, &group_id)?;
+
+ let cg_id = crate::streaming::polling_consumer::ConsumerGroupId(cg_meta.id);
+ shard
+ .delete_consumer_group_offsets(cg_id, &stream_id, &topic_id, &cg_meta.partitions)
+ .await?;
+
+ let command = iggy_common::delete_consumer_group::DeleteConsumerGroup {
+ stream_id,
+ topic_id,
+ group_id,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::DeleteConsumerGroup(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::DeleteConsumerGroupResponse)
+ }
+ ShardRequestPayload::CreatePersonalAccessToken {
+ user_id,
+ name,
+ expiry,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "CreatePersonalAccessToken should only be handled by shard0"
+ );
+
+ let (personal_access_token, token) =
+ shard.create_personal_access_token(user_id, &name, expiry)?;
+
+ let command = iggy_common::create_personal_access_token::CreatePersonalAccessToken {
+ name,
+ expiry,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::CreatePersonalAccessToken(
+ crate::state::models::CreatePersonalAccessTokenWithHash {
+ hash: personal_access_token.token.to_string(),
+ command,
+ },
+ ),
+ )
+ .await?;
+
+ Ok(ShardResponse::CreatePersonalAccessTokenResponse(
+ personal_access_token,
+ token,
+ ))
+ }
+ ShardRequestPayload::DeletePersonalAccessToken { user_id, name } => {
+ assert_eq!(
+ shard.id, 0,
+ "DeletePersonalAccessToken should only be handled by shard0"
+ );
+
+ shard.delete_personal_access_token(user_id, &name)?;
+
+ let command =
+ iggy_common::delete_personal_access_token::DeletePersonalAccessToken { name };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::DeletePersonalAccessToken(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::DeletePersonalAccessTokenResponse)
+ }
+ ShardRequestPayload::LeaveConsumerGroupMetadataOnly {
+ stream_id,
+ topic_id,
+ group_id,
+ client_id,
+ } => {
+ assert_eq!(
+ shard.id, 0,
+ "LeaveConsumerGroupMetadataOnly should only be handled by shard0"
+ );
+
+ shard
+ .writer()
+ .leave_consumer_group(stream_id, topic_id, group_id, client_id);
+
+ Ok(ShardResponse::LeaveConsumerGroupMetadataOnlyResponse)
}
ShardRequestPayload::SocketTransfer {
fd,
@@ -318,13 +801,18 @@ async fn handle_request(
let registry = shard.task_registry.clone();
let registry_clone = registry.clone();
- let ns = IggyFullNamespace::new(stream_id, topic_id, partition_id);
let batch = shard.maybe_encrypt_messages(initial_data)?;
let messages_count = batch.count();
+ // Get numeric IDs for local_partitions lookup
+ let (numeric_stream_id, numeric_topic_id) =
+ shard.resolve_topic_id(&stream_id, &topic_id)?;
+
+ let namespace = IggyNamespace::new(numeric_stream_id, numeric_topic_id, partition_id);
+ shard.ensure_partition(&namespace).await?;
+
shard
- .streams
- .append_messages(&shard.config.system, &shard.task_registry, &ns, batch)
+ .append_messages_to_local_partition(&namespace, batch, &shard.config.system)
.await?;
shard.metrics.increment_messages(messages_count as u64);
@@ -364,6 +852,56 @@ async fn handle_request(
Ok(ShardResponse::SocketTransferResponse)
}
+ ShardRequestPayload::PurgeStream { user_id, stream_id } => {
+ assert_eq!(shard.id, 0, "PurgeStream should only be handled by shard0");
+
+ shard.purge_stream(&stream_id).await?;
+
+ let event = ShardEvent::PurgedStream {
+ stream_id: stream_id.clone(),
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ let command = iggy_common::purge_stream::PurgeStream { stream_id };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::PurgeStream(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::PurgeStreamResponse)
+ }
+ ShardRequestPayload::PurgeTopic {
+ user_id,
+ stream_id,
+ topic_id,
+ } => {
+ assert_eq!(shard.id, 0, "PurgeTopic should only be handled by shard0");
+
+ shard.purge_topic(&stream_id, &topic_id).await?;
+
+ let event = ShardEvent::PurgedTopic {
+ stream_id: stream_id.clone(),
+ topic_id: topic_id.clone(),
+ };
+ shard.broadcast_event_to_all_shards(event).await?;
+
+ let command = iggy_common::purge_topic::PurgeTopic {
+ stream_id,
+ topic_id,
+ };
+ shard
+ .state
+ .apply(
+ user_id,
+ &crate::state::command::EntryCommand::PurgeTopic(command),
+ )
+ .await?;
+
+ Ok(ShardResponse::PurgeTopicResponse)
+ }
}
}
@@ -372,19 +910,34 @@ pub async fn handle_event(shard: &Rc<IggyShard>, event: ShardEvent) -> Result<()
ShardEvent::DeletedPartitions {
stream_id,
topic_id,
- partitions_count,
+ partitions_count: _,
partition_ids,
} => {
- shard.delete_partitions_bypass_auth(
- &stream_id,
- &topic_id,
- partitions_count,
- partition_ids,
- )?;
- Ok(())
- }
- ShardEvent::UpdatedStream { stream_id, name } => {
- shard.update_stream_bypass_auth(&stream_id, &name)?;
+ // SharedMetadata was already updated by the request handler before broadcasting.
+ // Here we only need to clean up local local_partitions entries on all shards.
+ //
+ // For DeleteTopic, the topic is already removed from metadata, so we extract
+ // numeric IDs directly from the Identifier (which must be numeric in that case).
+ // For DeletePartitions, the topic still exists, so metadata lookup works.
+ let numeric_stream_id = stream_id
+ .get_u32_value()
+ .map(|v| v as usize)
+ .unwrap_or_else(|_| shard.metadata.get_stream_id(&stream_id).unwrap_or_default());
+ let numeric_topic_id =
+ topic_id
+ .get_u32_value()
+ .map(|v| v as usize)
+ .unwrap_or_else(|_| {
+ shard
+ .metadata
+ .get_topic_id(numeric_stream_id, &topic_id)
+ .unwrap_or_default()
+ });
+ let mut partitions = shard.local_partitions.borrow_mut();
+ for partition_id in partition_ids {
+ let ns = IggyNamespace::new(numeric_stream_id, numeric_topic_id, partition_id);
+ partitions.remove(&ns);
+ }
Ok(())
}
ShardEvent::PurgedStream { stream_id } => {
@@ -398,59 +951,6 @@ pub async fn handle_event(shard: &Rc<IggyShard>, event: ShardEvent) -> Result<()
shard.purge_topic_bypass_auth(&stream_id, &topic_id).await?;
Ok(())
}
- ShardEvent::CreatedUser {
- user_id,
- username,
- password,
- status,
- permissions,
- } => {
- shard.create_user_bypass_auth(
- user_id,
- &username,
- &password,
- status,
- permissions.clone(),
- )?;
- Ok(())
- }
- ShardEvent::DeletedUser { user_id } => {
- shard.delete_user_bypass_auth(&user_id)?;
- Ok(())
- }
- ShardEvent::ChangedPassword {
- user_id,
- current_password,
- new_password,
- } => {
- shard.change_password_bypass_auth(&user_id, ¤t_password, &new_password)?;
- Ok(())
- }
- ShardEvent::CreatedPersonalAccessToken {
- personal_access_token,
- } => {
- shard.create_personal_access_token_bypass_auth(personal_access_token.to_owned())?;
- Ok(())
- }
- ShardEvent::DeletedPersonalAccessToken { user_id, name } => {
- shard.delete_personal_access_token_bypass_auth(user_id, &name)?;
- Ok(())
- }
- ShardEvent::UpdatedUser {
- user_id,
- username,
- status,
- } => {
- shard.update_user_bypass_auth(&user_id, username.to_owned(), status)?;
- Ok(())
- }
- ShardEvent::UpdatedPermissions {
- user_id,
- permissions,
- } => {
- shard.update_permissions_bypass_auth(&user_id, permissions.to_owned())?;
- Ok(())
- }
ShardEvent::AddressBound { protocol, address } => {
info!(
"Received AddressBound event for {:?} with address: {}",
@@ -476,81 +976,42 @@ pub async fn handle_event(shard: &Rc<IggyShard>, event: ShardEvent) -> Result<()
}
Ok(())
}
- ShardEvent::CreatedStream { id, stream } => {
- let stream_id = shard.create_stream_bypass_auth(stream);
- assert_eq!(stream_id, id);
- Ok(())
- }
- ShardEvent::DeletedStream { id, stream_id } => {
- let stream = shard.delete_stream_bypass_auth(&stream_id);
- assert_eq!(stream.id(), id);
-
- Ok(())
- }
- ShardEvent::CreatedTopic { stream_id, topic } => {
- let topic_id_from_event = topic.id();
- let topic_id = shard.create_topic_bypass_auth(&stream_id, topic.clone());
- assert_eq!(topic_id, topic_id_from_event);
- Ok(())
- }
ShardEvent::CreatedPartitions {
stream_id,
topic_id,
partitions,
} => {
- shard
- .create_partitions_bypass_auth(&stream_id, &topic_id, partitions)
- .await?;
- Ok(())
- }
- ShardEvent::DeletedTopic {
- id,
- stream_id,
- topic_id,
- } => {
- let topic = shard.delete_topic_bypass_auth(&stream_id, &topic_id);
- assert_eq!(topic.id(), id);
- Ok(())
- }
- ShardEvent::UpdatedTopic {
- stream_id,
- topic_id,
- name,
- message_expiry,
- compression_algorithm,
- max_topic_size,
- replication_factor,
- } => {
- shard.update_topic_bypass_auth(
- &stream_id,
- &topic_id,
- name.clone(),
- message_expiry,
- compression_algorithm,
- max_topic_size,
- replication_factor,
- )?;
- Ok(())
- }
- ShardEvent::CreatedConsumerGroup {
- stream_id,
- topic_id,
- cg,
- } => {
- let cg_id = cg.id();
- let id = shard.insert_consumer_group(&stream_id, &topic_id, cg);
- assert_eq!(id, cg_id);
- Ok(())
- }
- ShardEvent::DeletedConsumerGroup {
- id,
- stream_id,
- topic_id,
- group_id,
- } => {
- let cg = shard.delete_consumer_group(&stream_id, &topic_id, &group_id)?;
- assert_eq!(cg.id(), id);
+ let numeric_stream_id = match shard.metadata.get_stream_id(&stream_id) {
+ Some(id) => id,
+ None => {
+ tracing::warn!(
+ "CreatedPartitions: stream {:?} not found in SharedMetadata",
+ stream_id
+ );
+ return Ok(());
+ }
+ };
+ let numeric_topic_id = match shard.metadata.get_topic_id(numeric_stream_id, &topic_id) {
+ Some(id) => id,
+ None => {
+ tracing::warn!(
+ "CreatedPartitions: topic {:?} not found in SharedMetadata for stream {}",
+ topic_id,
+ numeric_stream_id
+ );
+ return Ok(());
+ }
+ };
+
+ let shards_count = shard.get_available_shards_count();
+ for partition_info in partitions {
+ let ns = IggyNamespace::new(numeric_stream_id, numeric_topic_id, partition_info.id);
+ let owner_shard_id = crate::shard::calculate_shard_assignment(&ns, shards_count);
+ if shard.id == owner_shard_id as u16 {
+ shard.ensure_partition(&ns).await?;
+ }
+ }
Ok(())
}
ShardEvent::FlushUnsavedBuffer {
@@ -559,9 +1020,21 @@ pub async fn handle_event(shard: &Rc<IggyShard>, event: ShardEvent) -> Result<()
partition_id,
fsync,
} => {
- shard
- .flush_unsaved_buffer_base(&stream_id, &topic_id, partition_id, fsync)
- .await?;
+ let numeric_stream_id = match shard.metadata.get_stream_id(&stream_id) {
+ Some(id) => id,
+ None => return Ok(()),
+ };
+ let numeric_topic_id = match shard.metadata.get_topic_id(numeric_stream_id, &topic_id) {
+ Some(id) => id,
+ None => return Ok(()),
+ };
+
+ let ns = IggyNamespace::new(numeric_stream_id, numeric_topic_id, partition_id);
+ if shard.local_partitions.borrow().get(&ns).is_some() {
+ shard
+ .flush_unsaved_buffer_from_local_partitions(&ns, fsync)
+ .await?;
+ }
Ok(())
}
}
diff --git a/core/server/src/shard/mod.rs b/core/server/src/shard/mod.rs
index 93fd5eaac..68b0902c8 100644
--- a/core/server/src/shard/mod.rs
+++ b/core/server/src/shard/mod.rs
@@ -17,7 +17,6 @@ inner() * or more contributor license agreements. See the NOTICE file
*/
pub mod builder;
-pub mod namespace;
pub mod system;
pub mod task_registry;
pub mod tasks;
@@ -26,16 +25,17 @@ pub mod transmission;
mod communication;
pub mod handlers;
-// Re-export for backwards compatibility
+use ahash::AHashSet;
pub use communication::calculate_shard_assignment;
use self::tasks::{continuous, periodic};
use crate::{
configs::server::ServerConfig,
io::fs_locks::FsLocks,
+ metadata::{Metadata, MetadataWriter},
shard::{task_registry::TaskRegistry, transmission::frame::ShardFrame},
- slab::{streams::Streams, traits_ext::EntityMarker, users::Users},
state::file::FileState,
+ streaming::partitions::local_partitions::LocalPartitions,
streaming::{
clients::client_manager::ClientManager, diagnostics::metrics::Metrics, session::Session,
utils::ptr::EternalPtr,
@@ -45,13 +45,15 @@ use crate::{
use builder::IggyShardBuilder;
use dashmap::DashMap;
use iggy_common::sharding::{IggyNamespace, PartitionLocation};
-use iggy_common::{EncryptorKind, Identifier, IggyError};
-use metadata::permissioner::Permissioner;
+use iggy_common::{EncryptorKind, IggyError};
use std::{
cell::{Cell, RefCell},
net::SocketAddr,
rc::Rc,
- sync::atomic::{AtomicBool, Ordering},
+ sync::{
+ Arc,
+ atomic::{AtomicBool, AtomicU64, Ordering},
+ },
time::{Duration, Instant},
};
use tracing::{debug, error, info, instrument};
@@ -66,7 +68,11 @@ pub struct IggyShard {
shards: Vec<ShardConnector<ShardFrame>>,
_version: SemanticVersion,
- pub(crate) streams: Streams,
+ pub(crate) metadata: Metadata,
+ pub(crate) metadata_writer: Option<RefCell<MetadataWriter>>,
+ pub(crate) local_partitions: RefCell<LocalPartitions>,
+ pub(crate) pending_partition_inits: RefCell<AHashSet<IggyNamespace>>,
+
pub(crate) shards_table: EternalPtr<DashMap<IggyNamespace, PartitionLocation>>,
pub(crate) state: FileState,
@@ -74,8 +80,6 @@ pub struct IggyShard {
pub(crate) encryptor: Option<EncryptorKind>,
pub(crate) config: ServerConfig,
pub(crate) client_manager: ClientManager,
- pub(crate) permissioner: RefCell<Permissioner>,
- pub(crate) users: Users,
pub(crate) metrics: Metrics,
pub(crate) is_follower: bool,
pub messages_receiver: Cell<Option<Receiver<ShardFrame>>>,
@@ -95,6 +99,13 @@ impl IggyShard {
Default::default()
}
+ pub fn writer(&self) -> std::cell::RefMut<'_, crate::metadata::MetadataWriter> {
+ self.metadata_writer
+ .as_ref()
+ .expect("MetadataWriter only available on shard 0")
+ .borrow_mut()
+ }
+
pub async fn init(&self) -> Result<(), IggyError> {
self.load_segments().await?;
let _ = self.load_users().await;
@@ -186,6 +197,8 @@ impl IggyShard {
async fn load_segments(&self) -> Result<(), IggyError> {
use crate::bootstrap::load_segments;
+ use crate::streaming::partitions::local_partition::LocalPartition;
+
for shard_entry in self.shards_table.iter() {
let (namespace, location) = shard_entry.pair();
@@ -203,33 +216,90 @@ impl IggyShard {
self.config
.system
.get_partition_path(stream_id, topic_id, partition_id);
- let stats = self.streams.with_partition_by_id(
- &Identifier::numeric(stream_id as u32).unwrap(),
- &Identifier::numeric(topic_id as u32).unwrap(),
- partition_id,
- |(_, stats, ..)| stats.clone(),
+
+ let init_info = self
+ .metadata
+ .get_partition_init_info(stream_id, topic_id, partition_id)
+ .expect("Partition must exist in SharedMetadata");
+ let created_at = init_info.created_at;
+ let stats = init_info.stats;
+
+ use crate::streaming::partitions::helpers::create_message_deduplicator;
+ use crate::streaming::partitions::storage::{
+ load_consumer_group_offsets, load_consumer_offsets,
+ };
+ use ahash::HashMap;
+
+ let consumer_offset_path =
+ self.config
+ .system
+ .get_consumer_offsets_path(stream_id, topic_id, partition_id);
+ let consumer_group_offsets_path = self
+ .config
+ .system
+ .get_consumer_group_offsets_path(stream_id, topic_id, partition_id);
+
+ let consumer_offsets = Arc::new(
+ load_consumer_offsets(&consumer_offset_path)
+ .unwrap_or_default()
+ .into_iter()
+ .map(|offset| (offset.consumer_id as usize, offset))
+ .collect::<HashMap<usize, _>>()
+ .into(),
);
+
+ let consumer_group_offsets = Arc::new(
+ load_consumer_group_offsets(&consumer_group_offsets_path)
+ .unwrap_or_default()
+ .into_iter()
+ .collect::<HashMap<_, _>>()
+ .into(),
+ );
+
+ let message_deduplicator =
+ create_message_deduplicator(&self.config.system).map(Arc::new);
+
match load_segments(
&self.config.system,
stream_id,
topic_id,
partition_id,
partition_path,
- stats,
+ stats.clone(),
)
.await
{
Ok(loaded_log) => {
- self.streams.with_partition_by_id_mut(
- &Identifier::numeric(stream_id as u32).unwrap(),
- &Identifier::numeric(topic_id as u32).unwrap(),
- partition_id,
- |(_, _, _, offset, .., log)| {
- *log = loaded_log;
- let current_offset = log.active_segment().end_offset;
- offset.store(current_offset, Ordering::Relaxed);
- },
+ let current_offset = loaded_log.active_segment().end_offset;
+ stats.set_current_offset(current_offset);
+
+ // Only increment offset if we have messages (current_offset > 0).
+ // When current_offset is 0 and we have no messages, first message
+ // should get offset 0.
+ let should_increment_offset = current_offset > 0;
+
+ let revision_id = self
+ .metadata
+ .get_partition_init_info(stream_id, topic_id, partition_id)
+ .map(|info| info.revision_id)
+ .unwrap_or(0);
+
+ let partition = LocalPartition::with_log(
+ loaded_log,
+ stats,
+ Arc::new(AtomicU64::new(current_offset)),
+ consumer_offsets,
+ consumer_group_offsets,
+ message_deduplicator,
+ created_at,
+ revision_id,
+ should_increment_offset,
);
+
+ self.local_partitions
+ .borrow_mut()
+ .insert(*namespace, partition);
+
info!(
"Successfully loaded segments for stream: {}, topic: {}, partition: {}",
stream_id, topic_id, partition_id
@@ -250,12 +320,7 @@ impl IggyShard {
}
async fn load_users(&self) -> Result<(), IggyError> {
- let users_list = self.users.values();
- let users_count = users_list.len();
- let mut permissioner = self.permissioner.borrow_mut();
- for user in &users_list {
- permissioner.init_permissions_for_user(user.id, user.permissions.clone());
- }
+ let users_count = self.metadata.users_count();
self.metrics.increment_users(users_count as u32);
info!("Initialized {} user(s).", users_count);
Ok(())
diff --git a/core/server/src/shard/namespace.rs b/core/server/src/shard/namespace.rs
deleted file mode 100644
index 289587179..000000000
--- a/core/server/src/shard/namespace.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-/* 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::slab::partitions;
-use iggy_common::Identifier;
-
-#[derive(Debug)]
-pub struct IggyFullNamespace {
- stream: Identifier,
- topic: Identifier,
- partition: partitions::ContainerId,
-}
-
-impl IggyFullNamespace {
- pub fn new(stream: Identifier, topic: Identifier, partition: partitions::ContainerId) -> Self {
- Self {
- stream,
- topic,
- partition,
- }
- }
-
- pub fn stream_id(&self) -> &Identifier {
- &self.stream
- }
-
- pub fn topic_id(&self) -> &Identifier {
- &self.topic
- }
-
- pub fn partition_id(&self) -> partitions::ContainerId {
- self.partition
- }
-
- pub fn decompose(self) -> (Identifier, Identifier, partitions::ContainerId) {
- (self.stream, self.topic, self.partition)
- }
-}
diff --git a/core/server/src/shard/system/clients.rs b/core/server/src/shard/system/clients.rs
index d3a4d5a67..25148ef0e 100644
--- a/core/server/src/shard/system/clients.rs
+++ b/core/server/src/shard/system/clients.rs
@@ -17,11 +17,15 @@
*/
use crate::shard::IggyShard;
+use crate::shard::transmission::frame::ShardResponse;
+use crate::shard::transmission::message::{
+ ShardMessage, ShardRequest, ShardRequestPayload, ShardSendRequestResult,
+};
use crate::streaming::clients::client_manager::Client;
use crate::streaming::session::Session;
use iggy_common::{Identifier, TransportProtocol};
use std::net::SocketAddr;
-use tracing::{error, info};
+use tracing::{error, info, warn};
impl IggyShard {
pub fn add_client(&self, address: &SocketAddr, transport: TransportProtocol) -> Session {
@@ -30,7 +34,7 @@ impl IggyShard {
session
}
- pub fn delete_client(&self, client_id: u32) {
+ pub async fn delete_client(&self, client_id: u32) {
let consumer_groups: Vec<(u32, u32, u32)>;
{
@@ -55,12 +59,44 @@ impl IggyShard {
}
for (stream_id, topic_id, consumer_group_id) in consumer_groups.into_iter() {
- _ = self.leave_consumer_group_base(
- &Identifier::numeric(stream_id).unwrap(),
- &Identifier::numeric(topic_id).unwrap(),
- &Identifier::numeric(consumer_group_id).unwrap(),
- client_id,
- )
+ let request = ShardRequest {
+ stream_id: Identifier::numeric(stream_id).unwrap(),
+ topic_id: Identifier::numeric(topic_id).unwrap(),
+ partition_id: 0,
+ payload: ShardRequestPayload::LeaveConsumerGroupMetadataOnly {
+ stream_id: stream_id as usize,
+ topic_id: topic_id as usize,
+ group_id: consumer_group_id as usize,
+ client_id,
+ },
+ };
+
+ let message = ShardMessage::Request(request);
+ match self.send_request_to_shard_or_recoil(None, message).await {
+ Ok(ShardSendRequestResult::Recoil(_)) => {
... 11837 lines suppressed ...