Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions service/history/ndc/workflow_state_replicator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,7 @@ func (r *WorkflowStateReplicatorImpl) backfillHistory(
sortedAncestors := sortAncestors(historyBranch.GetAncestors())
sortedAncestorsIdx := 0
var ancestors []*persistencespb.HistoryBranchRange
expectedEventID := common.FirstEventID

BackfillLoop:
for remoteHistoryIterator.HasNext() {
Expand All @@ -2091,14 +2092,29 @@ BackfillLoop:
return err
}

if isStateBased {
// If backfill suceeds but later event reapply fails, during task's next retry,
// we still need to reapply events that have been stored in local DB.
events, err := r.historySerializer.DeserializeEvents(historyBlob.rawHistory)
// The remote read can come back short or with a gap without returning an error, for example when the
// source moves the branch to another storage tier between pages. Persisting mutable state on top of such
// a history would lose events permanently, so every event is checked and the task retries instead.
batch, err := r.historySerializer.DeserializeEvents(historyBlob.rawHistory)
if err != nil {
return err
}
for _, event := range batch {
if event.GetEventId() != expectedEventID {
return serviceerror.NewInternalf("Backfill history is not contiguous. Expected event %v, but got %v", expectedEventID, event.GetEventId())
}
version, err := versionhistory.GetVersionHistoryEventVersion(currentVersionHistory, event.GetEventId())
if err != nil {
return err
}
for _, event := range events {
if version != event.GetVersion() {
return serviceerror.NewInternalf("Backfill event version does not match. Expected %v, but got %v", version, event.GetVersion())
}
expectedEventID++

if isStateBased {
// If backfill suceeds but later event reapply fails, during task's next retry,
// we still need to reapply events that have been stored in local DB.
mutableState.AddReapplyCandidateEvent(event)
r.addEventToCache(mutableState.GetWorkflowKey(), event)
}
Expand Down Expand Up @@ -2184,6 +2200,10 @@ BackfillLoop:
prevBranchID = branchID
}

if expectedEventID != lastEventItem.GetEventId()+1 {
return serviceerror.NewInternalf("Backfill history ended early. Expected events up to %v, but got up to %v", lastEventItem.GetEventId(), expectedEventID-1)
}

mutableState.GetExecutionInfo().LastFirstEventTxnId = prevTxnID
return mutableState.SetCurrentBranchToken(backfillBranchToken)
}
Expand Down
151 changes: 148 additions & 3 deletions service/history/ndc/workflow_state_replicator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ndc

import (
"context"
"errors"
"fmt"
"testing"
"time"
Expand Down Expand Up @@ -166,7 +167,7 @@ func (s *workflowReplicatorSuite) Test_ApplyWorkflowState_BrandNew() {
BranchToken: historyBranch.GetData(),
Items: []*historyspb.VersionHistoryItem{
{
EventId: int64(100),
EventId: int64(6),
Version: int64(100),
},
},
Expand Down Expand Up @@ -218,11 +219,26 @@ func (s *workflowReplicatorSuite) Test_ApplyWorkflowState_BrandNew() {
int64(100),
), nil).AnyTimes()
s.mockNamespaceCache.EXPECT().GetNamespaceName(namespace.ID(namespaceID)).Return(namespace.Name(namespaceName), nil).AnyTimes()
var historyBlobs []*commonpb.DataBlob
var nodeIDs []int64
for _, batch := range [][]*historypb.HistoryEvent{
{{EventId: 1, Version: 100}, {EventId: 2, Version: 100}},
{{EventId: 3, Version: 100}, {EventId: 4, Version: 100}, {EventId: 5, Version: 100}, {EventId: 6, Version: 100}},
} {
blob, err := s.serializer.SerializeEvents(batch)
s.NoError(err)
historyBlobs = append(historyBlobs, blob)
nodeIDs = append(nodeIDs, batch[0].GetEventId())
}
s.mockRemoteAdminClient.EXPECT().GetWorkflowExecutionRawHistoryV2(gomock.Any(), gomock.Any()).Return(
&adminservice.GetWorkflowExecutionRawHistoryV2Response{},
&adminservice.GetWorkflowExecutionRawHistoryV2Response{
HistoryBatches: historyBlobs,
HistoryNodeIds: nodeIDs,
},
nil,
)
s.mockExecutionManager.EXPECT().ReadHistoryBranchByBatch(gomock.Any(), gomock.Any()).Return(nil, serviceerror.NewNotFound("test"))
s.mockExecutionManager.EXPECT().AppendRawHistoryNodes(gomock.Any(), gomock.Any()).Return(nil, nil).Times(2)
s.mockExecutionManager.EXPECT().GetCurrentExecution(gomock.Any(), gomock.Any()).Return(nil, serviceerror.NewNotFound(""))
fakeStartHistory := &historypb.HistoryEvent{
Attributes: &historypb.HistoryEvent_WorkflowExecutionStartedEventAttributes{
Expand Down Expand Up @@ -275,7 +291,7 @@ func (s *workflowReplicatorSuite) Test_ApplyWorkflowState_Ancestors() {
BranchToken: historyBranch.GetData(),
Items: []*historyspb.VersionHistoryItem{
{
EventId: int64(100),
EventId: int64(6),
Version: int64(100),
},
},
Expand Down Expand Up @@ -342,33 +358,39 @@ func (s *workflowReplicatorSuite) Test_ApplyWorkflowState_Ancestors() {
Events: []*historypb.HistoryEvent{
{
EventId: 1,
Version: 100,
},
{
EventId: 2,
Version: 100,
},
},
},
{
Events: []*historypb.HistoryEvent{
{
EventId: 3,
Version: 100,
},
},
},
{
Events: []*historypb.HistoryEvent{
{
EventId: 4,
Version: 100,
},
},
},
{
Events: []*historypb.HistoryEvent{
{
EventId: 5,
Version: 100,
},
{
EventId: 6,
Version: 100,
},
},
},
Expand Down Expand Up @@ -420,6 +442,129 @@ func (s *workflowReplicatorSuite) Test_ApplyWorkflowState_Ancestors() {
s.NoError(err)
}

func (s *workflowReplicatorSuite) TestApplyWorkflowStateBackfillIncomplete() {
testCases := []struct {
name string
remoteBatches [][]*historypb.HistoryEvent
appends int
errorContains string
}{
{
// The source returned the ancestor range but nothing from the run's own branch.
name: "ends at ancestor",
remoteBatches: [][]*historypb.HistoryEvent{
{{EventId: 1, Version: 100}, {EventId: 2, Version: 100}},
{{EventId: 3, Version: 100}},
},
appends: 2,
errorContains: "Backfill history ended early. Expected events up to 6, but got up to 3",
},
{
name: "gap",
remoteBatches: [][]*historypb.HistoryEvent{
{{EventId: 1, Version: 100}, {EventId: 2, Version: 100}},
{{EventId: 5, Version: 100}, {EventId: 6, Version: 100}},
},
appends: 1,
errorContains: "Backfill history is not contiguous. Expected event 3, but got 5",
},
{
name: "version mismatch",
remoteBatches: [][]*historypb.HistoryEvent{
{{EventId: 1, Version: 100}, {EventId: 2, Version: 100}},
{{EventId: 3, Version: 99}},
},
appends: 1,
errorContains: "Backfill event version does not match. Expected 100, but got 99",
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
namespaceID := uuid.NewString()
runID := uuid.NewString()
branchInfo := &persistencespb.HistoryBranch{
TreeId: runID,
BranchId: uuid.NewString(),
Ancestors: []*persistencespb.HistoryBranchRange{
{
BranchId: uuid.NewString(),
BeginNodeId: 1,
EndNodeId: 4,
},
},
}
historyBranch, err := s.serializer.HistoryBranchToBlob(branchInfo)
s.NoError(err)
request := &historyservice.ReplicateWorkflowStateRequest{
WorkflowState: &persistencespb.WorkflowMutableState{
ExecutionInfo: &persistencespb.WorkflowExecutionInfo{
WorkflowId: s.workflowID,
NamespaceId: namespaceID,
VersionHistories: &historyspb.VersionHistories{
Histories: []*historyspb.VersionHistory{
{
BranchToken: historyBranch.GetData(),
Items: []*historyspb.VersionHistoryItem{{EventId: 6, Version: 100}},
},
},
},
CompletionEventBatchId: 5,
},
ExecutionState: &persistencespb.WorkflowExecutionState{
RunId: runID,
State: enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED,
Status: enumspb.WORKFLOW_EXECUTION_STATUS_TERMINATED,
},
NextEventId: 7,
},
RemoteCluster: "test",
NamespaceId: namespaceID,
}
mockWeCtx := historyi.NewMockWorkflowContext(s.controller)
s.mockWorkflowCache.EXPECT().GetOrCreateWorkflowExecution(
gomock.Any(),
s.mockShard,
namespace.ID(namespaceID),
&commonpb.WorkflowExecution{WorkflowId: s.workflowID, RunId: runID},
locks.PriorityHigh,
).Return(mockWeCtx, wcache.NoopReleaseFn, nil)
mockWeCtx.EXPECT().LoadMutableState(gomock.Any(), s.mockShard).Return(nil, serviceerror.NewNotFound("ms not found"))
s.mockNamespaceCache.EXPECT().GetNamespaceByID(namespace.ID(namespaceID)).Return(namespace.NewNamespaceForTest(
&persistencespb.NamespaceInfo{Name: "namespaceName"},
nil,
false,
nil,
int64(100),
), nil).AnyTimes()

var historyBlobs []*commonpb.DataBlob
var nodeIDs []int64
for _, batch := range tc.remoteBatches {
blob, err := s.serializer.SerializeEvents(batch)
s.NoError(err)
historyBlobs = append(historyBlobs, blob)
nodeIDs = append(nodeIDs, batch[0].GetEventId())
}
s.mockRemoteAdminClient.EXPECT().GetWorkflowExecutionRawHistoryV2(gomock.Any(), gomock.Any()).Return(
&adminservice.GetWorkflowExecutionRawHistoryV2Response{
HistoryBatches: historyBlobs,
HistoryNodeIds: nodeIDs,
},
nil,
)
s.mockExecutionManager.EXPECT().ReadHistoryBranchByBatch(gomock.Any(), gomock.Any()).Return(nil, serviceerror.NewNotFound("test"))
s.mockExecutionManager.EXPECT().AppendRawHistoryNodes(gomock.Any(), gomock.Any()).Return(nil, nil).Times(tc.appends)

err = s.workflowStateReplicator.SyncWorkflowState(context.Background(), request)
s.ErrorContains(err, tc.errorContains)
// Internal errors are retried by replication tasks; InvalidArgument and DataLoss are not.
_, isInternal := errors.AsType[*serviceerror.Internal](err)
s.True(isInternal)
})
}
}

func (s *workflowReplicatorSuite) Test_ApplyWorkflowState_NoClosedWorkflow_Error() {
err := s.workflowStateReplicator.SyncWorkflowState(context.Background(), &historyservice.ReplicateWorkflowStateRequest{
WorkflowState: &persistencespb.WorkflowMutableState{
Expand Down
Loading