This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new ee7e593311e SOLR-16398: Add V2 JAX-RS API to enable or disable
replication (#5219)
ee7e593311e is described below
commit ee7e593311ef5d19ad503fd19d83d97dd4e59578
Author: Jalaz Kumar <[email protected]>
AuthorDate: Tue Oct 27 22:20:09 2026 +0530
SOLR-16398: Add V2 JAX-RS API to enable or disable replication (#5219)
Co-authored-by: Eric Pugh <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
.../SOLR-16398-enable-disable-replication.yml | 11 +++
.../solr/client/api/endpoint/ReplicationApis.java | 12 +++
.../api/model/ReplicationEnabledRequestBody.java | 26 ++++++
.../apache/solr/handler/ReplicationHandler.java | 8 +-
.../solr/handler/admin/api/CoreReplication.java | 9 ++
.../solr/handler/admin/api/ReplicationAPIBase.java | 11 +++
.../handler/admin/api/CoreReplicationAPITest.java | 26 ++++++
.../admin/api/SetReplicationEnabledAPITest.java | 96 ++++++++++++++++++++++
.../pages/user-managed-index-replication.adoc | 38 +++++++++
.../web/js/angular/controllers/replication.js | 11 ++-
solr/webapp/web/js/angular/services.js | 6 ++
solr/webapp/web/partials/replication.html | 4 +-
12 files changed, 253 insertions(+), 5 deletions(-)
diff --git a/changelog/unreleased/SOLR-16398-enable-disable-replication.yml
b/changelog/unreleased/SOLR-16398-enable-disable-replication.yml
new file mode 100644
index 00000000000..3478a532225
--- /dev/null
+++ b/changelog/unreleased/SOLR-16398-enable-disable-replication.yml
@@ -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
diff --git
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
index 8b0d94618df..3457528fa2f 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplicationApis.java
@@ -22,8 +22,10 @@ import io.swagger.v3.oas.annotations.Operation;
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;
@@ -33,7 +35,9 @@ import org.apache.solr.client.api.model.CommitsResponse;
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")
@@ -124,4 +128,12 @@ public interface ReplicationApis {
@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);
}
diff --git
a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationEnabledRequestBody.java
b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationEnabledRequestBody.java
new file mode 100644
index 00000000000..0b7d64a4774
--- /dev/null
+++
b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationEnabledRequestBody.java
@@ -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;
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
index 87ac3063b6c..3196641a0f6 100644
--- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
@@ -353,10 +353,10 @@ public class ReplicationHandler extends RequestHandlerBase
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);
}
}
@@ -1035,6 +1035,10 @@ public class ReplicationHandler extends
RequestHandlerBase
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. */
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
index ea0341eb59e..ea5de6768ed 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CoreReplication.java
@@ -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;
@@ -26,7 +27,9 @@ import org.apache.solr.client.api.model.CommitsResponse;
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;
@@ -94,4 +97,10 @@ public class CoreReplication extends ReplicationAPIBase
implements ReplicationAp
return doFetchFile(
filePath, dirType, offset, len, compression, checksum,
maxWriteMBPerSec, gen);
}
+
+ @Override
+ @PermissionName(CORE_EDIT_PERM)
+ public SolrJerseyResponse
setReplicationEnabled(ReplicationEnabledRequestBody requestBody) {
+ return doSetReplicationEnabled(requestBody);
+ }
}
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
index a00261b388d..107d8818ecb 100644
---
a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
+++
b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
@@ -50,7 +50,9 @@ import org.apache.solr.client.api.model.FileListResponse;
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;
@@ -102,6 +104,15 @@ public abstract class ReplicationAPIBase extends
JerseyResource {
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);
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
index 2512e458555..0cd932f8c4e 100644
---
a/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/CoreReplicationAPITest.java
@@ -37,6 +37,7 @@ 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.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;
@@ -103,6 +104,31 @@ public class CoreReplicationAPITest extends SolrTestCaseJ4
{
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();
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/SetReplicationEnabledAPITest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/SetReplicationEnabledAPITest.java
new file mode 100644
index 00000000000..bcff126ed69
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/SetReplicationEnabledAPITest.java
@@ -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;
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
b/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
index 781c2c54861..277038c3686 100644
---
a/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
+++
b/solr/solr-ref-guide/modules/deployment-guide/pages/user-managed-index-replication.adoc
@@ -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.
diff --git a/solr/webapp/web/js/angular/controllers/replication.js
b/solr/webapp/web/js/angular/controllers/replication.js
index 6e5fd50086a..42c2e2700f1 100644
--- a/solr/webapp/web/js/angular/controllers/replication.js
+++ b/solr/webapp/web/js/angular/controllers/replication.js
@@ -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;
@@ -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};
diff --git a/solr/webapp/web/js/angular/services.js
b/solr/webapp/web/js/angular/services.js
index 42105030d76..bde85be4bc3 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -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()}, {
diff --git a/solr/webapp/web/partials/replication.html
b/solr/webapp/web/partials/replication.html
index a036b6feee1..62946841269 100644
--- a/solr/webapp/web/partials/replication.html
+++ b/solr/webapp/web/partials/replication.html
@@ -230,8 +230,8 @@ limitations under the License.
</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>