blob: 4e35f9179e504b0644a02802e73ba9c36ecdc514 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::{RUNTIME, ffi};
use iggy::prelude::{
Client as IggyConnectionClient, CompressionAlgorithm as RustCompressionAlgorithm, Consumer,
ConsumerGroupClient, Identifier as RustIdentifier, IggyClient as RustIggyClient,
IggyClientBuilder as RustIggyClientBuilder, IggyError, IggyExpiry as RustIggyExpiry,
IggyMessage, IggyTimestamp, MaxTopicSize as RustMaxTopicSize, MessageClient, PartitionClient,
Partitioning, PollingStrategy, SnapshotCompression as RustSnapshotCompression, StreamClient,
SystemClient as RustSystemClient, SystemSnapshotType as RustSystemSnapshotType, TopicClient,
UserClient,
};
use iggy_common::{
CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails, Stats as RustStats,
};
use std::collections::HashSet;
use std::convert::TryFrom;
use std::str::FromStr;
use std::sync::Arc;
/// Sentinel value passed from C++ to mean "no partition specified" — the server picks the
/// partition based on the consumer/strategy. Cxx FFI does not support `Option<u32>`, so we
/// reserve `u32::MAX` as the sentinel for `partition_id`.
const ANY_PARTITION_ID: u32 = u32::MAX;
impl From<RustClientInfo> for ffi::ClientInfo {
fn from(client: RustClientInfo) -> Self {
let has_user_id = client.user_id.is_some();
ffi::ClientInfo {
client_id: client.client_id,
has_user_id,
user_id: client.user_id.unwrap_or(u32::MAX),
address: client.address,
transport: client.transport,
consumer_groups_count: client.consumer_groups_count,
}
}
}
impl From<RustClientInfoDetails> for ffi::ClientInfoDetails {
fn from(client: RustClientInfoDetails) -> Self {
let has_user_id = client.user_id.is_some();
ffi::ClientInfoDetails {
client_id: client.client_id,
has_user_id,
user_id: client.user_id.unwrap_or(u32::MAX),
address: client.address,
transport: client.transport,
consumer_groups_count: client.consumer_groups_count,
consumer_groups: client
.consumer_groups
.into_iter()
.map(ffi::ConsumerGroupInfo::from)
.collect(),
}
}
}
impl TryFrom<Option<RustClientInfoDetails>> for ffi::ClientInfoDetails {
type Error = String;
fn try_from(client: Option<RustClientInfoDetails>) -> Result<Self, Self::Error> {
match client {
Some(client) => Ok(ffi::ClientInfoDetails::from(client)),
None => Err("client not found".to_string()),
}
}
}
impl From<(RustCacheMetricsKey, RustCacheMetrics)> for ffi::CacheMetricEntry {
fn from((key, metrics): (RustCacheMetricsKey, RustCacheMetrics)) -> Self {
ffi::CacheMetricEntry {
stream_id: key.stream_id,
topic_id: key.topic_id,
partition_id: key.partition_id,
hits: metrics.hits,
misses: metrics.misses,
hit_ratio: metrics.hit_ratio,
}
}
}
impl From<RustStats> for ffi::Stats {
fn from(stats: RustStats) -> Self {
let has_server_semver = stats.iggy_server_semver.is_some();
ffi::Stats {
process_id: stats.process_id,
cpu_usage: stats.cpu_usage,
total_cpu_usage: stats.total_cpu_usage,
memory_usage: stats.memory_usage.as_bytes_u64(),
total_memory: stats.total_memory.as_bytes_u64(),
available_memory: stats.available_memory.as_bytes_u64(),
run_time_micros: stats.run_time.as_micros(),
start_time_epoch_micros: stats.start_time.as_micros(),
read_bytes: stats.read_bytes.as_bytes_u64(),
written_bytes: stats.written_bytes.as_bytes_u64(),
messages_size_bytes: stats.messages_size_bytes.as_bytes_u64(),
streams_count: stats.streams_count,
topics_count: stats.topics_count,
partitions_count: stats.partitions_count,
segments_count: stats.segments_count,
messages_count: stats.messages_count,
clients_count: stats.clients_count,
consumer_groups_count: stats.consumer_groups_count,
hostname: stats.hostname,
os_name: stats.os_name,
os_version: stats.os_version,
kernel_version: stats.kernel_version,
iggy_server_version: stats.iggy_server_version,
has_server_semver,
iggy_server_semver: stats.iggy_server_semver.unwrap_or(0),
cache_metrics: stats
.cache_metrics
.into_iter()
.map(ffi::CacheMetricEntry::from)
.collect(),
threads_count: stats.threads_count,
free_disk_space: stats.free_disk_space.as_bytes_u64(),
total_disk_space: stats.total_disk_space.as_bytes_u64(),
}
}
}
pub struct Client {
pub inner: Arc<RustIggyClient>,
}
/// Creates a new client connection and returns a raw pointer to the underlying [`Client`].
///
/// # Ownership
///
/// The returned `*mut Client` is owned by the caller (the C++ side). The caller is responsible
/// for calling [`delete_connection`] exactly once to release the resources. Failing to do so
/// leaks the underlying tokio runtime resources and the open network connection.
///
/// # Safety
///
/// - Passing the pointer to [`delete_connection`] more than once is undefined behaviour
/// (double-free).
/// - Using the pointer after [`delete_connection`] has been called is undefined behaviour
/// (use-after-free).
/// - This function does not provide synchronisation. The pointer must not be used concurrently
/// from multiple threads unless the caller serialises access externally.
pub fn new_connection(connection_string: String) -> Result<*mut Client, String> {
let connection_str = connection_string.as_str();
let client = match connection_str {
"" => RustIggyClientBuilder::new()
.with_tcp()
.build()
.map_err(|error| format!("Could not build default connection: {error}"))?,
s if s.starts_with("iggy://") || s.starts_with("iggy+") => {
RustIggyClient::from_connection_string(s)
.map_err(|error| format!("Could not parse connection string '{s}': {error}"))?
}
s => RustIggyClientBuilder::new()
.with_tcp()
.with_server_address(connection_string.clone())
.build()
.map_err(|error| format!("Could not build connection for address '{s}': {error}"))?,
};
Ok(Box::into_raw(Box::new(Client {
inner: Arc::new(client),
})))
}
impl Client {
pub fn login_user(&self, username: String, password: String) -> Result<(), String> {
RUNTIME.block_on(async {
self.inner
.login_user(&username, &password)
.await
.map_err(|error| format!("Could not login user '{username}': {error}"))?;
Ok(())
})
}
pub fn connect(&self) -> Result<(), String> {
RUNTIME.block_on(async {
self.inner
.connect()
.await
.map_err(|error| format!("Could not connect: {error}"))?;
Ok(())
})
}
pub fn get_streams(&self) -> Result<Vec<ffi::Stream>, String> {
RUNTIME.block_on(async {
let streams = self
.inner
.get_streams()
.await
.map_err(|error| format!("Could not get streams: {error}"))?;
Ok(streams.into_iter().map(ffi::Stream::from).collect())
})
}
pub fn create_stream(&self, stream_name: String) -> Result<ffi::StreamDetails, String> {
RUNTIME.block_on(async {
let stream_details = self
.inner
.create_stream(&stream_name)
.await
.map_err(|error| format!("Could not create stream '{stream_name}': {error}"))?;
Ok(ffi::StreamDetails::from(stream_details))
})
}
pub fn get_stream(&self, stream_id: ffi::Identifier) -> Result<ffi::StreamDetails, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not get stream: {error}"))?;
RUNTIME.block_on(async {
let stream_details = self
.inner
.get_stream(&rust_stream_id)
.await
.map_err(|error| format!("Could not get stream '{rust_stream_id}': {error}"))?;
let stream_details =
stream_details.ok_or_else(|| format!("Stream '{rust_stream_id}' was not found"))?;
Ok(ffi::StreamDetails::from(stream_details))
})
}
pub fn delete_stream(&self, stream_id: ffi::Identifier) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not delete stream: {error}"))?;
RUNTIME.block_on(async {
self.inner
.delete_stream(&rust_stream_id)
.await
.map_err(|error| format!("Could not delete stream '{rust_stream_id}': {error}"))?;
Ok(())
})
}
pub fn purge_stream(&self, stream_id: ffi::Identifier) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not purge stream: {error}"))?;
RUNTIME.block_on(async {
self.inner
.purge_stream(&rust_stream_id)
.await
.map_err(|error| format!("Could not purge stream '{rust_stream_id}': {error}"))?;
Ok(())
})
}
#[allow(clippy::too_many_arguments)]
pub fn send_messages(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
partitioning_kind: String,
partitioning_value: Vec<u8>,
messages: Vec<ffi::IggyMessageToSend>,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not send messages: {error}"))?;
let rust_topic_id = RustIdentifier::try_from(topic_id)
.map_err(|error| format!("Could not send messages: {error}"))?;
let partitioning = match partitioning_kind.as_str() {
"balanced" => Partitioning::balanced(),
"partition_id" => {
if partitioning_value.len() != 4 {
return Err(format!(
"Could not send messages: partition_id requires exactly 4 bytes, got {}",
partitioning_value.len()
));
}
let id =
u32::from_le_bytes(partitioning_value.as_slice().try_into().map_err(|_| {
"Could not send messages: invalid partition_id value".to_string()
})?);
Partitioning::partition_id(id)
}
"messages_key" => {
if partitioning_value.is_empty() {
return Err(
"Could not send messages: messages_key requires a non-empty value"
.to_string(),
);
}
Partitioning::messages_key(&partitioning_value).map_err(|error| {
format!("Could not send messages: invalid messages key: {error}")
})?
}
_ => {
return Err(format!(
"Could not send messages: invalid partitioning kind: {partitioning_kind}"
));
}
};
let mut iggy_messages: Vec<IggyMessage> = messages
.into_iter()
.map(IggyMessage::try_from)
.collect::<Result<Vec<_>, _>>()?;
RUNTIME.block_on(async {
self.inner
.send_messages(
&rust_stream_id,
&rust_topic_id,
&partitioning,
&mut iggy_messages,
)
.await
.map_err(|error| format!("Could not send messages: {error}"))?;
Ok(())
})
}
#[allow(clippy::too_many_arguments)]
pub fn poll_messages(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
partition_id: u32,
consumer_kind: String,
consumer_id: ffi::Identifier,
polling_strategy_kind: String,
polling_strategy_value: u64,
count: u32,
auto_commit: bool,
) -> Result<ffi::PolledMessages, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not poll messages: {error}"))?;
let rust_topic_id = RustIdentifier::try_from(topic_id)
.map_err(|error| format!("Could not poll messages: {error}"))?;
let rust_consumer_id = RustIdentifier::try_from(consumer_id)
.map_err(|error| format!("Could not poll messages: {error}"))?;
let consumer = match consumer_kind.as_str() {
"consumer" => Consumer::new(rust_consumer_id),
"consumer_group" => Consumer::group(rust_consumer_id),
_ => {
return Err(format!(
"Could not poll messages: invalid consumer kind: {consumer_kind}"
));
}
};
let strategy = match polling_strategy_kind.as_str() {
"offset" => PollingStrategy::offset(polling_strategy_value),
"timestamp" => PollingStrategy::timestamp(IggyTimestamp::from(polling_strategy_value)),
"first" => PollingStrategy::first(),
"last" => PollingStrategy::last(),
"next" => PollingStrategy::next(),
_ => {
return Err(format!(
"Could not poll messages: invalid polling strategy: {polling_strategy_kind}"
));
}
};
let opt_partition = if partition_id == ANY_PARTITION_ID {
None
} else {
Some(partition_id)
};
RUNTIME.block_on(async {
let polled = self
.inner
.poll_messages(
&rust_stream_id,
&rust_topic_id,
opt_partition,
&consumer,
&strategy,
count,
auto_commit,
)
.await
.map_err(|error| format!("Could not poll messages: {error}"))?;
Ok(ffi::PolledMessages::from(polled))
})
}
#[allow(clippy::too_many_arguments)]
pub fn create_topic(
&self,
stream_id: ffi::Identifier,
topic_name: String,
partitions_count: u32,
compression_algorithm: String,
replication_factor: u8,
message_expiry_kind: String,
message_expiry_value: u64,
max_topic_size: String,
) -> Result<ffi::TopicDetails, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not create topic '{topic_name}': {error}"))?;
let rust_compression_algorithm = match compression_algorithm.to_lowercase().as_str() {
"" | "none" => RustCompressionAlgorithm::None,
_ => RustCompressionAlgorithm::from_str(&compression_algorithm).map_err(|error| {
format!(
"Could not create topic '{topic_name}': invalid compression algorithm '{compression_algorithm}': {error}"
)
})?,
};
let rust_replication_factor = Some(replication_factor.max(1));
let rust_message_expiry = match message_expiry_kind.as_str() {
"" | "server_default" | "default" => RustIggyExpiry::ServerDefault,
"never_expire" => RustIggyExpiry::NeverExpire,
"duration" => RustIggyExpiry::ExpireDuration(iggy::prelude::IggyDuration::from(
message_expiry_value,
)),
_ => {
return Err(format!(
"Could not create topic '{topic_name}': invalid message expiry kind '{message_expiry_kind}'"
));
}
};
let rust_max_topic_size = match max_topic_size.as_str() {
"" | "server_default" | "0" => RustMaxTopicSize::ServerDefault,
_ => RustMaxTopicSize::from_str(&max_topic_size).map_err(|error| {
format!(
"Could not create topic '{topic_name}': invalid max topic size '{max_topic_size}': {error}"
)
})?,
};
RUNTIME.block_on(async {
let topic_details = self
.inner
.create_topic(
&rust_stream_id,
&topic_name,
partitions_count,
rust_compression_algorithm,
rust_replication_factor,
rust_message_expiry,
rust_max_topic_size,
)
.await
.map_err(|error| {
format!(
"Could not create topic '{topic_name}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(ffi::TopicDetails::from(topic_details))
})
}
pub fn get_topic(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
) -> Result<ffi::TopicDetails, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not get topic: invalid stream identifier: {error}"))?;
let rust_topic_id = RustIdentifier::try_from(topic_id)
.map_err(|error| format!("Could not get topic: invalid topic identifier: {error}"))?;
RUNTIME.block_on(async {
let topic_details = self
.inner
.get_topic(&rust_stream_id, &rust_topic_id)
.await
.map_err(|error| {
format!(
"Could not get topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
let topic_details = topic_details.ok_or_else(|| {
format!(
"Topic '{rust_topic_id}' was not found on stream '{rust_stream_id}'"
)
})?;
Ok(ffi::TopicDetails::from(topic_details))
})
}
pub fn get_topics(&self, stream_id: ffi::Identifier) -> Result<Vec<ffi::Topic>, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not get topics: invalid stream identifier: {error}"))?;
RUNTIME.block_on(async {
let topics = self
.inner
.get_topics(&rust_stream_id)
.await
.map_err(|error| {
format!("Could not get topics on stream '{rust_stream_id}': {error}")
})?;
Ok(topics.into_iter().map(ffi::Topic::from).collect())
})
}
#[allow(clippy::too_many_arguments)]
pub fn update_topic(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
topic_name: String,
compression_algorithm: String,
replication_factor: u8,
message_expiry_kind: String,
message_expiry_value: u64,
max_topic_size: String,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id)
.map_err(|error| format!("Could not update topic '{topic_name}': {error}"))?;
let rust_topic_id = RustIdentifier::try_from(topic_id)
.map_err(|error| format!("Could not update topic '{topic_name}': {error}"))?;
let rust_compression_algorithm = match compression_algorithm.to_lowercase().as_str() {
"" | "none" => RustCompressionAlgorithm::None,
_ => RustCompressionAlgorithm::from_str(&compression_algorithm).map_err(|error| {
format!(
"Could not update topic '{topic_name}': invalid compression algorithm '{compression_algorithm}': {error}"
)
})?,
};
let rust_replication_factor = Some(replication_factor.max(1));
let rust_message_expiry = match message_expiry_kind.as_str() {
"" | "server_default" | "default" => RustIggyExpiry::ServerDefault,
"never_expire" => RustIggyExpiry::NeverExpire,
"duration" => RustIggyExpiry::ExpireDuration(iggy::prelude::IggyDuration::from(
message_expiry_value,
)),
_ => {
return Err(format!(
"Could not update topic '{topic_name}': invalid message expiry kind '{message_expiry_kind}'"
));
}
};
let rust_max_topic_size = match max_topic_size.as_str() {
"" | "server_default" | "0" => RustMaxTopicSize::ServerDefault,
_ => RustMaxTopicSize::from_str(&max_topic_size).map_err(|error| {
format!(
"Could not update topic '{topic_name}': invalid max topic size '{max_topic_size}': {error}"
)
})?,
};
RUNTIME.block_on(async {
self.inner
.update_topic(
&rust_stream_id,
&rust_topic_id,
&topic_name,
rust_compression_algorithm,
rust_replication_factor,
rust_message_expiry,
rust_max_topic_size,
)
.await
.map_err(|error| {
format!(
"Could not update topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn delete_topic(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not delete topic: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not delete topic: invalid topic identifier: {error}")
})?;
RUNTIME.block_on(async {
self.inner
.delete_topic(&rust_stream_id, &rust_topic_id)
.await
.map_err(|error| {
format!(
"Could not delete topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn purge_topic(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not purge topic: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id)
.map_err(|error| format!("Could not purge topic: invalid topic identifier: {error}"))?;
RUNTIME.block_on(async {
self.inner
.purge_topic(&rust_stream_id, &rust_topic_id)
.await
.map_err(|error| {
format!(
"Could not purge topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn create_partitions(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
partitions_count: u32,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not create partitions: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not create partitions: invalid topic identifier: {error}")
})?;
RUNTIME.block_on(async {
self.inner
.create_partitions(&rust_stream_id, &rust_topic_id, partitions_count)
.await
.map_err(|error| {
format!(
"Could not create {partitions_count} partitions for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn delete_partitions(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
partitions_count: u32,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not delete partitions: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not delete partitions: invalid topic identifier: {error}")
})?;
RUNTIME.block_on(async {
self.inner
.delete_partitions(&rust_stream_id, &rust_topic_id, partitions_count)
.await
.map_err(|error| {
format!(
"Could not delete {partitions_count} partitions for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn create_consumer_group(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
name: String,
) -> Result<ffi::ConsumerGroupDetails, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not create consumer group '{name}': invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not create consumer group '{name}': invalid topic identifier: {error}")
})?;
RUNTIME.block_on(async {
let group = self
.inner
.create_consumer_group(&rust_stream_id, &rust_topic_id, &name)
.await
.map_err(|error| {
format!(
"Could not create consumer group '{name}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(ffi::ConsumerGroupDetails::from(group))
})
}
pub fn get_consumer_group(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
group_id: ffi::Identifier,
) -> Result<ffi::ConsumerGroupDetails, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not get consumer group: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not get consumer group: invalid topic identifier: {error}")
})?;
let rust_group_id = RustIdentifier::try_from(group_id).map_err(|error| {
format!("Could not get consumer group: invalid group identifier: {error}")
})?;
RUNTIME.block_on(async {
let group = self
.inner
.get_consumer_group(&rust_stream_id, &rust_topic_id, &rust_group_id)
.await
.map_err(|error| {
format!(
"Could not get consumer group '{rust_group_id}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
let group = group.ok_or_else(|| {
format!(
"Consumer group '{rust_group_id}' was not found for topic '{rust_topic_id}' on stream '{rust_stream_id}'"
)
})?;
Ok(ffi::ConsumerGroupDetails::from(group))
})
}
pub fn get_consumer_groups(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
) -> Result<Vec<ffi::ConsumerGroup>, String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not get consumer groups: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not get consumer groups: invalid topic identifier: {error}")
})?;
RUNTIME.block_on(async {
let groups = self
.inner
.get_consumer_groups(&rust_stream_id, &rust_topic_id)
.await
.map_err(|error| {
format!(
"Could not get consumer groups for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(groups.into_iter().map(ffi::ConsumerGroup::from).collect())
})
}
pub fn delete_consumer_group(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
group_id: ffi::Identifier,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not delete consumer group: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not delete consumer group: invalid topic identifier: {error}")
})?;
let rust_group_id = RustIdentifier::try_from(group_id).map_err(|error| {
format!("Could not delete consumer group: invalid group identifier: {error}")
})?;
RUNTIME.block_on(async {
self.inner
.delete_consumer_group(&rust_stream_id, &rust_topic_id, &rust_group_id)
.await
.map_err(|error| {
format!(
"Could not delete consumer group '{rust_group_id}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn join_consumer_group(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
group_id: ffi::Identifier,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not join consumer group: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not join consumer group: invalid topic identifier: {error}")
})?;
let rust_group_id = RustIdentifier::try_from(group_id).map_err(|error| {
format!("Could not join consumer group: invalid group identifier: {error}")
})?;
RUNTIME.block_on(async {
self.inner
.join_consumer_group(&rust_stream_id, &rust_topic_id, &rust_group_id)
.await
.map_err(|error| {
format!(
"Could not join consumer group '{rust_group_id}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn get_stats(&self) -> Result<ffi::Stats, String> {
RUNTIME.block_on(async {
let stats = self
.inner
.get_stats()
.await
.map_err(|error| format!("Could not get stats: {error}"))?;
Ok(ffi::Stats::from(stats))
})
}
pub fn get_me(&self) -> Result<ffi::ClientInfoDetails, String> {
RUNTIME.block_on(async {
let client = self
.inner
.get_me()
.await
.map_err(|error| format!("Could not get current client info: {error}"))?;
Ok(ffi::ClientInfoDetails::from(client))
})
}
pub fn get_client(&self, client_id: u32) -> Result<ffi::ClientInfoDetails, String> {
RUNTIME.block_on(async {
let client = self
.inner
.get_client(client_id)
.await
.map_err(|error| format!("Could not get client '{client_id}': {error}"))?;
ffi::ClientInfoDetails::try_from(client)
.map_err(|error| format!("Could not get client '{client_id}': {error}"))
})
}
pub fn get_clients(&self) -> Result<Vec<ffi::ClientInfo>, String> {
RUNTIME.block_on(async {
let clients = self
.inner
.get_clients()
.await
.map_err(|error| format!("Could not get clients: {error}"))?;
Ok(clients.into_iter().map(ffi::ClientInfo::from).collect())
})
}
pub fn ping(&self) -> Result<(), String> {
RUNTIME.block_on(async {
self.inner
.ping()
.await
.map_err(|error| format!("Could not ping server: {error}"))?;
Ok(())
})
}
pub fn leave_consumer_group(
&self,
stream_id: ffi::Identifier,
topic_id: ffi::Identifier,
group_id: ffi::Identifier,
) -> Result<(), String> {
let rust_stream_id = RustIdentifier::try_from(stream_id).map_err(|error| {
format!("Could not leave consumer group: invalid stream identifier: {error}")
})?;
let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| {
format!("Could not leave consumer group: invalid topic identifier: {error}")
})?;
let rust_group_id = RustIdentifier::try_from(group_id).map_err(|error| {
format!("Could not leave consumer group: invalid group identifier: {error}")
})?;
RUNTIME.block_on(async {
self.inner
.leave_consumer_group(&rust_stream_id, &rust_topic_id, &rust_group_id)
.await
.map_err(|error| {
format!(
"Could not leave consumer group '{rust_group_id}' for topic '{rust_topic_id}' on stream '{rust_stream_id}': {error}"
)
})?;
Ok(())
})
}
pub fn heartbeat_interval(&self) -> u64 {
// The upstream client exposes this config-derived value via an async API,
// so the synchronous C++ wrapper reads it by blocking on the runtime.
RUNTIME.block_on(async { self.inner.heartbeat_interval().await.as_micros() })
}
pub fn snapshot(
&self,
snapshot_compression: String,
snapshot_types: Vec<String>,
) -> Result<Vec<u8>, String> {
let rust_compression = match snapshot_compression.trim() {
"" => {
return Err(
"Could not capture snapshot: snapshot_compression must not be empty"
.to_string(),
);
}
value => RustSnapshotCompression::from_str(value).map_err(|error| {
format!("Could not capture snapshot: invalid compression '{value}': {error}")
})?,
};
if snapshot_types.is_empty() {
return Err("Could not capture snapshot: snapshot_types must not be empty".to_string());
}
let mut seen_snapshot_types = HashSet::new();
let rust_snapshot_types = snapshot_types
.into_iter()
.filter(|snapshot_type| seen_snapshot_types.insert(snapshot_type.clone()))
.map(|snapshot_type| {
RustSystemSnapshotType::from_str(&snapshot_type).map_err(|error| {
format!(
"Could not capture snapshot: invalid snapshot type '{snapshot_type}': {error}"
)
})
})
.collect::<Result<Vec<_>, _>>()?;
RUNTIME.block_on(async {
let snapshot = self
.inner
.snapshot(rust_compression, rust_snapshot_types)
.await
.map_err(|error| format!("Could not capture snapshot: {error}"))?;
let iggy_common::Snapshot(bytes) = snapshot;
Ok(bytes)
})
}
}
pub unsafe fn delete_connection(client: *mut Client) -> Result<(), String> {
if client.is_null() {
return Ok(());
}
// `Box::from_raw` below runs unconditionally, so the client is always released regardless
// of `logout_result`. The result is only used to surface a logout error to the caller — there
// is no leak path here.
let logout_result = RUNTIME.block_on(async { unsafe { &*client }.inner.logout_user().await });
unsafe {
drop(Box::from_raw(client));
}
match logout_result {
Ok(()) | Err(IggyError::Unauthenticated | IggyError::Disconnected) => Ok(()),
Err(error) => Err(format!(
"Could not logout user during deletion of client: {error}"
)),
}
}