mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-30 14:56:19 +00:00
## What changed? <!-- Describe what has changed in this PR --> Delay Update abortion with effects package. ## Why? <!-- Tell your future self why have you made these changes --> To address race condition case when Update is accepted and WF is completed on the same WFT. In this scenario Update result waiters should have similar experience as if Update was failed with error on the same WFT, meaning that even waiters for `Accepted` stage should get Update result (which is abort failure). Previous implementation had race condition and those waiter might get Update failure, or "Update is accepted" response. ## How did you test it? <!-- How have you verified this change? Tested locally? Added a unit test? Checked in staging env? --> Added unit tests. ## Potential risks <!-- Assuming the worst case, what can be broken when deploying this change to production? --> No risks. ## Documentation <!-- Have you made sure this change doesn't falsify anything currently stated in `docs/`? If significant new behavior is added, have you described that in `docs/`? --> Yes, updated. ## Is hotfix candidate? <!-- Is this PR a hotfix candidate or does it require a notification to be sent to the broader community? (Yes/No) --> No.
1215 lines
40 KiB
Go
1215 lines
40 KiB
Go
// The MIT License
|
|
//
|
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
|
//
|
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package update_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
. "go.temporal.io/api/enums/v1"
|
|
failurepb "go.temporal.io/api/failure/v1"
|
|
historypb "go.temporal.io/api/history/v1"
|
|
protocolpb "go.temporal.io/api/protocol/v1"
|
|
"go.temporal.io/api/serviceerror"
|
|
updatepb "go.temporal.io/api/update/v1"
|
|
"go.temporal.io/server/common/future"
|
|
"go.temporal.io/server/common/payloads"
|
|
. "go.temporal.io/server/common/testing/protoutils"
|
|
"go.temporal.io/server/common/testing/testvars"
|
|
"go.temporal.io/server/internal/effect"
|
|
"go.temporal.io/server/service/history/consts"
|
|
"go.temporal.io/server/service/history/workflow/update"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
)
|
|
|
|
const (
|
|
testAcceptedEventID int64 = 1234
|
|
testSequencingEventID int64 = 2203
|
|
includeAlreadySent = true
|
|
skipAlreadySent = false
|
|
)
|
|
|
|
var (
|
|
rejectionFailure = &failurepb.Failure{Message: "rejection failure"}
|
|
rejectionOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Failure{Failure: rejectionFailure}}
|
|
failureOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Failure{Failure: &failurepb.Failure{Message: "outcome failure"}}}
|
|
successOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Success{Success: payloads.EncodeString("success")}}
|
|
abortedOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Failure{Failure: update.AbortFailure}}
|
|
immediateTimeout = time.Duration(0)
|
|
immediateCtx, _ = context.WithTimeout(context.Background(), immediateTimeout)
|
|
)
|
|
|
|
func TestUpdateState(t *testing.T) {
|
|
type stateTest struct {
|
|
title string
|
|
apply func()
|
|
failOnEffect bool
|
|
}
|
|
|
|
var (
|
|
tv = testvars.New(t)
|
|
completed bool
|
|
upd *update.Update
|
|
effects *effect.Buffer
|
|
store mockEventStore
|
|
readonlyStore mockEventStore
|
|
)
|
|
|
|
initTest := func() {
|
|
t.Helper()
|
|
upd = nil // test will initialize it
|
|
completed = false
|
|
effects = &effect.Buffer{}
|
|
store = mockEventStore{Controller: effects}
|
|
readonlyStore = mockEventStore{
|
|
Controller: effects,
|
|
CanAddEventFunc: func() bool { return false }, // ie workflow is already complete
|
|
}
|
|
}
|
|
|
|
runTests := func(t *testing.T, initState func(), tests []*stateTest) {
|
|
t.Helper()
|
|
for _, tc := range tests {
|
|
t.Run(tc.title, func(t *testing.T) {
|
|
initTest()
|
|
initState()
|
|
if tc.failOnEffect == true {
|
|
// any state transition would fail now
|
|
store.Controller = nil
|
|
}
|
|
tc.apply()
|
|
})
|
|
}
|
|
}
|
|
|
|
t.Run("in stateCreated", func(t *testing.T) {
|
|
runTests(t,
|
|
func() {
|
|
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
|
|
},
|
|
[]*stateTest{{
|
|
title: "send back status admitted to waiters requesting any pre-accepted stage",
|
|
apply: func() {
|
|
assertAdmitted(t, upd)
|
|
},
|
|
}, {
|
|
title: "time out waiters requesting any stage post-admitted",
|
|
apply: func() {
|
|
assertNotAcceptedYet(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
},
|
|
}, {
|
|
title: "transition to stateAdmitted",
|
|
apply: func() {
|
|
err := admit(t, store, upd)
|
|
require.NoError(t, err)
|
|
effects.Apply(context.Background())
|
|
|
|
err = respondSuccess(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Response message while in state Admitted")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAdmitted on invalid request",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
|
|
err := upd.Admit(nil, nil)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "body is not set")
|
|
|
|
err = upd.Admit(&updatepb.Request{}, nil)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "meta is not set")
|
|
|
|
err = upd.Admit(&updatepb.Request{Meta: &updatepb.Meta{}}, nil)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "meta.update_id is not set")
|
|
|
|
err = upd.Admit(&updatepb.Request{Meta: &updatepb.Meta{UpdateId: tv.UpdateID()}}, nil)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "input is not set")
|
|
|
|
err = upd.Admit(&updatepb.Request{Meta: &updatepb.Meta{UpdateId: tv.UpdateID()}, Input: &updatepb.Input{}}, nil)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "input.name is not set")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAdmitted on store write failure",
|
|
apply: func() {
|
|
err := admit(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
err = accept(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Acceptance message while in state ProvisionallyAdmitted")
|
|
|
|
// rollback (ie store write failure)
|
|
effects.Cancel(context.Background())
|
|
|
|
err = accept(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Acceptance message while in state Created")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAdmitted when workflow is already complete",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
|
|
err := admit(t, readonlyStore, upd) // NOTE the store!
|
|
require.ErrorIs(t, consts.ErrWorkflowCompleted, err)
|
|
effects.Apply(context.Background())
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, consts.ErrWorkflowCompleted, waiterRes)
|
|
|
|
// new waiter receives same response
|
|
_, err = upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, immediateTimeout)
|
|
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateSent",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.Nil(t, send(t, upd, includeAlreadySent), "update should not be sent")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAccepted",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
err := accept(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Acceptance message while in state Created")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateRejected",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
err := reject(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Rejection message while in state Created")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateCompleted",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
err := respondSuccess(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Response message while in state Created")
|
|
},
|
|
}, {
|
|
title: "aborted because registry is cleared",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonRegistryCleared)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, update.WorkflowUpdateAbortedErr)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completed",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, consts.ErrWorkflowCompleted)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completing",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowContinuing)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, consts.ErrWorkflowClosing)
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("in stateAdmitted", func(t *testing.T) {
|
|
runTests(t,
|
|
func() {
|
|
t.Helper()
|
|
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
|
|
require.NoError(t, admit(t, store, upd))
|
|
effects.Apply(context.Background())
|
|
},
|
|
[]*stateTest{{
|
|
title: "send back status admitted to waiters requesting any pre-accepted stage",
|
|
apply: func() {
|
|
assertAdmitted(t, upd)
|
|
},
|
|
}, {
|
|
title: "time out waiters requesting any stage post-admitted",
|
|
apply: func() {
|
|
assertNotAcceptedYet(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
},
|
|
}, {
|
|
title: "transition to stateSent",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.NotNil(t, send(t, upd, skipAlreadySent), "update should be sent")
|
|
|
|
err := respondSuccess(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Response message while in state Sent")
|
|
},
|
|
}, {
|
|
// See comment in onAcceptanceMsg about stateAdmitted.
|
|
// This test is to ensure that transition from stateAdmitted to stateAccepted.
|
|
title: "transition to stateAccepted",
|
|
apply: func() {
|
|
// NOTE: skipping transition to stateSent!
|
|
err := accept(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
assertAdmitted(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
|
|
assertAccepted(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
},
|
|
}, {
|
|
// See comment in onAcceptanceMsg about stateAdmitted.
|
|
// This test is to ensure that transition from stateAdmitted to stateCompleted (rejected) is valid.
|
|
title: "transition to stateRejected",
|
|
apply: func() {
|
|
// NOTE: skipping transition to stateSent!
|
|
err := reject(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
assertNotAcceptedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.True(t, completed, "update state should now be completed")
|
|
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateCompleted",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
err := respondSuccess(t, store, upd)
|
|
require.ErrorContains(t, err, "received *update.Response message while in state Admitted")
|
|
},
|
|
}, {
|
|
title: "ignore another admit request",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.NoError(t, admit(t, store, upd))
|
|
},
|
|
}, {
|
|
title: "aborted because registry is cleared",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonRegistryCleared)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, update.WorkflowUpdateAbortedErr)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completed",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, consts.ErrWorkflowCompleted)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completing",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowContinuing)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, consts.ErrWorkflowClosing)
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("in stateSent", func(t *testing.T) {
|
|
runTests(t,
|
|
func() {
|
|
t.Helper()
|
|
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
|
|
require.NoError(t, admit(t, store, upd))
|
|
effects.Apply(context.Background())
|
|
require.NotNil(t, send(t, upd, includeAlreadySent), "update should be sent")
|
|
},
|
|
[]*stateTest{{
|
|
title: "send back status admitted to waiters requesting any pre-accepted stage",
|
|
apply: func() {
|
|
assertAdmitted(t, upd)
|
|
},
|
|
}, {
|
|
title: "time out waiters requesting any stage post-admitted",
|
|
apply: func() {
|
|
assertNotAcceptedYet(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
},
|
|
}, {
|
|
title: "transition to stateAccepted",
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status
|
|
}()
|
|
|
|
var gotAcceptedRequestSequencingEventId int64
|
|
store.AddWorkflowExecutionUpdateAcceptedEventFunc = func(
|
|
updateID string,
|
|
acceptedRequestMessageId string,
|
|
acceptedRequestSequencingEventId int64,
|
|
acceptedRequest *updatepb.Request,
|
|
) (*historypb.HistoryEvent, error) {
|
|
gotAcceptedRequestSequencingEventId = acceptedRequestSequencingEventId
|
|
return &historypb.HistoryEvent{}, nil
|
|
}
|
|
|
|
err := accept(t, store, upd)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testSequencingEventID, gotAcceptedRequestSequencingEventId)
|
|
assertNotAcceptedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, &update.Status{Stage: UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}, waiterRes)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAccepted/stateCompleted on store write failure",
|
|
apply: func() {
|
|
// acceptance and response message arrived *together*
|
|
err := accept(t, store, upd)
|
|
require.NoError(t, err)
|
|
err = respondSuccess(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
// rollback (ie store write failure)
|
|
effects.Cancel(context.Background())
|
|
|
|
assertAdmitted(t, upd)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAccepted on invalid request",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
|
|
err := upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestSequencingEventId: testSequencingEventID,
|
|
}),
|
|
}, store)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "accepted_request_message_id is not set")
|
|
|
|
err = upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: tv.MessageID(),
|
|
}),
|
|
}, store)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "accepted_request_sequencing_event_id is not set")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateAccepted when workflow is already complete",
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
|
|
// accept update
|
|
err := accept(t, readonlyStore, upd) // NOTE the store!
|
|
require.NoError(t, err)
|
|
effects.Apply(context.Background())
|
|
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, immediateTimeout)
|
|
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
|
|
require.Nil(t, status)
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, consts.ErrWorkflowCompleted, waiterRes)
|
|
|
|
// new waiter still sees same result
|
|
_, err = upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, immediateTimeout)
|
|
require.EqualExportedValues(t, consts.ErrWorkflowCompleted, err)
|
|
},
|
|
}, {
|
|
title: "transition to stateRejected",
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
|
|
// reject update
|
|
err := reject(t, store, upd)
|
|
require.NoError(t, err)
|
|
assertNotCompletedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.True(t, completed, "update state should now be completed")
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, rejectionOutcome, waiterRes)
|
|
|
|
// new waiter receives same response
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateRejected on store write failure",
|
|
apply: func() {
|
|
err := reject(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
// rollback (ie store write failure)
|
|
effects.Cancel(context.Background())
|
|
|
|
assertAdmitted(t, upd)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateRejected when workflow is already complete",
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
|
|
// reject update
|
|
err := reject(t, readonlyStore, upd) // NOTE the store!
|
|
require.NoError(t, err)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.True(t, completed, "update state should now be completed")
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, rejectionOutcome, waiterRes)
|
|
|
|
// new waiter receives same response
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
}, {
|
|
// it is possible for the acceptance message and the completion message to
|
|
// be part of the same message batch, and thus they will be delivered without
|
|
// an intermediate call to apply pending effects
|
|
title: "transition to stateCompleted via stateAccepted in one WFT",
|
|
apply: func() {
|
|
// start waiters
|
|
accptCh := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
|
|
if err != nil {
|
|
accptCh <- err
|
|
return
|
|
}
|
|
accptCh <- status.Outcome
|
|
}()
|
|
complCh := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
complCh <- err
|
|
return
|
|
}
|
|
complCh <- status.Outcome
|
|
}()
|
|
|
|
err := accept(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
// NOTE: no call to apply pending effects between these messages!
|
|
|
|
err = respondSuccess(t, store, upd)
|
|
require.NoError(t, err)
|
|
require.False(t, completed, "update state should not be completed yet")
|
|
|
|
assertNotAcceptedYet(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.True(t, completed, "update state should now be completed")
|
|
|
|
// ensure both waiter received completed response
|
|
accptWaiterRes := <-accptCh
|
|
complWaiterRes := <-complCh
|
|
require.EqualExportedValues(t, successOutcome, accptWaiterRes)
|
|
require.EqualExportedValues(t, successOutcome, complWaiterRes)
|
|
|
|
// new waiter receives same response
|
|
assertCompleted(t, upd, successOutcome)
|
|
},
|
|
}, {
|
|
// it is possible for the acceptance message and the WF completion command to
|
|
// be part of the same message batch, and thus they will be delivered without
|
|
// an intermediate call to apply pending effects
|
|
title: "transition to stateAborted via stateAccepted in one WFT",
|
|
apply: func() {
|
|
// start waiters
|
|
accptCh := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
|
|
if err != nil {
|
|
accptCh <- err
|
|
return
|
|
}
|
|
accptCh <- status.Outcome
|
|
}()
|
|
complCh := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
complCh <- err
|
|
return
|
|
}
|
|
complCh <- status.Outcome
|
|
}()
|
|
|
|
err := accept(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
// NOTE: no call to apply pending effects between these messages!
|
|
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
require.False(t, completed, "completed call back should not be called when aborted")
|
|
|
|
assertNotAcceptedYet(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.False(t, completed, "completed call back should not be called when aborted")
|
|
|
|
// ensure both waiter received completed response
|
|
accptWaiterRes := <-accptCh
|
|
complWaiterRes := <-complCh
|
|
require.EqualExportedValues(t, abortedOutcome, accptWaiterRes)
|
|
require.EqualExportedValues(t, abortedOutcome, complWaiterRes)
|
|
|
|
// new waiter receives same response
|
|
assertAborted(t, upd, nil)
|
|
},
|
|
}, {
|
|
title: "ignore another send request; unless explicitly requested",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.Nil(t, send(t, upd, skipAlreadySent), "update should not be sent")
|
|
require.NotNil(t, send(t, upd, includeAlreadySent), "update should be sent")
|
|
},
|
|
}, {
|
|
title: "aborted because registry is cleared",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonRegistryCleared)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, update.WorkflowUpdateAbortedErr)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completed",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, consts.ErrWorkflowCompleted)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completing",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowContinuing)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, consts.ErrWorkflowClosing)
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("in stateAccepted", func(t *testing.T) {
|
|
runTests(t,
|
|
func() {
|
|
t.Helper()
|
|
upd = update.NewAccepted(tv.UpdateID(), testAcceptedEventID, update.ObserveCompletion(&completed))
|
|
},
|
|
[]*stateTest{{
|
|
title: "send back status accepted to waiters requesting any pre-completed stage",
|
|
apply: func() {
|
|
assertAccepted(t, upd)
|
|
},
|
|
}, {
|
|
title: "time out waiters requesting stage completed",
|
|
apply: func() {
|
|
assertNotCompletedYet(t, upd)
|
|
},
|
|
}, {
|
|
title: "transition to stateCompleted with success response",
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
|
|
var gotAcceptedEventID int64
|
|
store.AddWorkflowExecutionUpdateCompletedEventFunc = func(
|
|
acceptedEventID int64,
|
|
resp *updatepb.Response,
|
|
) (*historypb.HistoryEvent, error) {
|
|
gotAcceptedEventID = acceptedEventID
|
|
return &historypb.HistoryEvent{}, nil
|
|
}
|
|
|
|
// receive update response
|
|
err := respondSuccess(t, store, upd)
|
|
require.NoError(t, err)
|
|
require.False(t, completed, "update state should not be completed yet")
|
|
require.Equal(t, testAcceptedEventID, gotAcceptedEventID)
|
|
|
|
assertNotCompletedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.True(t, completed, "update state should now be completed")
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, successOutcome, waiterRes)
|
|
|
|
// new waiter receives same response
|
|
assertCompleted(t, upd, successOutcome)
|
|
},
|
|
}, {
|
|
title: "transition to stateCompleted with failure response",
|
|
apply: func() {
|
|
// start waiter
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
|
|
// receive update response
|
|
err := respondFailure(t, store, upd)
|
|
require.NoError(t, err)
|
|
require.False(t, completed, "update state should not be completed yet")
|
|
|
|
assertNotCompletedYet(t, upd)
|
|
|
|
// apply pending effect
|
|
effects.Apply(context.Background())
|
|
require.True(t, completed, "update state should now be completed")
|
|
|
|
// ensure waiter received response
|
|
waiterRes := <-ch
|
|
require.EqualExportedValues(t, failureOutcome, waiterRes)
|
|
|
|
// new waiter receives same response
|
|
assertCompleted(t, upd, failureOutcome)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateComplete on invalid request",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
|
|
err := upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Response{}),
|
|
}, store)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "meta is not set")
|
|
|
|
err = upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Response{
|
|
Outcome: failureOutcome,
|
|
}),
|
|
}, store)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "meta is not set")
|
|
|
|
err = upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{},
|
|
}),
|
|
}, store)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "update_id is not set")
|
|
|
|
err = upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{UpdateId: upd.ID()},
|
|
}),
|
|
}, store)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "outcome is not set")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateRejected",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
err := reject(t, store, upd)
|
|
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "received *update.Rejection message while in state Accepted")
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateCompleted when workflow is already complete",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
err := respondSuccess(t, readonlyStore, upd) // NOTE the store!
|
|
require.NoError(t, err)
|
|
effects.Apply(context.Background())
|
|
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, status)
|
|
require.Equal(t, "Workflow Update failed because the Workflow completed before the Update completed.", status.Outcome.GetFailure().Message)
|
|
},
|
|
}, {
|
|
title: "fail to transition to stateCompleted on store write failure",
|
|
apply: func() {
|
|
err := respondSuccess(t, store, upd)
|
|
require.NoError(t, err)
|
|
|
|
assertAccepted(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
|
|
// rollback (ie store write failure)
|
|
effects.Cancel(context.Background())
|
|
|
|
assertAccepted(t, upd)
|
|
assertNotCompletedYet(t, upd)
|
|
},
|
|
}, {
|
|
title: "ignore another admit request",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.NoError(t, admit(t, store, upd))
|
|
},
|
|
}, {
|
|
title: "ignore another send request; even when explicitly requested",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.Nil(t, send(t, upd, skipAlreadySent), "update should not be sent")
|
|
require.Nil(t, send(t, upd, includeAlreadySent), "update should not be sent")
|
|
},
|
|
}, {
|
|
title: "aborted because registry is cleared",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonRegistryCleared)
|
|
effects.Apply(context.Background())
|
|
// Because Update was already accepted, clear registry doesn't require retry.
|
|
assertAccepted(t, upd)
|
|
|
|
// And even completed stage returns accepted status (as most advanced status reached).
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completed",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, nil)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completing",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowContinuing)
|
|
effects.Apply(context.Background())
|
|
assertAborted(t, upd, nil)
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("in stateRejected", func(t *testing.T) {
|
|
runTests(t,
|
|
func() {
|
|
t.Helper()
|
|
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
|
|
require.NoError(t, admit(t, store, upd))
|
|
effects.Apply(context.Background())
|
|
require.NotNil(t, send(t, upd, includeAlreadySent), "update should be sent")
|
|
require.NoError(t, reject(t, store, upd))
|
|
effects.Apply(context.Background())
|
|
},
|
|
[]*stateTest{{
|
|
title: "send back status completed to waiters immediately for any stage",
|
|
apply: func() {
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
}, {
|
|
title: "aborted because registry is cleared",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonRegistryCleared)
|
|
effects.Apply(context.Background())
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completed",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
effects.Apply(context.Background())
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completing",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowContinuing)
|
|
effects.Apply(context.Background())
|
|
assertCompleted(t, upd, rejectionOutcome)
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("in stateCompleted", func(t *testing.T) {
|
|
runTests(t,
|
|
func() {
|
|
t.Helper()
|
|
upd = update.NewCompleted(tv.UpdateID(), future.NewReadyFuture(successOutcome, nil))
|
|
assertCompleted(t, upd, successOutcome)
|
|
},
|
|
[]*stateTest{{
|
|
title: "send back status completed to waiters immediately for any stage",
|
|
apply: func() {
|
|
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
|
|
} {
|
|
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.EqualExportedValues(t, successOutcome, status.Outcome)
|
|
}
|
|
},
|
|
}, {
|
|
title: "ignore another admit request",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.NoError(t, admit(t, store, upd))
|
|
},
|
|
}, {
|
|
title: "ignore another send request; even when explicitly requested",
|
|
failOnEffect: true,
|
|
apply: func() {
|
|
require.Nil(t, send(t, upd, skipAlreadySent), "update should not be sent")
|
|
require.Nil(t, send(t, upd, includeAlreadySent), "update should not be sent")
|
|
},
|
|
}, {
|
|
title: "aborted because registry is cleared",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonRegistryCleared)
|
|
effects.Apply(context.Background())
|
|
assertCompleted(t, upd, successOutcome)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completed",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowCompleted)
|
|
effects.Apply(context.Background())
|
|
assertCompleted(t, upd, successOutcome)
|
|
},
|
|
}, {
|
|
title: "aborted because Workflow completing",
|
|
apply: func() {
|
|
abort(t, store, upd, update.AbortReasonWorkflowContinuing)
|
|
effects.Apply(context.Background())
|
|
assertCompleted(t, upd, successOutcome)
|
|
},
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestOnProtocolMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var tv = testvars.New(t)
|
|
|
|
// various state transitions are already tested above, this only tests special edge cases
|
|
|
|
t.Run("junk message", func(t *testing.T) {
|
|
upd := update.New(tv.UpdateID())
|
|
err := upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: &anypb.Any{
|
|
TypeUrl: "nonsense",
|
|
Value: []byte("even more nonsense"),
|
|
},
|
|
}, nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("nil message", func(t *testing.T) {
|
|
upd := update.New(tv.UpdateID())
|
|
err := upd.OnProtocolMessage(nil, mockEventStore{})
|
|
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "received nil message")
|
|
})
|
|
|
|
t.Run("nil message body", func(t *testing.T) {
|
|
upd := update.New(tv.UpdateID())
|
|
emptyMsg := &protocolpb.Message{}
|
|
err := upd.OnProtocolMessage(emptyMsg, mockEventStore{})
|
|
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "nil body")
|
|
})
|
|
|
|
t.Run("unsupported message type", func(t *testing.T) {
|
|
upd := update.New(tv.UpdateID())
|
|
msg := protocolpb.Message{}
|
|
msg.Body = MarshalAny(t, &historypb.HistoryEvent{})
|
|
err := upd.OnProtocolMessage(&msg, mockEventStore{})
|
|
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "not supported")
|
|
})
|
|
}
|
|
|
|
func mustAdmit(t *testing.T, store mockEventStore, upd *update.Update) {
|
|
t.Helper()
|
|
require.NoError(t, admit(t, store, upd))
|
|
assertAdmitted(t, upd)
|
|
}
|
|
|
|
func assertAdmitted(t *testing.T, upd *update.Update) {
|
|
t.Helper()
|
|
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome, "outcome should not be available yet")
|
|
|
|
status, err = upd.WaitLifecycleStage(immediateCtx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome, "outcome should not be available yet")
|
|
}
|
|
|
|
func admit(t *testing.T, store mockEventStore, upd *update.Update) error {
|
|
t.Helper()
|
|
return upd.Admit(&updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: upd.ID()},
|
|
Input: &updatepb.Input{Name: "not_empty"},
|
|
}, store)
|
|
}
|
|
|
|
func send(t *testing.T, upd *update.Update, includeAlreadySent bool) *protocolpb.Message {
|
|
t.Helper()
|
|
return upd.Send(includeAlreadySent, &protocolpb.Message_EventId{EventId: testSequencingEventID})
|
|
}
|
|
|
|
func reject(t *testing.T, store mockEventStore, upd *update.Update) error {
|
|
t.Helper()
|
|
return upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Rejection{
|
|
RejectedRequestMessageId: "update1/request",
|
|
RejectedRequest: &updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: upd.ID()},
|
|
Input: &updatepb.Input{Name: "not_empty"},
|
|
},
|
|
Failure: rejectionFailure,
|
|
})}, store)
|
|
}
|
|
|
|
func mustAccept(t *testing.T, store mockEventStore, upd *update.Update) {
|
|
t.Helper()
|
|
require.NoError(t, accept(t, store, upd), "update should be accepted")
|
|
assertAccepted(t, upd)
|
|
}
|
|
|
|
func accept(t *testing.T, store mockEventStore, upd *update.Update) error {
|
|
tv := testvars.New(t)
|
|
return upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: tv.MessageID(),
|
|
AcceptedRequestSequencingEventId: testSequencingEventID,
|
|
})}, store)
|
|
}
|
|
|
|
func assertAccepted(t *testing.T, upd *update.Update) {
|
|
t.Helper()
|
|
|
|
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
|
|
} {
|
|
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome, "outcome should not be available yet")
|
|
}
|
|
}
|
|
|
|
func assertNotAcceptedYet(t *testing.T, upd *update.Update) {
|
|
t.Helper()
|
|
|
|
// on client timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
|
t.Cleanup(cancel)
|
|
_, err := upd.WaitLifecycleStage(ctx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
|
|
// on server timeout
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome, "outcome should not be available yet")
|
|
|
|
}
|
|
|
|
func respondSuccess(t *testing.T, store mockEventStore, upd *update.Update) error {
|
|
t.Helper()
|
|
return upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{UpdateId: upd.ID()},
|
|
Outcome: successOutcome,
|
|
})}, store)
|
|
}
|
|
|
|
func respondFailure(t *testing.T, store mockEventStore, upd *update.Update) error {
|
|
t.Helper()
|
|
return upd.OnProtocolMessage(&protocolpb.Message{
|
|
Body: MarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{UpdateId: upd.ID()},
|
|
Outcome: failureOutcome,
|
|
})}, store)
|
|
}
|
|
|
|
func assertCompleted(t *testing.T, upd *update.Update, outcome *updatepb.Outcome) {
|
|
t.Helper()
|
|
|
|
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
|
|
} {
|
|
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.EqualExportedValues(t, outcome, status.Outcome)
|
|
}
|
|
}
|
|
|
|
func assertNotCompletedYet(t *testing.T, upd *update.Update) {
|
|
t.Helper()
|
|
|
|
// on client timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
|
t.Cleanup(cancel)
|
|
_, err := upd.WaitLifecycleStage(ctx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
|
|
// on server timeout
|
|
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, immediateTimeout)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.Nil(t, status.Outcome, "outcome should not be available yet")
|
|
}
|
|
|
|
func abort(t *testing.T, store mockEventStore, upd *update.Update, reason update.AbortReason) {
|
|
t.Helper()
|
|
upd.Abort(reason, store)
|
|
}
|
|
|
|
func assertAborted(t *testing.T, upd *update.Update, expectedErr error) {
|
|
t.Helper()
|
|
|
|
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
|
|
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
|
|
} {
|
|
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
|
|
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immediateTimeout)
|
|
if expectedErr == nil {
|
|
// expectedErr == nil means update was aborted with failure instead of error.
|
|
require.NoError(t, err)
|
|
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.EqualExportedValues(t, abortedOutcome, status.Outcome)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, expectedErr)
|
|
}
|
|
}
|
|
}
|