Skip to content

Workflow purge#6163

Merged
artursouza merged 25 commits into
dapr:masterfrom
RyanLettieri:workflow-purge
Apr 27, 2023
Merged

Workflow purge#6163
artursouza merged 25 commits into
dapr:masterfrom
RyanLettieri:workflow-purge

Conversation

@RyanLettieri

@RyanLettieri RyanLettieri commented Mar 30, 2023

Copy link
Copy Markdown
Contributor

Description

Addition of Purge method for dapr workflow.

Issue reference

Note that this PR will not pass until the following PR is submitted into components-contrib: dapr/components-contrib#2729

Please reference the issue this PR will close: #[6043]

Checklist

Please make sure you've completed the relevant tasks for this PR, out of the following list:

Comment thread dapr/proto/runtime/v1/dapr.proto Outdated
Comment thread pkg/actors/actors.go
Comment on lines 401 to 402
}

var resp *invokev1.InvokeMethodResponse

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
var resp *invokev1.InvokeMethodResponse
}
var resp *invokev1.InvokeMethodResponse

Because there'll be a change on this file getting merged very soon and I want to spare a merge conflict :)

Comment thread pkg/runtime/wfengine/activity.go Outdated

// Try to load activity state. If we find any, that means the activity invocation is a duplicate.
if state, err := a.loadActivityState(ctx, actorID, ar.Generation); err != nil {
var state activityState

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhm something feels odd here. state is not read from a.loadActivityState anymore, so it will always be empty? and if state.Generation> 0 will always be false? I'm not sure what this should have been.

Comment thread pkg/runtime/wfengine/activity.go Outdated
if methodName == "PurgeWorkflowState" {
if err := a.purgeActivityState(ctx, actorID, state); err != nil {
return nil, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method return after processing this? We don't need to schedule a reminder, do we?

Comment thread pkg/runtime/wfengine/activity.go Outdated

func getActivityInvocationKey(generation uint64) string {
return fmt.Sprintf("activityreq-%d", generation)
// return fmt.Sprintf("activityreq-%d", generation)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removing the generation?


"github.com/dapr/components-contrib/workflows"
componentsV1alpha1 "github.com/dapr/dapr/pkg/apis/components/v1alpha1"
componentsV1alpha1 "github.com/dapr/dapr/pkg/apis/components/v1alpha1" // This will be removed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to remove this and ComponentDefinition? :)

Comment thread pkg/runtime/wfengine/component.go Outdated

func (c *workflowEngineComponent) Purge(ctx context.Context, req *workflows.PurgeRequest) error {
if req.InstanceID == "" {
return fmt.Errorf("a workflow instance ID is required")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("a workflow instance ID is required")
return errors.New("a workflow instance ID is required")

If no format is included, please use errors.New (it's faster and makes less allocs)

Comment thread pkg/runtime/wfengine/workflow.go Outdated

// Loop through the history of the loaded workflow state and schedule purge calls for all the activities that have been completed

// RRL TODO: Looping through history and invoking each actor once. They are not in the same transaction. Eqch transaction happens as its own call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the initial review during our 1:1 earlier today, this needs to change - although I admit I don't fully understand how this works so I can't tell you exactly how to fix this yet 🙃

However, the problem is that the "purge" needs to happen in an atomic way. We discussed this and you explained that you need to delete 3 things:

  • Each activity (this can be more than one)
  • The history object
  • The inbox object

These all have to be removed at once. We do that in Dapr by using the transactional state stores and performing a "transaction" (which are just operations performed atomically as a batch), but here, each activity is removed separately, in its own "transaction" (with a single opeation). Each activity is removed by the actor that you're invoking within the loop, and an error in the loop causes the entire method to exit, which means that some state will have been removed, but not all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ItalyPaleAle. The state exists across potentially multiple actors, and AFAIK, we don't support transactions across multiple actors. We instead go for the next best thing, which is to do independent purge operations in a particular order so that failures that happen midway can be retried until there is finally a successful completion. This is why, for example, we delete state for each activity first before deleting the history (without the history, we wouldn't know which actors need to be deleted). Does that make sense? Let me know if you feel we need to meet and discuss this more in depth.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. But what happens if there's a crash while the process is not complete?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crash will leave you in a partially deleted state. However, the workflow will still be visible from the GetWorkflow management APIs, and re-issuing the purge call should succeed.

Signed-off-by: Ryan Lettieri <[email protected]>
Comment thread dapr/proto/runtime/v1/dapr.proto Outdated
rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (WorkflowActivityResponse) {}

// Purge Workflow
rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (WorkflowActivityResponse) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standard practice for APIs that don't return anything is to use google.protobuf.Empty. I suggest we do the same here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this got missed?

Comment thread pkg/grpc/universalapi/api_workflow.go Outdated
}

func (a *UniversalAPI) RaiseEventWorkflowAlpha1(ctx context.Context, in *runtimev1pb.RaiseEventWorkflowRequest) (*runtimev1pb.RaiseEventWorkflowResponse, error) {
func (a *UniversalAPI) RaiseEventWorkflowAlpha1(ctx context.Context, in *runtimev1pb.RaiseEventWorkflowRequest) (*runtimev1pb.WorkflowActivityResponse, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest reverting these changes because I'm making additional changes to these signatures in a nother PR.

Comment thread pkg/messages/predefined.go Outdated
Comment thread pkg/runtime/wfengine/activity.go Outdated
Comment thread pkg/runtime/wfengine/activity.go Outdated
Comment thread pkg/runtime/wfengine/wfengine_test.go Outdated
Comment thread pkg/runtime/wfengine/wfengine_test.go Outdated
Comment thread pkg/runtime/wfengine/wfengine_test.go Outdated
Comment thread pkg/runtime/wfengine/workflow.go Outdated
for _, e := range state.History {
if ts := e.GetTaskScheduled(); ts != nil {
targetActorID := getActivityActorID(actorID, e.EventId)
eventData, err := backend.MarshalHistoryEvent(e)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be including the event data in the purge request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be addressed (though the problem has moved to a new line now)

Comment thread pkg/runtime/wfengine/backend.go Outdated
req := invokev1.
NewInvokeMethodRequest(PurgeWorkflowStateMethod).
WithActor(be.config.workflowActorType, string(id)).
WithContentType(invokev1.OctetStreamContentType)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the WithContentType line since we're not sending any content.

Comment thread dapr/proto/runtime/v1/dapr.proto Outdated
rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (WorkflowActivityResponse) {}

// Purge Workflow
rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (WorkflowActivityResponse) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this got missed?

Comment thread pkg/runtime/wfengine/wfengine_test.go Outdated
for _, opt := range GetTestOptions() {
t.Run(opt(engine), func(t *testing.T) {
id, err := client.ScheduleNewOrchestration(ctx, "ActivityChainToPurge")
if assert.NoError(t, err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce nesting, let's replace assert.NoError throughout this test with require.NoError if remove the if-blocks

Comment thread pkg/runtime/wfengine/workflow.go Outdated
for _, e := range state.History {
if ts := e.GetTaskScheduled(); ts != nil {
targetActorID := getActivityActorID(actorID, e.EventId)
eventData, err := backend.MarshalHistoryEvent(e)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be addressed (though the problem has moved to a new line now)

@cgillum cgillum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to mention that we should add a new test.

}
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add one more test in order to ensure we're covering the multi-generation case. Basically, copy the TestContinueAsNewWorkflow logic and do a purge against that workflow state, validating that all state store content was successfully removed.

@RyanLettieri
RyanLettieri marked this pull request as ready for review April 25, 2023 17:20
@RyanLettieri
RyanLettieri requested review from a team as code owners April 25, 2023 17:20
cachedState := result.(activityState)

// Make sure the cached state is for the same generation of the workflow.
if cachedState.Generation == generation {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're not using the Generation field, let's delete it from activityState.

}

for _, item := range keysPostPurge {
if strings.Contains(item, string(id)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use assert.True(t, strings.Contains(item, string(id))) here?

Comment thread pkg/runtime/wfengine/wfengine_test.go Outdated
client, engine, stateStore := startEngineAndGetStore(ctx, r)
for _, opt := range GetTestOptions() {
t.Run(opt(engine), func(t *testing.T) {
// Second Test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still relevant or can it be removed?

Comment thread pkg/runtime/wfengine/workflow.go Outdated

// Loop through the history of the loaded workflow state and schedule purge calls for all the activities that have been completed

// RRL TODO: Looping through history and invoking each actor once. They are not in the same transaction. Each transaction happens as its own call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like a TODO, but more like a description of the code.

Comment thread pkg/runtime/wfengine/workflow.go Outdated
// Loop through the history of the loaded workflow state and schedule purge calls for all the activities that have been completed

// RRL TODO: Looping through history and invoking each actor once. They are not in the same transaction. Each transaction happens as its own call
// We are deleting one by one in sequence and not in builk.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems redundant with the previous one (and has a typo). I suggest we clean up these comments.

Comment thread pkg/runtime/wfengine/workflow.go Outdated
req := invokev1.
NewInvokeMethodRequest(PurgeWorkflowStateMethod).
WithActor(wf.config.activityActorType, targetActorID).
WithRawDataBytes(activityRequestBytes).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we don't use the generation anymore, can we remove the payload completely from the actor invocation?


// The logic/for loop below purges/removes any leftover state from a completed or failed activity
// TODO: for optimization make multiple go routines and run them in parallel
for _, e := range state.Inbox {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this logic into its own method.

func addPurgeStateOperations(req *actors.TransactionalRequest, keyPrefix string, events []*backend.HistoryEvent) error {
// TODO: Investigate whether Dapr state stores put limits on batch sizes. It seems some storage
// providers have limits and we need to know if that impacts this algorithm:
// https://learn.microsoft.com/azure/cosmos-db/nosql/transactional-batch#limitations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should open a bug to track doing purges in smaller batches to avoid running into transactional batch limitations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codecov

codecov Bot commented Apr 26, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6163 (834d7dc) into master (b9370ee) will decrease coverage by 0.21%.
The diff coverage is 43.70%.

@@            Coverage Diff             @@
##           master    #6163      +/-   ##
==========================================
- Coverage   64.96%   64.76%   -0.21%     
==========================================
  Files         189      189              
  Lines       18335    18471     +136     
==========================================
+ Hits        11912    11962      +50     
- Misses       5492     5565      +73     
- Partials      931      944      +13     
Impacted Files Coverage Δ
pkg/actors/actors.go 66.00% <ø> (ø)
pkg/grpc/endpoints.go 100.00% <ø> (ø)
pkg/grpc/universalapi/api_workflow.go 89.04% <0.00%> (-10.96%) ⬇️
pkg/runtime/wfengine/component.go 0.00% <0.00%> (ø)
pkg/runtime/wfengine/activity.go 59.31% <25.00%> (-9.08%) ⬇️
pkg/runtime/wfengine/workflow.go 54.05% <40.67%> (-3.51%) ⬇️
pkg/runtime/wfengine/backend.go 61.40% <77.77%> (+1.96%) ⬆️
pkg/runtime/wfengine/workflowstate.go 79.36% <80.95%> (+0.31%) ⬆️
pkg/http/api.go 80.25% <100.00%> (+0.17%) ⬆️

... and 2 files with indirect coverage changes

@cgillum cgillum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to other reviewers that we decided to remove the de-dupe detection for activities since it was turning out to be a problematic (undocumented) optimization. We'll revisit whether it makes sense to add it back in the future.

A couple more things before signing off.

Comment thread pkg/runtime/wfengine/workflow.go Outdated
Comment thread pkg/runtime/wfengine/activity.go
cgillum
cgillum previously approved these changes Apr 26, 2023
@berndverst

Copy link
Copy Markdown
Member

Once this PR is merged, @RyanLettieri you'll need to generate the protos in the various SDK PRs you have open for workflow management.

Implement the purge operation too, please.

Signed-off-by: Bernd Verst <[email protected]>
@artursouza
artursouza dismissed ItalyPaleAle’s stale review April 27, 2023 23:16

Alessandro is on vacation.

@artursouza
artursouza merged commit 07a6e03 into dapr:master Apr 27, 2023
@ItalyPaleAle ItalyPaleAle added this to the v1.11 milestone May 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants