-
-
Notifications
You must be signed in to change notification settings - Fork 55
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 around steps #85
Add around steps #85
Conversation
These can be used for implementing database transactions and similar things
78d6d3e
to
fc3651a
Compare
Looks like all APIs is private so there is not much to document, we should add docs on the site instead. |
lib/dry/transaction/step.rb
Outdated
end | ||
end | ||
|
||
def once(next_step) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is looking smarter then needed but distinguishing around
steps from others would require changing the API of step adapters but I'm not sure if we want to do it this time, although we could.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you think things would look if you did change the step adapter API? If we're going to change it, now is kind of the time to do it, while we're still pre-1.0 and discovering (with the introduction of this step) the various different things we might want to be doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So AFAICT the reason for this is that the step adapter may call the &continue
proc more than once? Would you mind pointing out where/how this might happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re API, I need to play with it a bit, I'll come up with an example later. Maybe adding something like yields?
to the adapter would be enough
re once
continue
will be called once on every regular step and called twice on an around step: the first time in yield
of the step definition and the second on the explicit continue.(result)
call. TBH, having a second thought I realized this has some drawbacks, for instance, it doesn't allow to halt the execution in an around step and I imagine this could be useful somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @flash-gordon, this is super neat and I'm really happy to see this kind of thing built into dry-transaction at last 🙏
Everything overall feels good to me. I left a couple of comments about minor things inline.
As for some more general thoughts:
When I first saw this PR I wondered about naming. e.g. if something is called around, shouldn't it be clear exactly around what it's wrapping? i.e. I thought to myself, wouldn't it better if we could do this...
around :transaction do
step :persist_user
step :persist_account
end
Instead of the linear approach of:
around :transaction
step :persist_user
step :persist_account
But I realise this'd probably add a significant degree of complexity to the DSL and could open up the possibility of a lot of errors if people tried to nest steps inside other steps that don't support this around-style wrapping.
So I guess we just need to make it clear in the docs that "around" really means "around all the subsequent steps".
What do you think?
Thanks for tidying up the specs, too. That's looking much neater.
lib/dry/transaction/step.rb
Outdated
@@ -7,6 +7,7 @@ module Transaction | |||
# @api private | |||
class Step | |||
UNDEFINED = Object.new.freeze | |||
NOOP = :itself.to_proc.freeze |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice this same NOOP
proc is called LOOPBACK
inside Stack
. Is the difference in naming intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sort of, I'd say they serve different purposes. In fact, NOOP
is only needed for unit tests and we could pass it there instead
lib/dry/transaction/step.rb
Outdated
end | ||
end | ||
|
||
def once(next_step) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So AFAICT the reason for this is that the step adapter may call the &continue
proc more than once? Would you mind pointing out where/how this might happen?
lib/dry/transaction/step.rb
Outdated
broadcast :step_succeeded, step_name, *args | ||
value | ||
}.or { |value| | ||
broadcast :step_failed, step_name, *args, value | ||
Failure(StepFailure.new(self, value)) | ||
} | ||
|
||
continue.(result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you feel this line might make more sense underneath step_adapter.call(self, *args, &continue)
inside the with_broadcast do ...
block above? Feels like this particular line isn't about broadcasting at all...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, though we cannot have it inside the block because this would continue
the execution before broadcasting and I believe we want to emit step_{succeeded,failed}
before calling the next step.
lib/dry/transaction/stack.rb
Outdated
@stack.(m) | ||
end | ||
|
||
def compile(steps) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I reckon it'd be good to drop this under a private
, if you don't mind :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing
Speaking of blocks in the DSL, I thought about it too and abandoned the idea quite soon as this would complicate the understanding of how transactions work. I think it's better to have nicer docs where we should refer to Rack as a similar concept. Interestingly, if you apply this similarity further you may find out that having blocks in the DSL is equal to implementing a router engine :) |
This commit changes the existign adapter API, however, the changes are essentially cosmetic. Step adapters don't get steps as input parameters anymore, this was clearly a mixing of concerns, as in, a step calls its adapter and then the adapter calls the step back. I added two simple wrapper classes called StepAdater and Callable. StepAdater simply wraps an adater object. Since we don't use inheritance (and this is awesome) we don't have common behavior between adapters besides the `.call` method. A StepAdapter instance stores step options and the actual operation to be called. It also dynamically determines whether the adaper yields a block thus make it possible to distinguish around-ish steps from before-ish ones. To do so it relies on the list of arguments of the adapter, if it has a block object then the adapter can control the execution. I know this might look unnecessary smart but I think there will be not that many around-like adapters, in fact, I think there will be exactly one so it's better to have the adapter interface thiner. Callable is a simple wrapper around the actual operation, it exists only for checking operation's arity. These classes simplified the Step class quite significantly and allowed me to separate flows for around-like adapters without any hacks.
@timriley here we go 😅 It took me a few hours to come up with this variant of API. In fact, it didn't change too much. I extracted some code from the Also, I simplified the unit tests for adapters, they obviously had some redundant checks. Hope you'll like it 😄 As always, I'll be happy to tweak things if needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@flash-gordon I love it! I really like how you fixed the conflation of concerns between Step and StepAdapter too. I also agree that avoiding blocks in the DSL is for the best :)
Anyway, really happy with this. Are you OK for this to be merged in now?
lib/dry/transaction/stack.rb
Outdated
@@ -2,7 +2,7 @@ module Dry | |||
module Transaction | |||
# @api private | |||
class Stack | |||
LOOPBACK = :itself.to_proc.freeze | |||
RETURN = -> x { x } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a nice naming improvement :)
Yes! |
But what if you want to make some dirty things like sending http request? Class.new do
include Dry::Transaction(container: Test::Container)
around :transaction
step :persist_user
step :send_http_request
end You wouldn't be able to use |
@ilnurnasyrov what's dry-transaction supposed to do in this case in your opinion? My personal advice is don't call external services from transactions, use event subscriptions. |
Oh, i understand. I just thought that variant with block is awesome, because it is flexible. Class.new do
include Dry::Transaction(container: Test::Container)
around :transaction do
step :persist_user
end
step :send_http_request
end |
I'm not against this syntax but it's not that easy to implement without quite a bit of rewriting the gem. And I think, it's unlikely you're gonna have cases like this in real life, and if you are you always can add one more method for placing the transaction block there. I really do want people to report something meaningful/reasonable then I'll be happy to discuss and revisit the syntax. |
Around steps are supposed to be a default means for support for database transactions and similar things. If you know how rack middleware or rpsec's around blocks work, it's the same, otherwise, examples will follow. Around steps are useful when you need to keep track of some externally-provided resource which cannot be handled automatically. For instance, you may create a temporary file and want to clean it in any case, no matter if something failed or not. But the most common use case is DB transactions, no doubt. Handling them with around steps is still cumbersome but, nevertheless, more reliable and straight-forward than other options:
Here exceptions are used for control flow to rollback a transaction but this way both ActiveRecord and Sequel successfully handle this case. I'll have another look if we can come up with a better example for this. In any case, we could provide a default implementation for this case with a simple wrapper: