Skip to content

feat: added interleave/4 #83

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

apreifsteck
Copy link

What is being changed

Adding: Sage.interleave/4. This allows the user to add a transaction to be executed after every stage they had manually defined before.

Rationale

Say you're working with a "token" struct, and you want to saga-fy this kind of code where you want to do something
with the intermediate results

t = new_big_struct()
with {:ok, %BigStruct{} = t} <- op1(t),
  log(t),
  {:ok, %BigStruct{} = t} <- op2(t),
  log(t)
 #... more steps

This code is a bit tedious to write to begin with and this PR is meant to enable Sage to help with this kind of pattern by turning the above code into

t = new_big_struct()
Sage.new()
|> Sage.run(:op1, fn _, _ -> op1(t) end)
|> Sage.run(:op2, fn _, _ -> op2(t) end)
... more steps
|> Sage.interleave(:log, fn effects, _attrs, last_effect_name -> {:ok, log(effects[last_effect_name])} end)

@apreifsteck apreifsteck changed the title feat: added interleave/3 feat: added interleave/4 Mar 6, 2025
sage :: t(),
name :: stage_name(),
intermediate_transaction :: intermediate_transaction(),
compensation :: compensation()
Copy link
Member

Choose a reason for hiding this comment

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

Do we also want compensations to be reported in your use case? If so we'll need to add a type for it too or reuse intermediate_transaction here.

Copy link
Author

Choose a reason for hiding this comment

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

Good question. My use case actually doesn't require compensations at all, I just added them for completeness/generality.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should add it for completeness. If there is some reporter on the saga progress, it should also be able to report compensation progress.

Copy link
Author

Choose a reason for hiding this comment

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

So are you saying that an interleaved compensation should be passed the previously run compensation, to mirror the intermediate_transaction?

lib/sage.ex Outdated

[stage, {name, build_operation!(:run, transaction, compensation)}]
end)
|> Enum.reduce(new(), fn stage, sage ->
Copy link
Member

Choose a reason for hiding this comment

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

I think you don't need to reconstruct the Sage completely, just update the existing one like so: %{sage | steps: steps}. Struct can be extended in the future and we don't want to re-apply everything else that is in there or lose it if we forget about it.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I did this because I didn't want to lose the duplicate stage name checking that add_step gives. But I can refactor it for sure.

Copy link
Author

Choose a reason for hiding this comment

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

I took a stab at a refactor, but I'm not sold on it. I am leaning towards thinking that the best approach is to rebuild the saga and refactor later if need be, but let me know what you think of the refactor attempt.

Copy link
Member

Choose a reason for hiding this comment

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

@apreifsteck if we use the suggestion below and namespace the interleaves then there can't be a collisition with the regular stage names, so we can do something like this:
%{sage | stage_names: MapSet.add(sage.stage_names, {:interleave, name}), steps: steps)
then just add a manual check for another interleave not colliding with the {:interleave, name} right in this function.

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented your suggestion to use tuples as the stage names. I'm not sure if I understand the advantage of adding a check in this function to look for duplicate stage names. That seems like a separate responsibility that interleave and run could both take advantage of.

@AndrewDryga
Copy link
Member

Thank you!

@apreifsteck apreifsteck requested a review from AndrewDryga March 13, 2025 18:44
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