This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 6749e9716dd5978fec94424117c40e270d6a5cec
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]>
    (cherry picked from commit ee7e593311ef5d19ad503fd19d83d97dd4e59578)
---
 .../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 a6380701919..0badbcf2b27 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;
@@ -62,6 +62,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 3a85680e9d3..39441a90724 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -233,6 +233,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>

Reply via email to