test(integration): remove suites duplicating existing coverage (#3823)
diff --git a/Cargo.lock b/Cargo.lock
index 3cd7dcc..b2af9ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11933,7 +11933,6 @@
"hwlocality",
"hyper",
"hyper-util",
- "iggy",
"iggy_binary_protocol",
"iggy_common",
"journal",
diff --git a/core/integration/tests/cluster/metadata_checkpoint_restart.rs b/core/integration/tests/cluster/metadata_checkpoint_restart.rs
index 79f3f82..75ae60f 100644
--- a/core/integration/tests/cluster/metadata_checkpoint_restart.rs
+++ b/core/integration/tests/cluster/metadata_checkpoint_restart.rs
@@ -16,7 +16,8 @@
// under the License.
//! Checkpoint-shaped edge cases for metadata + client-table state transfer,
-//! metadata-plane commands only (`CreateStream` over the raw VSR wire).
+//! metadata-plane commands only (`CreateStream`). The 3-node cases drive the
+//! raw VSR wire; the solo case at the bottom uses the regular TCP client.
//!
//! `metadata_state_transfer` pins the base case: one checkpoint, a live
//! post-checkpoint tail, one restart. This file covers the shapes around it:
@@ -30,22 +31,27 @@
//! from the TRANSFERRED reply ring - the WAL entry that produced the reply
//! was drained on every node, so nothing but the shipped table can answer;
//! - a second restart of the same node (the installed snapshot must persist
-//! and serve as the local floor for another transfer round).
+//! and serve as the local floor for another transfer round);
+//! - a SOLO replica restarting after its own checkpoint: no transfer in the
+//! picture, so recovery has to fold the local snapshot back in as the floor
+//! and replay the WAL suffix on top.
//!
-//! Checkpoint placement is config-driven: with `metadata.journal_slots = 256`
-//! and the built-in checkpoint margin (64), a checkpoint fires when the
-//! journal holds 192 committed ops, i.e. at op 192, 384, ... The register
-//! commits at op 1 and request K at op K + 1, so request 191 lands checkpoint
-//! one and request 383 lands checkpoint two. Requests here are sequential
-//! with one in flight, which keeps that arithmetic exact; the
-//! `forced checkpoint completed` markers below pin it rather than trust it.
+//! Checkpoint placement in the 3-node cases is config-driven: with
+//! `metadata.journal_slots = 256` and the built-in checkpoint margin (64), a
+//! checkpoint fires when the journal holds 192 committed ops, i.e. at op 192,
+//! 384, ... The register commits at op 1 and request K at op K + 1, so request
+//! 191 lands checkpoint one and request 383 lands checkpoint two. Requests
+//! here are sequential with one in flight, which keeps that arithmetic exact;
+//! the `forced checkpoint completed` markers below pin it rather than trust
+//! it.
#![cfg(feature = "vsr")]
use super::client_table_restart::{
commit_request, create_stream_payload, register, resume_request, tcp_addr, tcp_addrs,
};
-use integration::harness::TestHarness;
+use iggy::prelude::*;
+use integration::harness::{TestBinary, TestHarness};
use integration::iggy_harness;
use std::time::Duration;
use tokio::time::{Instant, sleep};
@@ -274,3 +280,115 @@
)
.await;
}
+
+/// The prepare WAL has `SLOT_COUNT = 1024` slots and the coordinator checkpoints at
+/// `<= CHECKPOINT_MARGIN` (64) remaining, so after ~960 uncheckpointed ops. 1024
+/// stream creates clears that with room for a WAL suffix above the checkpoint, and
+/// stays under the 4096 stream namespace cap.
+const STREAMS: u32 = 1024;
+
+const RECOVER_TIMEOUT: Duration = Duration::from_secs(60);
+const POLL_INTERVAL: Duration = Duration::from_millis(200);
+
+fn stream_name(index: u32) -> String {
+ format!("ckpt-stream-{index}")
+}
+
+/// Connect a root-authenticated TCP client to the solo node.
+async fn connect(harness: &TestHarness) -> IggyClient {
+ harness
+ .node(0)
+ .tcp_client()
+ .expect("tcp client builder")
+ .with_root_login()
+ .connect()
+ .await
+ .expect("connect to the solo node")
+}
+
+/// Poll the solo node until it is back up and serving `stream`, returning the connected
+/// client. Panics on timeout.
+async fn wait_for_stream(harness: &TestHarness, stream: &str) -> IggyClient {
+ let stream_id = Identifier::named(stream).unwrap();
+ let deadline = Instant::now() + RECOVER_TIMEOUT;
+ loop {
+ if let Ok(builder) = harness.node(0).tcp_client()
+ && let Ok(client) = builder.with_root_login().connect().await
+ && matches!(client.get_stream(&stream_id).await, Ok(Some(_)))
+ {
+ return client;
+ }
+ assert!(
+ Instant::now() < deadline,
+ "solo node did not recover and serve {stream} within {RECOVER_TIMEOUT:?}"
+ );
+ sleep(POLL_INTERVAL).await;
+ }
+}
+
+// Metadata checkpoint-fold recovery across a solo restart, over
+// `iggy-server-ng`'s production snapshot and WAL path.
+//
+// Between checkpoints a replica recovers its metadata by replaying the WAL. Once the
+// WAL fills, the `SnapshotCoordinator` checkpoints: it persists `snapshot.bin`, pairs
+// it in the superblock, and DRAINS the snapshotted prefix from the WAL. A restart
+// after that must fold the snapshot back in as the recovery floor and replay only the
+// committed suffix on top, rather than rely on a full WAL replay, which no longer
+// holds the drained ops. This drives a real checkpoint by pushing the metadata WAL
+// past `CHECKPOINT_MARGIN`, restarts the process, and asserts that a stream from the
+// drained prefix, recoverable only from the snapshot, and one from the WAL suffix
+// both survive.
+//
+// Solo on purpose: 1-of-1 quorum commits every op the instant it is journaled, so
+// bulk creation is fast and the WAL is fully committed with no uncommitted suffix to
+// reconcile, exercising checkpoint and snapshot-fold recovery in isolation without an
+// election in the mix.
+#[iggy_harness(cluster_nodes = 1, server(system.sharding.cpu_allocation = "0..1"))]
+async fn given_checkpointed_metadata_when_solo_replica_restarts_should_recover_from_snapshot_and_wal(
+ harness: &mut TestHarness,
+) {
+ let client = connect(harness).await;
+ for index in 0..STREAMS {
+ client
+ .create_stream(&stream_name(index))
+ .await
+ .unwrap_or_else(|e| panic!("create stream {index}: {e}"));
+ }
+ drop(client);
+
+ // Crossing CHECKPOINT_MARGIN must have driven the coordinator to persist a snapshot
+ // and drain the WAL prefix behind it.
+ let snapshot_path = harness
+ .node(0)
+ .data_path()
+ .join("metadata")
+ .join("snapshot.bin");
+ let snapshot_len = std::fs::metadata(&snapshot_path).map(|m| m.len()).ok();
+ assert!(
+ snapshot_len.is_some_and(|len| len > 0),
+ "{STREAMS} committed metadata ops must cross CHECKPOINT_MARGIN and persist a \
+ non-empty snapshot at {}, got {snapshot_len:?}",
+ snapshot_path.display()
+ );
+
+ // Restart the solo node: recovery loads the snapshot, holding the drained prefix,
+ // and replays the committed WAL suffix on top.
+ harness.node_mut(0).stop().expect("stop the solo node");
+ harness.node_mut(0).start().expect("restart the solo node");
+
+ // The first stream sits far below the checkpoint op, so it was drained from the WAL
+ // and can come back only from the snapshot; the last stream is in the WAL suffix
+ // above the checkpoint. Both surviving proves snapshot-fold plus suffix recovery,
+ // not a bare WAL replay.
+ let client = wait_for_stream(harness, &stream_name(0)).await;
+ for name in [stream_name(0), stream_name(STREAMS - 1)] {
+ assert!(
+ client
+ .get_stream(&Identifier::named(&name).unwrap())
+ .await
+ .expect("get stream after restart")
+ .is_some(),
+ "stream {name} must survive the checkpointed restart"
+ );
+ }
+}
diff --git a/core/integration/tests/sdk/mcp_parity.rs b/core/integration/tests/sdk/mcp_parity.rs
deleted file mode 100644
index 4ec5422..0000000
--- a/core/integration/tests/sdk/mcp_parity.rs
+++ /dev/null
@@ -1,608 +0,0 @@
-// 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.
-
-//! Plain-SDK reproduction of the `mcp::*` suite, without the MCP server. Each
-//! MCP tool maps 1:1 to an SDK client method, so these tests exercise the same
-//! server-ng behaviour under the `vsr` feature over TCP, without the MCP/HTTP
-//! environment overhead. Assertions mirror the MCP counterparts.
-
-#![cfg(feature = "vsr")]
-
-use iggy::prelude::*;
-use integration::{
- harness::{TestHarness, seeds},
- iggy_harness,
-};
-
-/// The consumer under which `seeds::mcp_standard` stores its offset (matches the
-/// MCP server's default consumer name).
-fn seed_consumer() -> Consumer {
- Consumer::new(Identifier::named(seeds::names::CONSUMER).unwrap())
-}
-
-async fn root_client(harness: &TestHarness) -> IggyClient {
- let client = harness.new_client().await.unwrap();
- client
- .login_user(DEFAULT_ROOT_USERNAME, DEFAULT_ROOT_PASSWORD)
- .await
- .unwrap();
- client
-}
-
-// mcp::should_handle_ping
-#[iggy_harness(test_client_transport = [Tcp])]
-async fn should_handle_ping(harness: &TestHarness) {
- let client = root_client(harness).await;
- client.ping().await.unwrap();
-}
-
-// mcp::should_return_cluster_metadata
-// TODO: MCP polls `get_cluster_metadata` via a dedicated tool; there is no
-// equivalent single SDK client method to assert cluster name + node count, so
-// this case is intentionally not ported here.
-
-// mcp::should_return_list_of_streams
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_list_of_streams(harness: &TestHarness) {
- let client = root_client(harness).await;
- let streams = client.get_streams().await.unwrap();
-
- assert_eq!(streams.len(), 1);
- assert_eq!(streams[0].name, seeds::names::STREAM);
- assert_eq!(streams[0].topics_count, 1);
-}
-
-// mcp::should_return_stream_details
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_stream_details(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let stream = client
- .get_stream(&stream_id)
- .await
- .unwrap()
- .expect("stream exists");
-
- assert_eq!(stream.name, seeds::names::STREAM);
- assert_eq!(stream.topics_count, 1);
- assert_eq!(stream.messages_count, 1);
-}
-
-// mcp::should_create_stream
-#[iggy_harness(test_client_transport = [Tcp])]
-async fn should_create_stream(harness: &TestHarness) {
- let client = root_client(harness).await;
- let name = "new_stream";
- let stream = client.create_stream(name).await.unwrap();
-
- assert_eq!(stream.name, name);
- assert_eq!(stream.topics_count, 0);
- assert_eq!(stream.messages_count, 0);
-}
-
-// mcp::should_update_stream
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_update_stream(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- client
- .update_stream(&stream_id, "updated_stream")
- .await
- .unwrap();
-}
-
-// mcp::should_delete_stream
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_stream(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- client.delete_stream(&stream_id).await.unwrap();
-}
-
-// mcp::should_purge_stream
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_purge_stream(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- client.purge_stream(&stream_id).await.unwrap();
-}
-
-// mcp::should_return_list_of_topics
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_list_of_topics(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topics = client.get_topics(&stream_id).await.unwrap();
-
- assert_eq!(topics.len(), 1);
- assert_eq!(topics[0].name, seeds::names::TOPIC);
- assert_eq!(topics[0].partitions_count, 1);
- assert_eq!(topics[0].messages_count, 1);
-}
-
-// mcp::should_return_topic_details
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_topic_details(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let topic = client
- .get_topic(&stream_id, &topic_id)
- .await
- .unwrap()
- .expect("topic exists");
-
- assert_eq!(topic.id, 0);
- assert_eq!(topic.name, seeds::names::TOPIC);
- assert_eq!(topic.messages_count, 1);
-}
-
-// mcp::should_create_topic
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_create_topic(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let name = "new_topic";
- let topic = client
- .create_topic(
- &stream_id,
- name,
- 1,
- CompressionAlgorithm::None,
- None,
- IggyExpiry::ServerDefault,
- MaxTopicSize::ServerDefault,
- )
- .await
- .unwrap();
-
- assert_eq!(topic.id, 1);
- assert_eq!(topic.name, name);
- assert_eq!(topic.partitions_count, 1);
- assert_eq!(topic.messages_count, 0);
-}
-
-// mcp::should_update_topic
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_update_topic(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client
- .update_topic(
- &stream_id,
- &topic_id,
- "updated_topic",
- CompressionAlgorithm::None,
- None,
- IggyExpiry::ServerDefault,
- MaxTopicSize::ServerDefault,
- )
- .await
- .unwrap();
-}
-
-// mcp::should_delete_topic
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_topic(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client.delete_topic(&stream_id, &topic_id).await.unwrap();
-}
-
-// mcp::should_purge_topic
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_purge_topic(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client.purge_topic(&stream_id, &topic_id).await.unwrap();
-}
-
-// mcp::should_create_partitions
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_create_partitions(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client
- .create_partitions(&stream_id, &topic_id, 3)
- .await
- .unwrap();
-}
-
-// mcp::should_delete_partitions
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_partitions(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client
- .delete_partitions(&stream_id, &topic_id, 1)
- .await
- .unwrap();
-}
-
-// mcp::should_delete_segments
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_segments(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client
- .delete_segments(&stream_id, &topic_id, 0, 1)
- .await
- .unwrap();
-}
-
-// mcp::should_poll_messages
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_poll_messages(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let messages = client
- .poll_messages(
- &stream_id,
- &topic_id,
- Some(0),
- &seed_consumer(),
- &PollingStrategy::offset(0),
- 10,
- false,
- )
- .await
- .unwrap();
-
- assert_eq!(messages.messages.len(), 1);
- assert_eq!(messages.messages[0].header.offset, 0);
- let payload = messages.messages[0]
- .payload_as_string()
- .expect("Failed to parse payload");
- assert_eq!(payload, seeds::names::MESSAGE_PAYLOAD);
-}
-
-// mcp::should_send_messages
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_send_messages(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let mut messages = vec![
- IggyMessage::builder()
- .payload("test".into())
- .build()
- .unwrap(),
- ];
- client
- .send_messages(
- &stream_id,
- &topic_id,
- &Partitioning::partition_id(0),
- &mut messages,
- )
- .await
- .unwrap();
-}
-
-// mcp::should_return_stats
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_stats(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stats = client.get_stats().await.unwrap();
-
- assert!(!stats.hostname.is_empty());
- assert_eq!(stats.messages_count, 1);
-}
-
-// mcp::should_return_me
-#[iggy_harness(test_client_transport = [Tcp])]
-async fn should_return_me(harness: &TestHarness) {
- let client = root_client(harness).await;
- let me = client.get_me().await.unwrap();
-
- assert!(me.client_id > 0);
-}
-
-// mcp::should_return_clients
-#[iggy_harness(test_client_transport = [Tcp])]
-async fn should_return_clients(harness: &TestHarness) {
- let client = root_client(harness).await;
- let clients = client.get_clients().await.unwrap();
-
- assert!(!clients.is_empty());
-}
-
-// mcp::should_handle_snapshot
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_handle_snapshot(harness: &TestHarness) {
- let client = root_client(harness).await;
- let snapshot = client
- .snapshot(
- SnapshotCompression::default(),
- SystemSnapshotType::all_snapshot_types(),
- )
- .await
- .unwrap();
-
- assert!(!snapshot.0.is_empty());
-}
-
-// mcp::should_return_consumer_groups
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_consumer_groups(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let groups = client
- .get_consumer_groups(&stream_id, &topic_id)
- .await
- .unwrap();
-
- assert!(!groups.is_empty());
-}
-
-// mcp::should_return_consumer_group_details
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_consumer_group_details(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let group_id = Identifier::named(seeds::names::CONSUMER_GROUP).unwrap();
- let group = client
- .get_consumer_group(&stream_id, &topic_id, &group_id)
- .await
- .unwrap()
- .expect("consumer group exists");
-
- assert_eq!(group.name, seeds::names::CONSUMER_GROUP);
- assert_eq!(group.partitions_count, 1);
- assert_eq!(group.members_count, 0);
- assert!(group.members.is_empty());
-}
-
-// mcp::should_create_consumer_group
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_create_consumer_group(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let name = "new_group";
- let group = client
- .create_consumer_group(&stream_id, &topic_id, name)
- .await
- .unwrap();
-
- assert_eq!(group.name, name);
- assert_eq!(group.partitions_count, 1);
- assert_eq!(group.members_count, 0);
- assert!(group.members.is_empty());
-}
-
-// mcp::should_delete_consumer_group
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_consumer_group(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let group_id = Identifier::named(seeds::names::CONSUMER_GROUP).unwrap();
- client
- .delete_consumer_group(&stream_id, &topic_id, &group_id)
- .await
- .unwrap();
-}
-
-// mcp::should_return_consumer_offset
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_consumer_offset(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let offset = client
- .get_consumer_offset(&seed_consumer(), &stream_id, &topic_id, Some(0))
- .await
- .unwrap()
- .expect("Expected consumer offset");
-
- assert_eq!(offset.partition_id, 0);
- assert_eq!(offset.stored_offset, 0);
- assert_eq!(offset.current_offset, 0);
-}
-
-// mcp::should_store_consumer_offset
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_store_consumer_offset(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client
- .store_consumer_offset(&seed_consumer(), &stream_id, &topic_id, Some(0), 0)
- .await
- .unwrap();
-}
-
-// mcp::should_delete_consumer_offset
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_consumer_offset(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- client
- .delete_consumer_offset(&seed_consumer(), &stream_id, &topic_id, Some(0))
- .await
- .unwrap();
-}
-
-// Regression (no MCP counterpart): deleting an offset that was never stored
-// must return a terminal `ConsumerOffsetNotFound`, NOT hang. The primary rejects
-// at admission with an explicit error reply instead of dropping the request
-// (which left the SDK replaying until its read-timeout).
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn delete_of_missing_consumer_offset_returns_not_found(harness: &TestHarness) {
- let client = root_client(harness).await;
- let stream_id = Identifier::named(seeds::names::STREAM).unwrap();
- let topic_id = Identifier::named(seeds::names::TOPIC).unwrap();
- let never_stored = Consumer::new(Identifier::named("never_stored").unwrap());
- let result = client
- .delete_consumer_offset(&never_stored, &stream_id, &topic_id, Some(0))
- .await;
- assert!(
- matches!(result, Err(IggyError::ConsumerOffsetNotFound(_))),
- "expected ConsumerOffsetNotFound, got {result:?}"
- );
-}
-
-// mcp::should_return_personal_access_tokens
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_personal_access_tokens(harness: &TestHarness) {
- let client = root_client(harness).await;
- let tokens = client.get_personal_access_tokens().await.unwrap();
-
- assert_eq!(tokens.len(), 1);
- assert_eq!(tokens[0].name, seeds::names::PERSONAL_ACCESS_TOKEN);
-}
-
-// mcp::should_create_personal_access_token
-#[iggy_harness(test_client_transport = [Tcp])]
-async fn should_create_personal_access_token(harness: &TestHarness) {
- let client = root_client(harness).await;
- let name = "test_token";
- let token = client
- .create_personal_access_token(name, PersonalAccessTokenExpiry::NeverExpire)
- .await
- .unwrap();
-
- assert!(!token.token.is_empty());
-}
-
-// mcp::should_delete_personal_access_token
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_personal_access_token(harness: &TestHarness) {
- let client = root_client(harness).await;
- client
- .delete_personal_access_token(seeds::names::PERSONAL_ACCESS_TOKEN)
- .await
- .unwrap();
-}
-
-// mcp::should_return_users
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_users(harness: &TestHarness) {
- let client = root_client(harness).await;
- let users = client.get_users().await.unwrap();
-
- assert_eq!(users.len(), 2);
-}
-
-// mcp::should_return_user_details
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_return_user_details(harness: &TestHarness) {
- let client = root_client(harness).await;
- let user_id = Identifier::named(seeds::names::USER).unwrap();
- let user = client
- .get_user(&user_id)
- .await
- .unwrap()
- .expect("user exists");
-
- assert_eq!(user.username, seeds::names::USER);
-}
-
-// mcp::should_create_user
-#[iggy_harness(test_client_transport = [Tcp])]
-async fn should_create_user(harness: &TestHarness) {
- let client = root_client(harness).await;
- let username = "test-mcp-user";
- let user = client
- .create_user(username, "secret", UserStatus::Active, None)
- .await
- .unwrap();
-
- assert_eq!(user.username, username);
-}
-
-// mcp::should_update_user
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_update_user(harness: &TestHarness) {
- let client = root_client(harness).await;
- let user_id = Identifier::named(seeds::names::USER).unwrap();
- client
- .update_user(&user_id, Some("updated-user"), Some(UserStatus::Inactive))
- .await
- .unwrap();
-}
-
-// mcp::should_delete_user
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_delete_user(harness: &TestHarness) {
- let client = root_client(harness).await;
- let user_id = Identifier::named(seeds::names::USER).unwrap();
- client.delete_user(&user_id).await.unwrap();
-}
-
-// mcp::should_update_permissions
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_update_permissions(harness: &TestHarness) {
- let client = root_client(harness).await;
- let user_id = Identifier::named(seeds::names::USER).unwrap();
-
- let mut topics = std::collections::BTreeMap::new();
- topics.insert(
- 1,
- TopicPermissions {
- manage_topic: true,
- read_topic: true,
- poll_messages: true,
- send_messages: true,
- },
- );
- let mut streams = std::collections::BTreeMap::new();
- streams.insert(
- 1,
- StreamPermissions {
- manage_stream: true,
- manage_topics: true,
- topics: Some(topics),
- ..Default::default()
- },
- );
- let permissions = Permissions {
- global: GlobalPermissions {
- manage_servers: true,
- read_users: true,
- ..Default::default()
- },
- streams: Some(streams),
- };
-
- client
- .update_permissions(&user_id, Some(permissions))
- .await
- .unwrap();
-}
-
-// mcp::should_change_password
-#[iggy_harness(test_client_transport = [Tcp], seed = seeds::mcp_standard)]
-async fn should_change_password(harness: &TestHarness) {
- let client = root_client(harness).await;
- let user_id = Identifier::named(seeds::names::USER).unwrap();
- client
- .change_password(&user_id, seeds::names::USER_PASSWORD, "new_secret")
- .await
- .unwrap();
-}
diff --git a/core/integration/tests/sdk/messages.rs b/core/integration/tests/sdk/messages.rs
deleted file mode 100644
index 34ce3d7..0000000
--- a/core/integration/tests/sdk/messages.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-// 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.
-
-//! VSR data-plane round trip: `send_messages` / `poll_messages` and the
-//! consumer-offset store/get pair against a 3-node cluster, exercising the
-//! partition-reconciliation loop end to end.
-
-#![cfg(feature = "vsr")]
-
-use iggy::prelude::*;
-use integration::iggy_harness;
-
-// server-ng partition ids are 0-based (CreateTopic assigns them from 0).
-const PARTITION_ID: u32 = 0;
-const MESSAGES_COUNT: u32 = 10;
-
-#[iggy_harness(test_client_transport = [Tcp, WebSocket, Quic])]
-async fn send_and_poll_messages_round_trip(harness: &TestHarness) {
- let client = harness.new_client().await.unwrap();
- client
- .login_user(DEFAULT_ROOT_USERNAME, DEFAULT_ROOT_PASSWORD)
- .await
- .unwrap();
-
- client.create_stream("data-stream").await.unwrap();
- client
- .create_topic(
- &Identifier::named("data-stream").unwrap(),
- "data-topic",
- 1,
- CompressionAlgorithm::default(),
- None,
- IggyExpiry::NeverExpire,
- MaxTopicSize::ServerDefault,
- )
- .await
- .unwrap();
-
- let mut messages: Vec<IggyMessage> = (0..MESSAGES_COUNT)
- .map(|i| {
- IggyMessage::builder()
- .id(u128::from(i + 1))
- .payload(format!("payload-{i}").into())
- .build()
- .expect("message build")
- })
- .collect();
-
- client
- .send_messages(
- &Identifier::named("data-stream").unwrap(),
- &Identifier::named("data-topic").unwrap(),
- &Partitioning::partition_id(PARTITION_ID),
- &mut messages,
- )
- .await
- .expect("send_messages");
-
- let consumer = Consumer::default();
- let polled = client
- .poll_messages(
- &Identifier::named("data-stream").unwrap(),
- &Identifier::named("data-topic").unwrap(),
- Some(PARTITION_ID),
- &consumer,
- &PollingStrategy::offset(0),
- MESSAGES_COUNT,
- false,
- )
- .await
- .expect("poll_messages");
-
- assert_eq!(
- polled.messages.len() as u32,
- MESSAGES_COUNT,
- "all sent messages must come back"
- );
- for (i, message) in polled.messages.iter().enumerate() {
- assert_eq!(message.header.offset, i as u64, "offsets must be dense");
- assert_eq!(
- message.payload,
- bytes::Bytes::from(format!("payload-{i}")),
- "payload round trip for message {i}"
- );
- }
-
- client.logout_user().await.unwrap();
-}
-
-#[iggy_harness(test_client_transport = [Tcp, WebSocket, Quic])]
-async fn consumer_offset_store_get_round_trip(harness: &TestHarness) {
- let client = harness.new_client().await.unwrap();
- client
- .login_user(DEFAULT_ROOT_USERNAME, DEFAULT_ROOT_PASSWORD)
- .await
- .unwrap();
-
- client.create_stream("offset-stream").await.unwrap();
- client
- .create_topic(
- &Identifier::named("offset-stream").unwrap(),
- "offset-topic",
- 1,
- CompressionAlgorithm::default(),
- None,
- IggyExpiry::NeverExpire,
- MaxTopicSize::ServerDefault,
- )
- .await
- .unwrap();
-
- let mut messages: Vec<IggyMessage> = (0..5u32)
- .map(|i| {
- IggyMessage::builder()
- .id(u128::from(i + 1))
- .payload(format!("offset-payload-{i}").into())
- .build()
- .expect("message build")
- })
- .collect();
- client
- .send_messages(
- &Identifier::named("offset-stream").unwrap(),
- &Identifier::named("offset-topic").unwrap(),
- &Partitioning::partition_id(PARTITION_ID),
- &mut messages,
- )
- .await
- .expect("send_messages");
-
- let consumer = Consumer::default();
- client
- .store_consumer_offset(
- &consumer,
- &Identifier::named("offset-stream").unwrap(),
- &Identifier::named("offset-topic").unwrap(),
- Some(PARTITION_ID),
- 3,
- )
- .await
- .expect("store_consumer_offset");
-
- let stored = client
- .get_consumer_offset(
- &consumer,
- &Identifier::named("offset-stream").unwrap(),
- &Identifier::named("offset-topic").unwrap(),
- Some(PARTITION_ID),
- )
- .await
- .expect("get_consumer_offset")
- .expect("offset must exist after store");
-
- assert_eq!(stored.stored_offset, 3, "stored offset must round trip");
- assert_eq!(stored.partition_id, PARTITION_ID);
-
- client.logout_user().await.unwrap();
-}
diff --git a/core/integration/tests/sdk/mod.rs b/core/integration/tests/sdk/mod.rs
index 7d72480..1182d9e 100644
--- a/core/integration/tests/sdk/mod.rs
+++ b/core/integration/tests/sdk/mod.rs
@@ -21,10 +21,6 @@
mod hello_world;
#[cfg(feature = "vsr")]
mod http_refresh;
-#[cfg(feature = "vsr")]
-mod mcp_parity;
-#[cfg(feature = "vsr")]
-mod messages;
mod producer;
#[cfg(feature = "vsr")]
mod protocol_version;
diff --git a/core/integration/tests/server/general.rs b/core/integration/tests/server/general.rs
index cb51ac3..c9a5ad6 100644
--- a/core/integration/tests/server/general.rs
+++ b/core/integration/tests/server/general.rs
@@ -18,9 +18,9 @@
#[cfg(not(feature = "vsr"))]
use crate::server::scenarios::bench_scenario;
use crate::server::scenarios::{
- authentication_scenario, consumer_timestamp_polling_scenario, create_message_payload,
- invalid_consumer_offset_scenario, message_headers_scenario, permissions_scenario,
- snapshot_scenario, stream_size_validation_scenario, system_scenario, user_scenario,
+ authentication_scenario, consumer_timestamp_polling_scenario, invalid_consumer_offset_scenario,
+ message_headers_scenario, permissions_scenario, snapshot_scenario,
+ stream_size_validation_scenario, system_scenario, user_scenario,
};
use integration::iggy_harness;
@@ -99,19 +99,6 @@
quic.keep_alive_interval = "15s"
)
)]
-async fn create_message_payload_scenario(harness: &TestHarness) {
- create_message_payload::run(harness).await;
-}
-
-#[iggy_harness(
- test_client_transport = [Tcp, Http, Quic, WebSocket],
- server(
- tcp.socket.override_defaults = true,
- tcp.socket.nodelay = true,
- quic.max_idle_timeout = "500s",
- quic.keep_alive_interval = "15s"
- )
-)]
async fn stream_size_validation(harness: &TestHarness) {
stream_size_validation_scenario::run(harness).await;
}
diff --git a/core/integration/tests/server/metadata_checkpoint_recovery_vsr.rs b/core/integration/tests/server/metadata_checkpoint_recovery_vsr.rs
deleted file mode 100644
index 4fca67b..0000000
--- a/core/integration/tests/server/metadata_checkpoint_recovery_vsr.rs
+++ /dev/null
@@ -1,138 +0,0 @@
-// 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.
-
-//! Metadata checkpoint-fold recovery across a solo restart, over
-//! `iggy-server-ng`'s production snapshot and WAL path.
-//!
-//! Between checkpoints a replica recovers its metadata by replaying the WAL. Once the
-//! WAL fills, the `SnapshotCoordinator` checkpoints: it persists `snapshot.bin`, pairs
-//! it in the superblock, and DRAINS the snapshotted prefix from the WAL. A restart
-//! after that must fold the snapshot back in as the recovery floor and replay only the
-//! committed suffix on top, rather than rely on a full WAL replay, which no longer
-//! holds the drained ops. This drives a real checkpoint by pushing the metadata WAL
-//! past `CHECKPOINT_MARGIN`, restarts the process, and asserts that a stream from the
-//! drained prefix, recoverable only from the snapshot, and one from the WAL suffix
-//! both survive.
-//!
-//! Solo on purpose: 1-of-1 quorum commits every op the instant it is journaled, so
-//! bulk creation is fast and the WAL is fully committed with no uncommitted suffix to
-//! reconcile, exercising checkpoint and snapshot-fold recovery in isolation without an
-//! election in the mix.
-//!
-//! vsr-only: the metadata snapshot/superblock checkpoint pairing is server-ng's.
-
-use std::time::{Duration, Instant};
-
-use iggy::prelude::*;
-use integration::harness::{TestBinary, TestHarness};
-use integration::iggy_harness;
-use tokio::time::sleep;
-
-/// The prepare WAL has `SLOT_COUNT = 1024` slots and the coordinator checkpoints at
-/// `<= CHECKPOINT_MARGIN` (64) remaining, so after ~960 uncheckpointed ops. 1024
-/// stream creates clears that with room for a WAL suffix above the checkpoint, and
-/// stays under the 4096 stream namespace cap.
-const STREAMS: u32 = 1024;
-
-const RECOVER_TIMEOUT: Duration = Duration::from_secs(60);
-const POLL_INTERVAL: Duration = Duration::from_millis(200);
-
-fn stream_name(index: u32) -> String {
- format!("ckpt-stream-{index}")
-}
-
-#[iggy_harness(cluster_nodes = 1, server(system.sharding.cpu_allocation = "0..1"))]
-async fn given_checkpointed_metadata_when_solo_replica_restarts_should_recover_from_snapshot_and_wal(
- harness: &mut TestHarness,
-) {
- let client = connect(harness).await;
- for index in 0..STREAMS {
- client
- .create_stream(&stream_name(index))
- .await
- .unwrap_or_else(|e| panic!("create stream {index}: {e}"));
- }
- drop(client);
-
- // Crossing CHECKPOINT_MARGIN must have driven the coordinator to persist a snapshot
- // and drain the WAL prefix behind it.
- let snapshot_path = harness
- .node(0)
- .data_path()
- .join("metadata")
- .join("snapshot.bin");
- let snapshot_len = std::fs::metadata(&snapshot_path).map(|m| m.len()).ok();
- assert!(
- snapshot_len.is_some_and(|len| len > 0),
- "{STREAMS} committed metadata ops must cross CHECKPOINT_MARGIN and persist a \
- non-empty snapshot at {}, got {snapshot_len:?}",
- snapshot_path.display()
- );
-
- // Restart the solo node: recovery loads the snapshot, holding the drained prefix,
- // and replays the committed WAL suffix on top.
- harness.node_mut(0).stop().expect("stop the solo node");
- harness.node_mut(0).start().expect("restart the solo node");
-
- // The first stream sits far below the checkpoint op, so it was drained from the WAL
- // and can come back only from the snapshot; the last stream is in the WAL suffix
- // above the checkpoint. Both surviving proves snapshot-fold plus suffix recovery,
- // not a bare WAL replay.
- let client = wait_for_stream(harness, &stream_name(0)).await;
- for name in [stream_name(0), stream_name(STREAMS - 1)] {
- assert!(
- client
- .get_stream(&Identifier::named(&name).unwrap())
- .await
- .expect("get stream after restart")
- .is_some(),
- "stream {name} must survive the checkpointed restart"
- );
- }
-}
-
-/// Connect a root-authenticated TCP client to the solo node.
-async fn connect(harness: &TestHarness) -> IggyClient {
- harness
- .node(0)
- .tcp_client()
- .expect("tcp client builder")
- .with_root_login()
- .connect()
- .await
- .expect("connect to the solo node")
-}
-
-/// Poll the solo node until it is back up and serving `stream`, returning the connected
-/// client. Panics on timeout.
-async fn wait_for_stream(harness: &TestHarness, stream: &str) -> IggyClient {
- let stream_id = Identifier::named(stream).unwrap();
- let deadline = Instant::now() + RECOVER_TIMEOUT;
- loop {
- if let Ok(builder) = harness.node(0).tcp_client()
- && let Ok(client) = builder.with_root_login().connect().await
- && matches!(client.get_stream(&stream_id).await, Ok(Some(_)))
- {
- return client;
- }
- assert!(
- Instant::now() < deadline,
- "solo node did not recover and serve {stream} within {RECOVER_TIMEOUT:?}"
- );
- sleep(POLL_INTERVAL).await;
- }
-}
diff --git a/core/integration/tests/server/mod.rs b/core/integration/tests/server/mod.rs
index 5681bf7..8c3f6e0 100644
--- a/core/integration/tests/server/mod.rs
+++ b/core/integration/tests/server/mod.rs
@@ -48,10 +48,6 @@
// across a replica restart.
#[cfg(feature = "vsr")]
mod cluster_view_durability_vsr;
-// A metadata checkpoint must drain the WAL and recover from the snapshot fold plus
-// the WAL suffix across a restart.
-#[cfg(feature = "vsr")]
-mod metadata_checkpoint_recovery_vsr;
// 80-case race matrix with hardcoded HTTP variants (test_matrix bypasses
// the harness transport filter).
mod concurrent_addition;
diff --git a/core/integration/tests/server/scenarios/create_message_payload.rs b/core/integration/tests/server/scenarios/create_message_payload.rs
deleted file mode 100644
index 3bb00d1..0000000
--- a/core/integration/tests/server/scenarios/create_message_payload.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-// 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::Bytes;
-use iggy::prelude::*;
-use integration::harness::{TestHarness, assert_clean_system};
-use std::collections::BTreeMap;
-
-const STREAM_NAME: &str = "test-stream";
-const TOPIC_NAME: &str = "test-topic";
-const PARTITIONS_COUNT: u32 = 3;
-const MESSAGES_COUNT: u32 = 1000;
-const PARTITION_ID: u32 = 1;
-
-pub async fn run(harness: &TestHarness) {
- let client = harness
- .root_client()
- .await
- .expect("Failed to get root client");
- init_system(&client).await;
-
- // 1. Send messages with the included headers
- let mut messages = Vec::new();
- for offset in 0..MESSAGES_COUNT {
- let id = (offset + 1) as u128;
- let payload = create_message_payload(offset as u64);
- let headers = create_message_headers();
- let message = IggyMessage::builder()
- .id(id)
- .payload(payload)
- .user_headers(headers)
- .build()
- .expect("Failed to create message with headers");
- messages.push(message);
- }
-
- client
- .send_messages(
- &Identifier::named(STREAM_NAME).unwrap(),
- &Identifier::named(TOPIC_NAME).unwrap(),
- &Partitioning::partition_id(PARTITION_ID),
- &mut messages,
- )
- .await
- .unwrap();
-
- // 2. Poll messages and validate the headers
- let polled_messages = client
- .poll_messages(
- &Identifier::named(STREAM_NAME).unwrap(),
- &Identifier::named(TOPIC_NAME).unwrap(),
- Some(PARTITION_ID),
- &Consumer::default(),
- &PollingStrategy::offset(0),
- MESSAGES_COUNT,
- false,
- )
- .await
- .unwrap();
-
- assert_eq!(polled_messages.messages.len() as u32, MESSAGES_COUNT);
- for i in 0..MESSAGES_COUNT {
- let message = polled_messages.messages.get(i as usize).unwrap();
- assert!(message.user_headers.is_some());
- let headers = message.user_headers_map().unwrap().unwrap();
- assert_eq!(headers.len(), 3);
- assert_eq!(
- headers
- .get(&HeaderKey::try_from("key_1").unwrap())
- .unwrap()
- .as_str()
- .unwrap(),
- "Value 1"
- );
- assert!(
- headers
- .get(&HeaderKey::try_from("key 2").unwrap())
- .unwrap()
- .as_bool()
- .unwrap(),
- );
- assert_eq!(
- headers
- .get(&HeaderKey::try_from("key-3").unwrap())
- .unwrap()
- .as_uint64()
- .unwrap(),
- 123456
- );
- }
- cleanup_system(&client).await;
- assert_clean_system(&client).await;
-}
-
-async fn init_system(client: &IggyClient) {
- // 1. Create the stream
- client.create_stream(STREAM_NAME).await.unwrap();
-
- // 2. Create the topic
- client
- .create_topic(
- &STREAM_NAME.try_into().unwrap(),
- TOPIC_NAME,
- PARTITIONS_COUNT,
- Default::default(),
- None,
- IggyExpiry::NeverExpire,
- MaxTopicSize::ServerDefault,
- )
- .await
- .unwrap();
-}
-
-async fn cleanup_system(client: &IggyClient) {
- client
- .delete_stream(&STREAM_NAME.try_into().unwrap())
- .await
- .unwrap();
-}
-
-fn create_message_payload(offset: u64) -> Bytes {
- Bytes::from(format!("message {offset}"))
-}
-
-fn create_message_headers() -> BTreeMap<HeaderKey, HeaderValue> {
- let mut headers = BTreeMap::new();
- headers.insert(
- HeaderKey::try_from("key_1").unwrap(),
- HeaderValue::try_from("Value 1").unwrap(),
- );
- headers.insert(HeaderKey::try_from("key 2").unwrap(), true.into());
- headers.insert(HeaderKey::try_from("key-3").unwrap(), 123456u64.into());
- headers
-}
diff --git a/core/integration/tests/server/scenarios/mod.rs b/core/integration/tests/server/scenarios/mod.rs
index 5d701d1..99d20bc 100644
--- a/core/integration/tests/server/scenarios/mod.rs
+++ b/core/integration/tests/server/scenarios/mod.rs
@@ -28,7 +28,6 @@
pub mod consumer_group_with_multiple_clients_polling_messages_scenario;
pub mod consumer_group_with_single_client_polling_messages_scenario;
pub mod consumer_timestamp_polling_scenario;
-pub mod create_message_payload;
// Cross-protocol PAT visibility (create via HTTP, list via TCP across shards,
// and the reverse). Runs under vsr too: server-ng serves the PAT routes on its
// shard-0 HTTP listener and the create/delete commit through the metadata STM,
diff --git a/core/server-ng/Cargo.toml b/core/server-ng/Cargo.toml
index ce6a86d..6299081 100644
--- a/core/server-ng/Cargo.toml
+++ b/core/server-ng/Cargo.toml
@@ -87,10 +87,7 @@
disable-mimalloc = []
mimalloc = ["dep:mimalloc"]
iggy-web = ["dep:rust-embed", "dep:mime_guess"]
-# forward iggy/vsr: the SDK gates its VsrSessionControl impls on its own
-# feature while the trait surface follows iggy_common/vsr; without the
-# forward a bare `-p server-ng --features vsr` build breaks in the SDK
-vsr = ["iggy_common/vsr", "iggy/vsr"]
+vsr = ["iggy_common/vsr"]
[dependencies]
ahash = { workspace = true }
@@ -183,7 +180,6 @@
[dev-dependencies]
assert_cmd = { workspace = true }
bytemuck = { workspace = true }
-iggy = { workspace = true }
# Reconciler unit tests assert on `ShardMetrics` snapshots and
# `IggyShard::parked_frame_count`, gated to test/simulator so they cannot grow
# production callers. `shard`'s own `cfg(test)` is false when compiled as our
diff --git a/core/server-ng/tests/sdk_e2e.rs b/core/server-ng/tests/sdk_e2e.rs
index e7f5fcc..2cbd5d6 100644
--- a/core/server-ng/tests/sdk_e2e.rs
+++ b/core/server-ng/tests/sdk_e2e.rs
@@ -15,32 +15,18 @@
// specific language governing permissions and limitations
// under the License.
-//! End-to-end smoke test for the partition reconciliation loop.
+//! Process-level boot smoke test for the `iggy-server-ng` binary.
//!
-//! Spawns the `iggy-server-ng` binary with an isolated tempdir + ephemeral
-//! TCP port, drives an SDK client through the metadata commit path
-//! (`Login` → `CreateStream` → `CreateTopic` with N partitions), then
-//! verifies the per-partition on-disk hierarchy exists. That hierarchy is
-//! created only by
-//! [`server_ng::partition_helpers::create_partition_file_hierarchy`],
-//! inside the reconciler's
-//! [`build_partition_fresh`](server_ng::partition_helpers::build_partition_fresh)
-//! path, so a materialisation observable from outside the process proves
-//! the full commit-notifier → reconciler → disk pipeline.
-//!
-//! Deliberately stops short of `SendMessages` / `PollMessages`: the
-//! partition data-plane SDK round-trip in server-ng is still evolving, so
-//! a smoke test failing for unrelated data-plane reasons would be noisy.
-//! The on-disk hierarchy assertion proves the reconciler ran without
-//! depending on the still-evolving wire surface.
+//! Spawns the production binary against an isolated tempdir with every
+//! transport except TCP disabled and `IGGY_TCP_ADDRESS` bound to port 0,
+//! reads the OS-assigned port back from `runtime/current_config.toml`, and
+//! asserts the listener accepts connections. Scope is the bootstrap path
+//! only: the binary starts, resolves its configuration from the
+//! environment, and reaches a serving state.
use assert_cmd::prelude::CommandCargoExt;
-use iggy::prelude::{
- AutoLogin, Client, CompressionAlgorithm, IggyByteSize, IggyClient, IggyClientBuilder,
- IggyDuration, IggyExpiry, MaxTopicSize, StreamClient, TopicClient, UserClient,
-};
use std::net::{SocketAddr, TcpStream as StdTcpStream};
-use std::path::{Path, PathBuf};
+use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::thread::sleep;
use std::time::{Duration, Instant};
@@ -52,9 +38,9 @@
/// enough for slow CI runners without hanging the suite indefinitely.
const STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
-/// Cadence for polling `current_config.toml` and the partition
-/// directories. Small enough to keep observable latency under a second
-/// without wasting CPU.
+/// Cadence for polling `current_config.toml` while waiting for the
+/// listener to bind. Small enough to keep observable latency under a
+/// second without wasting CPU.
const POLL_INTERVAL: Duration = Duration::from_millis(50);
/// Spawned binary handle. Drops on test exit kill the child + wait so
@@ -80,11 +66,6 @@
.env("IGGY_HTTP_ENABLED", "false")
.env("IGGY_QUIC_ENABLED", "false")
.env("IGGY_WEBSOCKET_ENABLED", "false")
- // Tighten the reconciler tick so the post-CreateTopic
- // materialisation window stays small under CI latency
- // jitter. Hard-coded 200ms is still way above the per-tick
- // re-read cost yet small enough to bound test wall-time.
- .env("IGGY_SYSTEM_SHARDING_RECONCILE_PERIODIC_INTERVAL", "200 ms")
.stdout(Stdio::null())
.stderr(Stdio::piped());
@@ -111,10 +92,6 @@
data_dir,
}
}
-
- fn data_path(&self) -> &Path {
- self.data_dir.path()
- }
}
impl std::fmt::Debug for TestServer {
@@ -181,66 +158,8 @@
Ok(addr)
}
-/// Wait until every per-partition directory has been created by the
-/// reconciler. Returns `Ok(())` once all expected paths exist; surfaces
-/// a structured error on timeout listing which paths were missing.
-fn wait_for_partition_dirs(
- data_dir: &Path,
- stream_id: u32,
- topic_id: u32,
- partition_count: u32,
- timeout: Duration,
-) -> Result<(), String> {
- let expected: Vec<PathBuf> = (0..partition_count)
- .map(|partition_id| partition_dir(data_dir, stream_id, topic_id, partition_id))
- .collect();
- let deadline = Instant::now() + timeout;
- loop {
- let missing: Vec<PathBuf> = expected.iter().filter(|p| !p.exists()).cloned().collect();
- if missing.is_empty() {
- return Ok(());
- }
- if Instant::now() >= deadline {
- return Err(format!(
- "timed out after {timeout:?}; reconciler did not materialise: {missing:?}"
- ));
- }
- sleep(POLL_INTERVAL);
- }
-}
-
-fn partition_dir(data_dir: &Path, stream_id: u32, topic_id: u32, partition_id: u32) -> PathBuf {
- data_dir
- .join("streams")
- .join(stream_id.to_string())
- .join("topics")
- .join(topic_id.to_string())
- .join("partitions")
- .join(partition_id.to_string())
-}
-
-async fn connected_client(server_addr: SocketAddr) -> IggyClient {
- let client = IggyClientBuilder::new()
- .with_tcp()
- .with_server_address(server_addr.to_string())
- .with_auto_sign_in(AutoLogin::Disabled)
- .with_reconnection_max_retries(Some(3))
- .with_reconnection_interval(IggyDuration::from(200_000_u64))
- .build()
- .expect("TCP client build");
- client.connect().await.expect("SDK TCP connect");
- client
- .login_user("iggy", "iggy")
- .await
- .expect("SDK login as root");
- client
-}
-
/// Spawn `iggy-server-ng` and verify the production binary bootstraps
-/// cleanly. Proves the test infrastructure (binary path, env-var
-/// overrides, runtime config discovery) is wired correctly so the
-/// `#[ignore]`d full end-to-end test below can be flipped on the
-/// moment the wire bridge lands.
+/// cleanly.
///
/// Specifically asserts:
/// * The binary path is reachable via `cargo_bin`.
@@ -263,73 +182,3 @@
let _ = StdTcpStream::connect_timeout(&server.tcp_addr, Duration::from_secs(1))
.expect("server-ng TCP listener must accept connections post-bootstrap");
}
-
-/// End-to-end smoke test against a live `iggy-server-ng` binary.
-///
-/// Currently gated `#[ignore]` because server-ng's client-facing wire
-/// surface is not yet SDK-compatible: the SDK's TCP framing (length-
-/// prefixed binary, see `core/sdk/src/tcp/tcp_client.rs`) does not match
-/// the server-ng client listener which speaks the consensus
-/// [`RequestHeader`] / [`PrepareHeader`] frame layout. The TCP socket
-/// accepts, but `login_user` either hangs on the framing read or
-/// retries until the SDK surfaces
-/// [`iggy_common::IggyError::CannotEstablishConnection`].
-///
-/// The structural pieces this test exercises are still useful as
-/// infrastructure once the wire bridge lands (tracked as a follow-up):
-/// * tempdir + ephemeral-port spawn of the production binary
-/// * `runtime/current_config.toml` port discovery
-/// * SDK-driven `CreateStream` + `CreateTopic` round-trip
-/// * on-disk hierarchy assertion proving the reconciler
-/// materialised every partition end-to-end
-///
-/// Flip the `#[ignore]` to active once `iggy-server-ng` accepts SDK
-/// frames; no other changes should be needed.
-#[ignore = "server-ng client wire surface is not yet SDK-compatible"]
-#[tokio::test(flavor = "current_thread")]
-async fn reconciler_materialises_partitions_on_create_topic_e2e() {
- let server = TestServer::start();
- let client = connected_client(server.tcp_addr).await;
-
- // CreateStream commits a `CreateStream` op; the metadata STM
- // assigns slab key 0 (first stream on a fresh server). The notifier
- // does NOT broadcast a `MetadataCommitTick` for plain stream commits
- // (the reconciler only cares about partition-shaped ops) but the
- // call validates the wire is reachable before we move on.
- client
- .create_stream("e2e-stream")
- .await
- .expect("create stream");
-
- // CreateTopic commits as `CreateTopicWithAssignments` after the
- // primary's allocator stamps consensus_group_id values; the
- // notifier broadcasts `MetadataCommitTick`, the reconciler wakes,
- // and `build_partition_fresh` calls
- // `create_partition_file_hierarchy` for each assigned partition.
- let partitions: u32 = 3;
- client
- .create_topic(
- &"e2e-stream".try_into().expect("stream identifier"),
- "e2e-topic",
- partitions,
- CompressionAlgorithm::default(),
- None,
- IggyExpiry::ServerDefault,
- MaxTopicSize::Custom(IggyByteSize::from(1024_u64 * 1024 * 1024)),
- )
- .await
- .expect("create topic");
-
- // Reconciler is asynchronous: wait for the on-disk hierarchy to
- // appear. The 200ms periodic tick configured in `TestServer::start`
- // bounds the worst-case wait to ~two ticks even if the post-commit
- // wake-up frame got coalesced.
- wait_for_partition_dirs(
- server.data_path(),
- /* stream_id */ 0,
- /* topic_id */ 0,
- partitions,
- Duration::from_secs(5),
- )
- .expect("reconciler materialised all partition directories");
-}