diff --git a/core/common/src/traits/partitioner.rs b/core/common/src/traits/partitioner.rs index c665c82d38..2b2252e637 100644 --- a/core/common/src/traits/partitioner.rs +++ b/core/common/src/traits/partitioner.rs @@ -21,8 +21,19 @@ use crate::types::message::IggyMessage; use std::fmt::Debug; /// The trait represent the logic responsible for calculating the partition ID and is used by the `IggyClient`. -/// This might be especially useful when the partition ID is not constant and might be calculated based on the stream ID, topic ID and other parameters. +/// +/// Iggy uses a hierarchical model for append-only logs. A stream contains topics which hold partitions. Each partition is an append-only log.[^note] +/// A producer of messages such as an [`IggyProducer`], that appends messages to the log, may want to choose which partition to write the messages into. +/// To do that, a producer can take a type that implements this trait. +/// This is especially useful when computing the partition ID requires some client side info, i.e. stream ID, topic ID and/ or [`IggyMessage`] attributes. +/// +/// Note the difference between [`Partitioning`] and [`Partitioner`]. [`Partitioning`] is a type used to set the _partitioning strategy_ for a producer. +/// If you use both, the [`Partitioner`] overwrites the strategy, sets it to [`PartitioningKind::PartitionID`] and the partition ID is +/// calculated with with the logic implemented in [`Partitioner::calculate_partition_id()`]. +/// +/// [^note]: [Website docs on how Iggy organizes data.](https://iggy.apache.org/docs/#how-iggy-organizes-data) pub trait Partitioner: Send + Sync + Debug { + /// Calculate a partition ID. fn calculate_partition_id( &self, stream_id: &Identifier, diff --git a/core/common/src/types/message/partitioning.rs b/core/common/src/types/message/partitioning.rs index 34694e47c8..30a8e617e8 100644 --- a/core/common/src/types/message/partitioning.rs +++ b/core/common/src/types/message/partitioning.rs @@ -25,11 +25,18 @@ use std::{ hash::{Hash, Hasher}, }; -/// `Partitioning` is used to specify to which partition the messages should be sent. -/// It has the following kinds: +/// A type that defines a what strategy the server should choose to partition the messages. +/// +/// Iggy uses a hierarchical model for append-only logs. A stream contains topics which hold partitions. Each partition is an append-only log.[^note] +/// A producer of messages such as an [`IggyProducer`], that appends messages to the log can choose between three partitioning strategies. /// - `Balanced` - the partition ID is calculated by the server using the round-robin algorithm. -/// - `PartitionId` - the partition ID is provided by the client. /// - `MessagesKey` - the partition ID is calculated by the server using the hash of the provided messages key. +/// - `PartitionId` - the partition ID is provided by the client. +/// +/// Note, that using a [`Partitioner`] on top of [`Partitioning`] sets the strategy to [`PartitioningKind::PartitionId`]. The value is then computed +/// based on your concrete implementation of [`Partitioner::calculate_partition_id()`]. +/// +/// [^note]: [Website docs on how Iggy organizes data.](https://iggy.apache.org/docs/#how-iggy-organizes-data) #[serde_as] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct Partitioning { @@ -160,6 +167,9 @@ impl Partitioning { } /// Maximum size of the Partitioning struct + /// NOTE(haubur) I think this can be removed as it seems to be superseded + /// by implementing Sizeable/ get_size_bytes(). + #[doc(hidden)] pub const fn maximum_byte_size() -> usize { 2 + 255 } diff --git a/core/sdk/src/clients/client.rs b/core/sdk/src/clients/client.rs index dd7f91fd98..8eda4ca8bf 100644 --- a/core/sdk/src/clients/client.rs +++ b/core/sdk/src/clients/client.rs @@ -55,9 +55,166 @@ const SESSION_CONTROL_CODES: [u32; 5] = [ LOGIN_REGISTER_WITH_PAT_CODE, ]; -/// The main client struct which implements all the `Client` traits and wraps the underlying low-level client for the specific transport. +/// A high-level, transport-agnostic client for an Iggy server. /// -/// It also provides the additional builders for the standalone consumer, consumer group, and producer. +/// `IggyClient` wraps a transport-specific low-level **client** ([`ClientWrapper`]) and +/// **provides access to the full server API**. +/// Iggy comes with four options for client-server communication: TCP, QUIC, WebSocket and HTTP. +/// The `IggyClient` is configured with one of these transport modes, hence abstracting +/// transport specific implementations away. +/// +/// The [`ClientWrapper`] lives behind an [`IggyRwLock`] so that the connection +/// can be shared safely. You create a single client and use it from many tasks +/// at once (producers, consumers, the background heartbeat). Many operations can read +/// from the connection concurrently, while actions that reshape it, like +/// connecting, reconnecting, or logging in, briefly take exclusive access. +/// +/// A [`Partitioner`] and a client-side [`EncryptorKind`] are optional, and both +/// default to disabled. The [`Partitioner`] computes on the client-side the target +/// partition for messages published without an explicit partition. Hence, routing +/// can depend on the stream, topic, and/ or message contents. +/// +/// The [`EncryptorKind`] encrypts each message payload before it leaves the client and decrypts it on +/// the way back, keeping payloads opaque to the server. Attach either through +/// [`create()`]. +/// +/// # What you can do +/// +/// Configure a connection with an Iggy server and interact with it. +/// The `IggyClient` provides various methods to setup the connection using connection strings, +/// builder patterns or an already existing [`ClientWrapper`]. +/// You can spawn [`IggyConsumer`]s and [`IggyProducer`]s that share that connection. +/// +/// The full server API is split into domain-specific traits. +/// `IggyClient` implements [`Client`], the supertrait, which pulls every domain-specific trait. +/// Bring the one you need into scope to call its methods. +/// `use iggy::prelude::*` brings all of them in at once. +/// +/// - [`SystemClient`]: ping, server statistics, snapshots, and connected-client info. +/// - [`UserClient`]: create, inspect, update, and delete users and their permissions. +/// - [`PersonalAccessTokenClient`]: create, list, and delete personal access tokens, log in with one. +/// - [`StreamClient`]: create, get, update, delete, and purge streams. +/// - [`TopicClient`]: create, get, update, delete, and purge topics within a stream. +/// - [`PartitionClient`]: add and remove partitions on a topic. +/// - [`SegmentClient`]: delete closed segments from a partition. +/// - [`ConsumerGroupClient`]: create, get, delete, and join or leave consumer groups. +/// - [`ConsumerOffsetClient`]: store, read, and delete consumer offsets. +/// - [`MessageClient`]: send and poll messages, and flush the unsaved buffer. +/// +/// Additionally, you can bypass invoking methods from these traits and directly talk to the server with [`send_binary_request`] and [`send_http_request`] for http. +/// Both trade typed API's safety for low-level control. You need to know the server codes and the wire format. +/// +/// # Usage +/// +/// The typical lifecycle of an `IggyClient` is construct, [`connect()`], use, and finally shutdown. +/// +/// 1. Construct a client from a connection string ([`from_connection_string()`]), +/// from the [`builder()`], or by wrapping an existing transport client with +/// [`new()`] / [`create()`]. +/// 2. Call [`connect()`] to establish the connection. If the transport was +/// configured with auto-login, this also authenticates. Otherwise call +/// [`login_user()`] afterwards. +/// 3. Spawn [`IggyConsumer`]s and [`IggyProducer`]s to write to, and consume messages +/// from, the server. +/// 4. To shut everything down, call [`IggyConsumer::shutdown()`] on each consumer to store their +/// final offset and leave consumer groups. Then, call [`IggyProducer::shutdown()`] on each +/// [`IggyProducer`] so that _background_ producers flush the latest state. Finally, +/// call [`shutdown()`] on the [`IggyClient`] which closes the connection and stops the heartbeat. +/// Use [`disconnect()`] rather than [`shutdown()`] to close the connection but keep the client usable, as a +/// client that has been shut down cannot reconnect. +/// +/// # Examples +/// +/// Build a client from a connection string, connect, publish through a +/// background batching producer, consume with a standalone consumer, and shut down cleanly. +/// +/// ```no_run +/// use iggy::prelude::*; +/// use futures_util::StreamExt; +/// use std::str::FromStr; +/// +/// # async fn run() -> Result<(), IggyError> { +/// // Auto-logs in from the credentials in the string and retries forever on disconnect. +/// let client = IggyClient::builder_from_connection_string( +/// "iggy+tcp://user:secret@localhost:8090\ +/// ?reconnection_retries=unlimited&reconnection_interval=1s&heartbeat_interval=5s&nodelay=true", +/// )? +/// .build()?; +/// client.connect().await?; +/// +/// // A background producer batches in the background, retries failed sends, +/// // and creates a topic. +/// let producer = client +/// .producer("orders", "created")? +/// .background( +/// BackgroundConfig::builder() +/// .batch_length(1000) +/// .linger_time(IggyDuration::ONE_SECOND) +/// .build(), +/// ) +/// .partitioning(Partitioning::balanced()) +/// .send_retries(Some(3), Some(IggyDuration::ONE_SECOND)) +/// .create_topic_if_not_exists( +/// 3, +/// None, +/// IggyExpiry::ServerDefault, +/// MaxTopicSize::ServerDefault, +/// ) +/// .build(); +/// producer.init().await?; +/// producer +/// .send(vec![IggyMessage::from_str("our-first-message")?]) +/// .await?; +/// +/// // A consumer pinned to one partition, committing its offset +/// // automatically so a restart resumes where it left off. +/// let mut consumer = client +/// .consumer("consumer_name", "stream_name", "topic_name", 1)? +/// .auto_commit(AutoCommit::When(AutoCommitWhen::PollingMessages)) +/// .polling_strategy(PollingStrategy::next()) +/// .poll_interval(IggyDuration::ONE_SECOND) +/// .batch_length(1000) +/// .build(); +/// consumer.init().await?; +/// +/// while let Some(message) = consumer.next().await { +/// let message = message?; +/// // Handle `message.message.payload` here however required +/// break; +/// } +/// +/// client.shutdown().await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [`IggyConsumer`]: crate::prelude::IggyConsumer +/// [`IggyProducer`]: crate::prelude::IggyProducer +/// [`IggyConsumer::shutdown()`]: crate::prelude::IggyConsumer::shutdown +/// [`IggyProducer::shutdown()`]: crate::prelude::IggyProducer::shutdown +/// [`new()`]: IggyClient::new +/// [`create()`]: IggyClient::create +/// [`shutdown()`]: IggyClient::shutdown +/// [`login_user()`]: IggyClient::login_user +/// [`connect()`]: IggyClient::connect +/// [`disconnect()`]: IggyClient::disconnect +/// [`producer()`]: IggyClient::producer +/// [`consumer()`]: IggyClient::consumer +/// [`builder()`]: IggyClient::builder +/// [`from_connection_string()`]: IggyClient::from_connection_string +/// [`consumer_group()`]: IggyClient::consumer_group +/// [`Client`]: crate::prelude::Client +/// [`SystemClient`]: crate::prelude::SystemClient +/// [`UserClient`]: crate::prelude::UserClient +/// [`PersonalAccessTokenClient`]: crate::prelude::PersonalAccessTokenClient +/// [`StreamClient`]: crate::prelude::StreamClient +/// [`TopicClient`]: crate::prelude::TopicClient +/// [`PartitionClient`]: crate::prelude::PartitionClient +/// [`SegmentClient`]: crate::prelude::SegmentClient +/// [`MessageClient`]: crate::prelude::MessageClient +/// [`ConsumerOffsetClient`]: crate::prelude::ConsumerOffsetClient +/// [`ConsumerGroupClient`]: crate::prelude::ConsumerGroupClient +/// [`ClusterClient`]: crate::prelude::ClusterClient #[derive(Debug)] #[allow(dead_code)] pub struct IggyClient { @@ -73,19 +230,209 @@ impl Default for IggyClient { } impl IggyClient { - /// Creates a new `IggyClientBuilder`. + /// Returns an empty [`IggyClientBuilder`]. + /// + /// The returned builder is not ready to be [`IggyClientBuilder::build()`]. + /// It sill needs to configure a mode of transport. pub fn builder() -> IggyClientBuilder { IggyClientBuilder::new() } - /// Creates a new `IggyClientBuilder` from the provided connection string. + /// Creates an [`IggyClientBuilder`] with the transport preconfigured from a + /// connection string. + /// + /// The transport is selected from the scheme: + /// - `iggy://` defaults to TCP. + /// - `iggy+tcp://` for TCP. + /// - `iggy+quic://` for QUIC. + /// - `iggy+http://` for HTTP. + /// - `iggy+ws://` for WebSocket. + /// + /// Authentication at the server is mandatory. + /// - user + password: `:@:` + /// - personal access token: `@host:port` + /// + /// Optional `?key=value&key=value` queries carry transport specific + /// configuration. The query is parsed per transport, so the accepted keys differ + /// by scheme. An unknown key is rejected. + /// If no queries are provided optional configurations are automatically set to their default values. + /// + /// # Examples + /// + /// Each example lists configuration options per transport mode and shows + /// one concrete example. + /// + /// If the value of an option is + /// - a duration pass a `humantime` string such as `5s`, `500ms`, or `1h 1m 1s`. + /// These are cast into an [`IggyDuration`]. + /// Note, `unlimited`, `none`, `disabled`, and `0` parse to zero. + /// - a retry count use either the literal `unlimited` or a number such as `5`. + /// - a bool use the literal `true` or `false`. + /// - bytes and millisecond options provide a number such as `1024`. + /// + /// ## TCP + /// + /// The same options apply for `iggy://` and `iggy+tcp://`. + /// + /// - `tls`: bool. Enable/disable TLS. Default: `false`. + /// - `tls_domain`: string. Server name to validate the certificate against. Default: unset. + /// - `tls_ca_file`: filesystem path. Extra CA certificate to trust. Default: unset. + /// - `reconnection_retries`: "unlimited" or u32. Number of attempts to connect. Default: `unlimited`. + /// - `reconnection_interval`: [`IggyDuration`]. Wait between reconnection attempts. Default: `1s`. + /// - `reestablish_after`: [`IggyDuration`]. Grace period before reconnecting. Default: `5s`. + /// - `heartbeat_interval`: [`IggyDuration`]. Client heartbeat period. Default: `5s`. + /// - `nodelay`: `bool`. Disable Nagle's algorithm (`TCP_NODELAY`). Default: `false`. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # fn run() -> Result<(), IggyError> { + /// let client = IggyClient::builder_from_connection_string( + /// "iggy+tcp://user:secret@localhost:8090\ + /// ?tls=true&tls_domain=localhost&tls_ca_file=/etc/iggy/ca.pem\ + /// &reconnection_retries=unlimited&reconnection_interval=1s&reestablish_after=5s\ + /// &heartbeat_interval=5s&nodelay=true", + /// )? + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// ## QUIC + /// + /// - `validate_certificate`: bool. Verify the server certificate. Default: `false`. + /// - `heartbeat_interval`: [`IggyDuration`]. Client heartbeat period. Default: `5s`. + /// - `reconnection_max_retries`: "unlimited" or u32. Number of attempts to connect. Default: `unlimited`. + /// - `reconnection_interval`: [`IggyDuration`]. Wait between reconnection attempts. Default: `1s`. + /// - `reconnection_reestablish_after`: [`IggyDuration`]. Grace period before reconnecting. Default: `5s`. + /// - `response_buffer_size`: u64. Number of bytes in the response receive buffer. Default: `10000000`. + /// - `max_concurrent_bidi_streams`: u64. Number of concurrent bidirectional streams. Default: `10000`. + /// - `datagram_send_buffer_size`: u64. Number of bytes in the datagram send buffer. Default: `100000`. + /// - `initial_mtu`: u16. Initial MTU estimate (in bytes). Default: `1200`. + /// - `send_window`: u64. Number of bytes bytes of the flow-control send window. Default: `100000`. + /// - `receive_window`: u64. Number of bytes of the flow-control receive window. Default: `100000`. + /// - `keep_alive_interval`: u64. QUIC keep-alive period (in milliseconds). Default: `5000`. + /// - `max_idle_timeout`: u64. Close after this much idle time (in middleseconds). Default: `10000`. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # fn run() -> Result<(), IggyError> { + /// let client = IggyClient::builder_from_connection_string( + /// "iggy+quic://user:secret@localhost:8080\ + /// ?validate_certificate=true&heartbeat_interval=5s\ + /// &reconnection_max_retries=unlimited&reconnection_interval=1s&reconnection_reestablish_after=5s\ + /// &response_buffer_size=10000000&max_concurrent_bidi_streams=10000\ + /// &datagram_send_buffer_size=100000&initial_mtu=1200\ + /// &send_window=100000&receive_window=100000\ + /// &keep_alive_interval=5000&max_idle_timeout=10000", + /// )? + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// ## HTTP + /// + /// A REST transport. Iggy uses the `reqwest` crate to manage the HTTP client. + /// Hence, transport specific configuration is abstracted away. + /// + /// Configurable options are: + /// - `heartbeat_interval`: [`IggyDuration`]. Client heartbeat period. Default: `5s`. + /// - `retries`: u32. Number of retries when sending a request. Default: `3`. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # fn run() -> Result<(), IggyError> { + /// let client = IggyClient::builder_from_connection_string( + /// "iggy+http://user:secret@localhost:3000?heartbeat_interval=5s&retries=3", + /// )? + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// ## WebSocket + /// + /// - `heartbeat_interval`: [`IggyDuration`]. Client heartbeat period. Default: `5s`. + /// - `reconnection_retries`: "unlimited" or u32. Number of attempts to connect. Default: `unlimited`. + /// - `reconnection_interval`: [`IggyDuration`]. Wait between reconnection attempts. Default: `1s`. + /// - `reestablish_after`: [`IggyDuration`]. Grace period before reconnecting. Default: `5s`. + /// - `read_buffer_size`: usize. Size of the read buffer in bytes. Default: unset. + /// - `write_buffer_size`: usize. Size of the write buffer in bytes. Default: unset. + /// - `max_write_buffer_size`: usize. Maximum size of the write buffer in bytes. Default: unset. + /// - `max_message_size`: usize. Maximum accepted message size in bytes. Default: unset. + /// - `max_frame_size`: usize. Maximum accepted frame size in bytes. Default: unset. + /// - `accept_unmasked_frames`: bool. Accept/ decline unmasked frames. Default: `false`. + /// - `tls`: `bool`. Enable/disbale TLS. Default: `false`. + /// - `tls_domain`: string. Server name to validate the certificate against. Default: unset. + /// - `tls_ca_file`: filesystem path. Extra CA certificate to trust. Default: unset. + /// - `tls_validate_certificate`: bool. Whether to verify the server certificate. Default: `false`. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # fn run() -> Result<(), IggyError> { + /// let client = IggyClient::builder_from_connection_string( + /// "iggy+ws://user:secret@localhost:8090\ + /// ?heartbeat_interval=5s&reconnection_retries=unlimited&reconnection_interval=1s&reestablish_after=5s\ + /// &read_buffer_size=131072&write_buffer_size=131072&max_write_buffer_size=131072\ + /// &max_message_size=67108864&max_frame_size=16777216&accept_unmasked_frames=false\ + /// &tls=true&tls_domain=localhost&tls_ca_file=/etc/iggy/ca.pem&tls_validate_certificate=true", + /// )? + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`IggyError::InvalidConnectionString`] if the connection string is malformed. + /// + /// [`IggyDuration`]: crate::prelude::IggyDuration pub fn builder_from_connection_string( connection_string: &str, ) -> Result { IggyClientBuilder::from_connection_string(connection_string) } - /// Creates a new `IggyClient` with the provided client implementation for the specific transport. + /// Creates a new `IggyClient` from an already-constructed transport client. + /// + /// Use this when you built the transport client yourself + /// and want the full high-level `IggyClient` surface on top of it. To start + /// from a connection string instead, prefer + /// [`from_connection_string`](IggyClient::from_connection_string) or + /// [`builder_from_connection_string`](IggyClient::builder_from_connection_string). + /// To also attach a [`Partitioner`] or client-side [`EncryptorKind`], use + /// [`create`]. + /// + /// # Examples + /// + /// Build a [`TcpClient`] from a config, wrap it in a [`ClientWrapper`], and + /// hand it to `IggyClient` for the full server API. + /// + /// ```no_run + /// use iggy::prelude::*; + /// use std::sync::Arc; + /// + /// # fn run() -> Result<(), IggyError> { + /// let config = TcpClientConfigBuilder::new() + /// .with_server_address("127.0.0.1:8090".to_owned()) + /// .build()?; + /// let tcp_client = TcpClient::create(Arc::new(config))?; + /// + /// let client = IggyClient::new(ClientWrapper::Tcp(tcp_client)); + /// # let _ = client; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`create`]: IggyClient::create + /// [`Partitioner`]: crate::prelude::Partitioner + /// [`EncryptorKind`]: crate::prelude::EncryptorKind + /// [`TcpClient`]: crate::prelude::TcpClient + /// [`ClientWrapper`]: crate::prelude::ClientWrapper pub fn new(client: ClientWrapper) -> Self { let client = IggyRwLock::new(client); IggyClient { @@ -95,7 +442,23 @@ impl IggyClient { } } - /// Creates a new `IggyClient` from the provided connection string. + /// Creates a new `IggyClient` directly from a connection string. + /// + /// This is a shortcut for [`builder_from_connection_string`] followed by + /// [`build`](IggyClientBuilder::build) when no partitioner or encryptor is + /// needed. + /// + /// Refer to [`builder_from_connection_string`] for concise examples on how + /// to use a connection string. + /// To also attach a [`Partitioner`] or client-side [`EncryptorKind`], use + /// [`create`]. + /// + /// # Errors + /// + /// Returns [`IggyError::InvalidConnectionString`] if the connection string is + /// malformed. + /// + /// [`builder_from_connection_string`]: IggyClient::builder_from_connection_string pub fn from_connection_string(connection_string: &str) -> Result { match ConnectionStringUtils::parse_protocol(connection_string)? { TransportProtocol::Tcp => Ok(IggyClient::new(ClientWrapper::Tcp( @@ -113,7 +476,61 @@ impl IggyClient { } } - /// Creates a new `IggyClient` with the provided client implementation for the specific transport and the optional implementations for the `partitioner` and `encryptor`. + /// Creates a new `IggyClient` from a transport client, with an optional + /// [`Partitioner`] and client-side [`EncryptorKind`]. + /// + /// The partitioner picks the target partition for messages published without + /// an explicit partition assigned to them. Note, that setting a [`Partitioner`] overrides a producer's + /// [`Partitioning`](crate::prelude::Partitioning) the partition id is + /// computed client-side and the partitioning strategy is forced to [`PartitioningKind::PartitionID`] using the computed ID. + /// The encryptor encrypts payloads before they leave the client. Pass [`None`] for either to + /// disable it, just as [`new`](IggyClient::new) does for both. + /// + /// # Examples + /// + /// Wrap a [`TcpClient`] together with a custom [`Partitioner`] and an + /// AES-256-GCM payload [`EncryptorKind`]. + /// + /// ```no_run + /// use iggy::prelude::*; + /// use std::sync::Arc; + /// + /// // Routes every message to partition 1. + /// #[derive(Debug)] + /// struct FixedPartitioner; + /// + /// impl Partitioner for FixedPartitioner { + /// fn calculate_partition_id( + /// &self, + /// _stream_id: &Identifier, + /// _topic_id: &Identifier, + /// _messages: &[IggyMessage], + /// ) -> Result { + /// Ok(1) + /// } + /// } + /// + /// # fn run() -> Result<(), IggyError> { + /// let tcp_client = + /// TcpClient::from_connection_string("iggy+tcp://user:secret@localhost:8090")?; + /// + /// let partitioner: Arc = Arc::new(FixedPartitioner); + /// let encryptor = Arc::new(EncryptorKind::Aes256Gcm(Aes256GcmEncryptor::new(&[0u8; 32])?)); + /// + /// let client = IggyClient::create( + /// ClientWrapper::Tcp(tcp_client), + /// Some(partitioner), + /// Some(encryptor), + /// ); + /// # let _ = client; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Partitioner`]: crate::prelude::Partitioner + /// [`EncryptorKind`]: crate::prelude::EncryptorKind + /// [`TcpClient`]: crate::prelude::TcpClient + /// [`ClientWrapper`]: crate::prelude::ClientWrapper pub fn create( client: ClientWrapper, partitioner: Option>, @@ -134,12 +551,75 @@ impl IggyClient { } } - /// Returns the underlying client implementation for the specific transport. + /// Returns a handle to the underlying transport client. + /// + /// The returned [`ClientWrapper`] is behind an [`IggyRwLock`]. + /// Thus, the returned type shares ownership with this `IggyClient`, meaning + /// changes made through either are visible to both. + /// + /// # Examples + /// + /// Take the shared handle, acquire a read guard, and reach the underlying + /// transport directly, here to ping the server over the raw connection. + /// + /// ```no_run + /// use iggy::prelude::*; + /// use crate::iggy::prelude::locking::IggyRwLockFn; + /// + /// # async fn run(client: IggyClient) -> Result<(), IggyError> { + /// let handle = client.client(); + /// handle.read().await.ping().await?; + /// # Ok(()) + /// # } + /// ``` pub fn client(&self) -> IggyRwLock { self.client.clone() } - /// Returns the builder for the standalone consumer. + /// Returns an [`IggyConsumerBuilder`] to build a standalone consumer. + /// + /// Copies the client and the encryptor registered with the [`IggyClient`] + /// into a [`IggyConsumerBuilder`] and returns it. + /// Sets the consumer to [`ConsumerKind::Consumer`], i.e. a single consumer, + /// with the provided name. + /// Registers a consumer for `stream`, `topic` and `partition`. + /// + /// To get builder for a load-balanced consumer group, use + /// [`consumer_group`](IggyClient::consumer_group) instead. + /// + /// Refer to the [`IggyConsumer`] type and the [`IggyConsumerBuilder`] + /// for details on how a consumer can be configured. + /// + /// # Examples + /// + /// Connect a client, build a consumer pinned to partition 1, and configure + /// it to auto-commit its offset so a restart resumes where it left off. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # async fn run() -> Result<(), IggyError> { + /// let client = IggyClient::from_connection_string( + /// "iggy+tcp://user:secret@localhost:8090", + /// )?; + /// client.connect().await?; + /// + /// let consumer = client + /// .consumer("consumer_name", "stream_name", "topic_name", 1)? // returns IggyConsumerBuilder from IggyClient + /// .auto_commit(AutoCommit::When(AutoCommitWhen::PollingMessages)) + /// .polling_strategy(PollingStrategy::next()) + /// .poll_interval(IggyDuration::ONE_SECOND) + /// .batch_length(1000) + /// .build(); // returns IggyConsumer from IggyConsumerBuilder + /// # let _ = consumer; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`IggyError::InvalidIdentifier`] if `name`, `stream`, or `topic` + /// is not a valid identifier. pub fn consumer( &self, name: &str, @@ -159,7 +639,52 @@ impl IggyClient { )) } - /// Returns the builder for the consumer group. + /// Returns an [`IggyConsumerBuilder`] for a member of a consumer group. + /// + /// Copies the client and the encryptor registered with the [`IggyClient`] + /// into a [`IggyConsumerBuilder`] and returns it. + /// Sets the consumer to [`ConsumerKind::ConsumerGroup`], i.e. a member of a + /// load-balanced group. The provided name identifies the group. + /// Registers the member for every partition of `topic` in `stream`. The + /// group then balances those partitions across its members, so each message is + /// delivered to exactly one member. + /// + /// For a consumer pinned to a single partition, use + /// [`consumer`](IggyClient::consumer) instead. + /// + /// Refer to the [`IggyConsumer`] type and the [`IggyConsumerBuilder`] + /// for details on how a consumer can be configured. + /// + /// # Examples + /// + /// Connect a client, build a member of a consumer group, and configure it to + /// auto-commit its offset so a restart resumes where it left off. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # async fn run() -> Result<(), IggyError> { + /// let client = IggyClient::from_connection_string( + /// "iggy+tcp://user:secret@localhost:8090", + /// )?; + /// client.connect().await?; + /// + /// let consumer = client + /// .consumer_group("group_name", "stream_name", "topic_name")? // returns IggyConsumerBuilder from IggyClient + /// .auto_commit(AutoCommit::When(AutoCommitWhen::PollingMessages)) + /// .polling_strategy(PollingStrategy::next()) + /// .poll_interval(IggyDuration::ONE_SECOND) + /// .batch_length(1000) + /// .build(); // returns IggyConsumer from IggyConsumerBuilder + /// # let _ = consumer; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`IggyError::InvalidIdentifier`] if `name`, `stream`, or `topic` + /// is not a valid identifier. pub fn consumer_group( &self, name: &str, @@ -178,7 +703,56 @@ impl IggyClient { )) } - /// Returns the builder for the producer. + /// Returns an [`IggyProducerBuilder`]. + /// + /// Copies the client and the encryptor registered with the [`IggyClient`] + /// into a [`IggyProducerBuilder`] and returns it. + /// The [`Partitioner`] is applied by the client when messages are sent without + /// an explicit partition assigned to them. Note that, setting a [`Partitioner`] overrides the producer's + /// [`Partitioning`](crate::prelude::Partitioning). The partition id is + /// computed client-side and the partitioning strategy is forced to [`PartitioningKind::PartitionID`] using the computed id. + /// + /// Refer to the [`IggyProducer`] type and the [`IggyProducerBuilder`] + /// for details on how a producer can be configured. + /// + /// # Examples + /// + /// Connect a client, build a producer that creates the topic if it is + /// missing and retries failed sends, then publish a message. + /// + /// ```no_run + /// use iggy::prelude::*; + /// use std::str::FromStr; + /// + /// # async fn run() -> Result<(), IggyError> { + /// let client = IggyClient::from_connection_string( + /// "iggy+tcp://user:secret@localhost:8090", + /// )?; + /// client.connect().await?; + /// + /// let producer = client + /// .producer("stream_name", "topic_name")? // returns IggyProducerBuilder from IggyClient + /// .partitioning(Partitioning::balanced()) + /// .send_retries(Some(3), Some(IggyDuration::ONE_SECOND)) + /// .create_topic_if_not_exists( + /// 3, + /// None, + /// IggyExpiry::ServerDefault, + /// MaxTopicSize::ServerDefault, + /// ) + /// .build(); // returns IggyProducer from IggyProducerBuilder + /// producer.init().await?; + /// producer + /// .send(vec![IggyMessage::from_str("our-first-message")?]) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`IggyError::InvalidIdentifier`] if `stream` or `topic` is not a + /// valid identifier. pub fn producer(&self, stream: &str, topic: &str) -> Result { Ok(IggyProducerBuilder::new( self.client.clone(), @@ -191,21 +765,83 @@ impl IggyClient { )) } - /// Returns the current connection information including the transport protocol and server address. - /// This is useful for verifying which server the client is connected to, especially after - /// leader redirection in a clustered environment. + /// Returns the current [`ConnectionInfo`]. + /// + /// The transport protocol and the server address the client is connected to. + /// + /// # Examples + /// + /// Connect a client and print the transport protocol and server address it + /// is connected to. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # async fn run() -> Result<(), IggyError> { + /// let client = IggyClient::from_connection_string( + /// "iggy+tcp://user:secret@localhost:8090", + /// )?; + /// client.connect().await?; + /// + /// let info = client.get_connection_info().await; + /// println!("connected to {} over {}", info.server_address, info.protocol); + /// # Ok(()) + /// # } + /// ``` pub async fn get_connection_info(&self) -> ConnectionInfo { self.client.read().await.get_connection_info().await } - /// Send a raw binary command (`code` + serialized `payload`), returning the - /// raw response. Binary transports only (HTTP yields `FeatureUnavailable`). + /// Sends a raw binary command (`code` plus serialized `payload`) and returns + /// the raw response payload. + /// + /// Use this method for commands the typed API does not cover, for + /// example a command you added to a forked server. + /// + /// `code` selects the command. Available codes are defined in + /// [`iggy_binary_protocol::codes`] /// - /// Login and logout codes are rejected with `InvalidCommand`. Use the - /// `login_user` / `logout_user` methods so SDK session state stays correct. + /// `payload` is the command body already serialized in the Iggy wire format, + /// and the returned [`Bytes`] is the raw response body in that same format, + /// which you need to decode yourself. The wire frame that carries both (length, code, + /// status) is documented at the [`iggy_binary_protocol`]. You pass + /// and receive only the payload, the transport configured with the client frames it. /// - /// Custom codes are forwarded to the server, which is the authority on - /// whether it implements them. + /// Only the binary transports (TCP, QUIC, WebSocket) have a raw binary path. + /// The HTTP counterpart is + /// [`send_http_request`](IggyClient::send_http_request). + /// + /// # Examples + /// + /// Ping the server over the raw binary path. `PING_CODE` takes an empty + /// payload and the server replies with an empty payload. + /// + /// ```no_run + /// use iggy::prelude::*; + /// use iggy_binary_protocol::codes::PING_CODE; + /// use bytes::Bytes; + /// + /// # async fn run() -> Result<(), IggyError> { + /// let client = IggyClient::from_connection_string( + /// "iggy+tcp://user:secret@localhost:8090", + /// )?; + /// client.connect().await?; + /// + /// let response = client.send_binary_request(PING_CODE, Bytes::new()).await?; + /// assert!(response.is_empty()); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// [`IggyError::InvalidCommand`] if `code` is one of the session-control + /// codes (login, logout, and register). Use the typed `login_user` / + /// `logout_user` methods so the SDK's session state stays correct. + /// [`IggyError::FeatureUnavailable`] on the HTTP transport, which has no + /// binary path. + /// + /// [`iggy_binary_protocol`]: iggy_binary_protocol + /// [`iggy_binary_protocol::codes`]: iggy_binary_protocol::codes pub async fn send_binary_request(&self, code: u32, payload: Bytes) -> Result { if SESSION_CONTROL_CODES.contains(&code) { return Err(IggyError::InvalidCommand); @@ -218,8 +854,43 @@ impl IggyClient { } } - /// Invoke an arbitrary HTTP endpoint and return the raw response body. HTTP - /// transport only; binary transports yield `FeatureUnavailable`. + /// Invokes a HTTP endpoint and returns the raw response body. + /// + /// This is the HTTP counterpart to + /// [`send_binary_request`](IggyClient::send_binary_request). + /// + /// `method` is the HTTP verb and `path` is joined onto the + /// client's configured API URL, e.g. `/streams`. + /// `body` (if needed) is sent as-is as the request body. The client + /// attaches its bearer token. The returned [`Bytes`] is the raw response body + /// for a response, which you decode yourself. + /// + /// # Examples + /// + /// Fetch the server's stats over the raw HTTP path with a `GET` and no body. + /// + /// ```no_run + /// use iggy::prelude::*; + /// + /// # async fn run() -> Result<(), IggyError> { + /// let client = IggyClient::from_connection_string( + /// "iggy+http://user:secret@localhost:3000", + /// )?; + /// client.connect().await?; + /// + /// let response = client + /// .send_http_request(HttpMethod::Get, "/stats", None) + /// .await?; + /// // `response` is the raw JSON body, decode it however required. + /// # let _ = response; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// [`IggyError::FeatureUnavailable`] on the TCP, QUIC, and WebSocket + /// transports, which have no HTTP path. pub async fn send_http_request( &self, method: HttpMethod, diff --git a/core/sdk/src/lib.rs b/core/sdk/src/lib.rs index 50deca0e51..9aa2cb0cc8 100644 --- a/core/sdk/src/lib.rs +++ b/core/sdk/src/lib.rs @@ -15,6 +15,260 @@ // specific language governing permissions and limitations // under the License. +//! Apache Iggy is a high-performance, persistent message streaming platform written in Rust, +//! capable of processing millions of messages per second with ultra-low latency. +//! It is part of the [`Apache Incubating Program`] of the [`Apache Software Foundation`] (ASF). +//! +//! **This library is the Apache Iggy SDK.** +//! It exposes a low-level and a high-level API for the Apache Iggy message streaming infrastructure for the Rust programming language. +//! SDKs for other programming languages can be found in [`core/foreign`] of the root repository on GitHub. +//! Note, most of them wrap this SDK. Hence, newer features might be delayed in other languages. +//! +//! The core of the Iggy server is a persisted append-only log data structure. +//! It is concerned with allowing read and writes in the most efficient way. +//! Reading and writing to the server is the domain of this SDK. +//! The server exposes *commands* that can be triggered to change it's state. +//! These commands allow administrative tasks, such as handling users, permissions and setting up streams and topics +//! or writing and reading messages from the log. +//! A comprehensive overview of commands can be found in the [`schema spec`] on the website or checking the [`server command enum`] within the source code. +//! +//! The SDK provides tools to build production ready message-streaming applications. +//! It exposes its functionality at two levels. The [high-level API](#high-level-api) +//! is transport-agnostic and ships with the batching, retry, offset-tracking, and +//! connection-management machinery a production application needs. The +//! [low-level API](#low-level-api) is the set of concrete transport clients that +//! speak the wire protocol directly and that the high-level API is built on top of. +//! Start with the high-level API unless you have a specific reason not to. +//! +//! # High-level API +//! +//! The high-level API is most likely what you are looking for, especially if you are new to building +//! message-streaming applications with Iggy. +//! Clients provided by the high-level API already provide common message-streaming features, that +//! you would otherwise need to build yourself. +//! +//! # Choosing a high-level client +//! +//! There are three client types, layered from low to high level. They are not +//! alternatives to pick between so much as a control plane and two data-plane +//! helpers built on top of it. +//! +//! - [`IggyClient`] is the entry point and the full API surface. It owns the +//! connection and implements every domain trait, including [`MessageClient`] +//! with the raw [`send_messages`] and [`poll_messages`] primitives. Each call +//! is a single, stateless request: no batching, retries, offset tracking, or +//! polling loop. +//! - [`IggyProducer`] is a stateful helper for high-throughput sending, built on +//! [`send_messages`]. +//! - [`IggyConsumer`] is a stateful helper for continuous consumption, built on +//! [`poll_messages`]. +//! +//! You do not construct the producer and consumer independently. Spawn them +//! from an [`IggyClient`] with [`IggyClient::producer`] and +//! [`IggyClient::consumer`] so they share its connection. +//! +//! ## When to use each +//! +//! Reach for [`IggyClient`] directly for administrative tasks such as +//! creating streams, topics, users, and consumer groups, reading or storing +//! offsets, or sending and polling a handful of messages in a script. Anything +//! [`IggyClient`] cannot do, neither can the producer or consumer, since both +//! are built on to of it. +//! +//! Reach for [`IggyProducer`] and [`IggyConsumer`] when producing and consuming messages. +//! You could use the [`IggyClient`] for that, however the former two come with some +//! additional features already implemented that are frequentlly required when building messages +//! streaming applications. +//! +//! The [`IggyProducer`] adds, on top of [`send_messages`]: +//! +//! - **Background batching** that flushes by size, message count, or a linger +//! interval, instead of one network round-trip per send. +//! - **Retries** with a configurable count and interval (three attempts one +//! second apart by default). +//! - A pluggable **partitioning strategy**, so the target partition is not +//! passed on every call. +//! - **In-flight and ordering control**, optional payload **encryption**, and +//! `create_stream_if_not_exists` / `create_topic_if_not_exists` convenience. +//! +//! The [`IggyConsumer`] adds, on top of [`poll_messages`]: +//! +//! - A [`futures::Stream`] implementation, so a `while let Some(message) = +//! consumer.next().await` loop drives polling, paging, and the poll interval +//! for you. +//! - A **polling strategy** (`next`, `offset`, or `timestamp`) that tracks +//! position instead of taking an offset on every call. +//! - **Auto-commit** and offset storage on an interval or after a number of +//! messages, so a restart resumes where it left off. +//! - **Auto-join** of consumer groups, assignment refresh and reconnection +//! handling, and payload **decryption**. +//! +//! # Stream builder API +//! +//! The stream builder API is a convenient way to use the high-level API. +//! [`IggyStream`], [`IggyStreamProducer`], and +//! [`IggyStreamConsumer`] are construct everything at once. +//! You can hand them an [`IggyClient`] (or just a connection string) together with a config, +//! and they hand back a ready, connected [`IggyProducer`] / [`IggyConsumer`]. +//! Compared to the **high-level API**, it changes how you construct +//! producers and consumers, not what they can do. Instead of chaining an +//! [`IggyProducerBuilder`] / [`IggyConsumerBuilder`] and setting each option +//! with a method call, you describe the whole setup once in an +//! [`IggyStreamConfig`] and build from it. The result is +//! the same [`IggyProducer`] and [`IggyConsumer`] the builders produce, backed +//! by the same [`IggyClient`]. +//! +//! ## When to use it +//! +//! Reach for the stream builder when you want a producer and consumer wired up +//! with the least ceremony, especially a matched pair on one topic, or when you +//! prefer to keep stream configuration in one declarative object instead of +//! spread across imperative builder calls. When you need finer control over +//! construction, drop down to [`IggyClient::producer`] and +//! [`IggyClient::consumer`] and configure the builders directly. The stream +//! builder offers nothing the high-level API cannot, since it is built entirely +//! on top of it. +//! +//! # Low-level API +//! +//! The low-level API is the set of concrete transport clients: [`TcpClient`], +//! [`QuicClient`], [`WebSocketClient`], and [`HttpClient`]. Each one implements +//! [`Client`], the supertrait that pulls in every domain-specific trait, so a +//! transport client on its own can already drive the full server API. The +//! high-level [`IggyClient`] is one more layer over exactly these types. +//! Anything the high-level API can do therefore ends up going through a low-level client. +//! +//! ## Differences to the high-level API +//! +//! - **Transport is fixed at compile time.** You name a concrete type +//! ([`TcpClient`], [`QuicClient`], and so on) instead of configuring a +//! transport-agnostic [`IggyClient`]. Swapping transports means swapping the +//! type, not changing a connection-string scheme. +//! - **No managed connection.** [`IggyClient`] owns a shared connection and +//! spawns a heartbeat task to keep it alive. A transport client does neither. +//! You own the connection lifecycle and must ping the server +//! yourself if you want that liveness signal. +//! - **No producer or consumer helpers.** [`IggyProducer`] and [`IggyConsumer`] +//! are spawned from an [`IggyClient`], so a raw transport client gives you no +//! background batching, retries, polling loop, auto-commit, consumer-group +//! auto-join, or payload encryption. You get the request-response primitives +//! ([`send_messages`], [`poll_messages`]) and nothing layered on top. +//! - **Raw wire access.** [`BinaryTransport::send_raw_with_response`] sends an +//! arbitrary command code and payload and returns the raw response bytes. +//! The high-level equivalents are [`IggyClient::send_binary_request`] and +//! [`IggyClient::send_http_request`]. +//! Either way you need to know the server command codes and the wire format. +//! +//! ## When to use it +//! +//! Prefer the high-level API. Reach for the low-level API only when you need one +//! of the things it exposes that [`IggyClient`] deliberately hides: +//! +//! - You want to own the connection lifecycle yourself, with custom pooling, +//! supervision, or a different heartbeat strategy, rather than let +//! [`IggyClient`] manage it. +//! - You are building your own abstraction on top of the SDK, for example a +//! different producer or consumer, and want the unadorned primitives. +//! - You forked the server and need to issue a command the typed API does not recognize and want the +//! raw [`send_raw_with_response`](BinaryTransport::send_raw_with_response) instruction. +//! +//! If none of these apply, the high-level API gives you the same reach with far +//! less to get wrong. +//! +//! # Async runtime +//! +//! The SDK is async and runs on the [Tokio] runtime. Note, this is a hard +//! requirement rather not optional. The SDK uses [quinn] (for QUIC), [reqwest] (for HTTP), +//! [tokio-tungstenite] (for WebSocket) and [tokio-rustls] (for TLS) which all build on Tokio. +//! The SDK also spawns its own background work with [`tokio::spawn`] (the +//! [`IggyClient::connect`] heartbeat, and the [`IggyProducer`] and +//! [`IggyConsumer`] tasks) and drives timeouts, retries, and poll intervals with +//! [`tokio::time`]. Note that dropping to the low-level transport clients does +//! not change this. They spawn and time out on Tokio internally too. +//! Thus, everything you do with the Rust SDK must happen inside a Tokio runtime. +//! +//! ```no_run +//! use iggy::prelude::*; +//! use futures_util::StreamExt; +//! use std::error::Error; +//! use std::str::FromStr; +//! +//! // `#[tokio::main]` starts the runtime the SDK requires. +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let client = IggyClient::from_connection_string( +//! "iggy://iggy:iggy@localhost:8090", +//! )?; +//! client.connect().await?; +//! +//! let producer = client.producer("stream_name", "topic_name")?.build(); +//! producer.init().await?; +//! producer +//! .send(vec![IggyMessage::from_str("some_message_payload")?]) +//! .await?; +//! +//! let mut consumer = client.consumer("consumer_name", "stream_name", "topic_name", 1)?.build(); +//! consumer.init().await?; +//! while let Some(message) = consumer.next().await { +//! let _message = message?; +//! break; +//! } +//! +//! client.shutdown().await?; +//! Ok(()) +//! } +//! ``` +//! +//! [`IggyClient`]: crate::prelude::IggyClient +//! [`IggyClient::producer`]: crate::prelude::IggyClient::producer +//! [`IggyClient::consumer`]: crate::prelude::IggyClient::consumer +//! [`IggyProducer`]: crate::prelude::IggyProducer +//! [`IggyConsumer`]: crate::prelude::IggyConsumer +//! [`MessageClient`]: crate::prelude::MessageClient +//! [`send_messages`]: crate::prelude::MessageClient::send_messages +//! [`poll_messages`]: crate::prelude::MessageClient::poll_messages +//! [`futures::Stream`]: https://docs.rs/futures/latest/futures/stream/trait.Stream.html +//! [`TcpClient`]: crate::prelude::TcpClient +//! [`QuicClient`]: crate::quic::quic_client::QuicClient +//! [`WebSocketClient`]: crate::prelude::WebSocketClient +//! [`HttpClient`]: crate::http::http_client::HttpClient +//! [`Client`]: crate::prelude::Client +//! [`StreamClient`]: crate::prelude::StreamClient +//! [`TopicClient`]: crate::prelude::TopicClient +//! [`ClientWrapper`]: crate::prelude::ClientWrapper +//! [`IggyRwLock`]: crate::prelude::locking::IggyRwLock +//! [`BinaryTransport`]: crate::binary::BinaryTransport +//! [`BinaryTransport::send_raw_with_response`]: crate::binary::BinaryTransport::send_raw_with_response +//! [`IggyClient::send_binary_request`]: crate::prelude::IggyClient::send_binary_request +//! [`IggyClient::send_http_request`]: crate::prelude::IggyClient::send_http_request +//! [`IggyStream`]: crate::prelude::IggyStream +//! [`IggyStream::build`]: crate::prelude::IggyStream::build +//! [`IggyStream::with_client_from_connection_string`]: crate::prelude::IggyStream::with_client_from_connection_string +//! [`IggyStreamProducer`]: crate::prelude::IggyStreamProducer +//! [`IggyStreamConsumer`]: crate::prelude::IggyStreamConsumer +//! [`IggyStreamConfig`]: crate::prelude::IggyStreamConfig +//! [`IggyProducerConfig`]: crate::prelude::IggyProducerConfig +//! [`IggyConsumerConfig`]: crate::prelude::IggyConsumerConfig +//! [`IggyProducerBuilder`]: crate::prelude::IggyProducerBuilder +//! [`IggyConsumerBuilder`]: crate::prelude::IggyConsumerBuilder +//! [`IggyClient::connect`]: crate::prelude::Client::connect +//! [`IggyError`]: crate::prelude::IggyError +//! +//! [Tokio]: https://tokio.rs +//! [`tokio::spawn`]: https://docs.rs/tokio/latest/tokio/task/fn.spawn.html +//! [`tokio::time`]: https://docs.rs/tokio/latest/tokio/time/index.html +//! [quinn]: https://docs.rs/quinn +//! [reqwest]: https://docs.rs/reqwest +//! [tokio-tungstenite]: https://docs.rs/tokio-tungstenite +//! [tokio-rustls]: https://docs.rs/tokio-rustls +//! [`StreamExt`]: https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html +//! +//! [`Apache Incubating Program`]: https://incubator.apache.org/ +//! [`Apache Software Foundation`]: https://www.apache.org/ +//! [`core/foreign`]: https://github.com/apache/iggy/tree/master/foreign +//! [`schema spec`]: https://iggy.apache.org/docs/server/schema/ +//! [`server command enum`]: https://github.com/apache/iggy/blob/3e27ebc8dd5dbf257b816993908dc0747c4f8849/core/server/src/binary/command.rs#L74 +//! [`website`]: https://iggy.apache.org/docs/introduction/architecture/ pub mod binary; pub mod client_provider; pub mod client_wrappers;