You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ab...@apache.org on 2017/09/26 16:20:42 UTC
lucene-solr:jira/solr-11285-sim: SOLR-11397: Implement simulated
distributed queue.
Repository: lucene-solr
Updated Branches:
refs/heads/jira/solr-11285-sim 956744b85 -> b6afb0f45
SOLR-11397: Implement simulated distributed queue.
Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/b6afb0f4
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/b6afb0f4
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/b6afb0f4
Branch: refs/heads/jira/solr-11285-sim
Commit: b6afb0f45c33965b61e9b0360a309957e33212ec
Parents: 956744b
Author: Andrzej Bialecki <ab...@apache.org>
Authored: Tue Sep 26 18:19:52 2017 +0200
Committer: Andrzej Bialecki <ab...@apache.org>
Committed: Tue Sep 26 18:19:52 2017 +0200
----------------------------------------------------------------------
.../autoscaling/sim/SimClusterDataProvider.java | 39 ++++
.../sim/SimDistributedQueueFactory.java | 202 ++++++++++++++++++
.../sim/TestClusterDataProvider.java | 61 ++++--
.../sim/TestSimDistributedQueue.java | 204 +++++++++++++++++++
.../cloud/autoscaling/AutoScalingConfig.java | 9 +
5 files changed, 497 insertions(+), 18 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b6afb0f4/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimClusterDataProvider.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimClusterDataProvider.java b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimClusterDataProvider.java
index 079e2b3..3617a750 100644
--- a/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimClusterDataProvider.java
+++ b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimClusterDataProvider.java
@@ -53,6 +53,45 @@ public class SimClusterDataProvider implements ClusterDataProvider {
private Map<String, Map<String, Object>> collProperties = new ConcurrentHashMap<>();
private Map<String, Map<String, Map<String, Object>>> sliceProperties = new ConcurrentHashMap<>();
+ /**
+ * Zero-arg constructor. The instance needs to be initialized using the <code>sim*</code> methods in order
+ * to ensure proper behavior, otherwise it will behave as a cluster with zero live nodes and zero replicas.
+ */
+ public SimClusterDataProvider() {
+
+ }
+
+ /**
+ * Construct simulated instance using initial cluster data.
+ * @param initialState cluster state
+ * @param autoScalingConfig autoscaling config
+ * @param nodeValues per-node values (eg. metrics)
+ */
+ public SimClusterDataProvider(ClusterState initialState, AutoScalingConfig autoScalingConfig, Map<String, Map<String, Object>> nodeValues) {
+ Collection<String> liveNodes = initialState.getLiveNodes();
+ if (nodeValues != null) {
+ nodeValues.entrySet().stream()
+ .filter(e -> liveNodes.contains(e.getKey()))
+ .forEach(e -> nodeValues.put(e.getKey(), e.getValue()));
+ }
+ if (autoScalingConfig != null) {
+ this.autoScalingConfig = (AutoScalingConfig)autoScalingConfig.clone();
+ }
+ initialState.forEachCollection(dc -> {
+ collProperties.computeIfAbsent(dc.getName(), name -> new HashMap<>()).putAll(dc.getProperties());
+ dc.getSlices().forEach(s -> {
+ sliceProperties.computeIfAbsent(dc.getName(), name -> new HashMap<>())
+ .computeIfAbsent(s.getName(), name -> new HashMap<>()).putAll(s.getProperties());
+ s.getReplicas().forEach(r -> {
+ ReplicaInfo ri = new ReplicaInfo(r.getName(), dc.getName(), s.getName(), r.getType(), r.getProperties());
+ if (liveNodes.contains(r.getNodeName())) {
+ nodes.computeIfAbsent(r.getNodeName(), rn -> new ArrayList<>()).add(ri);
+ }
+ });
+ });
+ });
+ }
+
// ============== SIMULATOR SETUP METHODS ====================
public void simAddNodes(Collection<String> nodeIds) {
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b6afb0f4/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimDistributedQueueFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimDistributedQueueFactory.java b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimDistributedQueueFactory.java
new file mode 100644
index 0000000..a07a444
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/SimDistributedQueueFactory.java
@@ -0,0 +1,202 @@
+package org.apache.solr.cloud.autoscaling.sim;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
+
+import com.google.common.base.Preconditions;
+import org.apache.solr.client.solrj.cloud.DistributedQueue;
+import org.apache.solr.client.solrj.cloud.autoscaling.SolrCloudDataProvider;
+import org.apache.solr.common.util.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+public class SimDistributedQueueFactory implements SolrCloudDataProvider.DistributedQueueFactory {
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ Map<String, SimDistributedQueue> queues = new ConcurrentHashMap<>();
+
+ public SimDistributedQueueFactory() {
+ }
+
+ @Override
+ public DistributedQueue makeQueue(String path) throws IOException {
+ return queues.computeIfAbsent(path, p -> new SimDistributedQueue());
+ }
+
+ @Override
+ public void removeQueue(String path) throws IOException {
+ queues.remove(path);
+ }
+
+ public static class SimDistributedQueue implements DistributedQueue {
+ private final Queue<Pair<String, byte[]>> queue = new ConcurrentLinkedQueue<>();
+ private final ReentrantLock updateLock = new ReentrantLock();
+ private final Condition changed = updateLock.newCondition();
+ private int seq = 0;
+
+ @Override
+ public byte[] peek() throws Exception {
+ Pair<String, byte[]> pair = queue.peek();
+ return pair != null ? pair.second() : null;
+ }
+
+ @Override
+ public byte[] peek(boolean block) throws Exception {
+ return block ? peek(Long.MAX_VALUE) : peek();
+ }
+
+ @Override
+ public byte[] peek(long wait) throws Exception {
+ Pair<String, byte[]> pair = peekInternal(wait);
+ return pair != null ? pair.second() : null;
+ }
+
+ private Pair<String, byte[]> peekInternal(long wait) throws Exception {
+ Preconditions.checkArgument(wait > 0);
+ long waitNanos = TimeUnit.MILLISECONDS.toNanos(wait);
+ updateLock.lockInterruptibly();
+ try {
+ while (waitNanos > 0) {
+ Pair<String, byte[]> pair = queue.peek();
+ if (pair != null) {
+ return pair;
+ }
+ waitNanos = changed.awaitNanos(waitNanos);
+ if (waitNanos < 0) { // timed out
+ return null;
+ }
+ }
+ } finally {
+ updateLock.unlock();
+ }
+ return null;
+ }
+
+ @Override
+ public byte[] poll() throws Exception {
+ updateLock.lockInterruptibly();
+ try {
+ Pair<String, byte[]> pair = queue.poll();
+ if (pair != null) {
+ changed.signalAll();
+ return pair.second();
+ } else {
+ return null;
+ }
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ @Override
+ public byte[] remove() throws Exception {
+ updateLock.lockInterruptibly();
+ try {
+ byte[] res = queue.remove().second();
+ changed.signalAll();
+ return res;
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ @Override
+ public byte[] take() throws Exception {
+ updateLock.lockInterruptibly();
+ try {
+ while (true) {
+ byte[] result = poll();
+ if (result != null) {
+ return result;
+ }
+ changed.await();
+ }
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ @Override
+ public void offer(byte[] data) throws Exception {
+ updateLock.lockInterruptibly();
+ try {
+ queue.offer(new Pair(String.format("qn-%010d", seq), data));
+ seq++;
+ LOG.info("=== offer " + System.nanoTime());
+ changed.signalAll();
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ @Override
+ public Collection<Pair<String, byte[]>> peekElements(int max, long waitMillis, Predicate<String> acceptFilter) throws Exception {
+ updateLock.lockInterruptibly();
+ try {
+ List<Pair<String, byte[]>> res = new LinkedList<>();
+ final int maximum = max < 0 ? Integer.MAX_VALUE : max;
+ final AtomicReference<Pair<String, byte[]>> pairRef = new AtomicReference<>();
+ queue.forEach(pair -> {
+ if (acceptFilter != null && !acceptFilter.test(pair.first())) {
+ return;
+ }
+ if (res.size() < maximum) {
+ pairRef.set(pair);
+ res.add(pair);
+ }
+ });
+ if (res.size() < maximum && waitMillis > 0) {
+ long waitNanos = TimeUnit.MILLISECONDS.toNanos(waitMillis);
+ waitNanos = changed.awaitNanos(waitNanos);
+ if (waitNanos < 0) {
+ return res;
+ }
+ AtomicBoolean seen = new AtomicBoolean(false);
+ queue.forEach(pair -> {
+ if (!seen.get()) {
+ if (pairRef.get() == null) {
+ seen.set(true);
+ } else {
+ if (pairRef.get().first().equals(pair.first())) {
+ seen.set(true);
+ return;
+ }
+ }
+ }
+ if (!seen.get()) {
+ return;
+ }
+ if (!acceptFilter.test(pair.first())) {
+ return;
+ }
+ if (res.size() < maximum) {
+ res.add(pair);
+ pairRef.set(pair);
+ } else {
+ return;
+ }
+ });
+ }
+ return res;
+ } finally {
+ updateLock.unlock();
+ }
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b6afb0f4/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestClusterDataProvider.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestClusterDataProvider.java b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestClusterDataProvider.java
index b154132..5089be2 100644
--- a/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestClusterDataProvider.java
+++ b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestClusterDataProvider.java
@@ -4,8 +4,10 @@ import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -52,7 +54,7 @@ public class TestClusterDataProvider extends SolrCloudTestCase {
@BeforeClass
public static void setupCluster() throws Exception {
simulated = random().nextBoolean();
- simulated = true;
+ //simulated = true;
configureCluster(NODE_COUNT)
.addConfig("conf", configset("cloud-minimal"))
@@ -103,41 +105,51 @@ public class TestClusterDataProvider extends SolrCloudTestCase {
simProvider.simSetCollectionProperties(name, docColl.getProperties());
});
ClusterState simState = simProvider.getClusterState();
- assertEquals(realState.getLiveNodes(), simState.getLiveNodes());
- assertEquals(realState.getCollectionsMap().keySet(), simState.getCollectionsMap().keySet());
- DocCollection realColl = realState.getCollection(CollectionAdminParams.SYSTEM_COLL);
- DocCollection simColl = simState.getCollection(CollectionAdminParams.SYSTEM_COLL);
- Map<String, Slice> realSlices = realColl.getSlicesMap();
- Map<String, Slice> simSlices = simColl.getSlicesMap();
- assertEquals(realSlices.keySet(), simSlices.keySet());
- realSlices.forEach((s, slice) -> {
- Slice sim = simSlices.get(s);
- for (Replica r : slice.getReplicas()) {
- Replica sr = sim.getReplica(r.getName());
- assertNotNull(sr);
- assertEquals(r, sr);
- }
- });
+ assertClusterStateEquals(realState, simState);
+ // test the other constructor
+ ClusterDataProvider anotherProvider = new SimClusterDataProvider(realState, autoScalingConfig, nodeValues);
+ assertClusterStateEquals(realState, anotherProvider.getClusterState());
clusterDataProvider = simProvider;
} else {
clusterDataProvider = realProvider;
}
}
- private void addNode() throws Exception {
+ private static void assertClusterStateEquals(ClusterState one, ClusterState two) {
+ assertEquals(one.getLiveNodes(), two.getLiveNodes());
+ assertEquals(one.getCollectionsMap().keySet(), two.getCollectionsMap().keySet());
+ one.forEachCollection(oneColl -> {
+ DocCollection twoColl = two.getCollection(oneColl.getName());
+ Map<String, Slice> oneSlices = oneColl.getSlicesMap();
+ Map<String, Slice> twoSlices = twoColl.getSlicesMap();
+ assertEquals(oneSlices.keySet(), twoSlices.keySet());
+ oneSlices.forEach((s, slice) -> {
+ Slice sTwo = twoSlices.get(s);
+ for (Replica oneReplica : slice.getReplicas()) {
+ Replica twoReplica = sTwo.getReplica(oneReplica.getName());
+ assertNotNull(twoReplica);
+ assertEquals(oneReplica, twoReplica);
+ }
+ });
+ });
+ }
+
+ private String addNode() throws Exception {
JettySolrRunner solr = cluster.startJettySolrRunner();
String nodeId = solr.getNodeName();
if (simulated) {
((SimClusterDataProvider)clusterDataProvider).simAddNode(nodeId);
}
+ return nodeId;
}
- private void deleteNode() throws Exception {
+ private String deleteNode() throws Exception {
String nodeId = cluster.getJettySolrRunner(0).getNodeName();
cluster.stopJettySolrRunner(0);
if (simulated) {
((SimClusterDataProvider)clusterDataProvider).simRemoveNode(nodeId);
}
+ return nodeId;
}
private void setAutoScalingConfig(AutoScalingConfig cfg) throws Exception {
@@ -149,6 +161,19 @@ public class TestClusterDataProvider extends SolrCloudTestCase {
}
@Test
+ public void testAddRemoveNode() throws Exception {
+ Set<String> lastNodes = new HashSet<>(clusterDataProvider.getLiveNodes());
+ String node = addNode();
+ Thread.sleep(2000);
+ assertFalse(lastNodes.contains(node));
+ assertTrue(clusterDataProvider.getLiveNodes().contains(node));
+ node = deleteNode();
+ Thread.sleep(2000);
+ assertTrue(lastNodes.contains(node));
+ assertFalse(clusterDataProvider.getLiveNodes().contains(node));
+ }
+
+ @Test
public void testAutoScalingConfig() throws Exception {
final CountDownLatch triggered = new CountDownLatch(1);
Watcher w = ev -> {
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b6afb0f4/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestSimDistributedQueue.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestSimDistributedQueue.java b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestSimDistributedQueue.java
new file mode 100644
index 0000000..cbfe553
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cloud/autoscaling/sim/TestSimDistributedQueue.java
@@ -0,0 +1,204 @@
+package org.apache.solr.cloud.autoscaling.sim;
+
+import java.nio.charset.Charset;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.cloud.DistributedQueue;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.SolrjNamedThreadFactory;
+import org.junit.After;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class TestSimDistributedQueue extends SolrTestCaseJ4 {
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+ protected ExecutorService executor = ExecutorUtil.newMDCAwareSingleThreadExecutor(new SolrjNamedThreadFactory("sdqtest-"));
+
+ @Test
+ public void testDistributedQueue() throws Exception {
+ String dqZNode = "/distqueue/test";
+ byte[] data = "hello world".getBytes(UTF8);
+
+ DistributedQueue dq = makeDistributedQueue(dqZNode);
+
+ // basic ops
+ assertNull(dq.poll());
+ try {
+ dq.remove();
+ fail("NoSuchElementException expected");
+ } catch (NoSuchElementException expected) {
+ // expected
+ }
+
+ dq.offer(data);
+ assertArrayEquals(dq.peek(500), data);
+ assertArrayEquals(dq.remove(), data);
+ assertNull(dq.poll());
+
+ dq.offer(data);
+ assertArrayEquals(dq.take(), data); // waits for data
+ assertNull(dq.poll());
+
+ dq.offer(data);
+ dq.peek(true); // wait until data is definitely there before calling remove
+ assertArrayEquals(dq.remove(), data);
+ assertNull(dq.poll());
+
+ // should block until the background thread makes the offer
+ (new QueueChangerThread(dq, 1000)).start();
+ assertNotNull(dq.peek(true));
+ assertNotNull(dq.remove());
+ assertNull(dq.poll());
+
+ // timeout scenario ... background thread won't offer until long after the peek times out
+ QueueChangerThread qct = new QueueChangerThread(dq, 1000);
+ qct.start();
+ assertNull(dq.peek(500));
+ qct.join();
+ }
+
+ @Test
+ public void testDistributedQueueBlocking() throws Exception {
+ String dqZNode = "/distqueue/test";
+ String testData = "hello world";
+
+ DistributedQueue dq = makeDistributedQueue(dqZNode);
+
+ assertNull(dq.peek());
+ Future<String> future = executor.submit(() -> new String(dq.peek(true), UTF8));
+ try {
+ future.get(1000, TimeUnit.MILLISECONDS);
+ fail("TimeoutException expected");
+ } catch (TimeoutException expected) {
+ assertFalse(future.isDone());
+ }
+
+ dq.offer(testData.getBytes(UTF8));
+ assertEquals(testData, future.get(1000, TimeUnit.MILLISECONDS));
+ assertNotNull(dq.poll());
+
+ assertNull(dq.peek(100));
+
+ // Rerun the earlier test make sure updates are still seen, post reconnection.
+ future = executor.submit(() -> new String(dq.peek(true), UTF8));
+ try {
+ future.get(1000, TimeUnit.MILLISECONDS);
+ fail("TimeoutException expected");
+ } catch (TimeoutException expected) {
+ assertFalse(future.isDone());
+ }
+
+ dq.offer(testData.getBytes(UTF8));
+ assertEquals(testData, future.get(1000, TimeUnit.MILLISECONDS));
+ assertNotNull(dq.poll());
+ assertNull(dq.poll());
+ }
+
+ @Test
+ public void testLocallyOffer() throws Exception {
+ String dqZNode = "/distqueue/test";
+ DistributedQueue dq = makeDistributedQueue(dqZNode);
+ dq.peekElements(1, 1, s -> true);
+ for (int i = 0; i < 100; i++) {
+ byte[] data = String.valueOf(i).getBytes(UTF8);
+ dq.offer(data);
+ assertNotNull(dq.peek());
+ dq.poll();
+ dq.peekElements(1, 1, s -> true);
+ }
+ }
+
+
+ @Test
+ public void testPeekElements() throws Exception {
+ String dqZNode = "/distqueue/test";
+ byte[] data = "hello world".getBytes(UTF8);
+
+ DistributedQueue dq = makeDistributedQueue(dqZNode);
+
+ // Populate with data.
+ dq.offer(data);
+ dq.offer(data);
+ dq.offer(data);
+
+ Predicate<String> alwaysTrue = s -> true;
+ Predicate<String> alwaysFalse = s -> false;
+
+ // Should be able to get 0, 1, 2, or 3 instantly
+ for (int i = 0; i <= 3; ++i) {
+ assertEquals(i, dq.peekElements(i, 0, alwaysTrue).size());
+ }
+
+ // Asking for more should return only 3.
+ assertEquals(3, dq.peekElements(4, 0, alwaysTrue).size());
+
+ // If we filter everything out, we should block for the full time.
+ long start = System.nanoTime();
+ assertEquals(0, dq.peekElements(4, 1000, alwaysFalse).size());
+ assertTrue(System.nanoTime() - start >= TimeUnit.MILLISECONDS.toNanos(500));
+
+ // If someone adds a new matching element while we're waiting, we should return immediately.
+ executor.submit(() -> {
+ try {
+ Thread.sleep(500);
+ dq.offer(data);
+ } catch (Exception e) {
+ // ignore
+ }
+ });
+ start = System.nanoTime();
+ assertEquals(1, dq.peekElements(4, 2000, child -> {
+ // The 4th element in the queue will end with a "3".
+ return child.endsWith("3");
+ }).size());
+ long elapsed = System.nanoTime() - start;
+ assertTrue(elapsed < TimeUnit.MILLISECONDS.toNanos(1000));
+ assertTrue(elapsed >= TimeUnit.MILLISECONDS.toNanos(250));
+ }
+
+
+ protected DistributedQueue makeDistributedQueue(String dqZNode) throws Exception {
+ return new SimDistributedQueueFactory.SimDistributedQueue();
+ }
+
+ private static class QueueChangerThread extends Thread {
+
+ DistributedQueue dq;
+ long waitBeforeOfferMs;
+
+ QueueChangerThread(DistributedQueue dq, long waitBeforeOfferMs) {
+ this.dq = dq;
+ this.waitBeforeOfferMs = waitBeforeOfferMs;
+ }
+
+ public void run() {
+ try {
+ Thread.sleep(waitBeforeOfferMs);
+ dq.offer(getName().getBytes(UTF8));
+ } catch (InterruptedException ie) {
+ // do nothing
+ } catch (Exception exc) {
+ throw new RuntimeException(exc);
+ }
+ }
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ try {
+ super.tearDown();
+ } catch (Exception exc) {
+ }
+ executor.shutdown();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b6afb0f4/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AutoScalingConfig.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AutoScalingConfig.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AutoScalingConfig.java
index 51e8f42..63a2365 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AutoScalingConfig.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AutoScalingConfig.java
@@ -478,6 +478,15 @@ public class AutoScalingConfig implements MapWriter {
return withTriggerListenerConfigs(configs);
}
+ @Override
+ public Object clone() {
+ if (jsonMap != null) {
+ return new AutoScalingConfig(jsonMap);
+ } else {
+ return new AutoScalingConfig(getPolicy(), getTriggerConfigs(), getTriggerListenerConfigs(), zkVersion);
+ }
+ }
+
/**
* Return the znode version that was used to create this configuration.
*/