Hi all,

We are running SolrCloud on Kubernetes with the Solr Operator and ran into a problem when changing the number of Solr pods. When the operator rebalances replicas after adding or removing a pod, Solr regularly fails part of the operation and leaves extra replicas behind. We traced it to the Collections API code (details below) and would like to know whether this is a known issue and whether we should open a JIRA ticket.

## Environment

- Solr 9.6.1 (official `solr:9.6.1` image); also reproduced on 9.11.0, see the note at the end
- Solr Operator v0.9.1, SolrCloud with:
  ```yaml
  scaling:
    vacatePodsOnScaleDown: true
    populatePodsOnScaleUp: true
  ```
- Overseer mode (default), embedded ZooKeeper provided by the operator
- 6 collections, each `numShards=3`, `replicationFactor=2` (36 replicas in total)

## Steps to reproduce

1. Deploy a SolrCloud with `spec.replicas: 2` and the `scaling` settings above. 2. Create 6 collections with `numShards=3&replicationFactor=2`. The collections can be empty. The two nodes now hold
   18 replicas each.
3. Scale the SolrCloud up by one pod:
   ```bash
   kubectl -n <namespace> patch solrcloud <name> --type merge -p '{"spec":{"replicas":3}}'
   ```
   (or change `replicas` in the manifest / Helm values and apply it). The operator creates the third pod, waits until
   it is ready and then calls `BALANCE_REPLICAS`. This moves about 12
   replicas to the new node.
4. Wait until the operator has finished (the `solr.apache.org/clusterOpsLock` annotation on the StatefulSet is gone),
   then check the replica layout with `CLUSTERSTATUS`.
5. Optionally scale back down to 2 pods. The operator empties the third pod with `REPLACENODE` /
   `MIGRATE_REPLICAS` (about 12 replicas again) before removing it.

## Expected result

All replicas are moved: every shard still has exactly 2 replicas, on 2 different nodes, and the replicas are spread
evenly over the 3 nodes.

## Actual result

- The rebalance fails. The operator logs:
  ```
  INFO Found async status  {"requestId": "balance-replicas-ScaleUp", "state": "failed"}   INFO Replica Balancing command failed. Will try again {"message": "found [balance-replicas-ScaleUp] in failed tasks"}
  ```
- The Overseer logs:
  ```
  WARN  o.a.s.c.a.c.ReplicaMigrationUtils Error deleting  => java.util.concurrent.RejectedExecutionException: Task ... rejected from org.apache.solr.common.util.ExecutorUtil$MDCAwareThreadPoolExecutor@7b091450[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = ...]   ERROR o.a.s.c.a.c.CollectionHandlingUtils Operation balance_replicas failed => java.util.concurrent.RejectedExecutionException: ...
  ```
- The new replicas on the third node exist and are active, but part of the source replicas were never deleted.
  Affected shards end up with 3 replicas.
- The operator's retry (another `BALANCE_REPLICAS`) reports success but does not remove the extra replicas; they stay
  until they are deleted by hand.
- Scaling down from 3 to 2 pods fails in the same way (`Operation replacenode failed`, same exception).

We tried multiple times and we saw this in 7 of 8 scaling steps that moved about 12 replicas (5 of 6 scale-ups from 2 to 3 pods, 2 of 2 scale-downs from 3 to 2 pods), with the `simple`, `minimizecores` and `affinity` placement plugins. Between 1 and 9 shards were affected per run, a different set each time. Steps that moved 9 or fewer replicas usually completed.

## Stack trace

```
java.util.concurrent.RejectedExecutionException: Task org.apache.solr.common.util.ExecutorUtil$MDCAwareThreadPoolExecutor$$Lambda$337/... rejected from org.apache.solr.common.util.ExecutorUtil$MDCAwareThreadPoolExecutor@7b091450[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = ...]     at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)     at java.base/java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)     at java.base/java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)     at org.apache.solr.common.util.ExecutorUtil$MDCAwareThreadPoolExecutor.execute(ExecutorUtil.java:344)     at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source)     at org.apache.solr.cloud.api.collections.DeleteReplicaCmd.deleteCore(DeleteReplicaCmd.java:359)     at org.apache.solr.cloud.api.collections.DeleteReplicaCmd.deleteReplica(DeleteReplicaCmd.java:107)     at org.apache.solr.cloud.api.collections.ReplicaMigrationUtils.cleanupReplicas(ReplicaMigrationUtils.java:275)     at org.apache.solr.cloud.api.collections.ReplicaMigrationUtils.migrateReplicas(ReplicaMigrationUtils.java:246)     at org.apache.solr.cloud.api.collections.BalanceReplicasCmd.call(BalanceReplicasCmd.java:81)     at org.apache.solr.cloud.api.collections.CollApiCmds$TraceAwareCommand.call(CollApiCmds.java:225)     at org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.processMessage(OverseerCollectionMessageHandler.java:130)     at org.apache.solr.cloud.OverseerTaskProcessor$Runner.run(OverseerTaskProcessor.java:564)
    ...
```

## What we think happens

(References to `solr/core/src/java/org/apache/solr/cloud/api/collections/` at 9.6.1.)

1. After the new replicas are active, `ReplicaMigrationUtils.cleanupReplicas` loops over all source replicas and    calls `DeleteReplicaCmd.deleteReplica` with `parallel=true` for each of them, without limiting how many deletes
   are in flight.
2. With `parallel=true`, `DeleteReplicaCmd.deleteCore` submits the delete to `ccc.getExecutorService()`, which in    Overseer mode is `OverseerCollectionMessageHandler.tpe`: an `MDCAwareThreadPoolExecutor(5, 10, 0L, ...,    new SynchronousQueue<>(), ...)` without a rejection handler. With a `SynchronousQueue` it accepts at most 10
   concurrent tasks and rejects the 11th (`AbortPolicy`).
3. `cleanupReplicas` catches `KeeperException` and continues, but rethrows any other exception. The    `RejectedExecutionException` therefore ends the loop, and the rejected delete and all deletes after it are never
   submitted.
4. The command fails without rolling back the added replicas or completing the deletes, and since Solr 9 does not
   enforce `replicationFactor`, nothing removes the extra replicas later.

So every operation that moves more than 10 replicas at once is likely to leave replicas behind, depending on how fast the first deletes finish. The same logic is still on `main` and `branch_9x`, and the pool used in the distributed Collection API mode (`DistributedCollectionConfigSetCommandRunner`) has the same size and queue type.

Possible fixes would be to bound the number of in-flight deletes (or delete sequentially), to handle the rejection instead of rethrowing, or to give the pool back-pressure (e.g. `CallerRunsPolicy`).

## Note: reproducible without the Solr Operator

The operator only triggers the Collections API calls; the failure is in Solr. We reproduced it without any operator rebalancing on a fresh SolrCloud with the official `solr:9.11.0` image (`populatePodsOnScaleUp: false`, so the
operator only provided the pods):

1. 2 nodes, 6 collections with `numShards=3&replicationFactor=2` (36 replicas).
2. Add a third node
3. A manual call to the balance API:
   ```bash
   curl -X POST -H 'Content-Type: application/json' -d '{}' http://<host>:8983/api/cluster/replicas/balance
   ```
4. The call returned HTTP 500 with
   `rejected from ...MDCAwareThreadPoolExecutor@6e9e33fc[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 0]`,    the Overseer logged `ReplicaMigrationUtils Error deleting => RejectedExecutionException` and    `Operation balance_replicas failed`, and the cluster was left with 38 replicas instead of 36 (2 shards with
   3 replicas each).

Is this a known problem? If not, we are happy to open a JIRA ticket with these details.

Thank you in advance

Reply via email to