Skip to content
Open
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
11 changes: 11 additions & 0 deletions changelog/unreleased/SOLR-16398-enable-disable-replication.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title:
Add V2 JAX-RS API to enable or disable replication (PUT /api/cores/coreName/replication, requires the core-admin-edit permission)
type: added
authors:
- name: Jalaz Kumar
nick: jaykay12
- name: Eric Pugh
nick: epugh
links:
- name: SOLR-16398
url: https://issues.apache.org/jira/browse/SOLR-16398
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
Expand All @@ -33,7 +35,9 @@
import org.apache.solr.client.api.model.FileListResponse;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
import org.apache.solr.client.api.model.ReplicationEnabledRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.client.api.util.CoreApiParameters;

@Path("/cores/{coreName}/replication")
Expand Down Expand Up @@ -124,4 +128,12 @@ StreamingOutput fetchFile(
@Parameter(description = "The generation number of the index", required = false)
@QueryParam("generation")
Long gen);

@PUT
@CoreApiParameters
@Operation(
summary = "Enable or disable replication.",
tags = {"replication"})
SolrJerseyResponse setReplicationEnabled(
@RequestBody(required = true) ReplicationEnabledRequestBody requestBody);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.client.api.model;

import com.fasterxml.jackson.annotation.JsonProperty;

/** Request body for enabling and disabling index replication from leader to followers. */
public class ReplicationEnabledRequestBody {

@JsonProperty(required = true)
public Boolean enabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,10 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw
true,
true);
} else if (CMD_ENABLE_REPL.equalsIgnoreCase(command)) {
replicationEnabled.set(true);
setReplicationEnabled(true);
rsp.add(STATUS, OK_STATUS);
} else if (CMD_DISABLE_REPL.equalsIgnoreCase(command)) {
replicationEnabled.set(false);
setReplicationEnabled(false);
rsp.add(STATUS, OK_STATUS);
}
}
Expand Down Expand Up @@ -1035,6 +1035,10 @@ public void initializeMetrics(SolrMetricsContext parentContext, Attributes attri
downloadSpeedMetric);
}

public void setReplicationEnabled(boolean enabled) {
replicationEnabled.set(enabled);
}

// TODO Should a failure retrieving any piece of info mark the overall request as a failure? Is
// there a core set of values that are required to make a response here useful?
/** Used for showing statistics and progress information. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.solr.handler.admin.api;

import static org.apache.solr.security.PermissionNameProvider.Name.CORE_EDIT_PERM;
import static org.apache.solr.security.PermissionNameProvider.Name.CORE_READ_PERM;

import jakarta.inject.Inject;
Expand All @@ -26,7 +27,9 @@
import org.apache.solr.client.api.model.FileListResponse;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
import org.apache.solr.client.api.model.ReplicationEnabledRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrCore;
import org.apache.solr.jersey.PermissionName;
Expand Down Expand Up @@ -94,4 +97,10 @@ public StreamingOutput fetchFile(
return doFetchFile(
filePath, dirType, offset, len, compression, checksum, maxWriteMBPerSec, gen);
}

@Override
@PermissionName(CORE_EDIT_PERM)
public SolrJerseyResponse setReplicationEnabled(ReplicationEnabledRequestBody requestBody) {
return doSetReplicationEnabled(requestBody);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
import org.apache.solr.client.api.model.FileMetaData;
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
import org.apache.solr.client.api.model.ReplicationEnabledRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.FastOutputStream;
import org.apache.solr.common.util.IOUtils;
Expand Down Expand Up @@ -102,6 +104,15 @@ protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollo
return replicationHandler.getReplicationDetails(showFollowerDetails);
}

protected SolrJerseyResponse doSetReplicationEnabled(ReplicationEnabledRequestBody requestBody) {
ensureRequiredRequestBodyProvided(requestBody);
ensureRequiredParameterProvided("enabled", requestBody.enabled);
ReplicationHandler replicationHandler =
(ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH);
replicationHandler.setReplicationEnabled(requestBody.enabled);
return instantiateJerseyResponse(SolrJerseyResponse.class);
}

protected RestoreStatusResponse doFetchRestoreStatus() {
ReplicationHandler replicationHandler =
(ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.apache.solr.client.api.model.IndexVersionResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse;
import org.apache.solr.client.api.model.ReplicationDetailsResponse.ReplicationDetails;
import org.apache.solr.client.api.model.ReplicationEnabledRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrCore;
Expand Down Expand Up @@ -103,6 +104,31 @@ public void testFetchReplicationDetails() {
assertSame(expectedResponse, withFollowerFalse);
}

@Test
public void testSetReplicationEnabled() {
for (boolean enabled : new boolean[] {true, false}) {
final var body = new ReplicationEnabledRequestBody();
body.enabled = enabled;
assertNotNull(coreReplicationAPI.setReplicationEnabled(body));
}
}

@Test
public void testSetReplicationEnabledRejectsNullBody() {
SolrException ex =
expectThrows(SolrException.class, () -> coreReplicationAPI.setReplicationEnabled(null));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
}

@Test
public void testSetReplicationEnabledRejectsBadBody() {
final var emptyBody = new ReplicationEnabledRequestBody();
SolrException ex =
expectThrows(
SolrException.class, () -> coreReplicationAPI.setReplicationEnabled(emptyBody));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
}

@Test
public void testGetRestoreStatus() {
RestoreStatusResponse expected = new RestoreStatusResponse();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.core.CoreContainer.ALLOW_PATHS_SYSPROP;

import org.apache.solr.SolrTestCase;
import org.apache.solr.client.solrj.RemoteSolrException;
import org.apache.solr.client.solrj.request.ReplicationApi;
import org.apache.solr.common.SolrException;
import org.apache.solr.util.ExternalPaths;
import org.apache.solr.util.SolrJettyTestRule;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;

/** HTTP tests for {@code PUT /api/cores/coreName/replication} via the generated SolrJ request. */
public class SetReplicationEnabledAPITest extends SolrTestCase {

private static final String CORE_NAME = "setReplicationEnabledApiTestCore";

@ClassRule public static final SolrJettyTestRule solrTestRule = new SolrJettyTestRule();

@BeforeClass
public static void setupSolr() throws Exception {
System.setProperty(ALLOW_PATHS_SYSPROP, ExternalPaths.SERVER_HOME.toAbsolutePath().toString());
solrTestRule.startSolr(createTempDir());
solrTestRule.newCollection(CORE_NAME).withConfigSet(ExternalPaths.DEFAULT_CONFIGSET).create();
}

@Test
public void testDisableThenEnableReplication() throws Exception {
assertEquals(Boolean.TRUE, fetchReplicationEnabledStatus());

setReplicationEnabled(false);
assertEquals(Boolean.FALSE, fetchReplicationEnabledStatus());

setReplicationEnabled(true);
assertEquals(Boolean.TRUE, fetchReplicationEnabledStatus());
}

@Test
public void testMissingEnabledIsRejectedAndStateUnchanged() throws Exception {
assertEquals(Boolean.TRUE, fetchReplicationEnabledStatus());

var request = new ReplicationApi.SetReplicationEnabled(CORE_NAME);
RemoteSolrException ex =
expectThrows(
RemoteSolrException.class,
() -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());

assertEquals(Boolean.TRUE, fetchReplicationEnabledStatus());
}

@Test
public void testNullEnabledIsRejectedAndStateUnchanged() throws Exception {
assertEquals(Boolean.TRUE, fetchReplicationEnabledStatus());

var request = new ReplicationApi.SetReplicationEnabled(CORE_NAME);
request.setEnabled(null);
RemoteSolrException ex =
expectThrows(
RemoteSolrException.class,
() -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());

assertEquals(Boolean.TRUE, fetchReplicationEnabledStatus());
}

private static void setReplicationEnabled(boolean enabled) throws Exception {
var request = new ReplicationApi.SetReplicationEnabled(CORE_NAME);
request.setEnabled(enabled);
var response = request.process(solrTestRule.getSolrClient(CORE_NAME));
assertNull(response.error);
}

private static Boolean fetchReplicationEnabledStatus() throws Exception {
var request = new ReplicationApi.FetchReplicationDetails(CORE_NAME);
return request.process(solrTestRule.getSolrClient(CORE_NAME)).details.leader.replicationEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,52 @@ You can use the HTTP commands below to control the ReplicationHandler's operatio
`enablereplication`::
Enable replication on the "leader" for all its followers.
+
[tabs#enablereplication-request]
======
V1 API::
+
====
[source,bash]
----
http://{leaderHost}:{port}/solr/{coreName}/replication?command=enablereplication
----
====

V2 API::
+
====
[source,bash]
----
curl -X PUT -H 'Content-Type: application/json' -d '{"enabled": true}' \
http://{leaderHost}:{port}/api/cores/{coreName}/replication
----
====
======

`disablereplication`::
Disable replication on the leader for all its followers.
+
[tabs#disablereplication-request]
======
V1 API::
+
====
[source,bash]
----
http://{leaderHost}:{port}/solr/{coreName}/replication?command=disablereplication
----
====

V2 API::
+
====
[source,bash]
----
curl -X PUT -H 'Content-Type: application/json' -d '{"enabled": false}' \
http://{leaderHost}:{port}/api/cores/{coreName}/replication
----
====
======

`indexversion`::
Return the version of the latest replicatable index on the specified leader or follower.
Expand Down
11 changes: 10 additions & 1 deletion solr/webapp/web/js/angular/controllers/replication.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

solrAdminApp.controller('ReplicationController',
function($scope, $rootScope, $routeParams, $interval, $timeout, Replication, Constants) {
function($scope, $rootScope, $routeParams, $interval, $timeout, Replication, ReplicationV2, Constants, ApiErrorHandler) {
$scope.resetMenu("replication", Constants.IS_CORE_PAGE);

$scope.iterationCount = 1;
Expand Down Expand Up @@ -58,6 +58,15 @@ solrAdminApp.controller('ReplicationController',
Replication.command({core:$routeParams.core, command:command}, function(data){$scope.refresh()});
}

$scope.setReplicationEnabled = function(enabled) {
ReplicationV2.setReplicationEnabled($routeParams.core, {enabled: enabled}, function(error, data, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
$scope.refresh();
});
});
}

$scope.showIterations = function() { $scope.iterationCount = 100000}; // limitTo should accept undefined, but doesn't work.
$scope.hideIterations = function() { $scope.iterationCount = 1};

Expand Down
6 changes: 6 additions & 0 deletions solr/webapp/web/js/angular/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ solrAdminServices.factory('Metrics',
delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
return new solrApi.NodeApi();
})
.factory('ReplicationV2',
function() {
solrApi.ApiClient.instance.basePath = '/api';
delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
return new solrApi.ReplicationApi();
})
.factory('Replication',
['$resource', function($resource) {
return $resource(':core/replication', {'wt':'json', core: "@core", '_':Date.now()}, {
Expand Down
4 changes: 2 additions & 2 deletions solr/webapp/web/partials/replication.html
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@
</div>

<div class="leaderOnly" ng-show="!isFollower">
<button class="optional disable-replication warn" ng-click="execute('disablereplication')" ng-show="leader.replicationEnabled"><span>Disable Replication</span></button>
<button class="optional enable-replication warn" ng-click="execute('enablereplication')" ng-show="!leader.replicationEnabled"><span>Enable Replication</span></button>
<button class="optional disable-replication warn" ng-click="setReplicationEnabled(false)" ng-show="leader.replicationEnabled"><span>Disable Replication</span></button>
<button class="optional enable-replication warn" ng-click="setReplicationEnabled(true)" ng-show="!leader.replicationEnabled"><span>Enable Replication</span></button>
</div>

</div>
Expand Down
Loading