Satyam Agarwal
11/20/2021, 3:56 PMtypealias ErrorOr<A> = Either<Throwable, A>
suspend inline fun <reified A> Db.useTxn(noinline f: suspend (Handle) -> ErrorOr<A>): ErrorOr<A> = withContext(IO) {
Either
.catch { this@useTxn.open().begin() }
.flatMap { handle -> bracketCase(acquire = { handle }, use = f, release = { h, e -> h.closeTxn(e) }) }
}
suspend fun Handle.closeTxn(e: ExitCase): ErrorOr<Unit> {
return Either.catch {
guarantee(
{
when (e) {
is ExitCase.Completed -> this.commit().close()
is ExitCase.Cancelled -> this.rollback().close()
is ExitCase.Failure -> this.rollback().close()
}
},
{ this.close() }
)
}
}
Problem with this code is that it will never come to ExitCase.Failure as arrow-kt checks on what exception is thrown and then handles result :
val res = try {
use(acquired)
} catch (e: CancellationException) {
runReleaseAndRethrow(e) { release(acquired, ExitCase.Cancelled(e)) }
} catch (t: Throwable) {
runReleaseAndRethrow(t.nonFatalOrThrow()) { release(acquired, ExitCase.Failure(t.nonFatalOrThrow())) }
}
We changed from bracketCase
to Resource
with throwing exception inside use , but to mitigate the above flaw, with bracketCase
it will roughly look like this :
suspend inline fun <reified A> Db.useTxn(noinline f: suspend (Handle) -> ErrorOr<A>): ErrorOr<A> = withContext(IO) {
Either
.catch { this@useTxn.open().begin() }
.flatMap { handle ->
Either.catch {
bracketCase(
acquire = { handle },
use = { h -> f(h).fold({ error -> throw error }, ::identity) },
release = { h, e -> h.closeConn(e) }
)
}
}
}
This problem occured when I migrated from IO<A>
to suspend () -> Either<Throwable, A>
We have fixed in our code base like this, but *It would have been awesome we got Either based apis for bracketCase and Resource. *
Just wanted to highlight how can we easily mess up things 😄 🙈Peter
11/20/2021, 4:19 PMSatyam Agarwal
11/20/2021, 4:25 PMPeter
11/20/2021, 4:31 PMSatyam Agarwal
11/20/2021, 4:35 PMPeter
11/20/2021, 4:36 PMsimon.vergauwen
11/21/2021, 11:41 AMsimon.vergauwen
11/21/2021, 11:43 AMExitCase.Failure
?
What exception are you throwing from use
? Are you throwing specific exceptions to cause rollbacks?simon.vergauwen
11/21/2021, 11:43 AM*It would have been awesome we got Either based apis for bracketCase and Resource. *This should actually not be needed, you can just compose
either
and bracketCase/Resource
yourself too.simon.vergauwen
11/21/2021, 11:48 AMfun <E, A, B> bracketCaseEither(
acquire: EitherEffect<E>.() -> A,
use: EitherEffect<E>.(A) -> B,
release: EitherEffect<E>.(A, ExitCase) -> Unit
): Either<E, B> = either<E, B> {
bracketCase(
acquire = { acquire(this@either) },
use = { a -> use(this@either, a) },
release = { a, ex -> release(a, ex) }
)
}
So the reason we haven’t added any of the “API” specific for data types is that it’s never really needed.
I.e. parTraverseEither
is also complete redundant since it’s an alias for:
either<E, A> {
(0..100).parTraverse { i ->
i.right().bind()
}
} // Either.Right((0..100).toList())
However if some APIs, like the ones for Either are in demand enough then maybe it’s worth adding these aliases for convenience in Arrow.
Feel free to open a ticket for this @Satyam Agarwal so we can discuss on Arrow repo 😉
cc\\ @raulrajaSatyam Agarwal
11/21/2021, 2:06 PMsimon.vergauwen
11/22/2021, 8:57 AMimport arrow.core.Either
import arrow.core.computations.EitherEffect
import arrow.core.computations.either
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
typealias Query<E, A> = suspend () -> Either<E, A>
fun <E, A> query(f: suspend EitherEffect<E, *>.() -> A): Query<E, A> =
suspend { either(f) }
suspend fun <E, A> Query<E, A>.transact(): Either<E, A> =
newSuspendedTransaction(<http://Dispatchers.IO|Dispatchers.IO>) {
either {
try {
invoke()
.tapLeft { rollback() }
.bind()
} catch (e: Throwable) {
rollback()
throw e
}
}
}
Query
is probably not a great name here, since it might make you think it has something to do with the DB
but I didn’t like LazyEither
😛Peter
11/22/2021, 2:58 PMeither
computation block automagically wrap thrown exceptions into Either.Left<E>
? have i been adding Either.catch { }
lines in the comprehension for nothing 😄simon.vergauwen
11/22/2021, 3:15 PMeither
does not automatically wrap. What I’m doing here is rolling back on an unexpected exception, and rethrowing it. (Or does Exposed already do that too?)Peter
11/22/2021, 3:19 PMsimon.vergauwen
11/22/2021, 3:25 PME
generic, and allows you to work over domain errors of E
inside your Exposed code.
It’s possible to extract that bit though, and turn it into a lambda parameter too.
suspend fun <E, A> Query<E, A>.transact(recover: (Throwable) -> Either<E, A> = { throw it }): Either<E, A> =
newSuspendedTransaction(<http://Dispatchers.IO|Dispatchers.IO>) {
either {
try {
invoke()
.tapLeft { rollback() }
.bind()
} catch (e: Throwable) {
rollback()
recover(e)
}
}
}
Peter
11/22/2021, 3:35 PMfun eitherTransientRetryDecider(@Suppress("UNUSED_PARAMETER") retryStatus: RetryStatus, t: Throwable): RetryAction {
val isTransientException = anyCause(t) { cause ->
when (cause) {
is java.sql.SQLTransientConnectionException -> true
else -> false
}
}
return when (isTransientException) {
true -> RetryAction.ConsultRetryPolicy
false -> RetryAction.DontRetry
}
}
internal suspend fun <T>runWithContextAndTransaction(logger: KLogger, block: Transaction.() -> T): Either<Throwable, T> {
return recover(
defaultRetryPolicy,
::eitherTransientRetryDecider,
onRetry = logRetry(logger, defaultRetryPolicy),
) {
newSuspendedTransaction(<http://Dispatchers.IO|Dispatchers.IO>) {
if (logger.isDebugEnabled)
addLogger(StdOutSqlLogger)
block()
}
}
}
i haven’t really encountered the need for an error domain at that layer so i’ve gone a slightly different direction … but maybe i need to rethink that, i can envision in some cases you want some semantics associated with a violated unique constraints violated etcPeter
11/22/2021, 3:40 PMsimon.vergauwen
11/22/2021, 3:48 PME
hierarchy that can result in unexpected errors (500s).