ryerraguntla commented on code in PR #4043:
URL: https://github.com/apache/iggy/pull/4043#discussion_r4001457302


##########
gateways/kafka/src/bridge/iggy_bridge.rs:
##########
@@ -0,0 +1,381 @@
+// 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 std::time::Duration;
+
+use iggy::prelude::{
+    AutoLogin, Client, Credentials, Identifier, IggyClient, IggyClientBuilder, 
IggyError,
+    StreamClient, TopicClient, TopicCreateOptions,
+};
+use tracing::{debug, info};
+
+use crate::bridge::config::IggyBridgeConfig;
+use crate::bridge::error::BridgeError;
+
+/// Passes attempted, after the first, before [`IggyBridge::connect`] gives up 
and returns `Err`.
+///
+/// Not the SDK's own default (`TcpClientReconnectionConfig::default()` is 
`max_retries: None` -
+/// unlimited, one dial per second, forever). A Kafka client already retries 
at the wire-protocol
+/// level once a handler maps a bridge failure to a retriable error code; the 
bridge blocking a
+/// request task inside an unbounded internal reconnect loop would just add a 
second, invisible
+/// retry layer underneath that one instead of surfacing the failure so the 
mapped code can be
+/// sent.
+///
+/// This bounds the *count*, not the *wall-clock time*, of that inner retry 
loop - see
+/// [`CONNECT_TIMEOUT`] for the latter.
+const RECONNECTION_RETRIES: u32 = 3;
+
+/// Wall-clock ceiling on the whole `client.connect()` call in 
[`IggyBridge::connect`], including
+/// every attempt [`RECONNECTION_RETRIES`] makes internally.
+///
+/// Without this, an unreachable-but-not-refusing address hangs far longer 
than "a few seconds":
+/// `TcpClient::establish_bounded` only applies its own 
`FAILOVER_DIAL_TIMEOUT` (2s) when at least
+/// two failover candidates are configured (`tcp_client.rs`) - a bridge always 
configures exactly
+/// one address, so that guard never engages, and the plain 
`TcpStream::connect` underneath has no
+/// deadline of its own. Against a firewall that drops SYN packets instead of 
refusing them, each
+/// of the up to `RECONNECTION_RETRIES + 1` dial attempts pays the kernel's 
own SYN-retry timeout
+/// (minutes, not seconds) rather than the `reconnection_interval` between 
attempts - a closed port
+/// (instant RST) never exercises this path, so the failure mode only shows up 
in production.
+/// 15s comfortably covers a slow-but-alive server's handshake (well above p99 
login latency) while
+/// still failing well short of the pathological multi-minute case.
+const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
+
+/// Owns one connected `IggyClient` and resolves Kafka topics against it.
+///
+/// Produce/Fetch handler wiring is a separate, later change (`#3535`/`#3536`) 
- this type is the
+/// shared plumbing those handlers will call into, exercised standalone here 
via its own tests and
+/// an integration test against a real `iggy-server`.
+pub struct IggyBridge {
+    client: IggyClient,
+    config: IggyBridgeConfig,
+}
+
+impl IggyBridge {
+    /// Connects to Iggy using `config` and authenticates.
+    ///
+    /// Builds the client through the SDK's fluent TCP builder rather than 
hand-assembling an
+    /// `iggy://user:pass@host` connection string: that string format splits 
on `@` then `:`, so a
+    /// password containing either character (`p@ss:word`) would be misparsed 
into a garbled
+    /// address instead of failing with a diagnosable config error. The fluent 
builder passes
+    /// `username`/`password` as already-separated fields, sidestepping the 
ambiguity entirely.
+    ///
+    /// # Errors
+    ///
+    /// Returns [`BridgeError::InvalidConfig`] if `config.address` is empty. 
Returns
+    /// [`BridgeError::Iggy`] if the address is malformed, the TCP connection 
fails, connecting
+    /// takes longer than `CONNECT_TIMEOUT` (an 
unreachable-and-silently-dropping address, not
+    /// just a refused one, is covered - see that constant's doc), or 
authentication is rejected -
+    /// this is the boundary [`BridgeError::to_kafka_error_code`] exists for: 
a handler calling
+    /// this must map the error to a wire response, never panic or unwrap, 
since an unreachable
+    /// Iggy backend is an expected runtime condition, not a bug.
+    pub async fn connect(config: IggyBridgeConfig) -> Result<Self, 
BridgeError> {
+        if config.address.trim().is_empty() {
+            return Err(BridgeError::InvalidConfig(
+                "Iggy address must not be empty".to_string(),
+            ));
+        }
+
+        let credentials =
+            Credentials::UsernamePassword(config.username.clone(), 
config.password.clone());
+        let client = IggyClientBuilder::new()
+            .with_tcp()
+            .with_server_address(config.address.clone())
+            .with_auto_sign_in(AutoLogin::Enabled(credentials))
+            .with_reconnection_max_retries(Some(RECONNECTION_RETRIES))
+            .build()
+            .map_err(BridgeError::Iggy)?;
+        tokio::time::timeout(CONNECT_TIMEOUT, client.connect())
+            .await
+            .map_err(|_elapsed| 
BridgeError::Iggy(IggyError::CannotEstablishConnection))?
+            .map_err(BridgeError::Iggy)?;
+        info!("Iggy bridge connected to {}", config.address);
+
+        Ok(Self { client, config })
+    }
+
+    /// Tears down the underlying Iggy client, including its background 
heartbeat task.
+    ///
+    /// Not `IggyClient::disconnect`: that only tears down the transport
+    /// (`TcpClient::disconnect_transport`) and never touches 
`heartbeat_handle` - only
+    /// `IggyClient`'s own `Drop` aborts that task (`client.rs`). A 
`disconnect`ed-but-not-dropped
+    /// bridge would keep heartbeating on a schedule, hit `NotConnected` 
(itself in the SDK's
+    /// retriable set), and reconnect plus re-authenticate using the 
credentials `connect`
+    /// configured, so the "closed" client silently comes back. `shutdown` sets
+    /// `ClientState::Shutdown`, which the heartbeat loop's next `ping` 
observes as
+    /// `IggyError::ClientShutdown` and self-terminates on, and which 
`sign_in_credentials` never
+    /// dials past.
+    ///
+    /// Takes `self` by value: `shutdown` is terminal (no reconnect is coming 
back from it), so
+    /// nothing legitimate is left to call on this bridge afterward. This does 
mean a bridge shared
+    /// via `Arc` cannot call this directly (`Arc::try_unwrap` first) - not a 
concern before
+    /// `#3535`/`#3536` wire an owning caller in.
+    ///
+    /// # Errors
+    ///
+    /// Returns [`BridgeError::Iggy`] if the underlying client reports a 
shutdown failure (e.g. the
+    /// socket was already in a state that rejects a clean shutdown).
+    pub async fn close(self) -> Result<(), BridgeError> {
+        self.client.shutdown().await.map_err(BridgeError::Iggy)
+    }
+
+    /// Ensures the Iggy stream and topic backing `kafka_topic` exist, 
creating either or both if
+    /// missing. Resolves `kafka_topic` through the configured 
[`TopicMapping`](crate::bridge::topic_map::TopicMapping).
+    ///
+    /// Idempotent when repeated with the *same* `partition_count`: a `get` 
before each `create`
+    /// means calling this twice for the same topic is a no-op the second 
time, and a
+    /// `NameAlreadyExists` race from a concurrent caller creating the same 
stream/topic between
+    /// this call's `get` and `create` is treated as success, not an error - 
the desired end state
+    /// (it exists) is what idempotency actually promises, not that this call 
was the one that
+    /// created it. A *different* `partition_count` against an 
already-existing topic is not
+    /// idempotent - see [`BridgeError::PartitionCountMismatch`].
+    ///
+    /// # Errors
+    ///
+    /// Returns [`BridgeError::Iggy`] for connectivity/auth/invalid-name 
failures. Returns
+    /// [`BridgeError::PartitionCountMismatch`] if the topic already exists 
with a different
+    /// partition count than `partition_count`.
+    pub async fn ensure_stream_and_topic(

Review Comment:
   **All four addressed.** (1) kafka_topic now validated in both 
ensure_stream_and_topic and high_watermarks. (2) left partition_count == 0 as 
documented defense-in-depth, per your own framing. (3) documented 
once-per-topic calling instead of adding a cache - a cache would break the 
delete-and-recreate idempotency test, exactly as you'd expect. (4) the -1 
sentinel is flagged as a deferred decision in the doc comment rather than 
resolved here, since nothing calls this with it yet.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to