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.
    */