[ 
https://issues.apache.org/jira/browse/SOLR-18413?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ZhenyuLi updated SOLR-18413:
----------------------------
    Description: 
# Description

During a SolrCloud `MIGRATE`, the source slice receives a temporary routing 
rule that forwards matching updates to the target collection.

For example, migrating route key `a!` from `source` to `target` creates state 
similar to:

```text
source/shard1.routingRules["a!"] = {
  targetCollection: "target",
  ...
}
```

The routing rule references the target collection by name. Deleting `target` 
does not remove routing rules in other collections that still refer to it.

This can leave the cluster in the following state:

```text
source/shard1.routingRules["a!"].targetCollection = "target"

ClusterState:
  source -> exists
  target -> missing
```

While this routing rule remains active, an update to `source` with a matching 
ID such as `a!2` is first applied locally on the source shard leader.

The leader then enters `DistributedZkUpdateProcessor.doDistribAdd()`, which 
resolves routing-rule destinations before distributing the update to the source 
shard replicas:

```java
final List<SolrCmdDistributor.Node> nodesByRoutingRules =
    getNodesByRoutingRules(
        clusterState,
        coll,
        cmd.getIndexedIdStr(),
        cmd.getSolrInputDocument());

if (nodesByRoutingRules != null && !nodesByRoutingRules.isEmpty()) {
  // Forward to the MIGRATE target.
  ...
}

if (nodes != null) {
  // Distribute to replicas of the source shard.
  cmdDistrib.distribAdd(...);
}
```

For a matching routing rule, `getNodesByRoutingRules()` currently resolves the 
target with:

```java
DocCollection targetColl =
    cstate.getCollection(rule.getTargetCollectionName());
```

If the target collection has already been deleted, this throws:

```text
HTTP 400
Could not find collection : target
```

At this point, however, the source leader has already applied the update 
locally.

The exception prevents execution from reaching the subsequent source-replica 
distribution block.

The resulting state can therefore be:

```text
source/shard1 leader:
  a!2 exists

source/shard1 replica:
  a!2 missing

client:
  HTTP 400
```

This is more than a dangling-reference error. The request reports failure after 
partially applying the mutation, leaving replicas of the source shard 
inconsistent.

No update request is sent to the source replica. Therefore, 
`SolrCmdDistributor` does not observe a replica failure. Consequently, this 
request does not lower the replica's shard term or directly trigger recovery.

Until another recovery mechanism repairs the replica:

- Queries may return different results depending on which replica serves the 
request.
- A leader change may promote a replica that does not contain the update.
- The client cannot safely determine whether retrying the failed request is 
appropriate.
- Retrying non-idempotent atomic updates could apply the operation more than 
once.

## Execution flow

```text
Client: ADD source / id=a!2
              |
              v
      source shard leader
              |
              v
         versionAdd()
              |
              v
     local index/tlog update
              |
              v
   getNodesByRoutingRules()
              |
              v
   getCollection("target")
              |
              +---- target missing
              |
              v
        HTTP 400 exception
              |
              X
 source replica distribution
       is never executed
```

# Reproduction

1. Start a SolrCloud cluster with at least two nodes.

2. Create a `source` collection with one shard and two replicas:

```text
collection = source
numShards = 1
replicationFactor = 2
```

3. Create a `target` collection:

```text
collection = target
numShards = 1
replicationFactor = 1
```

4. Add and commit a document with a composite ID to the `source` collection:

```text
id = a!1
```

5. Run `MIGRATE` for route key `a!`:

```text
source collection = source
target collection = target
split.key = a!
forward.timeout = 45
```

6. Confirm that `source/shard1` contains an unexpired routing rule for `a!` 
whose target collection is `target`.

7. Delete the `target` collection and wait until it is absent from 
`ClusterState`.

8. Before the routing rule expires, add another matching document to the 
`source` collection:

```text
id = a!2
```

The request returns:

```text
HTTP 400
Could not find collection : target
```

9. Commit the `source` collection and query each source replica directly with 
distributed querying disabled:

```text
q=id:"a!2"
distrib=false
```

The result is:

```text
source/shard1 leader:
  a!2 exists

source/shard1 replica:
  a!2 missing
```

# Expected behavior

A routing rule whose target collection no longer exists should not abort a 
source update after the source leader has already applied it.

The missing target should invalidate the dangling routing rule. Solr should:

1. Stop forwarding updates through the stale routing rule.
2. Remove the dangling rule on a best-effort basis.
3. Continue distributing the update to all replicas of the source shard.

After the request completes:

```text
client:
  update succeeds

source/shard1 leader:
  a!2 exists

source/shard1 replica:
  a!2 exists

source routing rule:
  dangling a! rule removed
```

  was:
# Description

During a SolrCloud `MIGRATE`, the source slice receives a temporary routing 
rule that forwards matching updates to the target collection.

For example, migrating route key `a!` from `source` to `target` creates state 
similar to:

source/shard1.routingRules["a!"] = {
  targetCollection: "target",
  ...
}

The routing rule references the target collection by name. Deleting `target` 
does not remove routing rules in other collections that still refer to it.

This can leave the cluster in the following state:

source/shard1.routingRules["a!"].targetCollection = "target"

ClusterState:
  source -> exists
  target -> missing

While this routing rule remains active, an update to `source` with a matching 
ID such as `a!2` is first applied locally on the source shard leader.

The leader then enters `DistributedZkUpdateProcessor.doDistribAdd()`, which 
resolves routing-rule destinations before distributing the update to the source 
shard replicas:

final List<SolrCmdDistributor.Node> nodesByRoutingRules =
    getNodesByRoutingRules(
        clusterState,
        coll,
        cmd.getIndexedIdStr(),
        cmd.getSolrInputDocument());

if (nodesByRoutingRules != null && !nodesByRoutingRules.isEmpty()) {
  // Forward to the MIGRATE target.
  ...
}

if (nodes != null) {
  // Distribute to replicas of the source shard.
  cmdDistrib.distribAdd(...);
}

For a matching routing rule, `getNodesByRoutingRules()` currently resolves the 
target with:

DocCollection targetColl =
    cstate.getCollection(rule.getTargetCollectionName());

If the target collection has already been deleted, this throws:

HTTP 400
Could not find collection : target

At this point, however, the source leader has already applied the update 
locally.

The exception prevents execution from reaching the subsequent source-replica 
distribution block.

The resulting state can therefore be:

source/shard1 leader:
  a!2 exists

source/shard1 replica:
  a!2 missing

client:
  HTTP 400

This is more than a dangling-reference error. The request reports failure after 
partially applying the mutation, leaving replicas of the source shard 
inconsistent.

No update request is sent to the source replica. Therefore, 
`SolrCmdDistributor` does not observe a replica failure, and this request does 
not lower the replica's shard term or directly trigger recovery.

Until another recovery mechanism repairs the replica:

- Queries may return different results depending on which replica serves the 
request.
- A leader change may promote a replica that does not contain the update.
- The client cannot safely determine whether the failed request should be 
retried.
- Retrying non-idempotent atomic updates could apply the operation more than 
once.

## Execution flow

Client: ADD source / id=a!2
              |
              v
      source shard leader
              |
              v
         versionAdd()
              |
              v
     local index/tlog update
              |
              v
   getNodesByRoutingRules()
              |
              v
   getCollection("target")
              |
              +---- target missing
              |
              v
        HTTP 400 exception
              |
              X
 source replica distribution
       is never executed


# Reproduction

1. Start a SolrCloud cluster with at least two nodes.

2. Create a `source` collection with one shard and two replicas:

collection = source
numShards = 1
replicationFactor = 2

3. Create a `target` collection:

collection = target
numShards = 1
replicationFactor = 1

4. Add and commit a document with a composite ID to `source`:

id = a!1

5. Run `MIGRATE` for route key `a!`:

source collection = source
target collection = target
split.key = a!
forward.timeout = 45

6. Confirm that `source/shard1` contains an unexpired routing rule for `a!` 
whose target collection is `target`.

7. Delete the `target` collection and wait until it is absent from 
`ClusterState`.

8. Before the routing rule expires, add another matching document to `source`:

id = a!2

The request returns:

HTTP 400
Could not find collection : target

9. Commit the `source` collection and query each source replica directly with 
distributed querying disabled:

q=id:"a!2"
distrib=false

The result is:

source/shard1 leader:
  a!2 exists

source/shard1 replica:
  a!2 missing


# Expected behavior

A routing rule whose target collection no longer exists should not abort a 
source update after the source leader has already applied that update locally.

The missing target should invalidate the dangling routing rule.

Solr should:

1. Stop forwarding updates through the stale routing rule.
2. Remove the dangling rule on a best-effort basis.
3. Continue distributing the update to all replicas of the source shard.

After the request completes, the expected state is:

client:
  update succeeds

source/shard1 leader:
  a!2 exists

source/shard1 replica:
  a!2 exists

source routing rule:
  dangling a! rule removed


> Deleting a MIGRATE target collection can leave source shard replicas 
> inconsistent
> ---------------------------------------------------------------------------------
>
>                 Key: SOLR-18413
>                 URL: https://issues.apache.org/jira/browse/SOLR-18413
>             Project: Solr
>          Issue Type: Bug
>          Components: SolrCloud
>    Affects Versions: 9.10.1
>            Reporter: ZhenyuLi
>            Priority: Major
>
> # Description
> During a SolrCloud `MIGRATE`, the source slice receives a temporary routing 
> rule that forwards matching updates to the target collection.
> For example, migrating route key `a!` from `source` to `target` creates state 
> similar to:
> ```text
> source/shard1.routingRules["a!"] = {
>   targetCollection: "target",
>   ...
> }
> ```
> The routing rule references the target collection by name. Deleting `target` 
> does not remove routing rules in other collections that still refer to it.
> This can leave the cluster in the following state:
> ```text
> source/shard1.routingRules["a!"].targetCollection = "target"
> ClusterState:
>   source -> exists
>   target -> missing
> ```
> While this routing rule remains active, an update to `source` with a matching 
> ID such as `a!2` is first applied locally on the source shard leader.
> The leader then enters `DistributedZkUpdateProcessor.doDistribAdd()`, which 
> resolves routing-rule destinations before distributing the update to the 
> source shard replicas:
> ```java
> final List<SolrCmdDistributor.Node> nodesByRoutingRules =
>     getNodesByRoutingRules(
>         clusterState,
>         coll,
>         cmd.getIndexedIdStr(),
>         cmd.getSolrInputDocument());
> if (nodesByRoutingRules != null && !nodesByRoutingRules.isEmpty()) {
>   // Forward to the MIGRATE target.
>   ...
> }
> if (nodes != null) {
>   // Distribute to replicas of the source shard.
>   cmdDistrib.distribAdd(...);
> }
> ```
> For a matching routing rule, `getNodesByRoutingRules()` currently resolves 
> the target with:
> ```java
> DocCollection targetColl =
>     cstate.getCollection(rule.getTargetCollectionName());
> ```
> If the target collection has already been deleted, this throws:
> ```text
> HTTP 400
> Could not find collection : target
> ```
> At this point, however, the source leader has already applied the update 
> locally.
> The exception prevents execution from reaching the subsequent source-replica 
> distribution block.
> The resulting state can therefore be:
> ```text
> source/shard1 leader:
>   a!2 exists
> source/shard1 replica:
>   a!2 missing
> client:
>   HTTP 400
> ```
> This is more than a dangling-reference error. The request reports failure 
> after partially applying the mutation, leaving replicas of the source shard 
> inconsistent.
> No update request is sent to the source replica. Therefore, 
> `SolrCmdDistributor` does not observe a replica failure. Consequently, this 
> request does not lower the replica's shard term or directly trigger recovery.
> Until another recovery mechanism repairs the replica:
> - Queries may return different results depending on which replica serves the 
> request.
> - A leader change may promote a replica that does not contain the update.
> - The client cannot safely determine whether retrying the failed request is 
> appropriate.
> - Retrying non-idempotent atomic updates could apply the operation more than 
> once.
> ## Execution flow
> ```text
> Client: ADD source / id=a!2
>               |
>               v
>       source shard leader
>               |
>               v
>          versionAdd()
>               |
>               v
>      local index/tlog update
>               |
>               v
>    getNodesByRoutingRules()
>               |
>               v
>    getCollection("target")
>               |
>               +---- target missing
>               |
>               v
>         HTTP 400 exception
>               |
>               X
>  source replica distribution
>        is never executed
> ```
> # Reproduction
> 1. Start a SolrCloud cluster with at least two nodes.
> 2. Create a `source` collection with one shard and two replicas:
> ```text
> collection = source
> numShards = 1
> replicationFactor = 2
> ```
> 3. Create a `target` collection:
> ```text
> collection = target
> numShards = 1
> replicationFactor = 1
> ```
> 4. Add and commit a document with a composite ID to the `source` collection:
> ```text
> id = a!1
> ```
> 5. Run `MIGRATE` for route key `a!`:
> ```text
> source collection = source
> target collection = target
> split.key = a!
> forward.timeout = 45
> ```
> 6. Confirm that `source/shard1` contains an unexpired routing rule for `a!` 
> whose target collection is `target`.
> 7. Delete the `target` collection and wait until it is absent from 
> `ClusterState`.
> 8. Before the routing rule expires, add another matching document to the 
> `source` collection:
> ```text
> id = a!2
> ```
> The request returns:
> ```text
> HTTP 400
> Could not find collection : target
> ```
> 9. Commit the `source` collection and query each source replica directly with 
> distributed querying disabled:
> ```text
> q=id:"a!2"
> distrib=false
> ```
> The result is:
> ```text
> source/shard1 leader:
>   a!2 exists
> source/shard1 replica:
>   a!2 missing
> ```
> # Expected behavior
> A routing rule whose target collection no longer exists should not abort a 
> source update after the source leader has already applied it.
> The missing target should invalidate the dangling routing rule. Solr should:
> 1. Stop forwarding updates through the stale routing rule.
> 2. Remove the dangling rule on a best-effort basis.
> 3. Continue distributing the update to all replicas of the source shard.
> After the request completes:
> ```text
> client:
>   update succeeds
> source/shard1 leader:
>   a!2 exists
> source/shard1 replica:
>   a!2 exists
> source routing rule:
>   dangling a! rule removed
> ```



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to