Files
Jiechen Zhong 054203952f Handle zombie and orphan workflows on replication create path (#11052)
## What changed?

On the passive replication-apply path, when `dispatchForNewWorkflow`
finds no current execution record (`currentRunID == ""`), it now
preserves the incoming execution state and persists the run via
`CreateWorkflowModeBypassCurrent` in either of these cases:

- the incoming execution is already `ZOMBIE`; or
- the incoming execution is `COMPLETED` or `CORRUPTED` and already
records a successor through `NewExecutionRunId` or `SuccessorRunId`.

The new `nDCTransactionPolicyCreateBypassCurrent` policy makes this
behavior explicit: it does not call `SuppressBy`, does not transition
the workflow state, and does not create or update the current execution
record. `nDCTransactionPolicyCreateAsZombie` remains reserved for paths
that actually suppress an execution.

A genuinely brand-new workflow, or a completed/corrupted run with no
successor (the latest run), still becomes current as before.

<details>
<summary>Case 1: incoming snapshot is already Zombie</summary>

```text
  SOURCE / RETIRED CELL             |  REPLICATION TARGET
------------------------------------|--------------------------------------
                                    |
  R1 [Zombie, non-current]          |  R1 absent
                                    |  cur -> none
                                    |
  FORCE-REPLICATE R1 SNAPSHOT       |
  state = Zombie          ==R1==>   |  apply R1 (create path)
                                    |  currentRunID == ""
                                    |          |
                                    |          v
                                    |  BEFORE: CreateAsCurrent (BrandNew)
                                    |  -> rejected by persistence:
                                    |     "Invalid workflow create mode 0,
                                    |      state: Zombie"
                                    |
                                    |  AFTER: CreateBypassCurrent
                                    |  -> R1 remains Zombie
                                    |  -> cur -> none
```

The snapshot is already Zombie before it reaches the transaction
manager. Promoting it to current would violate the Zombie/current
invariant; converting it is also unnecessary. The correct operation is
to preserve it as a non-current execution.

</details>

<details>
<summary>Case 2: closed orphan still points to a deleted
successor</summary>

```text
  SOURCE                            |  REPLICATION TARGET
------------------------------------|--------------------------------------
                                    |
  [1] R1 continue-as-new -> R2      |
    R1 [completed, ->R2]            |  R1 absent
    R2 running                      |  R2 absent
    cur -> R2                       |  cur -> none
                                    |
  [2] DELETE R2 (current run)       |
    R1 [completed, orphan, ->R2]    |
    R2 [deleted]                    |
    cur -> none                     |  cur -> none
                                    |
  [3] FORCE-REPLICATE R1            |
    R1 [completed, ->R2]  ==R1==>   |  apply R1 (create path)
                                    |  run absent, cur -> none
                                    |          |
                                    |          v
                                    |  BEFORE: CreateAsCurrent (BrandNew)
                                    |  -> cur -> R1
                                    |     [deleted lineage resurrected]
                                    |
                                    |  AFTER: CreateBypassCurrent
                                    |  -> R1 remains Completed
                                    |  -> cur -> none
                                    |     [no resurrection]
```

Because R1 records R2 as its successor, R1 cannot be the lineage head
even though the target no longer has a current record. Bypass-current
preserves R1's history and state without promoting it.

</details>

## Why?

Force replication can reach the create path with a missing current
record in more than one form:

1. A migration/replication snapshot may already be `ZOMBIE`.
`CreateWorkflowModeBrandNew` rejects that state because a Zombie must
never own the current execution record.
2. A reset, continue-as-new, retry, or cron transition followed by
deletion of the successor can leave a completed/corrupted orphan that
still records its successor. Promoting that older run with
`CreateWorkflowModeBrandNew` resurrects a workflow whose lineage has
already moved on.

In both cases, the incoming run is known to be non-current. Persisting
it without changing its state and without writing a current record
preserves the replicated data while maintaining the current-execution
invariants.

## How did you test it?

- [x] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)

Added unit coverage verifies that:

- a Zombie without successor metadata is persisted with
`CreateWorkflowModeBypassCurrent` and remains Zombie;
- Completed executions with `NewExecutionRunId` or `SuccessorRunId`
remain Completed;
- a Corrupted execution with a successor remains Corrupted; and
- the preserve-state path does not call suppression or update the
current execution record.

Validated with:

```text
go test ./service/history/ndc -run TestTransactionMgrForNewWorkflowSuite -count=1
```

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 14:01:19 -07:00
..