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 --> Return `NotFound` error instead of server generated rejection. ## Why? <!-- Tell your future self why have you made these changes --> For consistensy with signals and other APIs. ## How did you test it? <!-- How have you verified this change? Tested locally? Added a unit test? Checked in staging env? --> Modified existing 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/`? --> Not yet. ## 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.
1109 lines
41 KiB
Go
1109 lines
41 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"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
enumspb "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/service/history/consts"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/payloads"
|
|
"go.temporal.io/server/common/testing/protorequire"
|
|
"go.temporal.io/server/internal/effect"
|
|
"go.temporal.io/server/service/history/workflow/update"
|
|
)
|
|
|
|
const (
|
|
testAcceptedEventID int64 = 1234
|
|
testSequencingEventID int64 = 2203
|
|
)
|
|
|
|
func successOutcome(t *testing.T, s string) *updatepb.Outcome {
|
|
return &updatepb.Outcome{
|
|
Value: &updatepb.Outcome_Success{
|
|
Success: payloads.EncodeString(t.Name() + s),
|
|
},
|
|
}
|
|
}
|
|
|
|
var eventStoreUnused update.EventStore
|
|
|
|
type mockEventStore struct {
|
|
effect.Controller
|
|
AddWorkflowExecutionUpdateAcceptedEventFunc func(
|
|
updateID string,
|
|
acceptedRequestMessageId string,
|
|
acceptedRequestSequencingEventId int64,
|
|
acceptedRequest *updatepb.Request,
|
|
) (*historypb.HistoryEvent, error)
|
|
|
|
AddWorkflowExecutionUpdateCompletedEventFunc func(
|
|
acceptedEventID int64,
|
|
resp *updatepb.Response,
|
|
) (*historypb.HistoryEvent, error)
|
|
|
|
CanAddEventFunc func() bool
|
|
}
|
|
|
|
func (m mockEventStore) AddWorkflowExecutionUpdateAcceptedEvent(
|
|
updateID string,
|
|
acceptedRequestMessageId string,
|
|
acceptedRequestSequencingEventId int64,
|
|
acceptedRequest *updatepb.Request,
|
|
) (*historypb.HistoryEvent, error) {
|
|
if m.AddWorkflowExecutionUpdateAcceptedEventFunc != nil {
|
|
return m.AddWorkflowExecutionUpdateAcceptedEventFunc(updateID, acceptedRequestMessageId, acceptedRequestSequencingEventId, acceptedRequest)
|
|
}
|
|
return &historypb.HistoryEvent{EventId: testAcceptedEventID}, nil
|
|
}
|
|
|
|
func (m mockEventStore) AddWorkflowExecutionUpdateCompletedEvent(
|
|
acceptedEventID int64,
|
|
resp *updatepb.Response,
|
|
) (*historypb.HistoryEvent, error) {
|
|
if m.AddWorkflowExecutionUpdateCompletedEventFunc != nil {
|
|
return m.AddWorkflowExecutionUpdateCompletedEventFunc(acceptedEventID, resp)
|
|
}
|
|
return &historypb.HistoryEvent{}, nil
|
|
}
|
|
|
|
func (m mockEventStore) CanAddEvent() bool {
|
|
if m.CanAddEventFunc != nil {
|
|
return m.CanAddEventFunc()
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestNilMessage(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
upd := update.New(t.Name() + "update-id")
|
|
err := upd.OnProtocolMessage(ctx, nil, mockEventStore{})
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
}
|
|
|
|
func TestNilMessageBody(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
upd := update.New(t.Name() + "update-id")
|
|
emptyMsg := &protocolpb.Message{}
|
|
err := upd.OnProtocolMessage(ctx, emptyMsg, mockEventStore{})
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
}
|
|
|
|
func TestUnsupportedMessageType(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
upd := update.New(t.Name() + "update-id")
|
|
msg := protocolpb.Message{}
|
|
msg.Body = mustMarshalAny(t, &historypb.HistoryEvent{})
|
|
err := upd.OnProtocolMessage(ctx, &msg, mockEventStore{})
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
}
|
|
|
|
func TestRequestSendAcceptComplete(t *testing.T) {
|
|
// this is the most common happy path - an update is created, requested,
|
|
// accepted, and finally completed
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
completed = false
|
|
effects = effect.Buffer{}
|
|
invalidArg *serviceerror.InvalidArgument
|
|
meta = updatepb.Meta{UpdateId: t.Name() + "-update-id"}
|
|
req = updatepb.Request{Meta: &meta, Input: &updatepb.Input{Name: t.Name()}}
|
|
acpt = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: "random",
|
|
AcceptedRequestSequencingEventId: 2208,
|
|
})}
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{Meta: &meta, Outcome: successOutcome(t, "success!")})}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
|
|
completedEventData *updatepb.Response
|
|
acceptedEventData = struct {
|
|
updateID string
|
|
acceptedRequestMessageId string
|
|
acceptedRequestSequencingEventId int64
|
|
acceptedRequest *updatepb.Request
|
|
}{}
|
|
store = mockEventStore{
|
|
Controller: &effects,
|
|
AddWorkflowExecutionUpdateAcceptedEventFunc: func(
|
|
updateID string,
|
|
acceptedRequestMessageId string,
|
|
acceptedRequestSequencingEventId int64,
|
|
acceptedRequest *updatepb.Request,
|
|
) (*historypb.HistoryEvent, error) {
|
|
acceptedEventData.updateID = updateID
|
|
acceptedEventData.acceptedRequestMessageId = acceptedRequestMessageId
|
|
acceptedEventData.acceptedRequestSequencingEventId = acceptedRequestSequencingEventId
|
|
acceptedEventData.acceptedRequest = acceptedRequest
|
|
return &historypb.HistoryEvent{EventId: testAcceptedEventID}, nil
|
|
},
|
|
AddWorkflowExecutionUpdateCompletedEventFunc: func(
|
|
acceptedEventID int64,
|
|
res *updatepb.Response,
|
|
) (*historypb.HistoryEvent, error) {
|
|
completedEventData = res
|
|
return &historypb.HistoryEvent{}, nil
|
|
},
|
|
}
|
|
upd = update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
)
|
|
|
|
t.Run("request", func(t *testing.T) {
|
|
err := upd.OnProtocolMessage(ctx, &acpt, store)
|
|
require.ErrorAs(t, err, &invalidArg,
|
|
"expected InvalidArgument from %T while in Admitted state", &acpt)
|
|
|
|
err = upd.OnProtocolMessage(ctx, &resp, store)
|
|
require.ErrorAsf(t, err, &invalidArg,
|
|
"expected InvalidArgument from %T while in Admitted state", &resp)
|
|
|
|
err = upd.Admit(ctx, &req, store)
|
|
require.NoError(t, err)
|
|
require.False(t, completed)
|
|
|
|
err = upd.OnProtocolMessage(ctx, &acpt, store)
|
|
require.ErrorAsf(t, err, &invalidArg,
|
|
"expected InvalidArgument from %T while in ProvisionallyRequested state", &resp)
|
|
|
|
effects.Apply(ctx)
|
|
t.Log("update state should now be Requested")
|
|
})
|
|
|
|
msg := upd.Send(ctx, false, sequencingID)
|
|
require.NotNil(t, msg)
|
|
effects.Apply(ctx)
|
|
|
|
t.Run("accept", func(t *testing.T) {
|
|
err := upd.OnProtocolMessage(ctx, &acpt, store)
|
|
require.NoError(t, err)
|
|
require.False(t, completed)
|
|
|
|
err = upd.OnProtocolMessage(ctx, &acpt, store)
|
|
require.ErrorAsf(t, err, &invalidArg,
|
|
"expected InvalidArgument from %T in while ProvisionallyAccepted state", &resp)
|
|
require.False(t, completed)
|
|
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded,
|
|
"update acceptance should not be observable until effects are applied")
|
|
require.Nil(t, status)
|
|
|
|
effects.Apply(ctx)
|
|
t.Log("update state should now be Accepted")
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 1*time.Second)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome,
|
|
"update is accepted but not completed so outcome should be nil")
|
|
})
|
|
|
|
t.Run("respond", func(t *testing.T) {
|
|
err := upd.OnProtocolMessage(ctx, &resp, store)
|
|
require.NoError(t, err)
|
|
require.False(t, completed)
|
|
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
_, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded,
|
|
"update outcome should not be observable until effects are applied")
|
|
|
|
effects.Apply(ctx)
|
|
t.Log("update state should now be Completed")
|
|
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Response](t, &resp).Outcome, status.Outcome)
|
|
require.True(t, completed)
|
|
})
|
|
|
|
require.Equal(t, meta.UpdateId, acceptedEventData.updateID)
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Acceptance](t, &acpt).AcceptedRequestSequencingEventId, acceptedEventData.acceptedRequestSequencingEventId)
|
|
protorequire.ProtoEqual(t, mustUnmarshalBody[*updatepb.Response](t, &resp), completedEventData)
|
|
}
|
|
|
|
func TestRequestSendReject(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
completed = false
|
|
effects = effect.Buffer{}
|
|
store = mockEventStore{Controller: &effects}
|
|
updateID = t.Name() + "-update-id"
|
|
upd = update.New(updateID, update.ObserveCompletion(&completed))
|
|
req = updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Input: &updatepb.Input{Name: t.Name()},
|
|
}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
|
|
t.Run("request", func(t *testing.T) {
|
|
err := upd.Admit(ctx, &req, store)
|
|
require.NoError(t, err)
|
|
require.False(t, completed)
|
|
effects.Apply(ctx)
|
|
t.Log("update state should now be Requested")
|
|
})
|
|
|
|
msg := upd.Send(ctx, false, sequencingID)
|
|
require.NotNil(t, msg)
|
|
effects.Apply(ctx)
|
|
|
|
t.Run("reject", func(t *testing.T) {
|
|
rej := protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Rejection{
|
|
RejectedRequest: &req,
|
|
Failure: &failurepb.Failure{Message: "An intentional failure"},
|
|
})}
|
|
err := upd.OnProtocolMessage(ctx, &rej, store)
|
|
require.NoError(t, err)
|
|
require.False(t, completed)
|
|
|
|
{
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded,
|
|
"update acceptance failure should not be observable until effects are applied")
|
|
}
|
|
{
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded,
|
|
"update acceptance failure should not be observable until effects are applied")
|
|
}
|
|
|
|
effects.Apply(ctx)
|
|
t.Log("update state should now be Completed")
|
|
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 1*time.Second)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Rejection](t, &rej).Failure, status.Outcome.GetFailure())
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Rejection](t, &rej).Failure, status.Outcome.GetFailure())
|
|
|
|
require.True(t, completed)
|
|
})
|
|
}
|
|
|
|
func TestWithProtocolMessage(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
store = mockEventStore{Controller: &effect.Buffer{}}
|
|
updateID = t.Name() + "-update-id"
|
|
upd = update.NewAccepted(updateID, 2208)
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Outcome: successOutcome(t, "success!"),
|
|
})}
|
|
)
|
|
|
|
t.Run("good message", func(t *testing.T) {
|
|
err := upd.OnProtocolMessage(ctx, &resp, store)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("junk message", func(t *testing.T) {
|
|
protocolMsg := &protocolpb.Message{
|
|
Body: &anypb.Any{
|
|
TypeUrl: "nonsense",
|
|
Value: []byte("even more nonsense"),
|
|
},
|
|
}
|
|
err := upd.OnProtocolMessage(ctx, protocolMsg, store)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestSend(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
effects effect.Buffer
|
|
store = mockEventStore{Controller: &effects}
|
|
updateID = t.Name() + "-update-id"
|
|
upd = update.New(updateID)
|
|
req = updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Input: &updatepb.Input{Name: t.Name()},
|
|
}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
|
|
t.Run("before request received", func(t *testing.T) {
|
|
require.False(t, upd.NeedToSend(false))
|
|
msg := upd.Send(ctx, false, sequencingID)
|
|
require.Nil(t, msg)
|
|
require.False(t, upd.IsSent())
|
|
})
|
|
t.Run("requested", func(t *testing.T) {
|
|
require.NoError(t, upd.Admit(ctx, &req, store))
|
|
effects.Apply(ctx)
|
|
require.True(t, upd.NeedToSend(false))
|
|
msg := upd.Send(ctx, false, sequencingID)
|
|
effects.Apply(ctx)
|
|
require.NotNil(t, msg)
|
|
require.Equal(t, msg.GetEventId(), testSequencingEventID)
|
|
require.True(t, upd.IsSent())
|
|
})
|
|
t.Run("sent", func(t *testing.T) {
|
|
require.False(t, upd.NeedToSend(false))
|
|
msg := upd.Send(ctx, false, sequencingID)
|
|
effects.Apply(ctx)
|
|
require.Nil(t, msg)
|
|
require.True(t, upd.IsSent())
|
|
require.True(t, upd.NeedToSend(true))
|
|
msg = upd.Send(ctx, true, sequencingID)
|
|
effects.Apply(ctx)
|
|
require.NotNil(t, msg)
|
|
require.Equal(t, msg.GetEventId(), testSequencingEventID)
|
|
require.True(t, upd.IsSent())
|
|
})
|
|
t.Run("after requested", func(t *testing.T) {
|
|
updAccepted1 := update.NewAccepted(updateID, testAcceptedEventID)
|
|
require.False(t, upd.NeedToSend(false))
|
|
msg := updAccepted1.Send(ctx, false, sequencingID)
|
|
require.Nil(t, msg)
|
|
require.False(t, updAccepted1.IsSent())
|
|
|
|
updAccepted2 := update.NewAccepted(updateID, testAcceptedEventID)
|
|
msg = updAccepted2.Send(ctx, true, sequencingID)
|
|
require.Nil(t, msg)
|
|
require.False(t, updAccepted2.IsSent())
|
|
})
|
|
}
|
|
|
|
func TestRejectAfterAcceptFails(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
effects = effect.Buffer{}
|
|
store = mockEventStore{Controller: &effects}
|
|
)
|
|
|
|
updateID := t.Name() + "-update-id"
|
|
upd := update.NewAccepted(updateID, testAcceptedEventID)
|
|
err := upd.OnProtocolMessage(ctx, &protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Rejection{})}, store)
|
|
var invalidArg *serviceerror.InvalidArgument
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "state")
|
|
}
|
|
|
|
func TestAcceptanceAndResponseInSameMessageBatch(t *testing.T) {
|
|
// 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
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
completed = false
|
|
effects = effect.Buffer{}
|
|
store = mockEventStore{Controller: &effects}
|
|
meta = updatepb.Meta{UpdateId: t.Name() + "-update-id"}
|
|
req = updatepb.Request{Meta: &meta, Input: &updatepb.Input{Name: t.Name()}}
|
|
acpt = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: "x",
|
|
AcceptedRequestSequencingEventId: testSequencingEventID,
|
|
AcceptedRequest: &req,
|
|
})}
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{Meta: &meta, Outcome: successOutcome(t, "success!")})}
|
|
upd = update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
|
|
require.NoError(t, upd.Admit(ctx, &req, store))
|
|
effects.Apply(ctx)
|
|
|
|
_ = upd.Send(ctx, false, sequencingID)
|
|
effects.Apply(ctx)
|
|
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &acpt, store))
|
|
// no call to effects.Apply between these messages
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &resp, store))
|
|
require.False(t, completed)
|
|
effects.Apply(ctx)
|
|
require.True(t, completed)
|
|
}
|
|
|
|
func TestDuplicateRequestNoError(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
updateID = t.Name() + "-update-id"
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
upd = update.NewAccepted(updateID, testAcceptedEventID)
|
|
)
|
|
|
|
err := upd.Admit(ctx, &updatepb.Request{}, eventStoreUnused)
|
|
require.NoError(t, err,
|
|
"a second request message should be ignored, not cause an error")
|
|
|
|
msg := upd.Send(ctx, false, sequencingID)
|
|
require.Nil(t, msg)
|
|
}
|
|
|
|
func TestMessageValidation(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
updateID = t.Name() + "-update-id"
|
|
meta = updatepb.Meta{UpdateId: updateID}
|
|
req = updatepb.Request{Meta: &meta, Input: &updatepb.Input{Name: t.Name()}}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
store = mockEventStore{Controller: effect.Immediate(ctx)}
|
|
invalidArg *serviceerror.InvalidArgument
|
|
)
|
|
|
|
t.Run("invalid request msg", func(t *testing.T) {
|
|
upd := update.New("")
|
|
err := upd.Admit(ctx, &updatepb.Request{}, eventStoreUnused)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "invalid")
|
|
})
|
|
t.Run("invalid accept msg", func(t *testing.T) {
|
|
upd := update.New(updateID)
|
|
err := upd.Admit(ctx, &req, store)
|
|
require.NoError(t, err)
|
|
_ = upd.Send(ctx, true, sequencingID)
|
|
|
|
err = upd.OnProtocolMessage(
|
|
ctx,
|
|
&protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{})},
|
|
store,
|
|
)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "invalid *update.Acceptance: accepted_request_sequencing_event_id is not set")
|
|
|
|
err = upd.OnProtocolMessage(
|
|
ctx,
|
|
&protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestSequencingEventId: testSequencingEventID,
|
|
})},
|
|
store,
|
|
)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "invalid *update.Acceptance: accepted_request_message_id is not set")
|
|
})
|
|
t.Run("invalid response msg", func(t *testing.T) {
|
|
upd := update.NewAccepted("", testAcceptedEventID)
|
|
err := upd.OnProtocolMessage(
|
|
ctx,
|
|
&protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{})},
|
|
store,
|
|
)
|
|
require.ErrorAs(t, err, &invalidArg)
|
|
require.ErrorContains(t, err, "invalid")
|
|
})
|
|
}
|
|
|
|
func TestDoubleRollback(t *testing.T) {
|
|
// Test that an Update that receives both an Acceptance and a Response in
|
|
// the same message batch but is then rolled back ends up in the proper
|
|
// state.
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
completed = false
|
|
effects = effect.Buffer{}
|
|
store = mockEventStore{Controller: &effects}
|
|
reqMsgID = t.Name() + "-req-msg-id"
|
|
meta = updatepb.Meta{UpdateId: t.Name() + "-update-id"}
|
|
req = updatepb.Request{Meta: &meta, Input: &updatepb.Input{Name: t.Name()}}
|
|
acpt = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: reqMsgID,
|
|
AcceptedRequestSequencingEventId: testSequencingEventID,
|
|
})}
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{Meta: &meta, Outcome: successOutcome(t, "success!")})}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
require.NoError(t, upd.Admit(ctx, &req, store))
|
|
effects.Apply(ctx)
|
|
|
|
_ = upd.Send(ctx, false, sequencingID)
|
|
effects.Apply(ctx)
|
|
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &acpt, store))
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &resp, store))
|
|
require.False(t, completed)
|
|
|
|
t.Log("pretend MutableState write to DB fails - unwind effects")
|
|
effects.Cancel(ctx)
|
|
|
|
t.Run("not accepted", func(t *testing.T) {
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
})
|
|
t.Run("not completed", func(t *testing.T) {
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
})
|
|
t.Run("back to requested state", func(t *testing.T) {
|
|
err := upd.OnProtocolMessage(ctx, &acpt, store)
|
|
require.NoError(t, err, "update should be back in Requested state")
|
|
})
|
|
}
|
|
|
|
func TestRollbackCompletion(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
completed = false
|
|
effects = effect.Buffer{}
|
|
store = mockEventStore{Controller: &effects}
|
|
updateID = t.Name() + "-update-id"
|
|
upd = update.NewAccepted(
|
|
updateID,
|
|
testAcceptedEventID,
|
|
update.ObserveCompletion(&completed),
|
|
)
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Outcome: successOutcome(t, "success!"),
|
|
})}
|
|
)
|
|
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &resp, store))
|
|
require.False(t, completed)
|
|
|
|
t.Log("pretend MutableState write to DB fails - unwind effects")
|
|
effects.Cancel(ctx)
|
|
|
|
t.Run("not completed", func(t *testing.T) {
|
|
ctx, cncl := context.WithTimeout(ctx, 5*time.Millisecond)
|
|
t.Cleanup(cncl)
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
require.False(t, completed)
|
|
})
|
|
t.Run("back to accepted state", func(t *testing.T) {
|
|
err := upd.OnProtocolMessage(ctx, &resp, store)
|
|
require.NoError(t, err, "update should be back in Accepted state")
|
|
})
|
|
}
|
|
|
|
func TestRejectionWithAcceptanceWaiter(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
store = mockEventStore{Controller: effect.Immediate(ctx)}
|
|
updateID = t.Name() + "-update-id"
|
|
upd = update.New(updateID)
|
|
req = updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Input: &updatepb.Input{Name: "not_empty"},
|
|
}
|
|
rej = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Rejection{
|
|
RejectedRequestMessageId: "not_empty",
|
|
RejectedRequest: &req,
|
|
Failure: &failurepb.Failure{
|
|
Message: "intentional falure from " + t.Name(),
|
|
},
|
|
})}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
ch := make(chan any, 1)
|
|
go func() {
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 1*time.Second)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- status.Outcome
|
|
}()
|
|
t.Log("give 5ms for the goro to get to the Future.Get call in WaitLifecycleStage")
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
t.Log("deliver request and rejection messages")
|
|
require.NoError(t, upd.Admit(ctx, &req, store))
|
|
|
|
_ = upd.Send(ctx, false, sequencingID)
|
|
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &rej, store))
|
|
|
|
retVal := <-ch
|
|
outcome, ok := retVal.(*updatepb.Outcome)
|
|
require.Truef(t, ok, "WaitLifecycleStage returned an unexpected type: %T", retVal)
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Rejection](t, &rej).Failure, outcome.GetFailure())
|
|
}
|
|
|
|
func TestAcceptEventIDInCompletedEvent(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ctx = context.Background()
|
|
effects = effect.Buffer{}
|
|
store = mockEventStore{Controller: &effects}
|
|
updateID = t.Name() + "-update-id"
|
|
upd = update.New(updateID)
|
|
req = updatepb.Request{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Input: &updatepb.Input{Name: "not_empty"},
|
|
}
|
|
acpt = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: "not empty",
|
|
AcceptedRequest: &req,
|
|
AcceptedRequestSequencingEventId: testSequencingEventID,
|
|
})}
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{
|
|
Meta: &updatepb.Meta{UpdateId: updateID},
|
|
Outcome: successOutcome(t, "success!"),
|
|
})}
|
|
wantAcceptedEventID int64 = 8675309
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
|
|
var gotAcceptedEventID int64
|
|
store.AddWorkflowExecutionUpdateAcceptedEventFunc = func(
|
|
updateID string,
|
|
acceptedRequestMessageId string,
|
|
acceptedRequestSequencingEventId int64,
|
|
acceptedRequest *updatepb.Request,
|
|
) (*historypb.HistoryEvent, error) {
|
|
t.Log("creating accepted event with the desired event ID")
|
|
return &historypb.HistoryEvent{EventId: wantAcceptedEventID}, nil
|
|
}
|
|
store.AddWorkflowExecutionUpdateCompletedEventFunc = func(
|
|
acceptedEventID int64,
|
|
resp *updatepb.Response,
|
|
) (*historypb.HistoryEvent, error) {
|
|
t.Log("capturing acceptedEventID written in completed event")
|
|
gotAcceptedEventID = acceptedEventID
|
|
return &historypb.HistoryEvent{}, nil
|
|
}
|
|
|
|
require.NoError(t, upd.Admit(ctx, &req, store))
|
|
effects.Apply(ctx)
|
|
_ = upd.Send(ctx, false, sequencingID)
|
|
effects.Apply(ctx)
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &acpt, store))
|
|
require.NoError(t, upd.OnProtocolMessage(ctx, &resp, store))
|
|
effects.Apply(ctx)
|
|
require.Equal(t, wantAcceptedEventID, gotAcceptedEventID)
|
|
}
|
|
|
|
func TestWaitLifecycleStage(t *testing.T) {
|
|
t.Parallel()
|
|
const (
|
|
serverTimeout = 10 * time.Millisecond
|
|
)
|
|
var (
|
|
completed = false
|
|
effects = effect.Buffer{}
|
|
meta = updatepb.Meta{UpdateId: t.Name() + "-update-id"}
|
|
req = updatepb.Request{Meta: &meta, Input: &updatepb.Input{Name: t.Name()}}
|
|
acpt = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{
|
|
AcceptedRequestMessageId: "random",
|
|
AcceptedRequestSequencingEventId: 2208,
|
|
})}
|
|
rej = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Rejection{
|
|
RejectedRequest: &req,
|
|
Failure: &failurepb.Failure{Message: "An intentional failure"},
|
|
})}
|
|
resp = protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{Meta: &meta, Outcome: successOutcome(t, "success!")})}
|
|
acceptedEventData = struct {
|
|
updateID string
|
|
acceptedRequestMessageId string
|
|
acceptedRequestSequencingEventId int64
|
|
acceptedRequest *updatepb.Request
|
|
}{}
|
|
store = mockEventStore{
|
|
Controller: &effects,
|
|
AddWorkflowExecutionUpdateAcceptedEventFunc: func(
|
|
updateID string,
|
|
acceptedRequestMessageId string,
|
|
acceptedRequestSequencingEventId int64,
|
|
acceptedRequest *updatepb.Request,
|
|
) (*historypb.HistoryEvent, error) {
|
|
acceptedEventData.updateID = updateID
|
|
acceptedEventData.acceptedRequestMessageId = acceptedRequestMessageId
|
|
acceptedEventData.acceptedRequestSequencingEventId = acceptedRequestSequencingEventId
|
|
acceptedEventData.acceptedRequest = acceptedRequest
|
|
return &historypb.HistoryEvent{EventId: testAcceptedEventID}, nil
|
|
},
|
|
AddWorkflowExecutionUpdateCompletedEventFunc: func(
|
|
acceptedEventID int64,
|
|
res *updatepb.Response,
|
|
) (*historypb.HistoryEvent, error) {
|
|
return &historypb.HistoryEvent{}, nil
|
|
},
|
|
}
|
|
sequencingID = &protocolpb.Message_EventId{EventId: testSequencingEventID}
|
|
)
|
|
|
|
assertAdmitted := func(ctx context.Context, t *testing.T, upd *update.Update) {
|
|
assert := func(status *update.Status, err error) {
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
}
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, time.Duration(0))
|
|
assert(status, err)
|
|
}
|
|
|
|
assertAccepted := func(ctx context.Context, t *testing.T, upd *update.Update, alreadyInState bool) {
|
|
assert := func(status *update.Status, err error) {
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
}
|
|
if alreadyInState {
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, time.Duration(0))
|
|
assert(status, err)
|
|
}
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, serverTimeout)
|
|
assert(status, err)
|
|
}
|
|
|
|
assertSuccess := func(ctx context.Context, t *testing.T, upd *update.Update, alreadyInState bool) {
|
|
assert := func(status *update.Status, err error) {
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.Nil(t, status.Outcome.GetFailure())
|
|
require.NotNil(t, status.Outcome.GetSuccess())
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Response](t, &resp).Outcome, status.Outcome)
|
|
}
|
|
if alreadyInState {
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, time.Duration(0))
|
|
assert(status, err)
|
|
}
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, serverTimeout)
|
|
assert(status, err)
|
|
}
|
|
|
|
assertFailure := func(ctx context.Context, t *testing.T, upd *update.Update, alreadyInState bool) {
|
|
assert := func(status *update.Status, err error) {
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.NotNil(t, status.Outcome.GetFailure())
|
|
require.Nil(t, status.Outcome.GetSuccess())
|
|
require.Equal(t, mustUnmarshalBody[*updatepb.Rejection](t, &rej).Failure, status.Outcome.GetFailure())
|
|
}
|
|
if alreadyInState {
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, time.Duration(0))
|
|
assert(status, err)
|
|
}
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, serverTimeout)
|
|
assert(status, err)
|
|
}
|
|
|
|
assertSoftTimeoutWhileWaitingForAccepted := func(ctx context.Context, t *testing.T, upd *update.Update) {
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, serverTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
}
|
|
|
|
assertSoftTimeoutWhileWaitingForCompleted := func(ctx context.Context, t *testing.T, upd *update.Update) {
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, serverTimeout)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
}
|
|
|
|
assertContextDeadlineExceededWhileWaitingForAccepted := func(ctx context.Context, t *testing.T, upd *update.Update) {
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, serverTimeout)
|
|
require.Error(t, err)
|
|
require.Error(t, ctx.Err())
|
|
require.True(t, common.IsContextDeadlineExceededErr(err))
|
|
require.True(t, common.IsContextDeadlineExceededErr(ctx.Err()))
|
|
}
|
|
|
|
assertContextDeadlineExceededWhileWaitingForCompleted := func(ctx context.Context, t *testing.T, upd *update.Update) {
|
|
_, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, serverTimeout)
|
|
require.Error(t, err)
|
|
require.Error(t, ctx.Err())
|
|
require.True(t, common.IsContextDeadlineExceededErr(err))
|
|
require.True(t, common.IsContextDeadlineExceededErr(ctx.Err()))
|
|
}
|
|
|
|
applyRequest := func(ctx context.Context, req *updatepb.Request, upd *update.Update) {
|
|
err := upd.Admit(ctx, req, store)
|
|
require.NoError(t, err)
|
|
effects.Apply(ctx)
|
|
}
|
|
|
|
applyMessage := func(ctx context.Context, msg *protocolpb.Message, upd *update.Update) {
|
|
err := upd.OnProtocolMessage(ctx, msg, store)
|
|
require.NoError(t, err)
|
|
effects.Apply(ctx)
|
|
}
|
|
|
|
send := func(ctx context.Context, upd *update.Update) {
|
|
_ = upd.Send(ctx, false, sequencingID)
|
|
effects.Apply(ctx)
|
|
}
|
|
|
|
t.Run("test non-blocking calls (accepted)", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
assertAdmitted(ctx, t, upd)
|
|
applyRequest(ctx, &req, upd) // => Requested
|
|
send(ctx, upd) // => Sent
|
|
applyMessage(ctx, &acpt, upd) // => Accepted
|
|
assertAccepted(ctx, t, upd, true)
|
|
applyMessage(ctx, &resp, upd) // => Completed (success)
|
|
assertSuccess(ctx, t, upd, true)
|
|
})
|
|
|
|
t.Run("test non-blocking calls (rejected)", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
applyRequest(ctx, &req, upd) // => Requested
|
|
send(ctx, upd) // => Sent
|
|
applyMessage(ctx, &rej, upd) // => Completed (failure)
|
|
assertFailure(ctx, t, upd, true)
|
|
})
|
|
|
|
t.Run("test blocking calls (accepted)", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
applyRequest(ctx, &req, upd) // => Requested
|
|
send(ctx, upd) // => Sent
|
|
done := make(chan any)
|
|
go func() {
|
|
assertAccepted(ctx, t, upd, false)
|
|
close(done)
|
|
}()
|
|
applyMessage(ctx, &acpt, upd) // => Accepted
|
|
<-done
|
|
|
|
ctx = context.Background()
|
|
done = make(chan any)
|
|
go func() {
|
|
assertSuccess(ctx, t, upd, false)
|
|
close(done)
|
|
}()
|
|
applyMessage(ctx, &resp, upd) // => Completed (success)
|
|
<-done
|
|
})
|
|
|
|
t.Run("test blocking calls (rejected)", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
applyRequest(ctx, &req, upd) // => Requested
|
|
send(ctx, upd) // => Sent
|
|
done := make(chan any)
|
|
go func() {
|
|
assertFailure(ctx, t, upd, false)
|
|
close(done)
|
|
}()
|
|
applyMessage(ctx, &rej, upd) // => Completed (failure)
|
|
<-done
|
|
})
|
|
|
|
t.Run("timeout is soft iff due to server-imposed deadline (accepted)", func(t *testing.T) {
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
ctx, cancel := context.WithTimeout(context.Background(), serverTimeout*2)
|
|
defer cancel()
|
|
assertSoftTimeoutWhileWaitingForAccepted(ctx, t, upd)
|
|
ctx, cancel = context.WithTimeout(context.Background(), serverTimeout/2)
|
|
defer cancel()
|
|
assertContextDeadlineExceededWhileWaitingForAccepted(ctx, t, upd)
|
|
})
|
|
|
|
t.Run("timeout is soft iff due to server-imposed deadline (completed)", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), serverTimeout*2)
|
|
defer cancel()
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
assertAdmitted(ctx, t, upd)
|
|
applyRequest(ctx, &req, upd) // => Requested
|
|
send(ctx, upd) // => Sent
|
|
applyMessage(ctx, &acpt, upd) // => Accepted
|
|
assertAccepted(ctx, t, upd, true)
|
|
assertSoftTimeoutWhileWaitingForCompleted(ctx, t, upd)
|
|
ctx, cancel = context.WithTimeout(context.Background(), serverTimeout/2)
|
|
defer cancel()
|
|
assertContextDeadlineExceededWhileWaitingForCompleted(ctx, t, upd)
|
|
})
|
|
|
|
t.Run("wait for Unspecified or Admitted states returns current status immediately", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(0))
|
|
defer cancel()
|
|
upd := update.New(meta.UpdateId, update.ObserveCompletion(&completed))
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, time.Duration(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, time.Duration(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
|
|
applyRequest(ctx, &req, upd) // => Requested
|
|
send(ctx, upd) // => Sent
|
|
applyMessage(ctx, &acpt, upd) // => Accepted
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, time.Duration(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, time.Duration(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
|
|
require.Nil(t, status.Outcome)
|
|
|
|
applyMessage(ctx, &resp, upd) // => Completed (success)
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, time.Duration(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.NotNil(t, status.Outcome)
|
|
|
|
status, err = upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, time.Duration(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.NotNil(t, status.Outcome)
|
|
})
|
|
}
|
|
|
|
func TestCompletedWorkflow(t *testing.T) {
|
|
var (
|
|
ctx = context.Background()
|
|
effects = effect.Immediate(ctx)
|
|
|
|
meta = updatepb.Meta{UpdateId: t.Name() + "-update-id"}
|
|
req = updatepb.Request{Meta: &meta, Input: &updatepb.Input{Name: t.Name()}}
|
|
|
|
store = mockEventStore{Controller: effects}
|
|
roStore = mockEventStore{Controller: effects}
|
|
)
|
|
roStore.CanAddEventFunc = func() bool { return false }
|
|
|
|
t.Run("admit returns error if workflow is completed", func(t *testing.T) {
|
|
upd := update.New(meta.UpdateId)
|
|
err := upd.Admit(ctx, &req, roStore)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
|
|
|
|
oneMsCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond)
|
|
defer cancel()
|
|
status, err := upd.WaitLifecycleStage(oneMsCtx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
|
|
require.Nil(t, status)
|
|
})
|
|
|
|
t.Run("sent update is aborted if completed workflow tries to accept it", func(t *testing.T) {
|
|
upd := update.New(meta.UpdateId)
|
|
_ = upd.Admit(ctx, &req, store)
|
|
upd.Send(ctx, false, &protocolpb.Message_EventId{EventId: testSequencingEventID})
|
|
|
|
acpt := protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Acceptance{AcceptedRequestSequencingEventId: 2208})}
|
|
err := upd.OnProtocolMessage(ctx, &acpt, roStore)
|
|
require.NoError(t, err)
|
|
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
|
|
require.Nil(t, status)
|
|
})
|
|
|
|
t.Run("sent update is rejected with user rejection if completed workflow rejects it", func(t *testing.T) {
|
|
upd := update.New(meta.UpdateId)
|
|
_ = upd.Admit(ctx, &req, store)
|
|
upd.Send(ctx, false, &protocolpb.Message_EventId{EventId: testSequencingEventID})
|
|
|
|
rej := protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Rejection{
|
|
RejectedRequest: &req,
|
|
Failure: &failurepb.Failure{Message: "An intentional failure"},
|
|
})}
|
|
err := upd.OnProtocolMessage(ctx, &rej, roStore)
|
|
require.NoError(t, err)
|
|
|
|
oneMsCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond)
|
|
defer cancel()
|
|
status, err := upd.WaitLifecycleStage(oneMsCtx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.NoError(t, err)
|
|
require.Equal(t, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
|
|
require.Equal(t, "An intentional failure", status.Outcome.GetFailure().GetMessage())
|
|
})
|
|
|
|
t.Run("accepted update is rejected if completed workflow completes it", func(t *testing.T) {
|
|
upd := update.NewAccepted(meta.UpdateId, testAcceptedEventID)
|
|
|
|
resp := protocolpb.Message{Body: mustMarshalAny(t, &updatepb.Response{Meta: &meta, Outcome: successOutcome(t, "success!")})}
|
|
err := upd.OnProtocolMessage(ctx, &resp, roStore)
|
|
require.NoError(t, err)
|
|
|
|
status, err := upd.WaitLifecycleStage(ctx, enumspb.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 1*time.Second)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
|
|
require.Nil(t, status)
|
|
})
|
|
}
|
|
|
|
func mustMarshalAny(t *testing.T, pb proto.Message) *anypb.Any {
|
|
t.Helper()
|
|
var a anypb.Any
|
|
require.NoError(t, a.MarshalFrom(pb))
|
|
return &a
|
|
}
|
|
|
|
func mustUnmarshalBody[T proto.Message](t *testing.T, protocolMsg *protocolpb.Message) T {
|
|
t.Helper()
|
|
pb := new(T)
|
|
ppb := reflect.ValueOf(pb).Elem()
|
|
pbNew := reflect.New(reflect.TypeOf(pb).Elem().Elem())
|
|
ppb.Set(pbNew)
|
|
|
|
require.NoError(t, protocolMsg.Body.UnmarshalTo(*pb))
|
|
return *pb
|
|
}
|