blob: 97de8658ed500ae1b9f2f19ee514a4de9831b259 [file]
use ahash::AHashMap;
use clap::Parser;
use futures_util::future::join_all;
use futures_util::StreamExt;
use iggy::client::{Client, StreamClient, TopicClient, UserClient};
use iggy::clients::client::IggyClient;
use iggy::clients::consumer::{AutoCommit, AutoCommitWhen, IggyConsumer};
use iggy::error::IggyError;
use iggy::identifier::Identifier;
use iggy::messages::poll_messages::PollingStrategy;
use iggy::models::permissions::{Permissions, StreamPermissions, TopicPermissions};
use iggy::models::user_status::UserStatus;
use iggy::users::defaults::{DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME};
use iggy::utils::duration::IggyDuration;
use iggy_examples::shared::args::Args;
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::str::FromStr;
use tokio::task::JoinHandle;
use tracing::{error, info};
const TOPICS: &[&str] = &["events", "logs", "notifications"];
const CONSUMER_GROUP: &str = "multi-tenant";
const PASSWORD: &str = "secret";
struct Tenant {
id: u32,
stream: String,
user: String,
client: IggyClient,
consumers: Vec<TenantConsumer>,
}
impl Tenant {
pub fn new(id: u32, stream: String, user: String, client: IggyClient) -> Self {
Self {
id,
stream,
user,
client,
consumers: Vec::new(),
}
}
pub fn add_consumers(&mut self, consumers: Vec<TenantConsumer>) {
self.consumers.extend(consumers);
}
}
struct TenantConsumer {
id: u32,
stream: String,
topic: String,
consumer: IggyConsumer,
}
impl TenantConsumer {
pub fn new(id: u32, stream: String, topic: String, consumer: IggyConsumer) -> Self {
Self {
id,
stream,
topic,
consumer,
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<(), Box<dyn Error>> {
let args = Args::parse();
tracing_subscriber::fmt::init();
let tenants_count = env::var("TENANTS_COUNT")
.unwrap_or_else(|_| 3.to_string())
.parse::<u32>()
.expect("Invalid tenants count");
let consumers_count = env::var("CONSUMERS_COUNT")
.unwrap_or_else(|_| 1.to_string())
.parse::<u32>()
.expect("Invalid consumers count");
let ensure_access = env::var("ENSURE_ACCESS")
.unwrap_or_else(|_| "true".to_string())
.parse::<bool>()
.expect("Invalid ensure stream access");
print_info(&format!("Multi-tenant consumers has started, tenants: {tenants_count}, consumers: {consumers_count}"));
let address = args.tcp_server_address;
print_info("Creating root client to manage streams and users");
let root_client = create_client(&address, DEFAULT_ROOT_USERNAME, DEFAULT_ROOT_PASSWORD).await?;
print_info("Creating users with stream permissions for each tenant");
let mut streams_with_users = HashMap::new();
for i in 1..=tenants_count {
let name = format!("tenant_{i}");
let stream = format!("{name}_stream");
let user = format!("{name}_consumer");
create_user(&stream, TOPICS, &user, &root_client).await?;
streams_with_users.insert(stream, user);
}
print_info("Disconnecting root client");
root_client.disconnect().await?;
print_info("Creating clients for each tenant");
let mut tenants = Vec::new();
let mut tenant_id = 1;
for (stream, user) in streams_with_users.into_iter() {
let client = create_client(&address, &user, PASSWORD).await?;
tenants.push(Tenant::new(tenant_id, stream, user, client));
tenant_id += 1;
}
if ensure_access {
print_info("Ensuring access to stream topics for each tenant");
for tenant in tenants.iter() {
let unavailable_streams = tenants
.iter()
.filter(|t| t.stream != tenant.stream)
.map(|t| t.stream.as_str())
.collect::<Vec<_>>();
ensure_stream_topics_access(
&tenant.client,
TOPICS,
&tenant.stream,
&unavailable_streams,
)
.await?;
}
}
print_info(&format!(
"Creating {consumers_count} consumer(s) for each tenant"
));
for tenant in tenants.iter_mut() {
let consumers = create_consumers(
&tenant.client,
consumers_count,
&tenant.stream,
TOPICS,
args.messages_per_batch,
&args.interval,
)
.await?;
tenant.add_consumers(consumers);
info!(
"Created {consumers_count} consumer(s) for tenant stream: {}, username: {}",
tenant.stream, tenant.user
);
}
print_info(&format!(
"Starting {consumers_count} consumers(s) for each tenant"
));
let mut tasks = Vec::new();
for tenant in tenants.into_iter() {
let consumer_tasks = start_consumers(tenant.id, tenant.consumers);
tasks.extend(consumer_tasks);
}
join_all(tasks).await;
print_info("Disconnecting clients");
Ok(())
}
async fn create_user(
stream_name: &str,
topics: &[&str],
username: &str,
client: &IggyClient,
) -> Result<(), IggyError> {
let stream = client
.get_stream(&stream_name.try_into()?)
.await?
.expect("Stream does not exist");
let mut topic_permissions = AHashMap::new();
for topic in topics {
let topic_id = Identifier::named(topic)?;
let topic = client
.get_topic(&stream_name.try_into()?, &topic_id)
.await?
.expect("Topic does not exist");
topic_permissions.insert(
topic.id,
TopicPermissions {
read_topic: true,
poll_messages: true,
..Default::default()
},
);
}
let mut streams_permissions = AHashMap::new();
streams_permissions.insert(
stream.id,
StreamPermissions {
read_stream: true,
topics: Some(topic_permissions),
..Default::default()
},
);
let permissions = Permissions {
streams: Some(streams_permissions),
..Default::default()
};
let user = client
.create_user(username, PASSWORD, UserStatus::Active, Some(permissions))
.await?;
info!(
"Created user: {username} with ID: {}, with permissions for topics: {:?} in stream: {stream_name}",
user.id, topics
);
Ok(())
}
fn start_consumers(tenant_id: u32, consumers: Vec<TenantConsumer>) -> Vec<JoinHandle<()>> {
let mut tasks = Vec::new();
for mut consumer in consumers {
let task = tokio::spawn(async move {
let consumer_id = consumer.id;
let stream = consumer.stream;
let topic = consumer.topic;
while let Some(message) = consumer.consumer.next().await {
if let Ok(message) = message {
let current_offset = message.current_offset;
let partition_id = message.partition_id;
let offset = message.message.offset;
let payload = std::str::from_utf8(&message.message.payload);
if payload.is_err() {
let error = payload.unwrap_err();
error!("Error while decoding the message payload at offset: {offset}, partition ID: {partition_id}, perhaps it's encrypted? {error}");
continue;
}
let payload = payload.unwrap();
info!("Tenant: {tenant_id} consumer: {consumer_id} received: {payload} from partition: {partition_id}, topic: {topic}, stream: {stream}, at offset: {offset}, current offset: {current_offset}");
} else if let Err(error) = message {
error!("Error while handling message: {error} by tenant: {tenant_id} consumer: {consumer_id}, topic: {topic}, stream: {stream}");
continue;
}
}
});
tasks.push(task);
}
tasks
}
async fn create_consumers(
client: &IggyClient,
consumers_count: u32,
stream: &str,
topics: &[&str],
batch_size: u32,
interval: &str,
) -> Result<Vec<TenantConsumer>, IggyError> {
let mut consumers = Vec::new();
for topic in topics {
for id in 1..=consumers_count {
let mut consumer = client
.consumer_group(CONSUMER_GROUP, stream, topic)?
.batch_size(batch_size)
.poll_interval(IggyDuration::from_str(interval).expect("Invalid duration"))
.polling_strategy(PollingStrategy::next())
.auto_join_consumer_group()
.auto_commit(AutoCommit::When(AutoCommitWhen::PollingMessages))
.build();
consumer.init().await?;
consumers.push(TenantConsumer::new(
id,
stream.to_owned(),
topic.to_string(),
consumer,
));
}
}
Ok(consumers)
}
async fn ensure_stream_topics_access(
client: &IggyClient,
topics: &[&str],
available_stream: &str,
unavailable_streams: &[&str],
) -> Result<(), IggyError> {
for topic in topics {
let topic_id = Identifier::named(topic)?;
client
.get_topic(&available_stream.try_into()?, &topic_id)
.await?
.unwrap_or_else(|| panic!("No access to topic: {topic} in stream: {available_stream}"));
info!("Ensured access to topic: {topic} in stream: {available_stream}");
for stream in unavailable_streams {
if client
.get_topic(&Identifier::named(stream)?, &topic_id)
.await?
.is_none()
{
info!("Ensured no access to topic: {topic} in stream: {stream}");
} else {
panic!("Access to topic: {topic} in stream: {stream} should not be allowed");
}
}
}
Ok(())
}
async fn create_client(
address: &str,
username: &str,
password: &str,
) -> Result<IggyClient, IggyError> {
let connection_string = format!("iggy://{username}:{password}@{address}");
let client = IggyClient::builder_from_connection_string(&connection_string)?.build()?;
client.connect().await?;
Ok(client)
}
fn print_info(message: &str) {
info!("\n\n--- {message} ---\n");
}