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


##########
gateways/kafka/src/protocol/handlers/create_topics.rs:
##########
@@ -17,44 +17,394 @@
 
 //! `CreateTopics` (API key 19).
 
+use std::collections::HashSet;
+use std::time::Duration;
+
 use bytes::Bytes;
-use kafka_protocol::messages::create_topics_request::CreatableTopic;
+use iggy::prelude::IggyError;
+use 
kafka_protocol::messages::create_topics_request::{CreatableReplicaAssignment, 
CreatableTopic};
 use kafka_protocol::messages::create_topics_response::CreatableTopicResult;
-use kafka_protocol::messages::{CreateTopicsRequest, CreateTopicsResponse};
+use kafka_protocol::messages::{BrokerId, CreateTopicsRequest, 
CreateTopicsResponse, TopicName};
+use kafka_protocol::protocol::StrBytes;
 
+use crate::bridge::{BridgeError, IggyBridge, TopicCreationOutcome};
 use crate::error::Result;
 use crate::protocol::api::{
-    API_KEY_CREATE_TOPICS, ApiVersionRange, ERROR_INVALID_PARTITIONS,
-    ERROR_INVALID_REPLICATION_FACTOR, ERROR_NONE, ERROR_NOT_CONTROLLER, 
GatewayState,
-    HandleOutcome,
+    API_KEY_CREATE_TOPICS, ApiVersionRange, ERROR_INVALID_CONFIG, 
ERROR_INVALID_PARTITIONS,
+    ERROR_INVALID_REPLICA_ASSIGNMENT, ERROR_INVALID_REPLICATION_FACTOR, 
ERROR_INVALID_REQUEST,
+    ERROR_NONE, ERROR_NOT_CONTROLLER, ERROR_REQUEST_TIMED_OUT, 
ERROR_TOPIC_ALREADY_EXISTS,
+    GatewayState, HandleOutcome,
 };
 use crate::protocol::bounds_guard::validate_create_topics_shape;
-use crate::protocol::handlers::{decode_guarded, encode_message, 
handle_versioned_request};
+use crate::protocol::handlers::{
+    decode_guarded, encode_message, handle_versioned_request, 
is_supported_version,
+    respond_or_close, unsupported_version_response,
+};
 
 pub const RANGE: ApiVersionRange = ApiVersionRange {
     api_key: API_KEY_CREATE_TOPICS,
     min_version: 2,
     max_version: 5,
 };
 
-#[expect(
-    clippy::unused_async,
-    reason = "the shared handler signature, kept until a handler awaits the 
bridge"
-)]
+/// KIP-464 `num_partitions = -1` with no manual assignment: the count this 
bridge creates.
+///
+/// Matches real Kafka's own out-of-box `num.partitions=1` broker default. Not 
read from any
+/// bridge config - there is no such config surface today.
+const DEFAULT_PARTITION_COUNT: u32 = 1;
+
+/// Cap on distinct topic names one `CreateTopics` request may address through 
the bridge.
+///
+/// `bounds_guard`'s `MAX_REQUEST_ELEMENTS` (4,096) is a pre-decode `DoS` 
ceiling, not a usability
+/// recommendation: each non-duplicate requested name here costs up to ~4 Iggy 
round trips
+/// (`ensure_stream` + `create_topic`, plus a possible race-retry read on 
either) against the
+/// single lockstep `IggyClient` every Kafka connection on this gateway shares
+/// (`bridge/iggy_bridge/mod.rs`'s "Concurrency ceiling"). 100 keeps a 
worst-case batch's
+/// aggregate bridge cost small relative to that shared resource while 
remaining generous for any
+/// real admin batch. Duplicate names never count against this cap - they're 
rejected by
+/// [`find_duplicate_names`] before ever reaching the bridge.
+const MAX_BRIDGE_BACKED_TOPICS: usize = 100;
+
+/// Bounds imposed on the request's own `timeout_ms` before it becomes the 
aggregate bridge-work
+/// deadline. That value is client-supplied and otherwise unchecked: `0` or 
negative would abort
+/// every topic on arrival, and an oversized one would tie up the shared 
`IggyClient` past any
+/// reasonable request.
+const MIN_REQUEST_TIMEOUT: Duration = Duration::from_millis(1_000);
+const MAX_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
+
+/// Clamps the wire's own `timeout_ms` (KIP-4's field for exactly this) into
+/// `[MIN_REQUEST_TIMEOUT, MAX_REQUEST_TIMEOUT]` - unlike 
`ListOffsets`/`Metadata`, `CreateTopics`
+/// carries a real client-supplied deadline to honor, not just a fixed 
internal ceiling.
+fn clamp_request_timeout(timeout_ms: i32) -> Duration {
+    let requested = 
Duration::from_millis(u64::try_from(timeout_ms).unwrap_or(0));
+    requested.clamp(MIN_REQUEST_TIMEOUT, MAX_REQUEST_TIMEOUT)
+}
+
 pub async fn handle(state: &GatewayState, api_version: i16, body: Bytes) -> 
HandleOutcome {
-    handle_versioned_request(
-        API_KEY_CREATE_TOPICS,
-        api_version,
-        body,
-        |v, b| {
-            decode_guarded::<CreateTopicsRequest>(v, b, |v, b| {
-                validate_create_topics_shape(v, b, state.max_frame_size)
+    let Some(bridge) = &state.bridge else {
+        return handle_versioned_request(
+            API_KEY_CREATE_TOPICS,
+            api_version,
+            body,
+            |v, b| {
+                decode_guarded::<CreateTopicsRequest>(v, b, |v, b| {
+                    validate_create_topics_shape(v, b, state.max_frame_size)
+                })
+            },
+            encode_response,
+            encode_error_response,
+            "CreateTopics",
+        );
+    };
+
+    if !is_supported_version(API_KEY_CREATE_TOPICS, api_version) {
+        return unsupported_version_response(API_KEY_CREATE_TOPICS, 
api_version, |version| {
+            encode_error_response(version, ERROR_INVALID_REQUEST)
+        });
+    }
+
+    let req = match decode_guarded::<CreateTopicsRequest>(api_version, body, 
|v, b| {
+        validate_create_topics_shape(v, b, state.max_frame_size)
+    }) {
+        Ok(req) => req,
+        Err(error) => {
+            // debug!, not warn!: attacker-controlled, not operator-actionable.
+            tracing::debug!(%error, "Failed to decode CreateTopics request");
+            return respond_or_close(
+                encode_error_response(api_version, ERROR_INVALID_REQUEST),
+                "CreateTopics",
+            );
+        }
+    };
+
+    let duplicate_names = find_duplicate_names(&req.topics);
+
+    let distinct_bridge_backed: HashSet<&TopicName> = req
+        .topics
+        .iter()
+        .map(|topic| &topic.name)
+        .filter(|name| !duplicate_names.contains(*name))
+        .collect();
+    if distinct_bridge_backed.len() > MAX_BRIDGE_BACKED_TOPICS {
+        tracing::warn!(
+            distinct_topics = distinct_bridge_backed.len(),
+            max = MAX_BRIDGE_BACKED_TOPICS,
+            "CreateTopics request addresses too many distinct topics; 
rejecting"
+        );
+        let results = req
+            .topics
+            .iter()
+            .map(|topic| {
+                CreatableTopicResult::default()
+                    .with_name(topic.name.clone())
+                    .with_error_code(ERROR_INVALID_REQUEST)
             })
-        },
-        encode_response,
-        encode_error_response,
-        "CreateTopics",
+            .collect();
+        let resp = CreateTopicsResponse::default().with_topics(results);
+        return respond_or_close(encode_message(&resp, api_version, 256), 
"CreateTopics");
+    }
+
+    let deadline = clamp_request_timeout(req.timeout_ms);
+    let results = match tokio::time::timeout(

Review Comment:
   CreateTopics/Metadata named-lookup now bound each bridge call individually 
(timeout_at against one shared deadline), keeping already-resolved results 
instead of discarding the whole
     batch. All-topics list_kafka_topics() now has a deadline where it had none.



-- 
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