diff --git a/changelog/unreleased/SOLR-16398-replication-phase1.yml b/changelog/unreleased/SOLR-16398-replication-phase1.yml new file mode 100644 index 000000000000..853a77cd7d99 --- /dev/null +++ b/changelog/unreleased/SOLR-16398-replication-phase1.yml @@ -0,0 +1,9 @@ +title: + Add V2 JAX-RS API for replication details (GET /api/cores/{coreName}/replication) +type: added +authors: + - name: Jalaz Kumar + nick: jaykay12 +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 1f0476f86612..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 @@ -31,11 +31,26 @@ 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 + @Path("/details") + @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..f1a3a0ffe2b0 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/model/ReplicationDetailsResponse.java @@ -0,0 +1,207 @@ +/* + * 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; +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 boolean isLeader; + + @JsonProperty("isFollower") + public boolean 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 boolean 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 boolean isPollingDisabled; + + @JsonProperty("isReplicating") + public boolean 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..8d07e594d481 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,11 @@ 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.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; @@ -324,7 +328,9 @@ 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 +1024,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) { + 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 = isLeader; + details.isFollower = 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 = 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 = isPollingDisabled(); boolean isReplicating = isReplicating(); - follower.add("isReplicating", String.valueOf(isReplicating)); + follower.isReplicating = isReplicating; if (isReplicating) { try { long bytesToDownload = 0; @@ -1090,9 +1098,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 +1129,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 +1148,105 @@ 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(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()) { + 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); + commits.add(commitDetail); + } + return commits; } - private void addVal( - BiConsumer 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(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..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 @@ -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..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 @@ -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) { + ReplicationHandler replicationHandler = + (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); + return replicationHandler.getReplicationDetails(showFollowerDetails); + } + protected IndexVersionResponse doFetchIndexVersion() throws IOException { ReplicationHandler replicationHandler = (ReplicationHandler) solrCore.getRequestHandler(ReplicationHandler.PATH); 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; + } } 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..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,6 +21,7 @@ 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; @@ -32,6 +33,8 @@ 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; @@ -70,6 +73,33 @@ public void setUp() throws Exception { coreReplicationAPI = new CoreReplicationAPIMock(mockCore, mockQueryRequest, queryResponse); } + @Test + public void testFetchReplicationDetails() { + 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()); + + when(mockReplicationHandler.getReplicationDetails(true)).thenReturn(expectedResponse); + when(mockReplicationHandler.getReplicationDetails(false)).thenReturn(expectedResponse); + + ReplicationDetailsResponse withDefaultFollower = + coreReplicationAPI.fetchReplicationDetails(null); + assertSame(expectedResponse, withDefaultFollower); + assertTrue(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");