Skip to content
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

ZIO failure in ZConnectionPool.transaction #198

Open
sullivan- opened this issue Sep 23, 2024 · 2 comments
Open

ZIO failure in ZConnectionPool.transaction #198

sullivan- opened this issue Sep 23, 2024 · 2 comments

Comments

@sullivan-
Copy link

sullivan- commented Sep 23, 2024

I'm getting an InterruptedException in ZConnectionPool.transaction. I am unable to mapError or tapError on the failure. When I run it unsafe, I get a zio.Exit.Failure. Nothing I have tried has allowed me to see the cause of the failure, e.g., the stack trace of the InterruptedException.

Here's a fairly minimal test case. I am using zio version 2.1.9 and zio-jdbc version 0.1.2

import zio.{ ZIO, ZLayer }
import zio.jdbc.{ ZConnectionPool, ZConnectionPoolConfig, stringToSql }
import zio.test.Assertion.equalTo
import zio.test.{ ZIOSpecDefault, assert }

class Api(val connectionPool: ZConnectionPool) {
  val query     = stringToSql("SELECT 'foo'").query[String]
  val selectOne = connectionPool.transaction(query.selectOne)

  val fooZIO: ZIO[Any, Throwable, Option[String]] = for {
    _   <- ZIO.debug("before selectOne")
    foo <- selectOne.tapError(e => ZIO.debug(s"tapError selectOne $e"))
    _   <- ZIO.debug("after selectOne")
  } yield foo
}

object ApiSpec extends ZIOSpecDefault {
  val connectionPoolConfigLayer = ZLayer.succeed(ZConnectionPoolConfig.default)

  val connectionPoolLayer = ZConnectionPool.postgres(
    "localhost", 5432, "test", Map("user" -> "test", "password" -> "test")
  )

  val apiLayer = ZLayer(ZIO.service[ZConnectionPool].map { connectionPool =>
    new Api(connectionPool)
  })

  val layer = connectionPoolConfigLayer >>> connectionPoolLayer >>> apiLayer

  def spec = test("api.fooZIO should be Some(foo)") {
    for {
      api      <- ZIO.service[Api].provide(layer)
      _        <- ZIO.debug("before fooZIO")
      response <- api.fooZIO
      _        <- ZIO.debug("after fooZIO")
    } yield {
      assert(response)(equalTo(Some("foo")))
    }
  }

}

Here is the output when I run sbt test:

before fooZIO
before selectOne
- api.fooZIO should be Some(foo)
  Exception in thread "zio-fiber-59,58,56,51" java.lang.InterruptedException: Interrupted by thread "zio-fiber-59"
  	at zio.jdbc.ZConnectionPool.make.tx(ZConnectionPool.scala:168)
  	at <empty>.Api.selectOne(ApiSpec.scala:8)
  	at <empty>.Api.fooZIO(ApiSpec.scala:12)
  	at <empty>.Api.fooZIO(ApiSpec.scala:14)
  	at <empty>.ApiSpec.spec(ApiSpec.scala:53)
  	at <empty>.ApiSpec.spec(ApiSpec.scala:54)
0 tests passed. 1 tests failed. 0 tests ignored.


- api.fooZIO should be Some(foo)
  Exception in thread "zio-fiber-59,58,56,51" java.lang.InterruptedException: Interrupted by thread "zio-fiber-59"
  	at zio.jdbc.ZConnectionPool.make.tx(ZConnectionPool.scala:168)
  	at <empty>.Api.selectOne(ApiSpec.scala:8)
  	at <empty>.Api.fooZIO(ApiSpec.scala:12)
  	at <empty>.Api.fooZIO(ApiSpec.scala:14)
  	at <empty>.ApiSpec.spec(ApiSpec.scala:53)
  	at <empty>.ApiSpec.spec(ApiSpec.scala:54)
Executed in 564 ms

[info] Completed tests
[error] Failed tests:
[error] 	ApiSpec
[error] (Test / testOnly) sbt.TestsFailedException: Tests unsuccessful

The error is resolved by moving the call to .provide(layer) to the end, like so:

  def spec = test("api.fooZIO should be Some(foo)") {
    val testResult = for {
      api      <- ZIO.service[Api]
      response <- api.fooZIO
    } yield {
      assert(response)(equalTo(Some("foo")))
    }
    testResult.provide(layer)
  }

The stack trace I got indicates that a fiber was unexpectedly interrupted. Presumably this is because I tried to access a DB connection that was already closed. Is that what it is? I'm not entirely sure.

Perhaps the original version of the test should fail. But I should be able to map or tap the error. And a user-facing error message such as, "operation attempted on a closed database connection," would probably have saved me many hours of troubleshooting.

@sullivan-
Copy link
Author

sullivan- commented Sep 23, 2024

I wonder if there is some way to enforce in the type system that a resource (in this case, a ZConnectionPool) doesn't leak out of the ZLayer scope where the resource is actually available.

@oridag
Copy link
Contributor

oridag commented Oct 9, 2024

Layers are scoped to the effect/workflow they are provided to, which means the connection pool is released (shut down) immediately after creation, which causes all connection borrow attempts to be immediately interrupted.

I agree that an error or a defect makes more sense in than an interrupt, in this case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants