Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add saga interceptor for worker hosting saga #936

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

ychensha
Copy link

@ychensha ychensha commented Oct 14, 2022

What was changed

add worker interceptor for hosting saga workflow

Why?

saga sample has many duplicate error handle,
besides, every one will have to learn Saga pattern and then to read&copy code from others, it sucks.
Interceptor and local state is suitable for temporal to impl Saga pattern.

Checklist

  1. Closes

  2. How was this tested:

  1. Any docs updates needed?

@ychensha ychensha requested a review from a team as a code owner October 14, 2022 02:51
@CLAassistant
Copy link

CLAassistant commented Oct 14, 2022

CLA assistant check
All committers have signed the CLA.

@cretz
Copy link
Member

cretz commented Oct 14, 2022

I am not sure this has value as a completely separate module in the SDK. It is a single, simple file. Can you add a new sagainterceptor sample in https://github.com/temporalio/samples-go instead?

@ychensha
Copy link
Author

I am not sure this has value as a completely separate module in the SDK. It is a single, simple file. Can you add a new sagainterceptor sample in https://github.com/temporalio/samples-go instead?

One reason of this PR is to ask for discussion of Saga pattern impl. I think Go sdk should ship Saga util in some form, not just samples, and interceptor is my proposal. Compensations should be done in a framework way instead of handling errors manually.

@cretz
Copy link
Member

cretz commented Oct 17, 2022

This general conversation may be best at temporalio/features#50 where we're tracking our cross-SDK features instead of specific to Go SDK. I am not sure it is best to separate the invocation of an activity from its compensation declaration or rollback or error handling. Also we often discourage hidden logic in interceptors as they make it hard for a developer to understand what is actually happening in their workflow when trying to follow the code while looking at history. A helper to call on defer is often clearer since it's more explicit.

If there's a general purpose Go Saga implementation accepted by the community, it could easily be used with Temporal. But I think you'll usually find explicit error handling and rollbacks to be the norm in the ecosystem.

There are a few issues with the code if you'd like me to give it a code review anyways.

@ychensha
Copy link
Author

This general conversation may be best at temporalio/sdk-features#50 where we're tracking our cross-SDK features instead of specific to Go SDK. I am not sure it is best to separate the invocation of an activity from its compensation declaration or rollback or error handling. Also we often discourage hidden logic in interceptors as they make it hard for a developer to understand what is actually happening in their workflow when trying to follow the code while looking at history. A helper to call on defer is often clearer since it's more explicit.

If there's a general purpose Go Saga implementation accepted by the community, it could easily be used with Temporal. But I think you'll usually find explicit error handling and rollbacks to be the norm in the ecosystem.

There are a few issues with the code if you'd like me to give it a code review anyways.

Thanks for the answers, I will move the code to samples. For my team, we can get the conclusion that SAGAs is trivial to be built on top of workflow engines, and transaction solutions like Seata is tuned for performance issue not for general business purpose.

@lorensr
Copy link
Contributor

lorensr commented Oct 17, 2022

Note that the "high performance" in the first bullet of Seata SAGA is compared to their other modes, which involve locking. Temporal doesn't require locking, is very high throughout, and it's easy to implement compensation logic—easier and more flexible to implement in Temporal workflow code than in Seata's JSON state machines.

image

Comment on lines +96 to +114
func testWorkflow(ctx workflow.Context, a int) error {
zap.L().Debug("enter workflow")
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
})
var id string
if err := workflow.ExecuteActivity(ctx, "createOrder", a).Get(ctx, &id); err != nil {
return err
}
zap.L().Debug("create order, id:", zap.String("id", id))
if err := workflow.ExecuteActivity(ctx, "stockDeduct", a).Get(ctx, nil); err != nil {
return err
}
if err := workflow.ExecuteActivity(ctx, "createPay", a).Get(ctx, nil); err != nil {
return err
}

return nil
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
func testWorkflow(ctx workflow.Context, a int) error {
zap.L().Debug("enter workflow")
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
})
var id string
if err := workflow.ExecuteActivity(ctx, "createOrder", a).Get(ctx, &id); err != nil {
return err
}
zap.L().Debug("create order, id:", zap.String("id", id))
if err := workflow.ExecuteActivity(ctx, "stockDeduct", a).Get(ctx, nil); err != nil {
return err
}
if err := workflow.ExecuteActivity(ctx, "createPay", a).Get(ctx, nil); err != nil {
return err
}
return nil
}
func testWorkflow(ctx workflow.Context, a int) error {
var txn txn
zap.L().Debug("enter workflow")
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
})
var id string
if err := workflow.ExecuteActivity(ctx, "createOrder", a).Get(ctx, &id); err != nil {
return err
}
zap.L().Debug("create order, id:", zap.String("id", id))
defer txn.executeRollbackActivity(ctx, "deleteOrder")
if err := workflow.ExecuteActivity(ctx, "stockDeduct", a).Get(ctx, nil); err != nil {
return err
}
defer txn.executeRollbackActivity(ctx, "stockInc")
if err := workflow.ExecuteActivity(ctx, "createPay", a).Get(ctx, nil); err != nil {
return err
}
txn.markSuccessful()
return nil
}
type txn bool
func (t *txn) markSuccessful() { *s = true }
func (t *txn) executeRollbackActivity(ctx workflow.Context, activity interface{}, args ...interface{}) {
if !*txn {
ctx, cancel := workflow.NewDisconnectedContext(ctx)
defer cancel()
if err := workflow.ExecuteActivity(ctx, activity, args...).Get(ctx); err != nil {
workflow.GetLogger(ctx).Error("failed to convert to compensate req", zap.Error(err))
}
}
}

Untested (just typed here in issue), but this is a much simpler pattern than hiding things behind interceptors and of course you can customize it if you need to convert arguments or whatever. Notice this is just using Go defer as it should be used which is much simpler/clearer to Go developers than separate interceptors.

@ychensha
Copy link
Author

ychensha commented Oct 18, 2022

Note that the "high performance" in the first bullet of Seata SAGA is compared to their other modes, which involve locking. Temporal doesn't require locking, is very high throughout, and it's easy to implement compensation logic—easier and more flexible to implement in Temporal workflow code than in Seata's JSON state machines.

image

We have tested the throughput of some frameworks for distributed transaction. With same hardware resources and SQL storage backend, these frameworks can get 1k tps per core, that's much better than temporal.

@lorensr
Copy link
Contributor

lorensr commented Oct 24, 2022

Oh interesting! If you write a post about the test, I'd be interested to read it. I wonder whether they made any reliability or flexibility tradeoffs to get there.

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.

4 participants