-
Notifications
You must be signed in to change notification settings - Fork 25
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
Implement ZStream support #49
Conversation
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.
Took me long enough, but I've finally gotten into taking a look into this. Thank you a lot for providing this!
There are few minor changes I'll add later (basic reformatting, adding some comments and documentation). However, there are a few more important points I'd like your input on.
def runTransactionStream[R, E, A](task: Connection => ZStream[R, E, A], commitOnFailure: => Boolean = false) | ||
(implicit errorStrategies: ErrorStrategiesRef, trace: Trace): ZStream[R, Either[DbException, E], A] = { | ||
|
||
ZStream.acquireReleaseWith(openConnection.tap(c => setAutoCommit(c, autoCommit = false)).mapError(Left(_))) |
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.
By setting the auto-commit in a tap
in the acquisition step (instead of doing it later, as is done in runTransaction
), you're introducing a potential issue: if setting the auto-commit fails, the connection won't get closed, as the acquisition step won't have ended with a success.
{ c => commitConnection(c).tapEither(_ => closeConnection(c)).orDie } | ||
.flatMap(c => task(c).mapError(Right(_)) | ||
.tapError(_ => (if (commitOnFailure) commitConnection(c) else rollbackConnection(c)).mapError(Left(_)))) |
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'm not sure why you use a different approach over the one in runTransaction
here?
As far as I can see, it behaves the same in case of a success (the transaction is committed). However, on a failure, the transaction is first rollbacked (line 55), then commited (line 53). It shouldn't have any adverse effect semantically, but it can impact performance.
=> Unless there's an advantage I'm missing, I prefer my approach: commit on success, rollback on failure (using tapBoth
instead of tapError
), and close the connection in the release 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.
Nevermind, I think I get it: the tap
argument is run for every entry in the stream, not a the end of the stream, duh.
I'm playing a bit with it, and I think I actually like having two levels of acquireReleaseWith
: one to get the connections, handling only opening and closing the connection, and a second one to either commit or rollback depending on what happened.
{ c => commitConnection(c).tapEither(_ => closeConnection(c)).orDie } | ||
.flatMap(c => task(c).mapError(Right(_)) | ||
.tapError(_ => (if (commitOnFailure) commitConnection(c) else rollbackConnection(c)).mapError(Left(_)))) | ||
.catchSomeCause { case Cause.Die(error: DbException, _) => ZStream.fail(Left(error)) } |
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.
Why do you want to catch the Die
here? As far as I can see, at this point, the only reason to have the stream die with a DbException
is if the connection could not be closed.
If that happens, your whole application is pretty much screwed. There's nothing you can do to recover from that apart from stopping the process and restarting. If there's a very corner case when someone might want to catch that and do something, they can catch the Die
themselves, but that shouldn't be the default behavior.
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.
It was a bug in ZIO framework were its not worked correctly with the resource,
I needed to play with it til I realised it, I also made the Connection to a private layer which could be used in both ZIO and ZStream
private def connectionLayer(commitOnFailure: => Boolean = false)(implicit
errorStrategies: ErrorStrategiesRef,
trace: Trace
): ZLayer[Any, DbException, Connection] =
ZLayer
.scoped {
ZIO.acquireRelease(openConnection)(c => closeConnection(c).orDie)
}
.flatMap(env =>
ZLayer
.scoped {
ZIO
.acquireReleaseExit(
setAutoCommit(env.get, autoCommit = false).as(env.get)
) {
case (c, Exit.Success(_)) => commitConnection(c).orDie
case (c, Exit.Failure(_)) =>
(if (commitOnFailure) commitConnection(c)
else rollbackConnection(c)).orDie
}
}
)
ZStream
.serviceWithStream[Connection](c => task(c).mapError(Right(_)))
.provideSomeLayer[R](connectionLayer(commitOnFailure).mapError(Left(_)))
ZIO
.serviceWith[Connection](c => task(c).mapError(Right(_)))
.provideSomeLayer[R](connectionLayer(commitOnFailure).mapError(Left(_)))
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.
Hmm, I like the idea of having a single transaction-handler for both the ZIO and ZStream methods 👍. I want to add an additional distinction though: if the ZIO or ZStream dies (not just an error), we shouldn't try to either commit or rollback. Only closing the connection is the way to go I think.
I'll try and integrate that next week-end.
(implicit errorStrategies: ErrorStrategiesRef, trace: Trace): ZStream[R, Either[DbException, E], A] = | ||
ZStream.acquireReleaseWith(openConnection.mapError(Left(_)))(closeConnection(_).orDie).flatMap { (c: Connection) => | ||
ZStream.fromZIO(setAutoCommit(c, autoCommit = true).mapError(Left(_))) | ||
.zipRight {task(c).mapError(Right(_))} |
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.
Should be crossRight
or we'll only keep the first element.
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 think executing the setAutoCommit to true is enough ones, it should stay the same for the whole connection
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.
Yes on the auto-commit sides, but it also only keeps one element on the task side (so only the first entry in the ZStream you've passed as a parameter). I've added some tests, and with zipRight
it crashes because the only the first entry in the stream is actually found in the 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.
Okay then you are are right I thought it was merging the two stream in case of zipRight as well just not executing the first before each element, only before start executing the stream. Then in this case what you think about a flatMap? I think it would be enough to execute the auto commit command ones.
I'll try to finish working on this next week-end, I don't have much time during the weeks. |
OK, so I've spent a lot of time trying to make this work, and kept running into issues when adding tests. I think I finally understand why: providing a Connection within the stream cannot actually work. Here's the problem. When I try to provide a Connection to a ZStream, I must use the acquire/release mechanism, to make sure the transaction is committed or rollbacked, and the connection is closed. However, as indicated in ZIO's documentation, the release is performed "after the stream is consumed". This means that the actual commit will NOT be done until the stream is actually consumed, taking into account everything that happened in the stream. Take this example: val queryStreamA: ZStream[Connection, DbException, String] = ???
val streamA: ZStream[Database, DbException, String] = Database.transactionOrWidenStream(queryStreamA)
val queryStreamB: ZStream[Connection, DbException, String] = ???
val streamB: ZStream[Database, DbException, String] = Database.transactionOrWidenStream(queryStreamB)
val result: ZIO[Database, DbException, Chunk[String]] = (streamA ++ streamB).runCollect In case any failure happens when running Therefore, I don't think there is any purpose in having a way to convert a Don't hesitate to correct me if you think I'm missing something. I'm going to keep my experimentation branch for a little while in case someone comes with an idea, but in the meantime I'm not going to spend more time on this. |
@gaelrenoux Can you add the full test because for me it is working if I replace the runDrain to runCollect.
|
I've looked at it again, and actually the issue was coming from one of my changes. I went back to your initial version for Let me recap. On the branch It's mostly your version. I've done some reformatting, but mostly I've added tests and fixed the On the branch It starts on branch On the last commit of the branch, I've added a separate test ( On the branch On the branch |
So I had some time today and looked at the I found this solution make all of the ConnectionSourceTest pass:
EDIT: I think is more precise:
|
Your first proposition doesn't work: with On your second proposal, doing the |
I think I have a working compromise, though. Given that ZStream's API doesn't allow us to catch the defects, let's embrace that. In branch I've asked on the ZIO Discord for precisions about how |
Its awesome 🚀 thank you for the work 😄 |
Hi I proposing this solution for the #35 so enable tranzactio to work with ZStream more easily
What are your thoughts about it, could this be okay?
I added some tests to make sure the transaction boundaries are correct.
In the API I used the ZIO one as inspiration so I have all of them just working with ZStream.