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

Rediscala replaced with Lettuce #301

Merged
merged 8 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@

## Changelog

### [:link: 5.0.0](https://github.com/KarelCemus/play-redis/tree/5.0.0)

Switched underlying connector from Rediscala, which is no longer maintained,
to Lettuce. [#301](https://github.com/KarelCemus/play-redis/pull/301)

### [:link: 4.1.0](https://github.com/KarelCemus/play-redis/tree/4.1.0)

Provided support for MasterSlave configuration, which writes data to the master,
Expand Down
27 changes: 15 additions & 12 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@ description := "Redis cache plugin for the Play framework 2"

organization := "com.github.karelcemus"

crossScalaVersions := Seq("2.13.12", "3.3.1")
crossScalaVersions := Seq("2.13.14", "3.3.3")

scalaVersion := crossScalaVersions.value.head

playVersion := "3.0.1"
playVersion := "3.0.2"

libraryDependencies ++= Seq(
// play framework cache API
"org.playframework" %% "play-cache" % playVersion.value % Provided,
"org.playframework" %% "play-cache" % playVersion.value % Provided,
// redis connector
"io.github.rediscala" %% "rediscala" % "1.14.0-pekko",
"io.lettuce" % "lettuce-core" % "6.3.2.RELEASE",
// test framework with mockito extension
"org.scalatest" %% "scalatest" % "3.2.18" % Test,
"org.scalamock" %% "scalamock" % "6.0.0-M2" % Test,
"org.scalatest" %% "scalatest" % "3.2.18" % Test,
"org.scalamock" %% "scalamock" % "6.0.0" % Test,
// test module for play framework
"org.playframework" %% "play-test" % playVersion.value % Test,
"org.playframework" %% "play-test" % playVersion.value % Test,
// to run integration tests
"com.dimafeng" %% "testcontainers-scala-core" % "0.41.2" % Test,
"com.dimafeng" %% "testcontainers-scala-core" % "0.41.3" % Test,
)

resolvers ++= Seq(
Expand All @@ -43,16 +43,19 @@ scalacOptions ++= {
if (scalaVersion.value.startsWith("2.")) Seq("-Ywarn-unused") else Seq.empty
}

enablePlugins(CustomReleasePlugin)
ThisBuild / version := "4.0.2"

//enablePlugins(CustomReleasePlugin)

// exclude from tests coverage
coverageExcludedFiles := ".*exceptions.*"

Test / fork := true
Test / test := (Test / testOnly).toTask(" * -- -l \"org.scalatest.Ignore\"").value
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oF")

semanticdbEnabled := true
semanticdbVersion := scalafixSemanticdb.revision
ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value)
semanticdbEnabled := true
semanticdbVersion := scalafixSemanticdb.revision

wartremoverWarnings ++= Warts.allBut(
Wart.Any,
Expand Down
2 changes: 1 addition & 1 deletion project/CustomReleasePlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ object CustomReleasePlugin extends AutoPlugin {
val nextVersion = st.extracted.runTask(releaseVersion, st)._2(currentV)
val bump = Version.Bump.Minor

val suggestedReleaseV: String = Version(nextVersion).map(_.bump(bump).string).getOrElse(versionFormatError(currentV))
val suggestedReleaseV: String = Version(nextVersion).map(_.bump(bump).unapply).getOrElse(versionFormatError(currentV))

st.log.info("Press enter to use the default value")

Expand Down
12 changes: 6 additions & 6 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ resolvers += Resolver.url("scoverage-bintray", url("https://dl.bintray.com/sksam
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4")

// code coverage and uploader of the coverage results into the coveralls.io
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12")
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.11")

// library release
addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0")
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0")

// linters
addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.0")
addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.1")
addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.6")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1")
164 changes: 164 additions & 0 deletions src/main/scala/play/api/cache/redis/connector/RedisClientFactory.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package play.api.cache.redis.connector

import io.lettuce.core._
import io.lettuce.core.api.StatefulConnection
import io.lettuce.core.masterreplica.StatefulRedisMasterReplicaConnection
import io.lettuce.core.resource.{ClientResources, NettyCustomizer}
import io.netty.channel.{Channel, ChannelDuplexHandler, ChannelHandlerContext}
import io.netty.handler.timeout.{IdleStateEvent, IdleStateHandler}
import play.api.cache.redis.configuration.RedisHost

import java.time.{Duration => JavaDuration}
import scala.concurrent.duration.FiniteDuration

private object RedisClientFactory {

implicit class RichRedisConnection[Connection <: StatefulConnection[String, String]](
private val thiz: Connection,
) extends AnyVal {

def withTimeout(maybeTimeout: Option[FiniteDuration]): Connection = {
maybeTimeout.foreach { timeout =>
thiz.setTimeout(JavaDuration.ofNanos(timeout.toNanos))
}
thiz
}

}

implicit class RichRedisMasterReplicaConnection[Connection <: StatefulRedisMasterReplicaConnection[String, String]](
private val thiz: Connection,
) extends AnyVal {

def withReadFrom(readFrom: ReadFrom): Connection = {
thiz.setReadFrom(readFrom)
thiz
}

}

implicit class RichRedisURIBuilder[Builder <: RedisURI.Builder](
private val thiz: Builder,
) extends AnyVal {

def withDatabase(database: Option[Int]): Builder = {
thiz.withDatabase(database.getOrElse(0)) // mutable
thiz
}

def withCredentials(
username: Option[String],
password: Option[String],
): Builder =
(username, password) match {
case (None, None) =>
thiz
case (Some(username), Some(password)) =>
thiz.withAuthentication(username, password) // mutable
thiz
case (None, Some(password)) =>
thiz.withPassword(password.toCharArray) // mutable
thiz
case (Some(username), None) =>
throw new IllegalArgumentException(s"Username is set to $username but password is missing")
}

def withSentinels(sentinels: Seq[RedisHost]): Builder = {
sentinels.foreach {
case RedisHost(host, port, _, _, None) =>
thiz.withSentinel(host, port) // mutable
case RedisHost(host, port, _, _, Some(password)) =>
thiz.withSentinel(host, port, password) // mutable
}
thiz
}

}

implicit class RichClientOptionsBuilder[T <: ClientOptions.Builder](
private val thiz: T,
) extends AnyVal {

def withDefaults(): T = {
// mutable calls
thiz.autoReconnect(true) // Auto-Reconnect
thiz.pingBeforeActivateConnection(true) // PING before activating connection
thiz
}

def withTimeout(maybeTimeout: Option[FiniteDuration]): T = {
val options = maybeTimeout match {
case Some(timeout) =>
TimeoutOptions.builder()
.timeoutCommands(true)
.fixedTimeout(JavaDuration.ofNanos(timeout.toNanos))
.build()
case None =>
TimeoutOptions.builder().build()
}

thiz.timeoutOptions(options) // mutable call
thiz
}

}

implicit class RichRedisClient[Client <: AbstractRedisClient](
private val thiz: Client,
) extends AnyVal {

def withOptions[Options <: ClientOptions](
f: Client => Options => Unit,
)(
options: Options,
): Client = {
f(thiz)(options)
thiz
}

}

def newClientResources(
ioThreadPoolSize: Int = 8,
computationThreadPoolSize: Int = 8,
afterChannelTime: Int = 60 * 4,
): ClientResources =
ClientResources.builder
// The number of threads in the I/O thread pools.
// The number defaults to the number of available processors that
// the runtime returns (which, as a well-known fact, sometimes does
// not represent the actual number of processors). Every thread
// represents an internal event loop where all I/O tasks are run.
// The number does not reflect the actual number of I/O threads because
// the client requires different thread pools for Network (NIO) and
// Unix Domain Socket (EPoll) connections. The minimum I/O threads are 3.
// A pool with fewer threads can cause undefined behavior.
.ioThreadPoolSize(ioThreadPoolSize)
// The number of threads in the computation thread pool. The number
// defaults to the number of available processors that the runtime returns
// (which, as a well-known fact, sometimes does not represent the actual
// number of processors). Every thread represents an internal event loop
// where all computation tasks are run. The minimum computation threads
// are 3. A pool with fewer threads can cause undefined behavior.
.computationThreadPoolSize(computationThreadPoolSize)
// Maintain connection to Redis every four minutes
.nettyCustomizer(
new NettyCustomizer() {

@SuppressWarnings(Array("org.wartremover.warts.IsInstanceOf"))
override def afterChannelInitialized(channel: Channel): Unit = {
val _ = channel.pipeline.addLast(new IdleStateHandler(afterChannelTime, 0, 0))
val _ = channel.pipeline.addLast(new ChannelDuplexHandler() {
@throws[Exception]
override def userEventTriggered(ctx: ChannelHandlerContext, evt: Object): Unit =
if (evt.isInstanceOf[IdleStateEvent]) {
val _ = ctx.disconnect().sync()
}
})
}

},
)
.build

}
Loading
Loading