Hei <@U0RM4EPC7> ! Long post, sorry for that. So...
# arrow
s
Hei @simon.vergauwen ! Long post, sorry for that. So a few days ago I had to suffer a little embarrassment when a colleague pointed out the obvious mistake in the helper api i built to do database operations functionally. 🙈😅🤣 here is the code :
Copy code
typealias 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 :
Copy code
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 :
Copy code
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 😄 🙈
👍 1
p
curious - which db library are you using that needs manually managing transaction lifecycles?
😮 1
s
It's just a choice I made. I like to commit and rollback manually.
p
oh cool - is that possible with exposed?
s
I never used exposed. It looked like an ORM-ish framework, and that does not sit well with me.
🤨 1
p
i hear ya.. im still scarred from hibernate in the mid 2000s 😂
😄 2
🤨 1
s
Hey @Peter, It’s possible with Exposed and I’m using that at a client. It’s pretty simple, I’ll share a snippet tomorrow. (Poke me if I forget 😅)
@Satyam Agarwal I’m not sure what you mean with it will never come
ExitCase.Failure
? What exception are you throwing from
use
? Are you throwing specific exceptions to cause rollbacks?
*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.
Copy code
fun <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:
Copy code
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\\ @raulraja
👍 3
❤️ 1
s
I'll follow this up once I am back from my vacation.
s
@Peter here is an example of how you can do something like this with exposed.
Copy code
import 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
😛
🙌 1
🙏 1
p
interesting things in there - does the
either
computation block automagically wrap thrown exceptions into
Either.Left<E>
? have i been adding
Either.catch { }
lines in the comprehension for nothing 😄
s
No,
either
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?)
p
ah ok .. curious why rethrowing then if you're returning an Either? i guess that allows you to keep E generic
s
Yes, otherwise you cannot keep
E
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.
Copy code
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)
      }
    }
  }
👍 1
p
Copy code
fun 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 etc
also maybe i need to think a bit more carefully about explicit rollbacks
s
That is very similar to what I’ve been using in a big client project. We also have a retry/circuit breaker mechanism in the function to make interaction with the databas resillient, and we have a custom
E
hierarchy that can result in unexpected errors (500s).
🙌 1