-
Notifications
You must be signed in to change notification settings - Fork 16
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
Transactor work, add zio-test, and bump zio #95
Conversation
I think this module's tests should only test the functionality of the Transactor... not all of the core functionality. 🤷♂️ All we really need to know is that the Transactor produced expected results on success or failure etc. I feel like the scope of the tests is far too broad for this tiny sub module. |
What is Transactor was a trait with a wrapper type? That would make it simple to create transactors for says Kyo/Cats effect etc... |
What I mean is - we could take good advantage of higher kinded types: trait TransactorOps[T[_]]:
def connect[A](f: DbCon ?=> A): T[A]
def transact[A](f: DbTx ?=> A): T[A]
type Plain[A] = A
object Plain:
given makePlain[A]: Conversion[A, Plain[A]] = a => a: Plain[A]
final case class Transactor(
private val dataSource: DataSource,
private val sqlLogger: SqlLogger = SqlLogger.Default,
private val connectionConfig: Connection => Unit = con => ()
) extends TransactorOps[Plain]:
def withSqlLogger(sqlLogger: SqlLogger) = copy(sqlLogger = sqlLogger)
def withConnectionConfig(connectionConfig: Connection => Unit) =
copy(connectionConfig = connectionConfig)
def connect[T](f: DbCon ?=> T): T =
Using.resource(dataSource.getConnection): con =>
connectionConfig(con)
f(using DbCon(con, sqlLogger))
def transact[T](f: DbTx ?=> T): T =
Using.resource(dataSource.getConnection): con =>
connectionConfig(con)
con.setAutoCommit(false)
try
val res = f(using DbTx(con, sqlLogger))
con.commit()
res
catch
case t =>
con.rollback()
throw t
end Transactor |
I added an experimental refactoring to demo a way to enforce operational consistency at a type level between Transactor implementations using the TransactorOps trait. Mostly for discussion 🗨️ |
"dev.zio" %% "zio-test" % "2.1.14" % Test, | ||
"dev.zio" %% "zio-test-sbt" % "2.1.14" % Test, | ||
"dev.zio" %% "zio-test-magnolia" % "2.1.14" % Test, | ||
"org.testcontainers" % "postgresql" % "1.20.4" % Test, |
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 not keeping testcontainers-scala-postgresql
? 🤔
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.
Totally could - this is just a pattern I am used to and my approach gives more direct control over the container scope (ZIO scope).
connectionConfig: Connection => Unit, | ||
semaphore: Option[Semaphore] | ||
): | ||
final case class Transactor( |
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.
🟡 IMO case class
represents data structures. This isn't a data structure so we don't need case
here
final case class Transactor( | |
final class Transactor( |
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.
In ZIO most services are case classes in zio libraries. Services sort of are data structures. If you look in the Zionomicon and the ZIO layer documentation you will see good reasons for them to be case classes. Still it is not strictly required. One advantage is maintenance. Case class provides copy which is more ergonomic than needing to call a full constructor. Still this is not that important to me.
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.
Yeah, I am thinking for a long time to change this documentation. I should have done it before.
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.
IMO This is not a big issue at all - I am happy to use ordinary classes in cases like this.
|
||
def connect[A](f: DbCon ?=> A)(using Trace): Task[A] = |
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 the need to remove the (using Trace)
parameter here shows the limit of the common trait idea. I don't really agree with the idea. I think leaving each integration free of bringing the best integration to its environment without constraints is the best approach.
Why would we need this trait? It's not like, as a Magnum user, you'd use it anyway to abstract over any Transator implementation.
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.
As a library author you would use it to generate a test kit - such that the tests are written to the trait and not to the impls - that makes it easier to plug the impls into the core tests without rewriting them.
To the library user they need not even know about the trait - notice the library still works just as it did before - a user need not care. But library maintainers often just want one set of core tests.
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.
When I look at most ZIO services - I don't see (using trace) anywhere on service methods - I am not sure we need it at all. If we find we do - I can think of a couple of ways to support it.
connectionConfig, | ||
semaphore | ||
) | ||
copy(sqlLogger = sqlLogger) |
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.
+1
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.
This is a benefit of services as case classes.
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.
indeed but I still prefer it not to be a case class
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.
No worries - I can respect that.
private val dataSource: DataSource, | ||
private val sqlLogger: SqlLogger, | ||
private val connectionConfig: Connection => Unit, | ||
private val semaphore: Option[Semaphore] |
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.
Indeed these should be private
🙂
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.
* Number of threads in your connection pool. This helps magzio be more | ||
* memory efficient by limiting the number of blocking pool threads used. | ||
* Not needed if using the ZIO virtual-thread based blocking executor | ||
* @return | ||
* Transactor UIO | ||
* The transactor layer | ||
*/ | ||
def layer( | ||
dataSource: DataSource, | ||
sqlLogger: SqlLogger, | ||
connectionConfig: Connection => Unit, | ||
maxBlockingThreads: Option[Int] | ||
): ULayer[Transactor] = | ||
ZLayer.fromZIO { | ||
ZIO | ||
.fromOption(maxBlockingThreads) | ||
.flatMap(threads => Semaphore.make(threads)) | ||
.unsome | ||
.map(semaphoreOpt => | ||
new Transactor( | ||
dataSource, | ||
sqlLogger, | ||
connectionConfig, | ||
semaphoreOpt | ||
) | ||
) | ||
def configured( | ||
sqlLogger: SqlLogger = SqlLogger.Default, | ||
connectionConfig: Connection => Unit = noOpConnectionConfig, | ||
maxBlockingThreads: Int = -1 | ||
) = | ||
val semaphore = ZLayer { | ||
if maxBlockingThreads < 1 then ZIO.succeed(None) | ||
else Semaphore.make(maxBlockingThreads).map(Some(_)) | ||
} | ||
defaultLogger ++ defaultConnectionConfig ++ semaphore >>> layer | ||
|
||
/** Construct a Transactor | ||
* | ||
* @param dataSource | ||
* Datasource to be used | ||
* @param sqlLogger | ||
* Logging configuration | ||
* @param connectionConfig | ||
* Customize the underlying JDBC Connections | ||
* @return | ||
* Transactor UIO | ||
*/ | ||
def layer( | ||
dataSource: DataSource, | ||
sqlLogger: SqlLogger, | ||
connectionConfig: Connection => Unit | ||
): ULayer[Transactor] = | ||
layer( | ||
dataSource, | ||
sqlLogger, | ||
connectionConfig, | ||
None | ||
) | ||
|
||
/** Construct a Transactor | ||
* | ||
* @param dataSource | ||
* Datasource to be used | ||
* @param sqlLogger | ||
* Logging configuration | ||
* @return | ||
* Transactor UIO | ||
*/ | ||
def layer(dataSource: DataSource, sqlLogger: SqlLogger): ULayer[Transactor] = | ||
layer(dataSource, sqlLogger, noOpConnectionConfig, None) | ||
|
||
/** Construct a Transactor | ||
* | ||
* @param dataSource | ||
* Datasource to be used | ||
* @return | ||
* Transactor UIO | ||
*/ | ||
def layer(dataSource: DataSource): ULayer[Transactor] = | ||
layer(dataSource, SqlLogger.Default, noOpConnectionConfig, None) | ||
|
||
/** Construct a Transactor | ||
* | ||
* @param dataSource | ||
* Datasource to be used | ||
* @param connectionConfig | ||
* Customize the underlying JDBC Connections | ||
* @return | ||
* Transactor UIO | ||
/** The default transactor layer with all dependencies satisfied except for | ||
* the DataSource. | ||
*/ | ||
def layer( | ||
dataSource: DataSource, | ||
connectionConfig: Connection => Unit | ||
): ULayer[Transactor] = | ||
layer(dataSource, SqlLogger.Default, connectionConfig, None) | ||
val default: ZLayer[DataSource, Nothing, Transactor] = | ||
defaultLogger ++ defaultConnectionConfig ++ defaultSemaphore >>> layer |
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 don't understand these changes.
I'd prefer we keep as it was.
1 issue amongst others in your code: the configured
function takes a sqlLogger: SqlLogger = SqlLogger.Default,
parameter but never use it
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.
Let me refine that a bit - this was sort of a half baked concept.... :)
@russwyte Gave some feedback. IMO, it'd be better if you separate the migration to zio-test and the |
I tend to agree that this PR is over-broad. 😅 I am happy to close this PR as I mostly just wanted to learn by example and discussion. A couple of notes on why I started this weekend coding blitz and some learning I got from it.
Thanks for this opportunity to learn! Great work on this so far! |
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.
@guizmaii @AugustNagro This what I was trying to facilitate with the trait. I needed only write the specs once to test them against both transactors.
Soon I intend to refine my idea into a new "test" (testkit) module that tests everything (like the root project) against any TransactorOps type class. This could make this library widely accessible - from those who want "pure" Scala to those who use any sort of effect or typeclass F[_] |
Closing in favor of a more focused testing PR to come later |
Very interesting @russwyte and that's an awesome end result. I would agree with Jules to the extent that we should try to see if we can scrap the test boilerplate while keeping the Transactor definition the same, since
However, to reduce the scope of your first contribution, can you create a PR with just the change to zio-test? That would be very helpful in and of itself. |
May at least partially address #84
Adds some Transactor refactoring for discussion.