Skip to content
Merged
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
4 changes: 4 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ Improvements

* SOLR-16504: Convert CLI tools to use Jetty HTTP 2 client. (Bence Szabo via Eric Pugh)

* SOLR-15737: Solr's collection-level "snapshot" APIs now have v2 equivalents. Snapshots can be created at `POST
/api/collections/collName/snapshots/snapshotName`, listed at `GET /api/collections/collName/snapshots`, and deleted at
`DELETE /api/collections/collName/snapshots/snapshotName`. (John Durham via Jason Gerlowski)

Optimizations
---------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,19 @@
import org.apache.solr.handler.admin.api.CreateAliasAPI;
import org.apache.solr.handler.admin.api.CreateCollectionAPI;
import org.apache.solr.handler.admin.api.CreateCollectionBackupAPI;
import org.apache.solr.handler.admin.api.CreateCollectionSnapshotAPI;
import org.apache.solr.handler.admin.api.CreateShardAPI;
import org.apache.solr.handler.admin.api.DeleteAliasAPI;
import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
import org.apache.solr.handler.admin.api.DeleteCollectionSnapshotAPI;
import org.apache.solr.handler.admin.api.DeleteNodeAPI;
import org.apache.solr.handler.admin.api.DeleteReplicaAPI;
import org.apache.solr.handler.admin.api.DeleteReplicaPropertyAPI;
import org.apache.solr.handler.admin.api.DeleteShardAPI;
import org.apache.solr.handler.admin.api.ForceLeaderAPI;
import org.apache.solr.handler.admin.api.InstallShardDataAPI;
import org.apache.solr.handler.admin.api.ListAliasesAPI;
import org.apache.solr.handler.admin.api.ListCollectionSnapshotsAPI;
import org.apache.solr.handler.admin.api.ListCollectionsAPI;
import org.apache.solr.handler.admin.api.MigrateDocsAPI;
import org.apache.solr.handler.admin.api.ModifyCollectionAPI;
Expand Down Expand Up @@ -1652,96 +1655,60 @@ public Map<String, Object> execute(
(req, rsp, h) -> {
req.getParams().required().check(COLLECTION_PROP, CoreAdminParams.COMMIT_NAME);

String extCollectionName = req.getParams().get(COLLECTION_PROP);
boolean followAliases = req.getParams().getBool(FOLLOW_ALIASES, false);
String collectionName =
followAliases
? h.coreContainer
.getZkController()
.getZkStateReader()
.getAliases()
.resolveSimpleAlias(extCollectionName)
: extCollectionName;
String commitName = req.getParams().get(CoreAdminParams.COMMIT_NAME);
ClusterState clusterState = h.coreContainer.getZkController().getClusterState();
if (!clusterState.hasCollection(collectionName)) {
throw new SolrException(
ErrorCode.BAD_REQUEST,
"Collection '" + collectionName + "' does not exist, no action taken.");
}
final String extCollectionName = req.getParams().get(COLLECTION_PROP);
final boolean followAliases = req.getParams().getBool(FOLLOW_ALIASES, false);
final String commitName = req.getParams().get(CoreAdminParams.COMMIT_NAME);
final String asyncId = req.getParams().get(ASYNC);

SolrZkClient client = h.coreContainer.getZkController().getZkClient();
if (SolrSnapshotManager.snapshotExists(client, collectionName, commitName)) {
throw new SolrException(
ErrorCode.BAD_REQUEST,
"Snapshot with name '"
+ commitName
+ "' already exists for collection '"
+ collectionName
+ "', no action taken.");
}
final CreateCollectionSnapshotAPI createCollectionSnapshotAPI =
new CreateCollectionSnapshotAPI(h.coreContainer, req, rsp);

Map<String, Object> params =
copy(
req.getParams(),
null,
COLLECTION_PROP,
FOLLOW_ALIASES,
CoreAdminParams.COMMIT_NAME);
return params;
final CreateCollectionSnapshotAPI.CreateSnapshotRequestBody requestBody =
new CreateCollectionSnapshotAPI.CreateSnapshotRequestBody();
requestBody.followAliases = followAliases;
requestBody.asyncId = asyncId;

final CreateCollectionSnapshotAPI.CreateSnapshotResponse createSnapshotResponse =
createCollectionSnapshotAPI.createSnapshot(
extCollectionName, commitName, requestBody);

V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, createSnapshotResponse);

return null;
}),
DELETESNAPSHOT_OP(
DELETESNAPSHOT,
(req, rsp, h) -> {
req.getParams().required().check(COLLECTION_PROP, CoreAdminParams.COMMIT_NAME);

String extCollectionName = req.getParams().get(COLLECTION_PROP);
String collectionName =
h.coreContainer
.getZkController()
.getZkStateReader()
.getAliases()
.resolveSimpleAlias(extCollectionName);
ClusterState clusterState = h.coreContainer.getZkController().getClusterState();
if (!clusterState.hasCollection(collectionName)) {
throw new SolrException(
ErrorCode.BAD_REQUEST,
"Collection '" + collectionName + "' does not exist, no action taken.");
}
final String extCollectionName = req.getParams().get(COLLECTION_PROP);
final String commitName = req.getParams().get(CoreAdminParams.COMMIT_NAME);
final boolean followAliases = req.getParams().getBool(FOLLOW_ALIASES, false);
final String asyncId = req.getParams().get(ASYNC);

Map<String, Object> params =
copy(
req.getParams(),
null,
COLLECTION_PROP,
FOLLOW_ALIASES,
CoreAdminParams.COMMIT_NAME);
return params;
final DeleteCollectionSnapshotAPI deleteCollectionSnapshotAPI =
new DeleteCollectionSnapshotAPI(h.coreContainer, req, rsp);

final DeleteCollectionSnapshotAPI.DeleteSnapshotResponse deleteSnapshotResponse =
deleteCollectionSnapshotAPI.deleteSnapshot(
extCollectionName, commitName, followAliases, asyncId);

V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, deleteSnapshotResponse);
return null;
}),
LISTSNAPSHOTS_OP(
LISTSNAPSHOTS,
(req, rsp, h) -> {
req.getParams().required().check(COLLECTION_PROP);

String extCollectionName = req.getParams().get(COLLECTION_PROP);
String collectionName =
h.coreContainer
.getZkController()
.getZkStateReader()
.getAliases()
.resolveSimpleAlias(extCollectionName);
ClusterState clusterState = h.coreContainer.getZkController().getClusterState();
if (!clusterState.hasCollection(collectionName)) {
throw new SolrException(
ErrorCode.BAD_REQUEST,
"Collection '" + collectionName + "' does not exist, no action taken.");
}
final ListCollectionSnapshotsAPI listCollectionSnapshotsAPI =
new ListCollectionSnapshotsAPI(h.coreContainer, req, rsp);

final ListCollectionSnapshotsAPI.ListSnapshotsResponse response =
listCollectionSnapshotsAPI.listSnapshots(req.getParams().get(COLLECTION_PROP));

NamedList<Object> snapshots = new NamedList<Object>();
SolrZkClient client = h.coreContainer.getZkController().getZkClient();
Collection<CollectionSnapshotMetaData> m =
SolrSnapshotManager.listSnapshots(client, collectionName);
for (CollectionSnapshotMetaData meta : m) {
NamedList<Object> snapshots = new NamedList<>();
for (CollectionSnapshotMetaData meta : response.snapshots.values()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] This code is simple enough, but unless I'm missing something it seems like it could be replaced by the V2ApiUtils.squashIntoSolrResponse pattern we use on other APIs. Any particular reason you didn't go that route here?

Copy link
Contributor Author

@jdurham2843 jdurham2843 Apr 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the approach above, because when I tried just squashing, the output I got back wouldn't fully serialize. I essentially would get this back:

	"responseHeader": {
		"status": 0,
		"QTime": 14
	},
	"snapshots": {
		"snapshot2": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@24e1513a",
		"snapshot1": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@50d291df",
		"snapshot9": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@55cf09d4",
		"snapshot8": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@542b33e3",
		"snapshot7": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@7a6dd0dd",
		"snapshot6": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@3ef5ea26",
		"snapshot5": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@13d9d585",
		"snapshot10": "org.apache.solr.core.snapshots.CollectionSnapshotMetaData:org.apache.solr.core.snapshots.CollectionSnapshotMetaData@1e604836"
	}
}

At the time, I couldn't figure out where to make changes to address the serialization problem, so I just sidestepped it. I'd be happy to take another stab at it though!

Copy link
Contributor

@gerlowskija gerlowskija Apr 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. Actually this makes complete sense. The "squash" pattern only knows how to serialize particular types and interfaces, and I guess CollectionSnapshotMetaData isn't one of those. Forget I said anything, as long as the output looks correct 👍

Copy link
Contributor Author

@jdurham2843 jdurham2843 Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, that makes sense. I went back and uncommented the for loop. CollectionSnapshotMetaData does have a toNamedList method, but its not an overriden method. Would it make sense at some point to create some kind of interface that exposes that method and then have it be used by serializers/MessageWriters? I think it would fall outside the scope of this change, but it could make sense if there were other classes out there that behaved the same way.

snapshots.add(meta.getName(), meta.toNamedList());
}

Expand Down Expand Up @@ -2047,7 +2014,10 @@ public Collection<Class<? extends JerseyResource>> getJerseyResources() {
CollectionPropertyAPI.class,
DeleteNodeAPI.class,
ListAliasesAPI.class,
AliasPropertyAPI.class);
AliasPropertyAPI.class,
ListCollectionSnapshotsAPI.class,
CreateCollectionSnapshotAPI.class,
DeleteCollectionSnapshotAPI.class);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ public static void validateZooKeeperAwareCoreContainer(CoreContainer coreContain
}
}

protected String resolveCollectionName(String collName, boolean followAliases) {
final String collectionName =
followAliases
? coreContainer
.getZkController()
.getZkStateReader()
.getAliases()
.resolveSimpleAlias(collName)
: collName;

final ClusterState clusterState = coreContainer.getZkController().getClusterState();
if (!clusterState.hasCollection(collectionName)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"Collection '" + collectionName + "' does not exist, no action taken.");
}

return collectionName;
}

/**
* TODO Taken from CollectionsHandler.handleRequestBody, but its unclear where (if ever) this gets
* cleared.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* 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.
*/
package org.apache.solr.handler.admin.api;

import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.params.CollectionParams;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.snapshots.SolrSnapshotManager;
import org.apache.solr.handler.admin.CollectionsHandler;
import org.apache.solr.jersey.AsyncJerseyResponse;
import org.apache.solr.jersey.JacksonReflectMapWriter;
import org.apache.solr.jersey.PermissionName;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;

/** V2 API for Creating Collection Snapshots. */
@Path("/collections/{collName}/snapshots")
public class CreateCollectionSnapshotAPI extends AdminAPIBase {

@Inject
public CreateCollectionSnapshotAPI(
CoreContainer coreContainer,
SolrQueryRequest solrQueryRequest,
SolrQueryResponse solrQueryResponse) {
super(coreContainer, solrQueryRequest, solrQueryResponse);
}

/** This API is analogous to V1's (POST /solr/admin/collections?action=CREATESNAPSHOT) */
@POST
@Path("/{snapshotName}")
@Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
@PermissionName(COLL_EDIT_PERM)
public CreateSnapshotResponse createSnapshot(
@Parameter(description = "The name of the collection.", required = true)
@PathParam("collName")
String collName,
@Parameter(description = "The name of the snapshot to be created.", required = true)
@PathParam("snapshotName")
String snapshotName,
@RequestBody(description = "Contains user provided parameters", required = true)
CreateSnapshotRequestBody requestBody)
throws Exception {

final CreateSnapshotResponse response = instantiateJerseyResponse(CreateSnapshotResponse.class);
final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
recordCollectionForLogAndTracing(collName, solrQueryRequest);

final String collectionName = resolveCollectionName(collName, requestBody.followAliases);

final SolrZkClient client = coreContainer.getZkController().getZkClient();
if (SolrSnapshotManager.snapshotExists(client, collectionName, snapshotName)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"Snapshot with name '"
+ snapshotName
+ "' already exists for collection '"
+ collectionName
+ "', no action taken.");
}

final ZkNodeProps remoteMessage =
createRemoteMessage(collName, requestBody.followAliases, snapshotName, requestBody.asyncId);
final SolrResponse remoteResponse =
CollectionsHandler.submitCollectionApiCommand(
coreContainer,
coreContainer.getDistributedCollectionCommandRunner(),
remoteMessage,
CollectionParams.CollectionAction.CREATESNAPSHOT,
DEFAULT_COLLECTION_OP_TIMEOUT);

if (remoteResponse.getException() != null) {
throw remoteResponse.getException();
}

response.collection = collName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] Just noticed the values being added to the response here. In general, I've been trying to change the response format as little as possible in these JAX-RS conversion PRs, and this is a bit of a departure from the existing v1 format.

That said, I think it's a change for the better and it's purely additive so it's not a backcompat concern. So let's go with it in this case.

response.followAliases = requestBody.followAliases;
response.snapshotName = snapshotName;
response.requestId = requestBody.asyncId;

return response;
}

/**
* The RequestBody for {@link CreateCollectionSnapshotAPI}'s {@link #createSnapshot(String,
* String, CreateSnapshotRequestBody)}
*/
public static class CreateSnapshotRequestBody implements JacksonReflectMapWriter {
@JsonProperty(value = "followAliases", defaultValue = "false")
public boolean followAliases;

@JsonProperty("async")
public String asyncId;
}

/**
* The Response for {@link CreateCollectionSnapshotAPI}'s {@link #createSnapshot(String, String,
* CreateSnapshotRequestBody)}
*/
public static class CreateSnapshotResponse extends AsyncJerseyResponse {
@Schema(description = "The name of the collection.")
@JsonProperty(COLLECTION_PROP)
String collection;

@Schema(description = "The name of the snapshot to be created.")
@JsonProperty("snapshot")
String snapshotName;

@Schema(description = "A flag that treats the collName parameter as a collection alias.")
@JsonProperty("followAliases")
boolean followAliases;
}

public static ZkNodeProps createRemoteMessage(
String collectionName, boolean followAliases, String snapshotName, String asyncId) {
final Map<String, Object> remoteMessage = new HashMap<>();

remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATESNAPSHOT.toLower());
remoteMessage.put(COLLECTION_PROP, collectionName);
remoteMessage.put(CoreAdminParams.COMMIT_NAME, snapshotName);
remoteMessage.put(FOLLOW_ALIASES, followAliases);
if (asyncId != null) remoteMessage.put(ASYNC, asyncId);

return new ZkNodeProps(remoteMessage);
}
}
Loading