Skip to content
8 changes: 8 additions & 0 deletions changelog/unreleased/SOLR-16398-disable-enable-poll.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: Added V2 APIs to enable and disable polling of a leader from its follower
type: added
authors:
- name: Jalaz Kumar
nick: jaykay12
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 @@ -36,6 +36,7 @@
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.ReplicationPollingRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.client.api.util.CoreApiParameters;
Expand Down Expand Up @@ -136,4 +137,13 @@ StreamingOutput fetchFile(
tags = {"replication"})
SolrJerseyResponse setReplicationEnabled(
@RequestBody(required = true) ReplicationEnabledRequestBody requestBody);

@PUT
@CoreApiParameters
@Path("/polling")
@Operation(
summary = "Enable or disable polling of the leader by a follower.",
tags = {"replication"})
SolrJerseyResponse setReplicationPolling(
@RequestBody(required = true) ReplicationPollingRequestBody 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 polling of the leader by a follower. */
public class ReplicationPollingRequestBody {

@JsonProperty(required = true)
public Boolean enabled;
}
38 changes: 22 additions & 16 deletions solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import org.apache.solr.client.api.model.ReplicationDetailsResponse.FollowerDetails;
import org.apache.solr.client.api.model.ReplicationDetailsResponse.LeaderDetails;
import org.apache.solr.client.api.model.ReplicationDetailsResponse.ReplicationDetails;
import org.apache.solr.client.api.model.ReplicationPollingRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.RestoreStatusResponse.RestoreState;
import org.apache.solr.client.api.model.RestoreStatusResponse.RestoreStatus;
Expand Down Expand Up @@ -331,9 +332,9 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw
} else if (command.equalsIgnoreCase(CMD_FETCH_INDEX)) {
fetchIndex(solrParams, rsp);
} else if (command.equalsIgnoreCase(CMD_DISABLE_POLL)) {
disablePoll(rsp);
respondWithPollingEnabled(false, req, rsp);
} else if (command.equalsIgnoreCase(CMD_ENABLE_POLL)) {
enablePoll(rsp);
respondWithPollingEnabled(true, req, rsp);
} else if (command.equalsIgnoreCase(CMD_ABORT_FETCH)) {
if (abortFetch()) {
rsp.add(STATUS, OK_STATUS);
Expand Down Expand Up @@ -856,23 +857,28 @@ public FileInfo(long lasmodified, String name, long size, long checksum) {
}
}

private void disablePoll(SolrQueryResponse rsp) {
if (pollingIndexFetcher != null) {
pollDisabled.set(true);
log.info("inside disable poll, value of pollDisabled = {}", pollDisabled);
rsp.add(STATUS, OK_STATUS);
} else {
reportErrorOnResponse(rsp, "No follower configured", null);
}
/**
* Enables or disables polling of the leader.
*
* @return false if this core is not configured as a follower
*/
public boolean setPollingEnabled(boolean enabled) {
if (pollingIndexFetcher == null) return false;
pollDisabled.set(!enabled);
log.info(
"polling {}, value of pollDisabled = {}", enabled ? "enabled" : "disabled", pollDisabled);
return true;
}

private void enablePoll(SolrQueryResponse rsp) {
if (pollingIndexFetcher != null) {
pollDisabled.set(false);
log.info("inside enable poll, value of pollDisabled = {}", pollDisabled);
private void respondWithPollingEnabled(
boolean enabled, SolrQueryRequest req, SolrQueryResponse rsp) {
final ReplicationPollingRequestBody requestBody = new ReplicationPollingRequestBody();
requestBody.enabled = enabled;
try {
new CoreReplication(core, req, rsp).setReplicationPolling(requestBody);
rsp.add(STATUS, OK_STATUS);
} else {
reportErrorOnResponse(rsp, "No follower configured", null);
} catch (SolrException e) {
reportErrorOnResponse(rsp, e.getMessage(), null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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.ReplicationPollingRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.common.SolrException;
Expand Down Expand Up @@ -103,4 +104,10 @@ public StreamingOutput fetchFile(
public SolrJerseyResponse setReplicationEnabled(ReplicationEnabledRequestBody requestBody) {
return doSetReplicationEnabled(requestBody);
}

@Override
@PermissionName(CORE_EDIT_PERM)
public SolrJerseyResponse setReplicationPolling(ReplicationPollingRequestBody requestBody) {
return doSetReplicationPolling(requestBody);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
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.ReplicationPollingRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.common.SolrException;
Expand Down Expand Up @@ -113,6 +114,17 @@ protected SolrJerseyResponse doSetReplicationEnabled(ReplicationEnabledRequestBo
return instantiateJerseyResponse(SolrJerseyResponse.class);
}

protected SolrJerseyResponse doSetReplicationPolling(ReplicationPollingRequestBody requestBody) {
ensureRequiredRequestBodyProvided(requestBody);
ensureRequiredParameterProvided("enabled", requestBody.enabled);
ReplicationHandler replicationHandler =
(ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH);
if (!replicationHandler.setPollingEnabled(requestBody.enabled)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No follower configured");
}
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 @@ -18,6 +18,7 @@
package org.apache.solr.handler.admin.api;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import io.opentelemetry.api.trace.Span;
Expand All @@ -38,6 +39,7 @@
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.ReplicationPollingRequestBody;
import org.apache.solr.client.api.model.RestoreStatusResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrCore;
Expand Down Expand Up @@ -129,6 +131,42 @@ public void testSetReplicationEnabledRejectsBadBody() {
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
}

@Test
public void testSetReplicationPolling() {
for (boolean enabled : new boolean[] {true, false}) {
when(mockReplicationHandler.setPollingEnabled(enabled)).thenReturn(true);
final var body = new ReplicationPollingRequestBody();
body.enabled = enabled;
assertNotNull(coreReplicationAPI.setReplicationPolling(body));
verify(mockReplicationHandler).setPollingEnabled(enabled);
}
}

@Test
public void testSetReplicationPollingRejectsNoFollower() {
final var body = new ReplicationPollingRequestBody();
body.enabled = true;
SolrException ex =
expectThrows(SolrException.class, () -> coreReplicationAPI.setReplicationPolling(body));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
}

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

@Test
public void testSetReplicationPollingRejectsBadBody() {
final var emptyBody = new ReplicationPollingRequestBody();
SolrException ex =
expectThrows(
SolrException.class, () -> coreReplicationAPI.setReplicationPolling(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,133 @@
/*
* 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 java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.io.file.PathUtils;
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.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;

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

private static final String CORE_NAME = "setReplicationPollingApiTestCore";
private static final String FOLLOWER_CORE_NAME = "setReplicationPollingApiTestFollowerCore";

@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());
System.setProperty("solr.security.allow.urls.enabled", "false");
final Path solrHome = createTempDir();
solrTestRule.startSolr(solrHome);
solrTestRule.newCollection(CORE_NAME).withConfigSet(ExternalPaths.DEFAULT_CONFIGSET).create();

final Path followerConfigSet = solrHome.resolve("followerConfigSet");
PathUtils.copyDirectory(ExternalPaths.DEFAULT_CONFIGSET, followerConfigSet);
final Path solrConfig = followerConfigSet.resolve("solrconfig.xml");
final String followerHandler =
"<requestHandler name=\"/replication\" class=\"solr.ReplicationHandler\">"
+ "<lst name=\"follower\"><str name=\"leaderUrl\">"
+ solrTestRule.getBaseUrl()
+ "/"
+ CORE_NAME
+ "</str><str name=\"pollInterval\">01:00:00</str></lst></requestHandler></config>";
Files.writeString(
solrConfig, Files.readString(solrConfig).replace("</config>", followerHandler));
solrTestRule.newCollection(FOLLOWER_CORE_NAME).withConfigSet(followerConfigSet).create();
}

@Before
public void beforeEach() throws Exception {
setPolling(true);
}

@Test
public void testDisableThenEnablePolling() throws Exception {
assertEquals(Boolean.FALSE, fetchPollingDisabled());

setPolling(false);
assertEquals(Boolean.TRUE, fetchPollingDisabled());

setPolling(true);
assertEquals(Boolean.FALSE, fetchPollingDisabled());
}

@Test
public void testMissingEnabledIsRejectedAndStateUnchanged() throws Exception {
var request = new ReplicationApi.SetReplicationPolling(FOLLOWER_CORE_NAME);
RemoteSolrException ex =
expectThrows(
RemoteSolrException.class,
() -> request.process(solrTestRule.getSolrClient(FOLLOWER_CORE_NAME)));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());

assertEquals(Boolean.FALSE, fetchPollingDisabled());
}

@Test
public void testNoFollowerIsRejected() {
var request = new ReplicationApi.SetReplicationPolling(CORE_NAME);
request.setEnabled(true);
RemoteSolrException ex =
expectThrows(
RemoteSolrException.class,
() -> request.process(solrTestRule.getSolrClient(CORE_NAME)));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
}

@Test
public void testNullEnabledIsRejected() {
var request = new ReplicationApi.SetReplicationPolling(FOLLOWER_CORE_NAME);
request.setEnabled(null);
RemoteSolrException ex =
expectThrows(
RemoteSolrException.class,
() -> request.process(solrTestRule.getSolrClient(FOLLOWER_CORE_NAME)));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
}

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

private static Boolean fetchPollingDisabled() throws Exception {
var request = new ReplicationApi.FetchReplicationDetails(FOLLOWER_CORE_NAME);
return request.process(solrTestRule.getSolrClient(FOLLOWER_CORE_NAME))
.details
.follower
.isPollingDisabled;
}
}
Loading
Loading