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 #222

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

ychensha
Copy link

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?

saga/interceptor.go Outdated Show resolved Hide resolved
saga/interceptor.go Outdated Show resolved Hide resolved
@@ -0,0 +1,199 @@
package saga
Copy link
Member

Choose a reason for hiding this comment

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

Still not completely convinced hiding this stuff behind an interceptor is the clearest way. Here's an alternative from a comment on the other PR at temporalio/sdk-go#936 (comment) that I didn't test (just wrote there in comment):

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", id)

	if err := workflow.ExecuteActivity(ctx, "stockDeduct", a).Get(ctx, nil); err != nil {
		return err
	}
	defer txn.executeRollbackActivity(ctx, "stockInc", a)

	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))
		}
	}
}

You can obviously convert the arg wherever you want. This uses much more familiar Go constructs like defer and is clearer to the developer what is called on rollback. There is no hiding things on interceptor and there is no pre-registration of what is run on which activity, etc.

Copy link
Author

Choose a reason for hiding this comment

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

I understand the concerns, but interceptor itself is hidden behind workflow. Action/Compensation is registered aside with workflow like the testing code, the removal of defer is by design.

Copy link
Member

@cretz cretz Oct 19, 2022

Choose a reason for hiding this comment

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

While I understand it is by design, the question is if that is the best design. Explicit defer at the callsite is clearer to a code writer than a hidden equivalent at the registration site. Using interceptors for this, besides hiding what is happening from someone reading the workflow code and matching against history, also is a bit error prone due to making sure that interceptors are always setup the same way for all workers and that the compensations never change in successive deployments. Not to mention interceptors also add more code, more complexity, and are more limiting feature wise (e.g. can't skip a compensation if it doesn't make sense, can't use a local variable as a parameter to the compensation, etc).

Copy link
Author

@ychensha ychensha Oct 20, 2022

Choose a reason for hiding this comment

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

I believe it is good enough to quick start. When number of code in workflow is large, and activity executions is in multi functions, defer is not a easy way to handle error and do compensations. Interceptor is designed to seperate non business logic apart.

Copy link
Author

Choose a reason for hiding this comment

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

So interceptor is not welcome to implement saga pattern?

Copy link
Member

@cretz cretz Oct 25, 2022

Choose a reason for hiding this comment

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

I believe it is good enough to quick start

Sure, the point is that it's easy to run something on complete when your code fails

defer is not a easy way to handle error and do compensations.

I disagree, I think it s very easy and common in Go. Many Go functions run defer after success of something but on failure of the function as a whole.

Interceptor is designed to seperate non business logic apart.

I am not sure we want to encourage that separation. Hiding some steps of a workflow can make it hard to match against history for readers. We want workflow actions to be explicit not implicit.

So interceptor is not welcome to implement saga pattern?

I am not sure it's the best way to implement the saga pattern as it hides so much from the user as opposed to the defer pattern. These samples are meant to document Temporal best practices, and I am not sure we want to encourage use of interceptors this way.

Let me see if I can get others' opinions here.

saga/interceptor.go Outdated Show resolved Hide resolved
saga/interceptor.go Outdated Show resolved Hide resolved
saga/interceptor.go Outdated Show resolved Hide resolved
saga/interceptor.go Outdated Show resolved Hide resolved
saga/interceptor.go Outdated Show resolved Hide resolved
saga/interceptor.go Outdated Show resolved Hide resolved
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.

2 participants