Resend parent workflow asynchronously during standby child completion verification - #11424
Merged
Merged
Conversation
michaely520
force-pushed
the
myoussef/verify-child-completion-context
branch
8 times, most recently
from
August 6, 2026 20:14
f2ceff4 to
c444514
Compare
michaely520
marked this pull request as ready for review
August 6, 2026 21:57
xwduan
reviewed
Aug 6, 2026
xwduan
reviewed
Aug 6, 2026
| // The zero value is ready to use; hold it by pointer, never copy it. | ||
| type InFlightResends struct { | ||
| mu sync.Mutex | ||
| keys map[definition.WorkflowKey]struct{} |
Contributor
There was a problem hiding this comment.
Maybe consider adding a max size for this map? So we can use it to control the maximum go routine count that are created for resending parent for each shard. Otherwise I am afraid some namespace pattern would cause sudden increase of new go routine created from this code path.
Contributor
Author
There was a problem hiding this comment.
my hope is this will reduce the outstanding number of go-routines, but as a sanity mechanism added a cap
yux0
reviewed
Aug 6, 2026
| workflowDeleteManager deletemanager.DeleteManager | ||
| serializer serialization.Serializer | ||
| workflowConsistencyChecker api.WorkflowConsistencyChecker | ||
| parentResends verifychildworkflowcompletionrecorded.InFlightResends |
Contributor
Author
There was a problem hiding this comment.
its initialized lazily on the first claim, we just use the zeroed value that is instantiated when the history engine is first instantiated per shard
michaely520
force-pushed
the
myoussef/verify-child-completion-context
branch
3 times, most recently
from
August 7, 2026 16:03
c02e9a0 to
5fc4b0d
Compare
xwduan
approved these changes
Aug 7, 2026
michaely520
force-pushed
the
myoussef/verify-child-completion-context
branch
from
August 7, 2026 18:54
5fc4b0d to
b6eb663
Compare
…ification A standby CloseExecutionTask verifies that a closed child workflow's parent recorded its completion. Past MaxLocalParentWorkflowVerificationDuration, that verification also resends (pulls) the parent workflow's state from the active cluster: a SyncWorkflowState artifact fetch, then ReplicateVersionedTransition, which may itself run a paginated GetWorkflowExecutionRawHistoryV2 backfill. That work can take minutes, but the whole call was bounded by the standby transfer task's hard-coded 3s taskTimeout, so it could never realistically finish. Measured at the active cluster's history shard, the deadline arriving at the innermost hop was 2.999s. Rather than lengthening the task's deadline -- which would park a host-level transfer-queue scheduler worker for the duration -- run the resend in the background and return immediately. The caller's task retries on the standby not-ready policy and succeeds once the parent lands. The background context is detached from the request (gRPC cancels that as soon as the handler returns), rooted at the shard's lifecycle context so the work stops if the shard does, and bounded by ReplicationTaskApplyTimeout -- the same setting that bounds this exact work when the replication stream performs it. Gated on history.enableAsyncParentWorkflowResend, default false. When disabled the resend runs inline, bounded by the caller's task deadline, as before. A shard-level guard allows one in-flight resend per parent, and caps concurrent resends per shard at history.parentWorkflowResendMaxInFlight (8), bounding the goroutines this path can create. Callers retry while an earlier resend is still running, and a parent may have many children closing at once; without the guard the xdc test below measured 5 full artifact fetches where 1 sufficed, each of which clones the parent's mutable state on the active cluster. Metrics on both paths: parent_workflow_resend_attempts, _skipped, _failures and _latency. Async failures are returned to no caller, so _failures is the signal to alert on and _latency is how to tell whether the timeout is sized correctly. Two client-side ceilings had to be lifted for ReplicationTaskApplyTimeout to reach the hops the resend makes; each applies min(parent, own timeout), so lifting one only exposed the next: admin.SyncWorkflowState standby -> active frontend (was 10s) history.SyncWorkflowState active frontend -> active history (was 30s) They now share a DefaultStateSyncTimeout backstop via a dedicated stateSyncTimeoutContext codegen tier. No existing constant changes: admin.DefaultLargeTimeout stays at 1m and GetReplicationMessages keeps using it. Also fixes the history-client codegen template, which hardcoded createContext and ignored the timeout-tier field, making tier entries for any history method dead config. Incidentally fixes a pre-existing bug: admin.SyncWorkflowState was bounded by admin.DefaultTimeout (10s) for all callers, including ExecutableTaskImpl.SyncState on the replication stream, so production's ReplicationTaskApplyTimeout of 5m was never reachable for that path's cross-cluster fetch either. Tests: the existing TestVerifyChildExecutionCompletionRecorded_ResendParent now pins the inline path; new unit tests cover the async path and the per-parent guard. Adds an xdc test that withholds the parent's replication tasks so the child must pull the parent, and stalls the active cluster's SyncWorkflowState for 4 minutes to prove the background resend completes and the retrying task verifies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes the test scaffolding added in the previous commit, leaving only the production change: - tests/xdc/verify_child_completion_parent_resend_test.go - testhooks.HistorySyncWorkflowStateInterceptor - the hook invocation in historyEngineImpl.SyncWorkflowState The test proved the deadline change end to end -- a SyncWorkflowState call blocked for a measured 240s still completed the parent pull, where every previous ceiling capped it at 3s, 10s or 30s -- but it cost ~5 minutes of wall clock, because the only way to hold the active cluster's response open is to actually sleep. See the previous commit to restore and run it. common/testing/testhooks/hooks.go and service/history/history_engine.go are restored byte-for-byte to their pre-change state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
michaely520
force-pushed
the
myoussef/verify-child-completion-context
branch
from
August 7, 2026 19:00
b6eb663 to
07ef34a
Compare
meiliang86
approved these changes
Aug 7, 2026
This was referenced Aug 7, 2026
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A standby child workflow's
CloseExecutionTaskverifies its parent recorded the completion, and pastMaxLocalParentWorkflowVerificationDurationalso resends the parent from the active cluster. That resend is a cross-cluster state sync plus a possibly paginated history backfill — minutes of work — but the whole call was bounded by the standby task's hard-coded 3staskTimeout. Measured at the active cluster's history shard, the deadline arriving there was2.999s. So the resend never completed.Change
ReplicationTaskApplyTimeout— the setting that already bounds this same work on the replication stream. The verify RPC returns immediately and the standby task retries until the parent lands, so it never holds a transfer-queue worker for the sync.history.parentWorkflowResendMaxInFlight(8) concurrent resends per shard. Callers retry while an earlier resend runs; without this the test measured 5 full state fetches where 1 suffices. The cap bounds the goroutines this path can create.admin.SyncWorkflowState(was 10s) andhistory.SyncWorkflowState(was 30s) now share aDefaultStateSyncTimeoutbackstop. This also fixes the same 10s cap on the replication stream'sExecutableTaskImpl.SyncState, where production's 5m setting was never reachable either.parent_workflow_resend_{attempts,skipped,limited,failures,latency}. Async failures reach no caller, so_failuresis the alert signal;_limitedmeans the shard is shedding resends. The background goroutine recovers panics, which would otherwise take down the process.Also fixes the history-client codegen template, which hardcoded
createContextand silently ignored the timeout-tier field.Rollout
history.enableAsyncParentWorkflowResend, default false. Disabled = the previous inline behavior, bounded by the caller's task deadline. Opt in per cell.Testing
Unit tests cover the inline, async, and per-parent-dedup paths.
An xdc test (added in abae9cec, removed in b6eb6631) withholds the parent's replication tasks so the child must pull it, asserts the parent is absent from the standby, then stalls the active cluster's
SyncWorkflowStatefor 4 minutes:During the stall, 4 verify RPCs reached the standby parent shard (t+0, +50s, +101s, +169s) and exactly 1
SyncWorkflowStatereached the active cluster: the task retried and the guard turned the retries away.4 minutes exceeds every deadline that previously bounded this path (3s / 10s / 30s) with ~1m headroom against the 5m setting, so the setting is demonstrably what governs.
To reproduce:
git revert b6eb6631, thengo test -tags test_dep ./tests/xdc/ -run TestVerifyChildCompletionParentResendSuite -timeout 30mKnown gaps
workflowNotFoundCacheTODO already in this file would address it.