Skip to content

Commit

Permalink
Rediscala replaced with Lettuce (#301)
Browse files Browse the repository at this point in the history
* Switch Rediscala to Redis official client Lettuce core

---------

Co-authored-by: 2690522020 <[email protected]>
  • Loading branch information
KarelCemus and zhaodaye2022 authored May 5, 2024
1 parent 5a00dd0 commit 57e54e8
Show file tree
Hide file tree
Showing 35 changed files with 1,056 additions and 643 deletions.
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

0 comments on commit 57e54e8

Please sign in to comment.