Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ protected boolean executeCqlStatement(SimpleStatement... statements) {
*/
private synchronized CqlSession createDriverSession() {
if (cqlSession == null) {
int port = Integer.getInteger(IntegrationTestUtils.CASSANDRA_CQL_PORT_PROP);
int port = getCassandraCqlPort();
String dc;
if (StargateTestResource.isDse() || StargateTestResource.isHcd()) {
dc = "dc1";
Expand All @@ -369,6 +369,14 @@ private synchronized CqlSession createDriverSession() {
return cqlSession;
}

/**
* Gets the Cassandra CQL port. Subclasses can override this if the port is not available via
* standard system properties.
*/
protected int getCassandraCqlPort() {
return Integer.getInteger(IntegrationTestUtils.CASSANDRA_CQL_PORT_PROP);
}

/** Helper method for determining if lexical search is available for the database backend */
protected boolean isLexicalAvailableForDB() {
return !"true".equals(System.getProperty("testing.db.lexical-disabled"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package io.stargate.sgv2.jsonapi.api.v1;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

import com.github.dockerjava.api.DockerClient;
import io.quarkus.logging.Log;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.stargate.sgv2.jsonapi.testresource.DseTestResource;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;

@QuarkusIntegrationTest
@QuarkusTestResource(
value = SessionEvictionIntegrationTest.SessionEvictionTestResource.class,
restrictToAnnotatedClass = true)
public class SessionEvictionIntegrationTest extends AbstractCollectionIntegrationTestBase {

/**
* A specialized TestResource that spins up a new HCD/DSE container exclusively for this test
* class.
*
* <p>Unlike the standard {@link DseTestResource} used by other tests, this resource ensures a
* dedicated database instance. This isolation is crucial because this test involves destructive
* operations that would negatively impact other tests sharing a common database.
*/
public static class SessionEvictionTestResource extends DseTestResource {

/**
* Holds the reference to the container started by this resource.
*
* <p>This field is {@code static} to act as a bridge between the {@link QuarkusTestResource}
* lifecycle (which manages the resource instance) and the test instance (where we need to
* access the container to perform operations).
*/
private static GenericContainer<?> sessionEvictionCassandraContainer;

/**
* Starts the container and captures the reference.
*
* <p>We override this method to capture the container instance created by the superclass into
* our static {@link #sessionEvictionCassandraContainer} field, making it accessible to the test
* methods.
*/
@Override
public Map<String, String> start() {
Map<String, String> props = super.start();
sessionEvictionCassandraContainer = super.getCassandraContainer();
return props;
}

/**
* Overridden to strictly prevent system property pollution.
*
* <p>The standard {@link DseTestResource} publishes connection details (like CQL port) to
* global System Properties. Since this test runs in parallel with others, publishing our
* isolated container's details would overwrite the shared container's configuration, causing
* other tests to connect to this container (which we are about to kill), leading to random
* failures in the test suite.
*/
@Override
protected void exposeSystemProperties(Map<String, String> props) {
// No-op: Do not expose system properties to avoid interfering with other tests running in
// parallel
}

public static GenericContainer<?> getSessionEvictionCassandraContainer() {
return sessionEvictionCassandraContainer;
}
}

/**
* Overridden to ensure we connect to the isolated container created for this test.
*
* <p>The base class implementation relies on global system properties, which point to the shared
* database container. To ensure this test correctly interacts with its dedicated container, we
* must retrieve the port directly from the isolated container instance.
*/
@Override
protected int getCassandraCqlPort() {
GenericContainer<?> container =
SessionEvictionTestResource.getSessionEvictionCassandraContainer();
if (container == null) {
throw new IllegalStateException("Session eviction IT Cassandra container not started!");
}
return container.getMappedPort(9042);
}

@Test
public void testSessionEvictionOnAllNodesFailed() throws Exception {

// 1. Insert initial data to ensure the database is healthy before the test
insertDoc(
"""
{
"insertOne": {
"document": {
"name": "before_crash"
}
}
}
""");

// 2. Stop the container to simulate DB failure
// IMPORTANT: We use dockerClient.stopContainerCmd() instead of container.stop().
// - container.stop() (Testcontainers) removes the container, so a restart would create a NEW
// container with a NEW random port.
// - dockerClient.stopContainerCmd() (Docker API) simply halts the process but keeps the
// container (and its port mapping) intact.
GenericContainer<?> dbContainer =
SessionEvictionTestResource.getSessionEvictionCassandraContainer();
DockerClient dockerClient = dbContainer.getDockerClient();
String containerId = dbContainer.getContainerId();

Log.error("Stopping Database Container to simulate failure...");
dockerClient.stopContainerCmd(containerId).exec();

try {
// 3. Verify failure: The application should receive a 500 error/AllNodesFailedException
givenHeadersPostJsonThen(
"""
{
"insertOne": {
"document": {
"name": "after_crash"
}
}
}
""")
.statusCode(500)
.body("errors[0].message", containsString("No node was available"));

} finally {
// 4. Restart the container to simulate recovery
Log.error("Restarting Database Container to simulate recovery...");
dockerClient.startContainerCmd(containerId).exec();
}

// 5. Wait for the database to become responsive again
waitForDbRecovery();

// 6. Verify Session Recovery: The application should have evicted the bad session
// and created a new one automatically.
insertDoc(
"""
{
"insertOne": {
"document": {
"name": "after_recovery"
}
}
}
""");

Log.error("Test Passed: Session recovered after DB restart.");
}

/** Polls the database until it becomes responsive again. */
private void waitForDbRecovery() {
Log.error("Waiting for DB to recover...");
long start = System.currentTimeMillis();
long timeout = 60000; // 60 seconds timeout

while (System.currentTimeMillis() - start < timeout) {
try {
// Perform a lightweight check (e.g., an empty find) to see if DB responds
String json =
"""
{
"find": {
"filter": {}
}
}
""";

int statusCode =
given()
.port(getTestPort())
.headers(getHeaders())
.contentType(io.restassured.http.ContentType.JSON)
.body(json)
.when()
.post(CollectionResource.BASE_PATH, keyspaceName, collectionName)
.getStatusCode();

// 200 OK means the DB handled the request (even if empty result)
if (statusCode == 200) {
Log.error("DB recovered!");
return;
}
} catch (Exception e) {
// Ignore connection errors and continue retrying
}

try {
Thread.sleep(1000); // Poll every 1s
} catch (InterruptedException ignored) {
}
}
throw new RuntimeException("DB failed to recover within " + timeout + "ms");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,24 @@ public Map<String, String> start() {
propsBuilder.put("stargate.jsonapi.embedding.providers.vertexai.enabled", "true");
propsBuilder.put(
"stargate.jsonapi.embedding.providers.vertexai.models[0].parameters[0].required", "true");

// Prefer instance-specific configuration from 'env' to support parallel execution and
// isolation. Fall back to global system properties only if instance-specific values are
// missing.
if (this.containerNetworkId.isPresent()) {
String host = System.getProperty("stargate.int-test.cassandra.host");
String host =
env.getOrDefault(
"stargate.int-test.cassandra.host",
System.getProperty("stargate.int-test.cassandra.host"));
propsBuilder.put("stargate.jsonapi.operations.database-config.cassandra-end-points", host);
} else {
int port = Integer.getInteger(IntegrationTestUtils.CASSANDRA_CQL_PORT_PROP);
propsBuilder.put(
"stargate.jsonapi.operations.database-config.cassandra-port", String.valueOf(port));
String port =
env.getOrDefault(
IntegrationTestUtils.CASSANDRA_CQL_PORT_PROP,
System.getProperty(IntegrationTestUtils.CASSANDRA_CQL_PORT_PROP));
propsBuilder.put("stargate.jsonapi.operations.database-config.cassandra-port", port);
}

if (isDse() || isHcd()) {
propsBuilder.put("stargate.jsonapi.operations.database-config.local-datacenter", "dc1");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.collect.ImmutableMap;
import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.stargate.sgv2.jsonapi.api.v1.util.IntegrationTestUtils;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
Expand Down Expand Up @@ -60,7 +61,8 @@ public Map<String, String> start() {
"stargate.int-test.cassandra.host",
cassandraContainer.getCurrentContainerInfo().getConfig().getHostName());
propsBuilder.put(
"stargate.int-test.cassandra.cql-port", cassandraContainer.getMappedPort(9042).toString());
IntegrationTestUtils.CASSANDRA_CQL_PORT_PROP,
cassandraContainer.getMappedPort(9042).toString());
propsBuilder.put("stargate.int-test.cluster.persistence", getPersistenceModule());

// 2. Runtime Limits Config: Override default limits for testing purposes
Expand All @@ -83,11 +85,21 @@ public Map<String, String> start() {
propsBuilder.put("stargate.jsonapi.operations.vectorize-enabled", "true");

ImmutableMap<String, String> props = propsBuilder.build();
props.forEach(System::setProperty);
exposeSystemProperties(props);
LOG.info("Using props map for the integration tests: %s".formatted(props));
return props;
}

/**
* Exposes the provided configuration properties as system properties. This makes the database
* connection and test-specific settings available to the whole test environment.
*
* @param props A map containing the properties to be set as system properties.
*/
protected void exposeSystemProperties(Map<String, String> props) {
props.forEach(System::setProperty);
}

@Override
public void stop() {
if (null != cassandraContainer && !cassandraContainer.isShouldBeReused()) {
Expand All @@ -110,6 +122,17 @@ public static String getPersistenceModule() {
"testing.containers.cluster-persistence", "persistence-cassandra-4.0");
}

/**
* Provides access to the backend database container. This allows subclasses to perform operations
* directly on the container, such as stop/start or pause/unpause it to simulate failure
* scenarios.
*
* @return The generic container instance for the backend database.
*/
protected GenericContainer<?> getCassandraContainer() {
return cassandraContainer;
}

public static boolean isDse() {
String dse = System.getProperty("testing.containers.cluster-dse", null);
return "true".equals(dse);
Expand Down
Loading