From 178ccca17c51a6597fa303add5a3bf6f640df574 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Tue, 13 Oct 2026 12:04:25 +0530 Subject: [PATCH 01/12] cl tidy --- changelog/unreleased/SOLR-16398-replication-phase1.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog/unreleased/SOLR-16398-replication-phase1.yml diff --git a/changelog/unreleased/SOLR-16398-replication-phase1.yml b/changelog/unreleased/SOLR-16398-replication-phase1.yml new file mode 100644 index 000000000000..1ee13c752299 --- /dev/null +++ b/changelog/unreleased/SOLR-16398-replication-phase1.yml @@ -0,0 +1,9 @@ +title: + Added JAX-RS enabled V2 APIs for missing Replication endpoints +type: added +authors: + - name: Jalaz Kumar + nick: jaykay12 +links: + - name: SOLR-16398 + url: https://issues.apache.org/jira/browse/SOLR-16398 From 16742b1a213163438400f0ccf7d1b6abcb1227e8 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 14:06:58 +0530 Subject: [PATCH 02/12] initial changes --- .../client/api/endpoint/ReplicationApis.java | 12 ++ .../api/model/ReplicationDetailsResponse.java | 191 ++++++++++++++++++ .../solr/handler/ReplicationHandler.java | 150 +++++++------- .../handler/admin/api/CoreReplication.java | 7 + .../handler/admin/api/ReplicationAPIBase.java | 7 + 5 files changed, 299 insertions(+), 68 deletions(-) create mode 100644 solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java 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 1f0476f86612..33f661d49a23 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 @@ -31,11 +31,23 @@ import java.io.IOException; 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.util.CoreApiParameters; @Path("/cores/{coreName}/replication") public interface ReplicationApis { + @GET + @CoreApiParameters + @Operation( + summary = "Return replication configuration and current status.", + tags = {"replication"}) + ReplicationDetailsResponse fetchReplicationDetails( + @Parameter(description = "When true on a follower, include nested details fetched from leader", required = false) + @QueryParam("follower") + @DefaultValue("true") + Boolean follower); + @GET @CoreApiParameters @Path("/indexversion") diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java new file mode 100644 index 000000000000..aed023934317 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java @@ -0,0 +1,191 @@ +package org.apache.solr.client.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import org.apache.solr.client.api.util.ReflectWritable; + +public class ReplicationDetailsResponse extends SolrJerseyResponse { + + @JsonProperty("status") + @Schema(description = "Request status, typically OK") + public String status; + + @JsonProperty("details") + @Schema(description = "Replication configuration and status details") + public ReplicationDetails details; + + public ReplicationDetailsResponse() {} + + public ReplicationDetailsResponse(ReplicationDetails details, String status) { + this.details = details; + this.status = status; + } + + /** Top-level replication details payload (nested under {@code details} in the response). */ + public static class ReplicationDetails implements ReflectWritable { + @JsonProperty("indexSize") + public String indexSize; + + @JsonProperty("indexPath") + public String indexPath; + + @JsonProperty("commits") + public List commits; + + @JsonProperty("isLeader") + public String isLeader; + + @JsonProperty("isFollower") + public String isFollower; + + @JsonProperty("indexVersion") + public Long indexVersion; + + @JsonProperty("generation") + public Long generation; + + @JsonProperty("leader") + public LeaderDetails leader; + + @JsonProperty("follower") + public FollowerDetails follower; + + /** Optional snapshot/backup stats (heterogeneous NamedList from the handler). */ + @JsonProperty("backup") + public Object backup; + } + + public static class CommitDetails implements ReflectWritable { + @JsonProperty("indexVersion") + public Long indexVersion; + + @JsonProperty("generation") + public Long generation; + + @JsonProperty("filelist") + public List filelist; + } + + public static class LeaderDetails implements ReflectWritable { + @JsonProperty("confFiles") + public String confFiles; + + @JsonProperty("replicateAfter") + public List replicateAfter; + + @JsonProperty("replicationEnabled") + public String replicationEnabled; + + @JsonProperty("replicableVersion") + public Long replicableVersion; + + @JsonProperty("replicableGeneration") + public Long replicableGeneration; + } + + public static class FollowerDetails implements ReflectWritable { + /** Nested leader details (remote NamedList or map), present when follower=true. */ + @JsonProperty("leaderDetails") + public Object leaderDetails; + + @JsonProperty("ERROR") + public String error; + + @JsonProperty("leaderUrl") + public String leaderUrl; + + @JsonProperty("pollInterval") + public String pollInterval; + + @JsonProperty("nextExecutionAt") + public String nextExecutionAt; + + @JsonProperty("indexReplicatedAt") + public String indexReplicatedAt; + + @JsonProperty("indexReplicatedAtList") + public List indexReplicatedAtList; + + @JsonProperty("replicationFailedAtList") + public List replicationFailedAtList; + + @JsonProperty("timesIndexReplicated") + public Integer timesIndexReplicated; + + @JsonProperty("confFilesReplicated") + public String confFilesReplicated; + + @JsonProperty("timesConfigReplicated") + public Integer timesConfigReplicated; + + @JsonProperty("confFilesReplicatedAt") + public String confFilesReplicatedAt; + + @JsonProperty("lastCycleBytesDownloaded") + public Long lastCycleBytesDownloaded; + + @JsonProperty("timesFailed") + public Integer timesFailed; + + @JsonProperty("replicationFailedAt") + public String replicationFailedAt; + + @JsonProperty("previousCycleTimeInSeconds") + public Long previousCycleTimeInSeconds; + + @JsonProperty("currentDate") + public String currentDate; + + @JsonProperty("isPollingDisabled") + public String isPollingDisabled; + + @JsonProperty("isReplicating") + public String isReplicating; + + @JsonProperty("filesToDownload") + public List filesToDownload; + + @JsonProperty("numFilesToDownload") + public String numFilesToDownload; + + @JsonProperty("bytesToDownload") + public String bytesToDownload; + + @JsonProperty("filesDownloaded") + public List filesDownloaded; + + @JsonProperty("numFilesDownloaded") + public String numFilesDownloaded; + + @JsonProperty("replicationStartTime") + public String replicationStartTime; + + @JsonProperty("timeElapsed") + public String timeElapsed; + + @JsonProperty("currentFile") + public String currentFile; + + @JsonProperty("currentFileSize") + public String currentFileSize; + + @JsonProperty("currentFileSizeDownloaded") + public String currentFileSizeDownloaded; + + @JsonProperty("currentFileSizePercent") + public String currentFileSizePercent; + + @JsonProperty("bytesDownloaded") + public String bytesDownloaded; + + @JsonProperty("totalPercent") + public String totalPercent; + + @JsonProperty("timeRemaining") + public String timeRemaining; + + @JsonProperty("downloadSpeed") + public String downloadSpeed; + } +} 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 01058b59c9b3..84e28eb7d9c4 100644 --- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java @@ -62,7 +62,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.zip.Adler32; @@ -77,6 +76,7 @@ import org.apache.solr.api.JerseyResource; 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.SolrJerseyResponse; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; @@ -324,7 +324,10 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw } else if (command.equals(CMD_SHOW_COMMITS)) { populateCommitInfo(rsp); } else if (command.equals(CMD_DETAILS)) { - getReplicationDetails(rsp, solrParams.getBool("follower", true)); + final CoreReplication coreReplicationAPI = new CoreReplication(core, req, rsp); + V2ApiUtils.squashIntoSolrResponseWithoutHeader( + rsp, coreReplicationAPI.fetchReplicationDetails(solrParams.getBool("follower", true)) + ); } else if (CMD_ENABLE_REPL.equalsIgnoreCase(command)) { replicationEnabled.set(true); rsp.add(STATUS, OK_STATUS); @@ -1018,63 +1021,65 @@ public void initializeMetrics(SolrMetricsContext parentContext, Attributes attri // 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. */ - private NamedList getReplicationDetails( - SolrQueryResponse rsp, boolean showFollowerDetails) { - NamedList details = new SimpleOrderedMap<>(); - NamedList leader = new SimpleOrderedMap<>(); - NamedList follower = new SimpleOrderedMap<>(); - - details.add("indexSize", NumberUtils.readableSize(core.getIndexSize())); - details.add("indexPath", core.getIndexDir()); - details.add(CMD_SHOW_COMMITS, getCommits()); - details.add("isLeader", String.valueOf(isLeader)); - details.add("isFollower", String.valueOf(isFollower)); + public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDetails) { + ReplicationDetailsResponse.ReplicationDetails details = new ReplicationDetailsResponse.ReplicationDetails(); + ReplicationDetailsResponse.LeaderDetails leader = new ReplicationDetailsResponse.LeaderDetails(); + ReplicationDetailsResponse.FollowerDetails follower = new ReplicationDetailsResponse.FollowerDetails(); + + boolean shouldFollowerBePopulated = false; + + details.indexSize = NumberUtils.readableSize(core.getIndexSize()); + details.indexPath = core.getIndexDir(); + details.commits = getCommitDetails(); + details.isLeader = String.valueOf(isLeader); + details.isFollower = String.valueOf(isFollower); CommitVersionInfo vInfo = getIndexVersion(); - details.add("indexVersion", null == vInfo ? 0 : vInfo.version); - details.add(GENERATION, null == vInfo ? 0 : vInfo.generation); + details.indexVersion = null == vInfo ? 0 : vInfo.version; + details.generation = null == vInfo ? 0 : vInfo.generation; IndexCommit commit = indexCommitPoint; // make a copy so it won't change if (isLeader) { - if (includeConfFiles != null) leader.add(CONF_FILES, includeConfFiles); - leader.add(REPLICATE_AFTER, getReplicateAfterStrings()); - leader.add("replicationEnabled", String.valueOf(replicationEnabled.get())); + if (includeConfFiles != null) leader.confFiles = includeConfFiles; + leader.replicateAfter = getReplicateAfterStrings(); + leader.replicationEnabled = String.valueOf(replicationEnabled.get()); } if (isLeader && commit != null) { CommitVersionInfo repCommitInfo = CommitVersionInfo.build(commit); - leader.add("replicableVersion", repCommitInfo.version); - leader.add("replicableGeneration", repCommitInfo.generation); + leader.replicableVersion = repCommitInfo.version; + leader.replicableGeneration = repCommitInfo.generation; } IndexFetcher fetcher = currentIndexFetcher; if (fetcher != null) { + shouldFollowerBePopulated = true; Properties props = loadReplicationProperties(); if (showFollowerDetails) { try { NamedList nl = fetcher.getDetails(); - follower.add("leaderDetails", nl.get(CMD_DETAILS)); + follower.leaderDetails = nl.get(CMD_DETAILS); } catch (Exception e) { log.warn("Exception while invoking 'details' method for replication on leader ", e); - follower.add(ERR_STATUS, "invalid_leader"); + follower.error = "invalid_leader"; } } - follower.add(LEADER_URL, fetcher.getLeaderCoreUrl()); + follower.leaderUrl = fetcher.getLeaderCoreUrl(); if (getPollInterval() != null) { - follower.add(ReplicationAPIBase.POLL_INTERVAL, getPollInterval()); + follower.pollInterval = getPollInterval(); } Date nextScheduled = getNextScheduledExecTime(); if (nextScheduled != null && !isPollingDisabled()) { - follower.add(NEXT_EXECUTION_AT, nextScheduled.toString()); + follower.nextExecutionAt = nextScheduled.toString(); } else if (isPollingDisabled()) { - follower.add(NEXT_EXECUTION_AT, "Polling disabled"); + follower.nextExecutionAt = "Polling disabled"; } - addReplicationProperties(follower::add, props); + addReplicationProperties(follower, props); - follower.add("currentDate", new Date().toString()); - follower.add("isPollingDisabled", String.valueOf(isPollingDisabled())); + follower.currentDate = new Date().toString(); + follower.isPollingDisabled = String.valueOf(isPollingDisabled()); boolean isReplicating = isReplicating(); - follower.add("isReplicating", String.valueOf(isReplicating)); + follower.isReplicating = String.valueOf(isReplicating); if (isReplicating) { try { long bytesToDownload = 0; @@ -1090,9 +1095,9 @@ private NamedList getReplicationDetails( bytesToDownload += (Long) file.get(SIZE); } - follower.add("filesToDownload", filesToDownload); - follower.add("numFilesToDownload", String.valueOf(filesToDownload.size())); - follower.add("bytesToDownload", NumberUtils.readableSize(bytesToDownload)); + follower.filesToDownload = filesToDownload; + follower.numFilesToDownload = String.valueOf(filesToDownload.size()); + follower.bytesToDownload = NumberUtils.readableSize(bytesToDownload); long bytesDownloaded = 0; List filesDownloaded = new ArrayList<>(); @@ -1121,17 +1126,17 @@ private NamedList getReplicationDetails( percentDownloaded = (float) (currFileSizeDownloaded * 100) / currFileSize; } } - follower.add("filesDownloaded", filesDownloaded); - follower.add("numFilesDownloaded", String.valueOf(filesDownloaded.size())); + follower.filesDownloaded = filesDownloaded; + follower.numFilesDownloaded = String.valueOf(filesDownloaded.size()); long estimatedTimeRemaining = 0; Date replicationStartTimeStamp = fetcher.getReplicationStartTimeStamp(); if (replicationStartTimeStamp != null) { - follower.add("replicationStartTime", replicationStartTimeStamp.toString()); + follower.replicationStartTime = replicationStartTimeStamp.toString(); } long elapsed = fetcher.getReplicationTimeElapsed(); - follower.add("timeElapsed", elapsed + "s"); + follower.timeElapsed = elapsed + "s"; if (bytesDownloaded > 0) estimatedTimeRemaining = @@ -1140,53 +1145,62 @@ private NamedList getReplicationDetails( long downloadSpeed = 0; if (bytesToDownload > 0) totalPercent = (float) (bytesDownloaded * 100) / bytesToDownload; if (elapsed > 0) downloadSpeed = (bytesDownloaded / elapsed); - if (currFile != null) follower.add("currentFile", currFile); - follower.add("currentFileSize", NumberUtils.readableSize(currFileSize)); - follower.add( - "currentFileSizeDownloaded", NumberUtils.readableSize(currFileSizeDownloaded)); - follower.add("currentFileSizePercent", String.valueOf(percentDownloaded)); - follower.add("bytesDownloaded", NumberUtils.readableSize(bytesDownloaded)); - follower.add("totalPercent", String.valueOf(totalPercent)); - follower.add("timeRemaining", String.valueOf(estimatedTimeRemaining) + "s"); - follower.add("downloadSpeed", NumberUtils.readableSize(downloadSpeed)); + if (currFile != null) follower.currentFile = currFile; + follower.currentFileSize = NumberUtils.readableSize(currFileSize); + follower.currentFileSizeDownloaded = NumberUtils.readableSize(currFileSizeDownloaded); + follower.currentFileSizePercent = String.valueOf(percentDownloaded); + follower.bytesDownloaded = NumberUtils.readableSize(bytesDownloaded); + follower.totalPercent = String.valueOf(totalPercent); + follower.timeRemaining = estimatedTimeRemaining + "s"; + follower.downloadSpeed = NumberUtils.readableSize(downloadSpeed); } catch (Exception e) { log.error("Exception while writing replication details: ", e); } } } - if (isLeader) details.add("leader", leader); - if (follower.size() > 0) details.add("follower", follower); + if (isLeader) details.leader = leader; + if (shouldFollowerBePopulated) details.follower = follower; NamedList snapshotStats = snapShootDetails; - if (snapshotStats != null) details.add(CMD_BACKUP, snapshotStats); + if (snapshotStats != null) details.backup = snapshotStats; - if (rsp.getValues().get(STATUS) == null) { - rsp.add(STATUS, OK_STATUS); - } - rsp.add(CMD_DETAILS, details); - return details; + return new ReplicationDetailsResponse(details, OK_STATUS); } - private void addReplicationProperties(BiConsumer consumer, Properties props) { - addVal(consumer, IndexFetcher.INDEX_REPLICATED_AT, props, Date.class); - addVal(consumer, IndexFetcher.INDEX_REPLICATED_AT_LIST, props, List.class); - addVal(consumer, IndexFetcher.REPLICATION_FAILED_AT_LIST, props, List.class); - addVal(consumer, IndexFetcher.TIMES_INDEX_REPLICATED, props, Integer.class); - addVal(consumer, IndexFetcher.CONF_FILES_REPLICATED, props, String.class); - addVal(consumer, IndexFetcher.TIMES_CONFIG_REPLICATED, props, Integer.class); - addVal(consumer, IndexFetcher.CONF_FILES_REPLICATED_AT, props, Date.class); - addVal(consumer, IndexFetcher.LAST_CYCLE_BYTES_DOWNLOADED, props, Long.class); - addVal(consumer, IndexFetcher.TIMES_FAILED, props, Integer.class); - addVal(consumer, IndexFetcher.REPLICATION_FAILED_AT, props, Date.class); - addVal(consumer, IndexFetcher.PREVIOUS_CYCLE_TIME_TAKEN, props, Long.class); + @SuppressWarnings("unchecked") + private void addReplicationProperties(ReplicationDetailsResponse.FollowerDetails followerDetails, Properties props) { + addVal(v -> followerDetails.indexReplicatedAt = (String) v, IndexFetcher.INDEX_REPLICATED_AT, props, Date.class); + addVal(v -> followerDetails.indexReplicatedAtList = (List) v, IndexFetcher.INDEX_REPLICATED_AT_LIST, props, List.class); + addVal(v -> followerDetails.replicationFailedAtList = (List) v, IndexFetcher.REPLICATION_FAILED_AT_LIST, props, List.class); + addVal(v -> followerDetails.timesIndexReplicated = (Integer) v, IndexFetcher.TIMES_INDEX_REPLICATED, props, Integer.class); + addVal(v -> followerDetails.confFilesReplicated = (String) v, IndexFetcher.CONF_FILES_REPLICATED, props, String.class); + addVal(v -> followerDetails.timesConfigReplicated = (Integer) v, IndexFetcher.TIMES_CONFIG_REPLICATED, props, Integer.class); + addVal(v -> followerDetails.confFilesReplicatedAt = (String) v, IndexFetcher.CONF_FILES_REPLICATED_AT, props, Date.class); + addVal(v -> followerDetails.lastCycleBytesDownloaded = (Long) v, IndexFetcher.LAST_CYCLE_BYTES_DOWNLOADED, props, Long.class); + addVal(v -> followerDetails.timesFailed = (Integer) v, IndexFetcher.TIMES_FAILED, props, Integer.class); + addVal(v -> followerDetails.replicationFailedAt = (String) v, IndexFetcher.REPLICATION_FAILED_AT, props, Date.class); + addVal(v -> followerDetails.previousCycleTimeInSeconds = (Long) v, IndexFetcher.PREVIOUS_CYCLE_TIME_TAKEN, props, Long.class); + } + + @SuppressWarnings("unchecked") + private List getCommitDetails() { + List commits = new ArrayList<>(); + for (NamedList namedList : getCommits()) { + ReplicationDetailsResponse.CommitDetails commitDetail = new ReplicationDetailsResponse.CommitDetails(); + commitDetail.indexVersion = (Long) namedList.get("indexVersion"); + commitDetail.generation = (Long) namedList.get(GENERATION); + commitDetail.filelist = (List) namedList.get(CMD_GET_FILE_LIST); + commits.add(commitDetail); + } + return commits; } private void addVal( - BiConsumer consumer, String key, Properties props, Class clzz) { + Consumer consumer, String key, Properties props, Class clzz) { Object val = formatVal(key, props, clzz); if (val != null) { - consumer.accept(key, val); + consumer.accept(val); } } 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 1389c6e780f2..0b920f6327ee 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 @@ -24,6 +24,7 @@ import org.apache.solr.client.api.endpoint.ReplicationApis; 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.common.SolrException; import org.apache.solr.core.SolrCore; import org.apache.solr.jersey.PermissionName; @@ -42,6 +43,12 @@ public CoreReplication(SolrCore solrCore, SolrQueryRequest req, SolrQueryRespons super(solrCore, req, rsp); } + @Override + @PermissionName(CORE_READ_PERM) + public ReplicationDetailsResponse fetchReplicationDetails(Boolean follower) { + return doFetchReplicationDetails(follower==null || follower); + } + @Override @PermissionName(CORE_READ_PERM) public IndexVersionResponse fetchIndexVersion() throws IOException { 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 743aad55e88e..b208a901b8c0 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 @@ -48,6 +48,7 @@ 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.common.SolrException; import org.apache.solr.common.util.FastOutputStream; import org.apache.solr.common.util.IOUtils; @@ -93,6 +94,12 @@ public ReplicationAPIBase( this.solrQueryResponse = solrQueryResponse; } + protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollowerDetails) throws IOException { + ReplicationHandler replicationHandler = + (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); + return null; + } + protected IndexVersionResponse doFetchIndexVersion() throws IOException { ReplicationHandler replicationHandler = (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); From 74d4cc8de8ebecf668ffbca5e269cdfabda21b83 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 14:10:18 +0530 Subject: [PATCH 03/12] tidying --- .../client/api/endpoint/ReplicationApis.java | 8 +- .../solr/handler/ReplicationHandler.java | 87 ++++++++++++++----- .../handler/admin/api/CoreReplication.java | 2 +- .../handler/admin/api/ReplicationAPIBase.java | 3 +- 4 files changed, 75 insertions(+), 25 deletions(-) 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 33f661d49a23..f95335dc407a 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 @@ -43,10 +43,12 @@ public interface ReplicationApis { summary = "Return replication configuration and current status.", tags = {"replication"}) ReplicationDetailsResponse fetchReplicationDetails( - @Parameter(description = "When true on a follower, include nested details fetched from leader", required = false) - @QueryParam("follower") + @Parameter( + description = "When true on a follower, include nested details fetched from leader", + required = false) + @QueryParam("follower") @DefaultValue("true") - Boolean follower); + Boolean follower); @GET @CoreApiParameters 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 84e28eb7d9c4..8109bca41b3f 100644 --- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java @@ -326,8 +326,7 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw } else if (command.equals(CMD_DETAILS)) { final CoreReplication coreReplicationAPI = new CoreReplication(core, req, rsp); V2ApiUtils.squashIntoSolrResponseWithoutHeader( - rsp, coreReplicationAPI.fetchReplicationDetails(solrParams.getBool("follower", true)) - ); + rsp, coreReplicationAPI.fetchReplicationDetails(solrParams.getBool("follower", true))); } else if (CMD_ENABLE_REPL.equalsIgnoreCase(command)) { replicationEnabled.set(true); rsp.add(STATUS, OK_STATUS); @@ -1022,9 +1021,12 @@ public void initializeMetrics(SolrMetricsContext parentContext, Attributes attri // there a core set of values that are required to make a response here useful? /** Used for showing statistics and progress information. */ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDetails) { - ReplicationDetailsResponse.ReplicationDetails details = new ReplicationDetailsResponse.ReplicationDetails(); - ReplicationDetailsResponse.LeaderDetails leader = new ReplicationDetailsResponse.LeaderDetails(); - ReplicationDetailsResponse.FollowerDetails follower = new ReplicationDetailsResponse.FollowerDetails(); + ReplicationDetailsResponse.ReplicationDetails details = + new ReplicationDetailsResponse.ReplicationDetails(); + ReplicationDetailsResponse.LeaderDetails leader = + new ReplicationDetailsResponse.LeaderDetails(); + ReplicationDetailsResponse.FollowerDetails follower = + new ReplicationDetailsResponse.FollowerDetails(); boolean shouldFollowerBePopulated = false; @@ -1169,25 +1171,71 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta } @SuppressWarnings("unchecked") - private void addReplicationProperties(ReplicationDetailsResponse.FollowerDetails followerDetails, Properties props) { - addVal(v -> followerDetails.indexReplicatedAt = (String) v, IndexFetcher.INDEX_REPLICATED_AT, props, Date.class); - addVal(v -> followerDetails.indexReplicatedAtList = (List) v, IndexFetcher.INDEX_REPLICATED_AT_LIST, props, List.class); - addVal(v -> followerDetails.replicationFailedAtList = (List) v, IndexFetcher.REPLICATION_FAILED_AT_LIST, props, List.class); - addVal(v -> followerDetails.timesIndexReplicated = (Integer) v, IndexFetcher.TIMES_INDEX_REPLICATED, props, Integer.class); - addVal(v -> followerDetails.confFilesReplicated = (String) v, IndexFetcher.CONF_FILES_REPLICATED, props, String.class); - addVal(v -> followerDetails.timesConfigReplicated = (Integer) v, IndexFetcher.TIMES_CONFIG_REPLICATED, props, Integer.class); - addVal(v -> followerDetails.confFilesReplicatedAt = (String) v, IndexFetcher.CONF_FILES_REPLICATED_AT, props, Date.class); - addVal(v -> followerDetails.lastCycleBytesDownloaded = (Long) v, IndexFetcher.LAST_CYCLE_BYTES_DOWNLOADED, props, Long.class); - addVal(v -> followerDetails.timesFailed = (Integer) v, IndexFetcher.TIMES_FAILED, props, Integer.class); - addVal(v -> followerDetails.replicationFailedAt = (String) v, IndexFetcher.REPLICATION_FAILED_AT, props, Date.class); - addVal(v -> followerDetails.previousCycleTimeInSeconds = (Long) v, IndexFetcher.PREVIOUS_CYCLE_TIME_TAKEN, props, Long.class); + private void addReplicationProperties( + ReplicationDetailsResponse.FollowerDetails followerDetails, Properties props) { + addVal( + v -> followerDetails.indexReplicatedAt = (String) v, + IndexFetcher.INDEX_REPLICATED_AT, + props, + Date.class); + addVal( + v -> followerDetails.indexReplicatedAtList = (List) v, + IndexFetcher.INDEX_REPLICATED_AT_LIST, + props, + List.class); + addVal( + v -> followerDetails.replicationFailedAtList = (List) v, + IndexFetcher.REPLICATION_FAILED_AT_LIST, + props, + List.class); + addVal( + v -> followerDetails.timesIndexReplicated = (Integer) v, + IndexFetcher.TIMES_INDEX_REPLICATED, + props, + Integer.class); + addVal( + v -> followerDetails.confFilesReplicated = (String) v, + IndexFetcher.CONF_FILES_REPLICATED, + props, + String.class); + addVal( + v -> followerDetails.timesConfigReplicated = (Integer) v, + IndexFetcher.TIMES_CONFIG_REPLICATED, + props, + Integer.class); + addVal( + v -> followerDetails.confFilesReplicatedAt = (String) v, + IndexFetcher.CONF_FILES_REPLICATED_AT, + props, + Date.class); + addVal( + v -> followerDetails.lastCycleBytesDownloaded = (Long) v, + IndexFetcher.LAST_CYCLE_BYTES_DOWNLOADED, + props, + Long.class); + addVal( + v -> followerDetails.timesFailed = (Integer) v, + IndexFetcher.TIMES_FAILED, + props, + Integer.class); + addVal( + v -> followerDetails.replicationFailedAt = (String) v, + IndexFetcher.REPLICATION_FAILED_AT, + props, + Date.class); + addVal( + v -> followerDetails.previousCycleTimeInSeconds = (Long) v, + IndexFetcher.PREVIOUS_CYCLE_TIME_TAKEN, + props, + Long.class); } @SuppressWarnings("unchecked") private List getCommitDetails() { List commits = new ArrayList<>(); for (NamedList namedList : getCommits()) { - ReplicationDetailsResponse.CommitDetails commitDetail = new ReplicationDetailsResponse.CommitDetails(); + ReplicationDetailsResponse.CommitDetails commitDetail = + new ReplicationDetailsResponse.CommitDetails(); commitDetail.indexVersion = (Long) namedList.get("indexVersion"); commitDetail.generation = (Long) namedList.get(GENERATION); commitDetail.filelist = (List) namedList.get(CMD_GET_FILE_LIST); @@ -1196,8 +1244,7 @@ private List getCommitDetails() { return commits; } - private void addVal( - Consumer consumer, String key, Properties props, Class clzz) { + private void addVal(Consumer consumer, String key, Properties props, Class clzz) { Object val = formatVal(key, props, clzz); if (val != null) { consumer.accept(val); 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 0b920f6327ee..0f74f55e2515 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 @@ -46,7 +46,7 @@ public CoreReplication(SolrCore solrCore, SolrQueryRequest req, SolrQueryRespons @Override @PermissionName(CORE_READ_PERM) public ReplicationDetailsResponse fetchReplicationDetails(Boolean follower) { - return doFetchReplicationDetails(follower==null || follower); + return doFetchReplicationDetails(follower == null || follower); } @Override 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 b208a901b8c0..833d5de1e639 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 @@ -94,7 +94,8 @@ public ReplicationAPIBase( this.solrQueryResponse = solrQueryResponse; } - protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollowerDetails) throws IOException { + protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollowerDetails) + throws IOException { ReplicationHandler replicationHandler = (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); return null; From c75c7762f9e177d5b53cfd91e784a561305fd3e0 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 14:11:30 +0530 Subject: [PATCH 04/12] license file --- .../api/model/ReplicationDetailsResponse.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java index aed023934317..aefcb13e45ee 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java @@ -1,3 +1,19 @@ +/* + * 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; From 9d0e65bf44185ea2c1e2264e1899ca8a49559364 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 14:22:35 +0530 Subject: [PATCH 05/12] fixed --- changelog/unreleased/SOLR-16398-replication-phase1.yml | 2 +- .../org/apache/solr/handler/admin/api/ReplicationAPIBase.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/unreleased/SOLR-16398-replication-phase1.yml b/changelog/unreleased/SOLR-16398-replication-phase1.yml index 1ee13c752299..853a77cd7d99 100644 --- a/changelog/unreleased/SOLR-16398-replication-phase1.yml +++ b/changelog/unreleased/SOLR-16398-replication-phase1.yml @@ -1,5 +1,5 @@ title: - Added JAX-RS enabled V2 APIs for missing Replication endpoints + Add V2 JAX-RS API for replication details (GET /api/cores/{coreName}/replication) type: added authors: - name: Jalaz Kumar 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 833d5de1e639..6b3e2efcdbd3 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 @@ -98,7 +98,7 @@ protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollo throws IOException { ReplicationHandler replicationHandler = (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); - return null; + return replicationHandler.getReplicationDetails(showFollowerDetails); } protected IndexVersionResponse doFetchIndexVersion() throws IOException { From 582831d74b388c24e251af68b8c43a4688050783 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 14:33:03 +0530 Subject: [PATCH 06/12] added tests --- .../handler/admin/api/ReplicationAPIBase.java | 3 +-- .../admin/api/CoreReplicationAPITest.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) 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 6b3e2efcdbd3..378c5699d31c 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 @@ -94,8 +94,7 @@ public ReplicationAPIBase( this.solrQueryResponse = solrQueryResponse; } - protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollowerDetails) - throws IOException { + protected ReplicationDetailsResponse doFetchReplicationDetails(boolean showFollowerDetails) { ReplicationHandler replicationHandler = (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); return replicationHandler.getReplicationDetails(showFollowerDetails); 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 cd7de60c095f..bcc6ea9f03c2 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 @@ -27,11 +27,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import jakarta.ws.rs.core.Response; import org.apache.lucene.util.Constants; import org.apache.solr.SolrTestCaseJ4; 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.common.SolrException; import org.apache.solr.core.SolrCore; import org.apache.solr.core.SolrResourceLoader; @@ -70,6 +72,31 @@ public void setUp() throws Exception { coreReplicationAPI = new CoreReplicationAPIMock(mockCore, mockQueryRequest, queryResponse); } + @Test + public void testFetchReplicationDetails() { + ReplicationDetailsResponse.ReplicationDetails details = new ReplicationDetailsResponse.ReplicationDetails(); + details.isLeader = "true"; + details.isFollower = "false"; + details.indexVersion = 42L; + details.generation = 7L; + ReplicationDetailsResponse expectedResponse = new ReplicationDetailsResponse(details, Response.Status.OK.getReasonPhrase()); + + when(mockReplicationHandler.getReplicationDetails(true)).thenReturn(expectedResponse); + when(mockReplicationHandler.getReplicationDetails(false)).thenReturn(expectedResponse); + + ReplicationDetailsResponse withDefaultFollower = coreReplicationAPI.fetchReplicationDetails(null); + assertSame(expectedResponse, withDefaultFollower); + assertEquals("true", withDefaultFollower.details.isLeader); + assertEquals(Long.valueOf(42L), withDefaultFollower.details.indexVersion); + + ReplicationDetailsResponse withFollowerTrue = coreReplicationAPI.fetchReplicationDetails(true); + assertSame(expectedResponse, withFollowerTrue); + + ReplicationDetailsResponse withFollowerFalse = coreReplicationAPI.fetchReplicationDetails(false); + assertSame(expectedResponse, withFollowerFalse); + + } + @Test public void testGetIndexVersion() throws Exception { IndexVersionResponse expected = new IndexVersionResponse(123L, 123L, "testGeneration"); From 6faf413692f90b0ffad8e2d84ea90fdb3a0b6260 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 15:14:45 +0530 Subject: [PATCH 07/12] converting into boolean --- .../api/model/ReplicationDetailsResponse.java | 10 +++--- .../solr/handler/ReplicationHandler.java | 33 +++++++++---------- .../admin/api/CoreReplicationAPITest.java | 21 +++++++----- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java index aefcb13e45ee..f1a3a0ffe2b0 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java @@ -50,10 +50,10 @@ public static class ReplicationDetails implements ReflectWritable { public List commits; @JsonProperty("isLeader") - public String isLeader; + public boolean isLeader; @JsonProperty("isFollower") - public String isFollower; + public boolean isFollower; @JsonProperty("indexVersion") public Long indexVersion; @@ -91,7 +91,7 @@ public static class LeaderDetails implements ReflectWritable { public List replicateAfter; @JsonProperty("replicationEnabled") - public String replicationEnabled; + public boolean replicationEnabled; @JsonProperty("replicableVersion") public Long replicableVersion; @@ -154,10 +154,10 @@ public static class FollowerDetails implements ReflectWritable { public String currentDate; @JsonProperty("isPollingDisabled") - public String isPollingDisabled; + public boolean isPollingDisabled; @JsonProperty("isReplicating") - public String isReplicating; + public boolean isReplicating; @JsonProperty("filesToDownload") public List filesToDownload; 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 8109bca41b3f..8d07e594d481 100644 --- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java @@ -77,6 +77,10 @@ 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.CommitDetails; +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.SolrJerseyResponse; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; @@ -1021,20 +1025,17 @@ public void initializeMetrics(SolrMetricsContext parentContext, Attributes attri // there a core set of values that are required to make a response here useful? /** Used for showing statistics and progress information. */ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDetails) { - ReplicationDetailsResponse.ReplicationDetails details = - new ReplicationDetailsResponse.ReplicationDetails(); - ReplicationDetailsResponse.LeaderDetails leader = - new ReplicationDetailsResponse.LeaderDetails(); - ReplicationDetailsResponse.FollowerDetails follower = - new ReplicationDetailsResponse.FollowerDetails(); + ReplicationDetails details = new ReplicationDetails(); + LeaderDetails leader = new LeaderDetails(); + FollowerDetails follower = new FollowerDetails(); boolean shouldFollowerBePopulated = false; details.indexSize = NumberUtils.readableSize(core.getIndexSize()); details.indexPath = core.getIndexDir(); details.commits = getCommitDetails(); - details.isLeader = String.valueOf(isLeader); - details.isFollower = String.valueOf(isFollower); + details.isLeader = isLeader; + details.isFollower = isFollower; CommitVersionInfo vInfo = getIndexVersion(); details.indexVersion = null == vInfo ? 0 : vInfo.version; details.generation = null == vInfo ? 0 : vInfo.generation; @@ -1044,7 +1045,7 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta if (isLeader) { if (includeConfFiles != null) leader.confFiles = includeConfFiles; leader.replicateAfter = getReplicateAfterStrings(); - leader.replicationEnabled = String.valueOf(replicationEnabled.get()); + leader.replicationEnabled = replicationEnabled.get(); } if (isLeader && commit != null) { @@ -1079,9 +1080,9 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta addReplicationProperties(follower, props); follower.currentDate = new Date().toString(); - follower.isPollingDisabled = String.valueOf(isPollingDisabled()); + follower.isPollingDisabled = isPollingDisabled(); boolean isReplicating = isReplicating(); - follower.isReplicating = String.valueOf(isReplicating); + follower.isReplicating = isReplicating; if (isReplicating) { try { long bytesToDownload = 0; @@ -1171,8 +1172,7 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta } @SuppressWarnings("unchecked") - private void addReplicationProperties( - ReplicationDetailsResponse.FollowerDetails followerDetails, Properties props) { + private void addReplicationProperties(FollowerDetails followerDetails, Properties props) { addVal( v -> followerDetails.indexReplicatedAt = (String) v, IndexFetcher.INDEX_REPLICATED_AT, @@ -1231,11 +1231,10 @@ private void addReplicationProperties( } @SuppressWarnings("unchecked") - private List getCommitDetails() { - List commits = new ArrayList<>(); + private List getCommitDetails() { + List commits = new ArrayList<>(); for (NamedList namedList : getCommits()) { - ReplicationDetailsResponse.CommitDetails commitDetail = - new ReplicationDetailsResponse.CommitDetails(); + CommitDetails commitDetail = new CommitDetails(); commitDetail.indexVersion = (Long) namedList.get("indexVersion"); commitDetail.generation = (Long) namedList.get(GENERATION); commitDetail.filelist = (List) namedList.get(CMD_GET_FILE_LIST); 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 bcc6ea9f03c2..306e8e378e0c 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 @@ -21,19 +21,20 @@ import static org.mockito.Mockito.when; import io.opentelemetry.api.trace.Span; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import jakarta.ws.rs.core.Response; import org.apache.lucene.util.Constants; import org.apache.solr.SolrTestCaseJ4; 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.ReplicationDetailsResponse.ReplicationDetails; import org.apache.solr.common.SolrException; import org.apache.solr.core.SolrCore; import org.apache.solr.core.SolrResourceLoader; @@ -74,27 +75,29 @@ public void setUp() throws Exception { @Test public void testFetchReplicationDetails() { - ReplicationDetailsResponse.ReplicationDetails details = new ReplicationDetailsResponse.ReplicationDetails(); - details.isLeader = "true"; - details.isFollower = "false"; + ReplicationDetails details = new ReplicationDetails(); + details.isLeader = true; + details.isFollower = false; details.indexVersion = 42L; details.generation = 7L; - ReplicationDetailsResponse expectedResponse = new ReplicationDetailsResponse(details, Response.Status.OK.getReasonPhrase()); + ReplicationDetailsResponse expectedResponse = + new ReplicationDetailsResponse(details, Response.Status.OK.getReasonPhrase()); when(mockReplicationHandler.getReplicationDetails(true)).thenReturn(expectedResponse); when(mockReplicationHandler.getReplicationDetails(false)).thenReturn(expectedResponse); - ReplicationDetailsResponse withDefaultFollower = coreReplicationAPI.fetchReplicationDetails(null); + ReplicationDetailsResponse withDefaultFollower = + coreReplicationAPI.fetchReplicationDetails(null); assertSame(expectedResponse, withDefaultFollower); - assertEquals("true", withDefaultFollower.details.isLeader); + assertTrue(withDefaultFollower.details.isLeader); assertEquals(Long.valueOf(42L), withDefaultFollower.details.indexVersion); ReplicationDetailsResponse withFollowerTrue = coreReplicationAPI.fetchReplicationDetails(true); assertSame(expectedResponse, withFollowerTrue); - ReplicationDetailsResponse withFollowerFalse = coreReplicationAPI.fetchReplicationDetails(false); + ReplicationDetailsResponse withFollowerFalse = + coreReplicationAPI.fetchReplicationDetails(false); assertSame(expectedResponse, withFollowerFalse); - } @Test From 6a6eddc51ba99a8a938cfd8e18b541694c8a8330 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 17 Oct 2026 15:20:10 +0530 Subject: [PATCH 08/12] cursor suggestion on review --- .../apache/solr/handler/api/V2ApiUtils.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java index 25758ccee038..2d60a259c60a 100644 --- a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java +++ b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java @@ -23,12 +23,15 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.solr.client.api.model.SolrJerseyResponse; +import org.apache.solr.client.api.util.ReflectWritable; import org.apache.solr.common.MapWriter.EntryWriter; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.common.util.StrUtils; import org.apache.solr.common.util.Utils; import org.apache.solr.response.PrometheusResponseWriter; @@ -120,7 +123,7 @@ public EntryWriter put(CharSequence key, Object value) { if (trimHeader && kStr.equals("responseHeader")) { return null; } - destination.add(kStr, value); + destination.add(kStr, convertReflectWriteableValue(value)); return this; // returning "this" means we can't use a lambda :-( } }; @@ -135,4 +138,20 @@ public EntryWriter put(CharSequence key, Object value) { return prop.value().isEmpty() ? field.getName() : prop.value(); }); } + + private static Object convertReflectWriteableValue(Object value) { + if (value instanceof ReflectWritable) { + NamedList result = new SimpleOrderedMap<>(); + squashObjectIntoNamedList(result, value, true); + return result; + } + if (value instanceof List list) { + List objects = new ArrayList<>(list.size()); + for (Object item : list) { + objects.add(convertReflectWriteableValue(item)); + } + return objects; + } + return value; + } } From b591ee3b1ce1c698323dd3321eeec7e48200ecab Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sun, 18 Oct 2026 12:41:46 +0530 Subject: [PATCH 09/12] using details --- .../org/apache/solr/client/api/endpoint/ReplicationApis.java | 1 + 1 file changed, 1 insertion(+) 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 f95335dc407a..509afa8518d0 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 @@ -39,6 +39,7 @@ public interface ReplicationApis { @GET @CoreApiParameters + @Path("/details") @Operation( summary = "Return replication configuration and current status.", tags = {"replication"}) From 02e1ed4ed0f4c5518f39ca519c7a381e08809908 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Tue, 20 Oct 2026 00:26:34 +0530 Subject: [PATCH 10/12] v1 api consistency --- .../client/api/model/ReplicationDetailsResponse.java | 10 +++++----- .../org/apache/solr/handler/ReplicationHandler.java | 10 +++++----- .../solr/handler/admin/api/CoreReplicationAPITest.java | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java index f1a3a0ffe2b0..aefcb13e45ee 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java @@ -50,10 +50,10 @@ public static class ReplicationDetails implements ReflectWritable { public List commits; @JsonProperty("isLeader") - public boolean isLeader; + public String isLeader; @JsonProperty("isFollower") - public boolean isFollower; + public String isFollower; @JsonProperty("indexVersion") public Long indexVersion; @@ -91,7 +91,7 @@ public static class LeaderDetails implements ReflectWritable { public List replicateAfter; @JsonProperty("replicationEnabled") - public boolean replicationEnabled; + public String replicationEnabled; @JsonProperty("replicableVersion") public Long replicableVersion; @@ -154,10 +154,10 @@ public static class FollowerDetails implements ReflectWritable { public String currentDate; @JsonProperty("isPollingDisabled") - public boolean isPollingDisabled; + public String isPollingDisabled; @JsonProperty("isReplicating") - public boolean isReplicating; + public String isReplicating; @JsonProperty("filesToDownload") public List filesToDownload; 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 8d07e594d481..d94d5b3d6cbc 100644 --- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java @@ -1034,8 +1034,8 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta details.indexSize = NumberUtils.readableSize(core.getIndexSize()); details.indexPath = core.getIndexDir(); details.commits = getCommitDetails(); - details.isLeader = isLeader; - details.isFollower = isFollower; + details.isLeader = String.valueOf(isLeader); + details.isFollower = String.valueOf(isFollower); CommitVersionInfo vInfo = getIndexVersion(); details.indexVersion = null == vInfo ? 0 : vInfo.version; details.generation = null == vInfo ? 0 : vInfo.generation; @@ -1045,7 +1045,7 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta if (isLeader) { if (includeConfFiles != null) leader.confFiles = includeConfFiles; leader.replicateAfter = getReplicateAfterStrings(); - leader.replicationEnabled = replicationEnabled.get(); + leader.replicationEnabled = String.valueOf(replicationEnabled.get()); } if (isLeader && commit != null) { @@ -1080,9 +1080,9 @@ public ReplicationDetailsResponse getReplicationDetails(boolean showFollowerDeta addReplicationProperties(follower, props); follower.currentDate = new Date().toString(); - follower.isPollingDisabled = isPollingDisabled(); + follower.isPollingDisabled = String.valueOf(isPollingDisabled()); boolean isReplicating = isReplicating(); - follower.isReplicating = isReplicating; + follower.isReplicating = String.valueOf(isReplicating); if (isReplicating) { try { long bytesToDownload = 0; 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 306e8e378e0c..969599f426ae 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 @@ -76,8 +76,8 @@ public void setUp() throws Exception { @Test public void testFetchReplicationDetails() { ReplicationDetails details = new ReplicationDetails(); - details.isLeader = true; - details.isFollower = false; + details.isLeader = "true"; + details.isFollower = "false"; details.indexVersion = 42L; details.generation = 7L; ReplicationDetailsResponse expectedResponse = @@ -89,7 +89,7 @@ public void testFetchReplicationDetails() { ReplicationDetailsResponse withDefaultFollower = coreReplicationAPI.fetchReplicationDetails(null); assertSame(expectedResponse, withDefaultFollower); - assertTrue(withDefaultFollower.details.isLeader); + assertEquals("true", withDefaultFollower.details.isLeader); assertEquals(Long.valueOf(42L), withDefaultFollower.details.indexVersion); ReplicationDetailsResponse withFollowerTrue = coreReplicationAPI.fetchReplicationDetails(true); From 09cb168b0ff3a9d2719a9bbd24da693a5e32a572 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Tue, 20 Oct 2026 00:46:05 +0530 Subject: [PATCH 11/12] adoc --- ...yml => SOLR-16398-replication-details.yml} | 2 +- .../pages/user-managed-index-replication.adoc | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) rename changelog/unreleased/{SOLR-16398-replication-phase1.yml => SOLR-16398-replication-details.yml} (88%) diff --git a/changelog/unreleased/SOLR-16398-replication-phase1.yml b/changelog/unreleased/SOLR-16398-replication-details.yml similarity index 88% rename from changelog/unreleased/SOLR-16398-replication-phase1.yml rename to changelog/unreleased/SOLR-16398-replication-details.yml index 853a77cd7d99..e225e3065160 100644 --- a/changelog/unreleased/SOLR-16398-replication-phase1.yml +++ b/changelog/unreleased/SOLR-16398-replication-details.yml @@ -1,5 +1,5 @@ title: - Add V2 JAX-RS API for replication details (GET /api/cores/{coreName}/replication) + Add V2 JAX-RS API for replication details (GET /api/cores/{coreName}/replication/details) type: added authors: - name: Jalaz Kumar 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 60513c1470a5..20c13cb794cd 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 @@ -430,8 +430,26 @@ http://{followerHost}:{port}/solr/{coreName}/replication?command=disablepoll `details`:: Retrieve configuration details and current status. + +[tabs#details-request] +====== +V1 API:: ++ +==== [source,bash] +---- http://{followerHost}:{port}/solr/{coreName}/replication?command=details +---- +==== + +V2 API:: ++ +==== +[source,bash] +---- +http://{followerHost}:{port}/api/cores/{coreName}/replication/details +---- +==== +====== `filelist`:: Retrieve a list of Lucene files present in the specified host's index. @@ -575,7 +593,7 @@ A snapshot with the name `snapshot._name_` must exist or an error will be return `repository`::: The name of the backup repository to use. When not specified, it defaults to local file system. - + `location`::: The location where the snapshot is created. From c0bc4331e1fc91ae2d28b07ffb922640b8c6b69f Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Tue, 20 Oct 2026 01:53:22 +0530 Subject: [PATCH 12/12] v2 sheet api --- changelog/unreleased/SOLR-16398-replication-details.yml | 2 +- .../apache/solr/client/api/endpoint/ReplicationApis.java | 1 - .../java/org/apache/solr/handler/ReplicationHandler.java | 6 ++++++ .../pages/user-managed-index-replication.adoc | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog/unreleased/SOLR-16398-replication-details.yml b/changelog/unreleased/SOLR-16398-replication-details.yml index e225e3065160..853a77cd7d99 100644 --- a/changelog/unreleased/SOLR-16398-replication-details.yml +++ b/changelog/unreleased/SOLR-16398-replication-details.yml @@ -1,5 +1,5 @@ title: - Add V2 JAX-RS API for replication details (GET /api/cores/{coreName}/replication/details) + Add V2 JAX-RS API for replication details (GET /api/cores/{coreName}/replication) type: added authors: - name: Jalaz Kumar 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 509afa8518d0..f95335dc407a 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 @@ -39,7 +39,6 @@ public interface ReplicationApis { @GET @CoreApiParameters - @Path("/details") @Operation( summary = "Return replication configuration and current status.", tags = {"replication"}) 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 d94d5b3d6cbc..02d69b3cbcc8 100644 --- a/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/ReplicationHandler.java @@ -73,6 +73,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.IOContext; import org.apache.lucene.store.IndexInput; +import org.apache.solr.api.Api; import org.apache.solr.api.JerseyResource; import org.apache.solr.client.api.model.FileMetaData; import org.apache.solr.client.api.model.IndexVersionResponse; @@ -1527,6 +1528,11 @@ public Boolean registerV2() { return Boolean.TRUE; } + @Override + public Collection getApis() { + return List.of(); + } + // check leader or follower is enabled private boolean isEnabled(NamedList params) { if (params == null) return false; 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 20c13cb794cd..b3e8484098e3 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 @@ -446,7 +446,7 @@ V2 API:: ==== [source,bash] ---- -http://{followerHost}:{port}/api/cores/{coreName}/replication/details +http://{followerHost}:{port}/api/cores/{coreName}/replication ---- ==== ======