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


##########
gateways/kafka/src/protocol/api.rs:
##########
@@ -0,0 +1,558 @@
+// 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 bytes::{Buf, Bytes};
+use kafka_protocol::messages::api_versions_response::ApiVersion;
+use kafka_protocol::messages::metadata_response::{MetadataResponseBroker, 
MetadataResponseTopic};
+use kafka_protocol::messages::{
+    ApiVersionsRequest, ApiVersionsResponse, BrokerId, CreateTopicsRequest, 
FetchRequest,
+    ListOffsetsRequest, MetadataRequest, MetadataResponse, ProduceRequest, 
TopicName,
+};
+use kafka_protocol::protocol::{Decodable, StrBytes};
+
+use crate::error::{KafkaProtocolError, Result};
+use crate::protocol::responses::{
+    encode_create_topics_error_response, encode_create_topics_response,
+    encode_fetch_error_response, encode_fetch_response, 
encode_list_offsets_error_response,
+    encode_list_offsets_response, encode_message, 
encode_produce_error_response,
+    encode_produce_response,
+};
+
+pub const API_KEY_PRODUCE: i16 = 0;
+pub const API_KEY_FETCH: i16 = 1;
+pub const API_KEY_LIST_OFFSETS: i16 = 2;
+pub const API_KEY_METADATA: i16 = 3;
+pub const API_KEY_API_VERSIONS: i16 = 18;
+pub const API_KEY_CREATE_TOPICS: i16 = 19;
+
+pub const DEFAULT_KAFKA_PORT: u16 = 9093;
+
+pub const ERROR_NONE: i16 = 0;
+pub const ERROR_UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
+/// Retriable; Produce stub uses this until the Iggy bridge persists records.
+pub const ERROR_NOT_LEADER_OR_FOLLOWER: i16 = 6;
+pub const ERROR_UNSUPPORTED_VERSION: i16 = 35;
+pub const ERROR_INVALID_PARTITIONS: i16 = 37;
+pub const ERROR_INVALID_REPLICATION_FACTOR: i16 = 38;
+/// `CreateTopics` stub: do not claim topics were created (no controller / no 
Iggy bridge).
+pub const ERROR_NOT_CONTROLLER: i16 = 41;
+pub const ERROR_INVALID_REQUEST: i16 = 42;
+
+/// Result of handling one Kafka request body.
+#[derive(Debug)]
+pub enum HandleOutcome {
+    /// Write this response body (with a response header).
+    Respond(Bytes),
+    /// Produce with `acks=0`: write nothing, keep the connection open.
+    NoResponse,
+    /// No parseable response exists for this request; close the TCP 
connection.
+    Close,
+}
+
+impl HandleOutcome {
+    /// Return the response body, or panic with `msg` if the outcome is not 
[`Self::Respond`].
+    ///
+    /// # Panics
+    ///
+    /// Panics when the outcome is [`Self::NoResponse`] or [`Self::Close`].
+    #[must_use]
+    pub fn expect_response(self, msg: &str) -> Bytes {
+        match self {
+            Self::Respond(body) => body,
+            Self::NoResponse => panic!("{msg}: got NoResponse"),
+            Self::Close => panic!("{msg}: got Close"),
+        }
+    }
+
+    #[must_use]
+    pub const fn is_no_response(&self) -> bool {
+        matches!(self, Self::NoResponse)
+    }
+
+    #[must_use]
+    pub const fn is_close(&self) -> bool {
+        matches!(self, Self::Close)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct BrokerAdvertise {
+    pub host: String,
+    pub port: i32,
+}
+
+impl Default for BrokerAdvertise {
+    fn default() -> Self {
+        Self {
+            host: "127.0.0.1".to_string(),
+            port: i32::from(DEFAULT_KAFKA_PORT),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct ApiVersionRange {
+    pub api_key: i16,
+    pub min_version: i16,
+    pub max_version: i16,
+}
+
+static SUPPORTED_RANGES: &[ApiVersionRange] = &[
+    ApiVersionRange {
+        api_key: API_KEY_PRODUCE,
+        min_version: 3,
+        max_version: 9,
+    },
+    ApiVersionRange {
+        api_key: API_KEY_FETCH,
+        min_version: 4,
+        max_version: 12,
+    },
+    ApiVersionRange {
+        api_key: API_KEY_LIST_OFFSETS,
+        min_version: 1,
+        max_version: 6,
+    },
+    ApiVersionRange {
+        api_key: API_KEY_METADATA,
+        min_version: 0,
+        max_version: 9,
+    },
+    ApiVersionRange {
+        api_key: API_KEY_API_VERSIONS,
+        min_version: 0,
+        max_version: 3,
+    },
+    ApiVersionRange {
+        api_key: API_KEY_CREATE_TOPICS,
+        min_version: 2,
+        max_version: 5,
+    },
+];
+
+#[must_use]
+pub fn supported_api_ranges() -> &'static [ApiVersionRange] {
+    SUPPORTED_RANGES
+}
+
+/// Handles one decoded request frame and returns how the connection should 
proceed.
+pub fn handle_request(
+    api_key: i16,
+    api_version: i16,
+    body: Bytes,
+    broker: &BrokerAdvertise,
+) -> HandleOutcome {
+    if api_key == API_KEY_PRODUCE {
+        return handle_produce_request(api_version, body);
+    }
+    handle_other_request(api_key, api_version, body, broker)
+}
+
+/// Decode `T` from the whole request body and reject unconsumed trailing 
bytes.
+///
+/// `kafka_protocol`'s `Decodable` stops once it has read the fields its 
schema defines; it does
+/// not know (or care) whether the caller handed it an exact-length body, so 
the trailing-bytes
+/// check has to live here.
+fn decode_exhaustive<T: Decodable>(version: i16, mut body: Bytes) -> Result<T> 
{
+    let value =
+        T::decode(&mut body, version).map_err(|e| 
KafkaProtocolError::Malformed(e.to_string()))?;
+    if body.has_remaining() {
+        return Err(KafkaProtocolError::Malformed(
+            "unexpected trailing bytes in request body".to_string(),
+        ));
+    }
+    Ok(value)
+}
+
+/// Turn an encode [`Result`] into a [`HandleOutcome`], closing the connection 
when encoding
+/// fails rather than propagating - there is no parseable response to send in 
that case.
+fn respond_or_close(result: Result<Bytes>, api_name: &str) -> HandleOutcome {
+    match result {
+        Ok(body) => HandleOutcome::Respond(body),
+        Err(error) => {
+            tracing::warn!(%error, "failed to encode {api_name} response; 
closing connection");
+            HandleOutcome::Close
+        }
+    }
+}
+
+/// Produce is the only request the wire protocol allows to go unanswered
+/// (`acks=0`), so it gets its own path that may return 
[`HandleOutcome::NoResponse`].
+///
+/// The firewall check runs AFTER decoding the request, not before: 
`ApiVersions` advertises
+/// Produce min=0 (see [`advertised_min_version`]) while the firewall's real 
floor is 3, so a
+/// spec-compliant client can legitimately send Produce v0-2 with `acks=0`. 
Rejecting those
+/// versions before reading `acks` would send an error response the client 
never expects,
+/// desyncing the next correlation id it reads.
+fn handle_produce_request(api_version: i16, body: Bytes) -> HandleOutcome {
+    // Above the encoder max there is no response parseable at the client's 
version, so close
+    // rather than decode (same policy the other APIs apply).
+    if api_version > 
supported_max_version(API_KEY_PRODUCE).unwrap_or(i16::MAX) {
+        return HandleOutcome::Close;
+    }
+    // `kafka_protocol`'s ProduceRequest/ProduceResponse schemas only go back 
to v3, so v0-2
+    // (still advertised as the min in ApiVersions per KAFKA-18659) can be 
neither decoded nor
+    // encoded by the crate - there is no parseable response at these versions 
regardless of
+    // body content. `acks` is always the first i16 on the wire there 
(`transactional_id` was
+    // added in v3), so it's peeked by hand: acks=0 must keep the connection 
open per the wire
+    // protocol's fire-and-forget rule even though no response can ever be 
encoded for it.
+    if api_version < 3 {
+        let acks = match body.get(0..2) {
+            Some(&[hi, lo]) => Some(i16::from_be_bytes([hi, lo])),
+            _ => None,
+        };
+        return match acks {
+            Some(0) | None => HandleOutcome::NoResponse,
+            Some(_) => unsupported_version_response(API_KEY_PRODUCE, 
api_version, |v| {
+                encode_produce_error_response(v, ERROR_UNSUPPORTED_VERSION)
+            }),
+        };
+    }
+    match decode_exhaustive::<ProduceRequest>(api_version, body) {
+        // acks=0 is fire-and-forget: the client isn't reading a response, so
+        // sending one desyncs the next correlation id it expects.
+        Ok(req) if req.acks == 0 => HandleOutcome::NoResponse,
+        Ok(req) => {
+            if !is_supported_version(API_KEY_PRODUCE, api_version) {
+                return unsupported_version_response(API_KEY_PRODUCE, 
api_version, |v| {
+                    encode_produce_error_response(v, ERROR_UNSUPPORTED_VERSION)
+                });
+            }

Review Comment:
   The dead if !is_supported_version(API_KEY_PRODUCE, api_version) { ... } 
guard inside the Ok(req) => arm is removed — you're right it could never fire: 
api_version > max closes at the top of the  function, api_version < 3 returns 
early above, so by the time this arm runs api_version is provably within [3, 
supported_max], i.e. always supported. Replaced with a comment (api.rs:254-256) 
stating that  invariant explicitly, so a future edit to the version-bounds 
logic above has something to falsify instead of a redundant re-check that reads 
as meaningful but isn't.



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