| // 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, ConsumerOffsetClient, Identifier as RustIdentifier, |
| IggyClient as RustIggyClient, IggyClientBuilder as RustIggyClientBuilder, |
| 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 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; |
| |
| fn resolve_consumer(consumer_kind: &str, consumer_id: RustIdentifier) -> Result<Consumer, String> { |
| match consumer_kind { |
| "consumer" => Ok(Consumer::new(consumer_id)), |
| "consumer_group" => Ok(Consumer::group(consumer_id)), |
| _ => Err(format!("invalid consumer kind: {consumer_kind}")), |
| } |
| } |
| |
| fn opt_partition(partition_id: u32) -> Option<u32> { |
| if partition_id == ANY_PARTITION_ID { |
| None |
| } else { |
| Some(partition_id) |
| } |
| } |
| |
| 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 logout_user(&self) -> Result<(), String> { |
| RUNTIME.block_on(async { |
| self.inner |
| .logout_user() |
| .await |
| .map_err(|error| format!("Could not logout user: {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 update_stream( |
| &self, |
| stream_id: ffi::Identifier, |
| stream_name: String, |
| ) -> Result<(), String> { |
| let rust_stream_id = RustIdentifier::try_from(stream_id) |
| .map_err(|error| format!("Could not update stream '{stream_name}': {error}"))?; |
| |
| RUNTIME.block_on(async { |
| self.inner |
| .update_stream(&rust_stream_id, &stream_name) |
| .await |
| .map_err(|error| { |
| format!( |
| "Could not update stream '{rust_stream_id}' to '{stream_name}': {error}" |
| ) |
| })?; |
| Ok(()) |
| }) |
| } |
| |
| 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(()) |
| }) |
| } |
| |
| pub fn flush_unsaved_buffer( |
| &self, |
| stream_id: ffi::Identifier, |
| topic_id: ffi::Identifier, |
| partition_id: u32, |
| fsync: bool, |
| ) -> Result<(), String> { |
| let rust_stream_id = RustIdentifier::try_from(stream_id) |
| .map_err(|error| format!("Could not flush unsaved buffer: {error}"))?; |
| let rust_topic_id = RustIdentifier::try_from(topic_id) |
| .map_err(|error| format!("Could not flush unsaved buffer: {error}"))?; |
| |
| RUNTIME.block_on(async { |
| self.inner |
| .flush_unsaved_buffer(&rust_stream_id, &rust_topic_id, partition_id, fsync) |
| .await |
| .map_err(|error| { |
| format!( |
| "Could not flush unsaved buffer for stream '{rust_stream_id}', topic '{rust_topic_id}', partition '{partition_id}': {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 = resolve_consumer(&consumer_kind, rust_consumer_id) |
| .map_err(|error| format!("Could not poll messages: {error}"))?; |
| |
| 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}" |
| )); |
| } |
| }; |
| |
| RUNTIME.block_on(async { |
| let polled = self |
| .inner |
| .poll_messages( |
| &rust_stream_id, |
| &rust_topic_id, |
| opt_partition(partition_id), |
| &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 store_consumer_offset( |
| &self, |
| stream_id: ffi::Identifier, |
| topic_id: ffi::Identifier, |
| partition_id: u32, |
| consumer_kind: String, |
| consumer_id: ffi::Identifier, |
| offset: u64, |
| ) -> Result<(), String> { |
| let rust_stream_id = RustIdentifier::try_from(stream_id) |
| .map_err(|error| format!("Could not store consumer offset: {error}"))?; |
| let rust_topic_id = RustIdentifier::try_from(topic_id) |
| .map_err(|error| format!("Could not store consumer offset: {error}"))?; |
| let rust_consumer_id = RustIdentifier::try_from(consumer_id) |
| .map_err(|error| format!("Could not store consumer offset: {error}"))?; |
| let consumer = resolve_consumer(&consumer_kind, rust_consumer_id) |
| .map_err(|error| format!("Could not store consumer offset: {error}"))?; |
| |
| RUNTIME.block_on(async { |
| self.inner |
| .store_consumer_offset( |
| &consumer, |
| &rust_stream_id, |
| &rust_topic_id, |
| opt_partition(partition_id), |
| offset, |
| ) |
| .await |
| .map_err(|error| { |
| format!( |
| "Could not store consumer offset for stream '{rust_stream_id}', topic '{rust_topic_id}': {error}" |
| ) |
| })?; |
| Ok(()) |
| }) |
| } |
| |
| pub fn get_consumer_offset( |
| &self, |
| stream_id: ffi::Identifier, |
| topic_id: ffi::Identifier, |
| partition_id: u32, |
| consumer_kind: String, |
| consumer_id: ffi::Identifier, |
| ) -> Result<ffi::ConsumerOffsetInfo, String> { |
| let rust_stream_id = RustIdentifier::try_from(stream_id) |
| .map_err(|error| format!("Could not get consumer offset: {error}"))?; |
| let rust_topic_id = RustIdentifier::try_from(topic_id) |
| .map_err(|error| format!("Could not get consumer offset: {error}"))?; |
| let rust_consumer_id = RustIdentifier::try_from(consumer_id) |
| .map_err(|error| format!("Could not get consumer offset: {error}"))?; |
| let consumer = resolve_consumer(&consumer_kind, rust_consumer_id) |
| .map_err(|error| format!("Could not get consumer offset: {error}"))?; |
| |
| RUNTIME.block_on(async { |
| let offset = self |
| .inner |
| .get_consumer_offset( |
| &consumer, |
| &rust_stream_id, |
| &rust_topic_id, |
| opt_partition(partition_id), |
| ) |
| .await |
| .map_err(|error| { |
| format!( |
| "Could not get consumer offset for stream '{rust_stream_id}', topic '{rust_topic_id}': {error}" |
| ) |
| })?; |
| ffi::ConsumerOffsetInfo::try_from(offset).map_err(|error| { |
| format!( |
| "Could not get consumer offset for stream '{rust_stream_id}', topic '{rust_topic_id}': {error}" |
| ) |
| }) |
| }) |
| } |
| |
| pub fn delete_consumer_offset( |
| &self, |
| stream_id: ffi::Identifier, |
| topic_id: ffi::Identifier, |
| partition_id: u32, |
| consumer_kind: String, |
| consumer_id: ffi::Identifier, |
| ) -> Result<(), String> { |
| let rust_stream_id = RustIdentifier::try_from(stream_id) |
| .map_err(|error| format!("Could not delete consumer offset: {error}"))?; |
| let rust_topic_id = RustIdentifier::try_from(topic_id) |
| .map_err(|error| format!("Could not delete consumer offset: {error}"))?; |
| let rust_consumer_id = RustIdentifier::try_from(consumer_id) |
| .map_err(|error| format!("Could not delete consumer offset: {error}"))?; |
| let consumer = resolve_consumer(&consumer_kind, rust_consumer_id) |
| .map_err(|error| format!("Could not delete consumer offset: {error}"))?; |
| |
| RUNTIME.block_on(async { |
| self.inner |
| .delete_consumer_offset( |
| &consumer, |
| &rust_stream_id, |
| &rust_topic_id, |
| opt_partition(partition_id), |
| ) |
| .await |
| .map_err(|error| { |
| format!( |
| "Could not delete consumer offset for stream '{rust_stream_id}', topic '{rust_topic_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() { |
| unsafe { |
| drop(Box::from_raw(client)); |
| } |
| } |
| |
| Ok(()) |
| } |