Skip to content

Commit

Permalink
Add API endpoint that allows altering the RF settings of a Keyspace
Browse files Browse the repository at this point in the history
  • Loading branch information
Eduard Tudenhoefner committed Jul 7, 2020
1 parent 7df937b commit 5bd7171
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,15 @@ public String getLocalDataCenter()
{
return ShimLoader.instance.get().getLocalDataCenter();
}

@Rpc(name = "alterKeyspace")
public void alterKeyspace(@RpcParam(name="keyspaceName") String keyspaceName, @RpcParam(name="replicationSettings") Map<String, Integer> replicationSettings) throws IOException
{
logger.debug("Creating keyspace {} with replication settings {}", keyspaceName, replicationSettings);

ShimLoader.instance.get().processQuery(SchemaBuilder.alterKeyspace(keyspaceName)
.withNetworkTopologyStrategy(replicationSettings)
.asCql(),
ConsistencyLevel.ONE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@

import com.datastax.mgmtapi.CqlService;
import com.datastax.mgmtapi.ManagementApplication;
import com.datastax.mgmtapi.resources.models.CreateKeyspaceRequest;
import com.datastax.mgmtapi.resources.models.CreateOrAlterKeyspaceRequest;
import com.datastax.mgmtapi.resources.models.KeyspaceRequest;
import io.swagger.v3.oas.annotations.Operation;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpStatus;

@Path("/api/v0/ops/keyspace")
Expand Down Expand Up @@ -79,7 +78,7 @@ public Response cleanup(KeyspaceRequest keyspaceRequest)
@Operation(summary = "Load newly placed SSTables to the system without restart")
public Response refresh(@QueryParam(value="keyspaceName")String keyspaceName, @QueryParam(value="table")String table)
{
try
return NodeOpsResources.handle(() ->
{
if (StringUtils.isBlank(keyspaceName))
{
Expand All @@ -94,50 +93,58 @@ public Response refresh(@QueryParam(value="keyspaceName")String keyspaceName, @Q
cqlService.executePreparedStatement(app.dbUnixSocketFile, "CALL NodeOps.loadNewSSTables(?, ?)", keyspaceName, table);

return Response.ok("OK").build();
}
catch (ConnectionClosedException e)
{
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity("Internal connection to Cassandra closed").build();
}
catch (Throwable t)
{
logger.error("Error when executing request", t);
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity(t.getLocalizedMessage()).build();
}
});
}

@POST
@Path("/create")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Create a new keyspace with the given name and replication settings")
public Response create(CreateKeyspaceRequest createKeyspaceRequest)
public Response create(CreateOrAlterKeyspaceRequest createOrAlterKeyspaceRequest)
{
try
return NodeOpsResources.handle(() ->
{
if (StringUtils.isBlank(createKeyspaceRequest.keyspaceName))
if (StringUtils.isBlank(createOrAlterKeyspaceRequest.keyspaceName))
{
return Response.status(HttpStatus.SC_BAD_REQUEST).entity("Keyspace creation failed. Non-empty 'keyspace_name' must be provided").build();
}

if (null == createKeyspaceRequest.replicationSettings || createKeyspaceRequest.replicationSettings.isEmpty())
if (null == createOrAlterKeyspaceRequest.replicationSettings || createOrAlterKeyspaceRequest.replicationSettings.isEmpty())
{
return Response.status(HttpStatus.SC_BAD_REQUEST).entity("Keyspace creation failed. 'replication_settings' must be provided").build();
}

cqlService.executePreparedStatement(app.dbUnixSocketFile, "CALL NodeOps.createKeyspace(?, ?)",
createKeyspaceRequest.keyspaceName, createKeyspaceRequest.replicationSettingsAsMap());
createOrAlterKeyspaceRequest.keyspaceName, createOrAlterKeyspaceRequest.replicationSettingsAsMap());

return Response.ok("OK").build();
}
catch (ConnectionClosedException e)
{
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity("Internal connection to Cassandra closed").build();
}
catch (Throwable t)
});
}

@POST
@Path("/alter")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(summary = "Alter the replication settings of an existing keyspace")
public Response alter(CreateOrAlterKeyspaceRequest createOrAlterKeyspaceRequest)
{
return NodeOpsResources.handle(() ->
{
logger.error("Error when executing request", t);
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity(t.getLocalizedMessage()).build();
}
if (StringUtils.isBlank(createOrAlterKeyspaceRequest.keyspaceName))
{
return Response.status(HttpStatus.SC_BAD_REQUEST).entity("Altering Keyspace failed. Non-empty 'keyspace_name' must be provided").build();
}

if (null == createOrAlterKeyspaceRequest.replicationSettings || createOrAlterKeyspaceRequest.replicationSettings.isEmpty())
{
return Response.status(HttpStatus.SC_BAD_REQUEST).entity("Altering Keyspace failed. 'replication_settings' must be provided").build();
}

cqlService.executePreparedStatement(app.dbUnixSocketFile, "CALL NodeOps.alterKeyspace(?, ?)",
createOrAlterKeyspaceRequest.keyspaceName, createOrAlterKeyspaceRequest.replicationSettingsAsMap());

return Response.ok("OK").build();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class CreateKeyspaceRequest implements Serializable
public class CreateOrAlterKeyspaceRequest implements Serializable
{
@JsonProperty(value = "keyspace_name", required = true)
public final String keyspaceName;
Expand All @@ -17,7 +17,7 @@ public class CreateKeyspaceRequest implements Serializable
public final List<ReplicationSetting> replicationSettings;

@JsonCreator
public CreateKeyspaceRequest(@JsonProperty("keyspace_name") String keyspaceName, @JsonProperty("replication_settings") List<ReplicationSetting> replicationSettings)
public CreateOrAlterKeyspaceRequest(@JsonProperty("keyspace_name") String keyspaceName, @JsonProperty("replication_settings") List<ReplicationSetting> replicationSettings)
{
this.keyspaceName = keyspaceName;
this.replicationSettings = replicationSettings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@
import com.datastax.mgmtapi.resources.NodeOpsResources;
import com.datastax.mgmtapi.resources.TableOpsResources;
import com.datastax.mgmtapi.resources.models.CompactRequest;
import com.datastax.mgmtapi.resources.models.CreateKeyspaceRequest;
import com.datastax.mgmtapi.resources.models.CreateOrAlterKeyspaceRequest;
import com.datastax.mgmtapi.resources.models.KeyspaceRequest;
import com.datastax.mgmtapi.resources.models.ReplicationSetting;
import com.datastax.mgmtapi.resources.models.ScrubRequest;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpStatus;
import org.assertj.core.api.Assertions;
import org.jboss.resteasy.core.messagebody.WriterUtility;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
Expand Down Expand Up @@ -1000,7 +998,7 @@ public void testGetStreamInfo() throws Exception
@Test
public void testCreatingKeyspace() throws IOException, URISyntaxException
{
CreateKeyspaceRequest keyspaceRequest = new CreateKeyspaceRequest("myKeyspace", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));
CreateOrAlterKeyspaceRequest keyspaceRequest = new CreateOrAlterKeyspaceRequest("myKeyspace", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));

Context context = setup();

Expand All @@ -1019,7 +1017,7 @@ public void testCreatingKeyspace() throws IOException, URISyntaxException
@Test
public void testCreatingEmptyKeyspaceShouldFail() throws IOException, URISyntaxException
{
CreateKeyspaceRequest keyspaceRequest = new CreateKeyspaceRequest("", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));
CreateOrAlterKeyspaceRequest keyspaceRequest = new CreateOrAlterKeyspaceRequest("", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));

Context context = setup();

Expand All @@ -1036,7 +1034,7 @@ public void testCreatingEmptyKeyspaceShouldFail() throws IOException, URISyntaxE
@Test
public void testCreatingEmptyReplicationSettingsShouldFail() throws IOException, URISyntaxException
{
CreateKeyspaceRequest keyspaceRequest = new CreateKeyspaceRequest("TestKeyspace", Collections.emptyList());
CreateOrAlterKeyspaceRequest keyspaceRequest = new CreateOrAlterKeyspaceRequest("TestKeyspace", Collections.emptyList());

Context context = setup();

Expand All @@ -1050,6 +1048,59 @@ public void testCreatingEmptyReplicationSettingsShouldFail() throws IOException,
assertThat(response.getContentAsString()).contains("Keyspace creation failed. 'replication_settings' must be provided");
}

@Test
public void testAlteringKeyspace() throws IOException, URISyntaxException
{
CreateOrAlterKeyspaceRequest keyspaceRequest = new CreateOrAlterKeyspaceRequest("myKeyspace", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));

Context context = setup();

when(context.cqlService.executePreparedStatement(any(), anyString()))
.thenReturn(null);

String keyspaceRequestAsJSON = WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON);
MockHttpResponse response = postWithBody("/ops/keyspace/alter", keyspaceRequestAsJSON, context);

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
assertThat(response.getContentAsString()).contains("OK");

verify(context.cqlService).executePreparedStatement(any(), eq("CALL NodeOps.alterKeyspace(?, ?)"), any());
}

@Test
public void testAlteringEmptyKeyspaceShouldFail() throws IOException, URISyntaxException
{
CreateOrAlterKeyspaceRequest keyspaceRequest = new CreateOrAlterKeyspaceRequest("", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));

Context context = setup();

when(context.cqlService.executePreparedStatement(any(), anyString()))
.thenReturn(null);

String keyspaceRequestAsJSON = WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON);
MockHttpResponse response = postWithBody("/ops/keyspace/alter", keyspaceRequestAsJSON, context);

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
assertThat(response.getContentAsString()).contains("Altering Keyspace failed. Non-empty 'keyspace_name' must be provided");
}

@Test
public void testAlteringEmptyReplicationSettingsShouldFail() throws IOException, URISyntaxException
{
CreateOrAlterKeyspaceRequest keyspaceRequest = new CreateOrAlterKeyspaceRequest("TestKeyspace", Collections.emptyList());

Context context = setup();

when(context.cqlService.executePreparedStatement(any(), anyString()))
.thenReturn(null);

String keyspaceRequestAsJSON = WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON);
MockHttpResponse response = postWithBody("/ops/keyspace/alter", keyspaceRequestAsJSON, context);

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
assertThat(response.getContentAsString()).contains("Altering Keyspace failed. 'replication_settings' must be provided");
}

private MockHttpResponse postWithBody(String path, String body, Context context) throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
.post(ROOT_PATH + path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@
import com.datastax.mgmtapi.helpers.IntegrationTestUtils;
import com.datastax.mgmtapi.helpers.NettyHttpClient;
import com.datastax.mgmtapi.resources.models.CompactRequest;
import com.datastax.mgmtapi.resources.models.CreateKeyspaceRequest;
import com.datastax.mgmtapi.resources.models.CreateOrAlterKeyspaceRequest;
import com.datastax.mgmtapi.resources.models.KeyspaceRequest;
import com.datastax.mgmtapi.resources.models.ReplicationSetting;
import com.datastax.mgmtapi.resources.models.ScrubRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.utils.URIBuilder;
import org.assertj.core.api.Assertions;
import org.jboss.resteasy.core.messagebody.WriterUtility;

import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -441,8 +440,33 @@ public void testCreateKeyspace() throws IOException, URISyntaxException
String localDc = client.get(new URIBuilder(BASE_PATH + "/metadata/localdc").build().toURL())
.thenApply(this::responseAsString).join();

createKeyspace(client, localDc, "someTestKeyspace");
}

@Test
public void testAlterKeyspace() throws IOException, URISyntaxException
{
assumeTrue(IntegrationTestUtils.shouldRun());
ensureStarted();

NettyHttpClient client = new NettyHttpClient(BASE_URL);
String localDc = client.get(new URIBuilder(BASE_PATH + "/metadata/localdc").build().toURL())
.thenApply(this::responseAsString).join();

String ks = "alteringKeyspaceTest";
createKeyspace(client, localDc, ks);

CreateKeyspaceRequest request = new CreateKeyspaceRequest("someTestKeyspace", Arrays.asList(new ReplicationSetting(localDc, 1)));
CreateOrAlterKeyspaceRequest request = new CreateOrAlterKeyspaceRequest(ks, Arrays.asList(new ReplicationSetting(localDc, 3)));
String requestAsJSON = WriterUtility.asString(request, MediaType.APPLICATION_JSON);

boolean requestSuccessful = client.post(new URIBuilder(BASE_PATH + "/ops/keyspace/alter").build().toURL(), requestAsJSON)
.thenApply(r -> r.status().code() == HttpStatus.SC_OK).join();
assertTrue(requestSuccessful);
}

private void createKeyspace(NettyHttpClient client, String localDc, String keyspaceName) throws IOException, URISyntaxException
{
CreateOrAlterKeyspaceRequest request = new CreateOrAlterKeyspaceRequest(keyspaceName, Arrays.asList(new ReplicationSetting(localDc, 1)));
String requestAsJSON = WriterUtility.asString(request, MediaType.APPLICATION_JSON);

URI uri = new URIBuilder(BASE_PATH + "/ops/keyspace/create")
Expand Down

0 comments on commit 5bd7171

Please sign in to comment.