ryerraguntla commented on code in PR #4245:
URL: https://github.com/apache/iggy/pull/4245#discussion_r4088969551
##########
gateways/kafka/src/auth.rs:
##########
@@ -241,12 +297,35 @@ impl SaslAuthenticator for IggyAuthenticator {
let client = builder.build().map_err(|error| classify(&error))?;
let connected = tokio::time::timeout(VERIFY_TIMEOUT,
client.connect()).await;
+ // The password verify the slot bounds is done. Holding it across the
permission read and
+ // teardown kept a slot busy for up to two more seconds per login, and
after a timed-out
+ // read for as long as the SDK's detached request still ran.
+ drop(slot);
let outcome = match connected {
Err(_elapsed) => Err(AuthError::Unavailable),
Ok(Err(error)) => Err(classify(&error)),
- Ok(Ok(())) => Ok(()),
+ // The login already proved the credentials. Reading the
principal's own record on the
+ // same session is the one extra round trip that lets
`DescribeAcls` answer later
+ // without a second login or a stored password. A user may always
read itself, with no
+ // permission required (`dispatch/authz.rs` exempts a
self-targeted read), so this
+ // cannot fail for want of a grant.
+ Ok(Ok(())) => fetch_permissions(&client,
&credentials.username).await,
};
+ // A timed-out permission read leaves the SDK's detached request task
holding the stream
+ // lock until its own response deadline, and `shutdown` needs that
lock first, so waiting
+ // on it could only burn `TEARDOWN_TIMEOUT` of the caller's
pre-authentication budget.
+ // Dropping the client instead still aborts the heartbeat, which is
the part that would
+ // reconnect. The socket itself lives on in that task until its
deadline, which nothing on
+ // this side of the SDK can shorten.
+ if matches!(outcome, Ok((_, PermissionRead::TimedOut))) {
Review Comment:
TimedOut get_user skips shutdown. Heartbeat ping() first (client.rs:971)
locksteps same stream. 1s cancel leaves SDK spawn (tcp_client.rs:1512) on
stream.lock() until RESPONSE_READ_TIMEOUT 30s. drop(slot) already :303. Iggy
TCP + client-table zombie, unbounded vs 16 auth slots. **Fix**: fetch before
heartbeat; do not timeout-cancel spawned lockstep read.
##########
gateways/kafka/tests/kafka_client_e2e_tests.rs:
##########
@@ -0,0 +1,702 @@
+// 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.
+
+//! End-to-end tests driving **real Kafka clients** against the gateway.
+//!
+//! Every other suite in this crate hand-builds wire frames, which cannot
catch a client
+//! compatibility problem by construction. One already slipped through that
way: advertising
+//! `SaslHandshake` from v1 rather than v0 passed every hand-built test and
made librdkafka report
+//! "SASL Handshake not supported by broker" before it sent anything. These
tests exist so that
+//! class of bug fails in CI instead of during manual testing.
+//!
+//! The stack is real on all three sides: a spawned `iggy-server` process, the
gateway in-process
+//! with a real `IggyAuthenticator`, and a client from a container. They
automate categories S and T
+//! of `docs/MANUAL_TESTING.md`.
+//!
+//! Prerequisites are Docker and an already-built `iggy-server`. Missing
either skips, the way the
+//! wire-fixture suites do, unless `KAFKA_E2E_REQUIRED=1` is set, which turns
a skip into a failure
+//! so a broken CI step cannot leave these silently green.
+
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::process::{Child, Command, Stdio};
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+
+use iggy_gateway_kafka::GatewayConfig;
+use iggy_gateway_kafka::auth::IggyAuthenticator;
+
+#[path = "common/server.rs"]
+mod server;
+
+use server::spawn_test_server_with_authenticator;
+
+const KCAT_IMAGE: &str = "edenhill/kcat:1.7.1";
+const KAFKA_IMAGE: &str = "apache/kafka:3.9.0";
+const ROOT_USER: &str = "iggy";
+const ROOT_PASSWORD: &str = "iggy";
+/// Password for every non-root principal these tests create.
+const USER_PASSWORD: &str = "s3cretpass";
+
+/// Budget for `iggy-server` to start listening. Generous: it is a cold
process start, and a debug
+/// build on a loaded machine is not quick.
+const SERVER_READY_TIMEOUT: Duration = Duration::from_secs(45);
+
+/// Wall-clock cap on one client container. The ACL test runs five in a row
inside nextest's 300s
+/// kill budget, so a single wedged client must fail on its own terms, well
before that budget
+/// kills the test and hides which step hung.
+const CLIENT_RUN_TIMEOUT: &str = "45s";
+
+/// Per-request and per-call budget for the Java admin tools. Their defaults
(30s and 60s) let one
+/// unanswered request eat most of `CLIENT_RUN_TIMEOUT` retrying.
+const JAVA_CLIENT_TIMEOUT_MS: u32 = 10_000;
+
+/// Reports why the suite cannot run, and whether that is fatal.
+///
+/// Returns `true` when the caller should skip. `KAFKA_E2E_REQUIRED=1` makes
it panic instead, so a
+/// CI job that means to run these fails loudly rather than reporting a pass
over zero assertions.
+fn skip(reason: &str) -> bool {
+ assert!(
+ std::env::var("KAFKA_E2E_REQUIRED").as_deref() != Ok("1"),
+ "KAFKA_E2E_REQUIRED=1 but the suite cannot run: {reason}"
+ );
+ eprintln!("skipping real-client end-to-end test: {reason}");
+ true
+}
+
+fn docker_missing() -> bool {
+ let available = Command::new("docker")
+ .arg("info")
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()
+ .is_ok_and(|status| status.success());
+ if available {
+ false
+ } else {
+ skip("docker is unavailable")
+ }
+}
+
+/// Whether `image` is already in the local Docker store.
+///
+/// Containers run with `--pull never`, so a missing image would otherwise
surface as a client that
+/// failed to start, reported against whichever feature that test happened to
cover.
+fn image_present(image: &str) -> bool {
+ Command::new("docker")
+ .args(["image", "inspect", image])
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()
+ .is_ok_and(|status| status.success())
+}
+
+/// Locates the already-built `iggy-server` alongside this test binary. Does
not build it.
+fn iggy_server_binary() -> Option<PathBuf> {
+ let mut dir = std::env::current_exe().ok()?;
+ // .../target/<profile>/deps/<test binary> -> .../target/<profile>
+ dir.pop();
+ dir.pop();
+ let candidate = dir.join(format!("iggy-server{}",
std::env::consts::EXE_SUFFIX));
+ candidate.is_file().then_some(candidate)
+}
+
+/// Outcome of waiting for the spawned server to come up.
+enum Ready {
+ Listening,
+ /// The process exited before it bound, carrying its status.
+ Exited(String),
+ TimedOut,
+}
+
+/// A spawned `iggy-server`, killed on drop.
+///
+/// The environment recipe mirrors `bridge_iggy_integration_tests.rs`, which
explains each setting:
+/// the other listeners are off so concurrently spawned servers do not fight
over their fixed
+/// ports, and the shard pool is capped so a spawned server does not size
itself to the whole
+/// machine against unrelated packages' tests in the same run.
+struct TestServer {
+ child: Child,
+ address: String,
+ http_address: String,
+ _data_dir: tempfile::TempDir,
+}
+
+impl TestServer {
+ fn spawn() -> Result<Self, String> {
+ let Some(binary) = iggy_server_binary() else {
+ return Err(
+ "iggy-server is not built; run `cargo build --bin iggy-server`
first".to_string(),
+ );
+ };
+ let data_dir = tempfile::tempdir().expect("create server data dir");
+ let port = free_port();
+ let address = format!("127.0.0.1:{port}");
+ let http_address = format!("127.0.0.1:{}", free_port());
+
+ let child = Command::new(binary)
+ .arg("--fresh")
+ .env("IGGY_PATH", data_dir.path().display().to_string())
+ .env("IGGY_TCP_ADDRESS", &address)
+ // Left on, unlike the sibling suite: the three-principal
procedure creates its
+ // non-root users over the HTTP API, which is the only
administrative surface reachable
+ // from a test without pulling in the SDK.
+ .env("IGGY_HTTP_ENABLED", "true")
+ .env("IGGY_HTTP_ADDRESS", &http_address)
+ .env("IGGY_QUIC_ENABLED", "false")
+ .env("IGGY_WEBSOCKET_ENABLED", "false")
+ .env("IGGY_SHARDING_PIN_CORES", "false")
+ .env("IGGY_SHARDING_CPU_ALLOCATION", "0..4")
+ .env("IGGY_ROOT_USERNAME", ROOT_USER)
+ .env("IGGY_ROOT_PASSWORD", ROOT_PASSWORD)
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .spawn()
+ .expect("spawn iggy-server");
+
+ let mut server = Self {
+ child,
+ address,
+ http_address,
+ _data_dir: data_dir,
+ };
+ match server.wait_ready() {
+ Ready::Listening => Ok(server),
+ Ready::Exited(status) => Err(format!(
+ "iggy-server exited during startup with {status}; its output
is suppressed, so \
+ rerun it by hand with the same environment to see why"
+ )),
+ Ready::TimedOut => Err(format!(
+ "iggy-server never bound {} within {SERVER_READY_TIMEOUT:?}",
+ server.address
+ )),
+ }
+ }
+
+ fn wait_ready(&mut self) -> Ready {
+ let deadline = Instant::now() + SERVER_READY_TIMEOUT;
+ while Instant::now() < deadline {
+ // Both listeners, not just the data one: the server binds HTTP
after TCP, so probing
+ // TCP alone declares readiness while the provisioning calls that
follow would still
+ // be refused.
+ if std::net::TcpStream::connect(&self.address).is_ok()
+ && std::net::TcpStream::connect(&self.http_address).is_ok()
+ {
+ return Ready::Listening;
+ }
+ // Without this a server that dies at boot burns the whole budget
and is then reported
+ // as a timeout, which sends the reader looking at the wrong thing
entirely.
+ if let Ok(Some(status)) = self.child.try_wait() {
+ return Ready::Exited(status.to_string());
+ }
+ std::thread::sleep(Duration::from_millis(200));
+ }
+ Ready::TimedOut
+ }
+}
+
+impl Drop for TestServer {
+ fn drop(&mut self) {
+ let _ = self.child.kill();
+ let _ = self.child.wait();
+ }
+}
+
+/// Binds an ephemeral port and releases it, so the server can take it.
+///
+/// A window exists between release and rebind. Acceptable here because this
suite is serialized
+/// into its own nextest group, so nothing else in it is drawing ports
concurrently.
+fn free_port() -> u16 {
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind
ephemeral port");
+ listener.local_addr().expect("local addr").port()
+}
+
+/// Starts the gateway with SASL on, verifying credentials against
`iggy_address`.
+async fn spawn_gateway(iggy_address: &str) -> SocketAddr {
+ let config = GatewayConfig {
+ sasl_enabled: true,
+ ..GatewayConfig::default()
+ };
+ let authenticator =
Arc::new(IggyAuthenticator::new(iggy_address.to_string()));
+ let (addr, shutdown) = spawn_test_server_with_authenticator(config,
authenticator).await;
+ // Held for the test's lifetime: dropping the sender shuts the gateway
down mid-exchange.
+ std::mem::forget(shutdown);
+ addr
+}
+
+/// Runs a container against the host network and returns its combined output.
+///
+/// `--network host` is what lets a containerised client reach a gateway bound
to the host's
+/// loopback. It is Linux-specific, which matches where CI runs.
+///
+/// This blocks the calling thread for the life of the container, which is why
every test here uses
+/// a multi-threaded runtime: the gateway runs as a spawned task, and on the
single-threaded runtime
+/// `#[tokio::test]` gives by default, this call would starve it and nothing
would ever listen.
+///
+/// `--pull never` keeps a registry download out of the test's time budget:
images are pulled up
+/// front, and `stack` skips when they are absent. `timeout` bounds the run
itself, and a run it
+/// cuts short exits non-zero, so `expect_ran` reports it rather than
asserting over partial output.
+fn run_client(image: &str, args: &[&str], mounts: &[(&str, &str)]) ->
ClientRun {
Review Comment:
GNU timeout --kill-after. macOS none. :269 expect panics after stack().
--network host Linux. **Fix**: skip unless GNU timeout / Linux host-net, or
wrap docker run.
##########
gateways/kafka/docs/TEST_SUITE.md:
##########
@@ -73,6 +74,34 @@ is test-only primitive encode/decode scaffolding for
hand-building legacy/advers
---
+## Real-client end-to-end suite
+
+`kafka_client_e2e_tests.rs` needs two things the rest of the suite does not:
Docker, and an
+already-built `iggy-server` in the same target directory. Missing either makes
it skip with a
Review Comment:
“20+ … ACLs, etc. | Later issues” while Phase 3+ [x] DescribeAcls (:153).
Table omits 17/29/36. Prior REQUEST CHANGES leftover. **Fix**: drop ACL from
later row; list SASL-gated keys.
--
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]