2

I am currently playing with Arrow.io in Kotlin and I would love to use the library (together with Spring Boot) in a project at work. One problem I don't quite know how to solve correctly is transaction management. IO<A>.bracketCase(...) seems to me to be the correct tool for the job. Here is the approach I'm currently working on:

interface TransactionProvider {
    fun startTransaction(): IO<Transaction>
}

interface Transaction {
    fun commit(): IO<Unit>
    fun rollback(): IO<Unit>
}

fun <A> TransactionProvider.runInTransaction(action: IO<A>): IO<A> =
    startTransaction()
        .bracketCase(::releaseTransaction) { action }

fun releaseTransaction(t: Transaction, exitCase: ExitCase<Throwable>): IO<Unit> =
    when (exitCase) {
        is ExitCase.Completed -> t.commit()
        else -> t.rollback()
    }

Sadly, it doesn't work as I expect it to: when an exception is thrown during execution of action, I would expect a rollback. But this is not the case (the following test fails for instance):

@Test
internal fun `transaction rolls back on failure`() {

    val transaction: Transaction =
        mock {
            on { commit() } doReturn IO.unit
            on { rollback() } doReturn IO.unit
        }

    val transactionProvider: TransactionProvider =
        mock{
            on { startTransaction() } doReturn IO.just(transaction)
        }

    val exception = IllegalArgumentException("Here I am!")
    val action = IO{ throw exception }


    val result: Either<Throwable, Unit> =
        transactionProvider
            .runInTransaction(action)
            .attempt()
            .unsafeRunSync()

    assertThat(result).isEqualTo(exception.left())

    verify(transaction, never()).commit()
    verify(transaction, times(1)).rollback()
}

I played around with this a lot now and no matter how I arrange my types and where I place the action in question - I never get bracketCase to roll back my transactions. What am I doing wrong? Is there a better approach to do this? I would prefer a typesafe way without using unsafeRun if that is at all possible.

MLProgrammer-CiM
  • 17,231
  • 5
  • 42
  • 75
Jascha
  • 21
  • 3

1 Answers1

1

I think you have everything right except the test code - maybe the mocks are not working correctly with IO. For example, the following test code works as expected using your Transaction definitions in the first snippet:

fun main(args: Array<String>): Unit {
    val transactor = object: Transaction {
        override fun commit(): IO<Unit> = IO { println("Commit"); }
        override fun rollback(): IO<Unit> = IO { println("Rollback"); }
    }

    val txProvider = object: TransactionProvider {
        override fun startTransaction(): IO<Transaction> = IO.just(transactor)
    }

    val actionThrow = IO { println("Throwing"); throw IllegalArgumentException("Exception!") }
    val actionSuccess = IO { println("Returning value"); 2 }

    val resultThrow = txProvider.runInTransaction(actionThrow).attempt().unsafeRunSync()
    println(resultThrow)
    val resultSuccess = txProvider.runInTransaction(actionSuccess).attempt().unsafeRunSync()
    println(resultSuccess)
}

and the output I get is as follows:

Throwing
Rollback
Left(a=java.lang.IllegalArgumentException: Exception!)
Returning value
Commit
Right(b=2)

which is exactly what I would expect using #bracketCase. (This is using Arrow 0.10.0-SNAPSHOT.)

BobG
  • 2,113
  • 17
  • 15