diff --git a/build.sbt b/build.sbt index ef89b230662eb58795b7842b632971c23cf73c9f..04ebaff449f5d1241b567624c5b8e6b309ceea66 100644 --- a/build.sbt +++ b/build.sbt @@ -13,18 +13,19 @@ lazy val scala211 = "2.11.12" lazy val scala213 = "2.13.8" lazy val lastVersionForExamples = "0.5.2" -lazy val configVersion = "1.4.2" -lazy val scalatestVersion = "3.2.12" -lazy val zioVersion = "1.0.14" -lazy val zioLoggingVersion = "0.5.14" -lazy val caffeineVersion = "2.9.3" -lazy val zioRedisVersion = "0.0.0+381-86c20614-SNAPSHOT" // 实验性质的 -lazy val zioSchemaVersion = "0.1.9" -lazy val scalaLoggingVersion = "3.9.5" -lazy val playJsonVersion = "2.7.4" -lazy val log4jVersion = "2.17.2" -lazy val jacksonScalaVersion = "2.13.3" -lazy val jraftVersion = "1.3.9" +lazy val configVersion = "1.4.2" +lazy val scalatestVersion = "3.2.12" +lazy val zioVersion = "1.0.14" +lazy val zioLoggingVersion = "0.5.14" +lazy val caffeineVersion = "2.9.3" +lazy val zioRedisVersion = "0.0.0+381-86c20614-SNAPSHOT" // 实验性质的 +lazy val zioSchemaVersion = "0.1.9" +lazy val scalaLoggingVersion = "3.9.5" +lazy val playJsonVersion = "2.7.4" +lazy val log4jVersion = "2.17.2" +lazy val jacksonScalaVersion = "2.13.3" +lazy val jraftVersion = "1.3.9" +lazy val scalaCollectionCompatVersion = "2.7.0" lazy val commonSettings = Seq( @@ -132,8 +133,9 @@ lazy val `smt-common` = (project in file("smt-common")) lazy val `smt-cache` = (project in file("smt-cache")) .settings(commonSettings) .settings( - name := "smt-cache", - crossScalaVersions := List(scala213, scala212, scala211) + name := "smt-cache", + crossScalaVersions := List(scala213, scala212, scala211), + libraryDependencies += "org.scala-lang.modules" %% "scala-collection-compat" % scalaCollectionCompatVersion ) .settings(Publishing.publishSettings) .settings(paradise()) diff --git a/smt-cache/src/main/scala/org/bitlap/cache/Cache.scala b/smt-cache/src/main/scala/org/bitlap/cache/Cache.scala index f3016cba5a7186edd1ca69bd51ec7cbb54a62f23..60da8595e8f49553bb703e8d06d9392952a170e2 100644 --- a/smt-cache/src/main/scala/org/bitlap/cache/Cache.scala +++ b/smt-cache/src/main/scala/org/bitlap/cache/Cache.scala @@ -27,6 +27,7 @@ import org.bitlap.common.{ CaseClassExtractor, CaseClassField } import java.util.concurrent.atomic.AtomicBoolean import scala.reflect.ClassTag import scala.reflect.runtime.universe._ +import scala.concurrent.{ ExecutionContext, Future } /** @author * 梦境迷离 @@ -34,30 +35,60 @@ import scala.reflect.runtime.universe._ */ object Cache { - def getCache[T <: Product](implicit - cache: Aux[String, T], + def getAsyncCache[T <: Product](implicit + cache: Aux[String, T, Future], + executionContext: ExecutionContext, classTag: ClassTag[T], typeTag: TypeTag[T] - ): CacheRef[String, T] = - new CacheRef[String, cache.Out] { + ): CacheRef[String, T, Future] = + new CacheRef[String, cache.Out, Future] { private lazy val initFlag = new AtomicBoolean(false) - override def init(initKvs: => Map[String, cache.Out]): Unit = + override def init(initKvs: => Map[String, cache.Out]): Future[Unit] = if (initFlag.compareAndSet(false, true)) { putTAll(initKvs) - } + } else Future.successful(()) - override def putTAll(map: => Map[String, cache.Out]): Unit = + override def putTAll(map: => Map[String, cache.Out]): Future[Unit] = + cache.putAll(map) + + override def getT(key: String)(implicit keyBuilder: CacheKeyBuilder[String]): Future[Option[cache.Out]] = + cache.get(key) + + override def putT(key: String, value: cache.Out)(implicit keyBuilder: CacheKeyBuilder[String]): Future[Unit] = + cache.put(key, value) + + override def getTField(key: String, field: CaseClassField)(implicit + keyBuilder: CacheKeyBuilder[String] + ): Future[Option[field.Field]] = + getT(key).map(opt => opt.flatMap(t => CaseClassExtractor.getFieldValueUnSafely[cache.Out](t, field))) + } + + def getSyncCache[T <: Product](implicit + cache: Aux[String, T, Identity], + classTag: ClassTag[T], + typeTag: TypeTag[T] + ): CacheRef[String, T, Identity] = + new CacheRef[String, cache.Out, Identity] { + private lazy val initFlag = new AtomicBoolean(false) + + override def init(initKvs: => Map[String, cache.Out]): Identity[Unit] = + if (initFlag.compareAndSet(false, true)) { + putTAll(initKvs) + } else () + + override def putTAll(map: => Map[String, cache.Out]): Identity[Unit] = map.foreach(kv => cache.put(kv._1, kv._2)) - override def getT(key: String)(implicit keyBuilder: CacheKeyBuilder[String]): Option[cache.Out] = cache.get(key) + override def getT(key: String)(implicit keyBuilder: CacheKeyBuilder[String]): Identity[Option[cache.Out]] = + cache.get(key) - override def putT(key: String, value: cache.Out)(implicit keyBuilder: CacheKeyBuilder[String]): Unit = + override def putT(key: String, value: cache.Out)(implicit keyBuilder: CacheKeyBuilder[String]): Identity[Unit] = cache.put(key, value) override def getTField(key: String, field: CaseClassField)(implicit keyBuilder: CacheKeyBuilder[String] - ): Option[field.Field] = + ): Identity[Option[field.Field]] = getT(key).flatMap(t => CaseClassExtractor.getFieldValueUnSafely[cache.Out](t, field)) } diff --git a/smt-cache/src/main/scala/org/bitlap/cache/CacheRef.scala b/smt-cache/src/main/scala/org/bitlap/cache/CacheRef.scala index a13fa10cde01a624903898d0c4a5f234b9d6f191..ca7889bd8aaed07e60d917127e80b1fca806ee54 100644 --- a/smt-cache/src/main/scala/org/bitlap/cache/CacheRef.scala +++ b/smt-cache/src/main/scala/org/bitlap/cache/CacheRef.scala @@ -27,15 +27,15 @@ import org.bitlap.common.CaseClassField * 梦境迷离 * @version 1.0,6/8/22 */ -trait CacheRef[In, T <: Product] { +trait CacheRef[In, T <: Product, F[_]] { - def init(initKvs: => Map[String, T]): Unit + def init(initKvs: => Map[String, T]): F[Unit] - def putTAll(map: => Map[String, T]): Unit + def putTAll(map: => Map[String, T]): F[Unit] - def getT(key: In)(implicit keyBuilder: CacheKeyBuilder[In]): Option[T] + def getT(key: In)(implicit keyBuilder: CacheKeyBuilder[In]): F[Option[T]] - def putT(key: In, value: T)(implicit keyBuilder: CacheKeyBuilder[String]): Unit + def putT(key: In, value: T)(implicit keyBuilder: CacheKeyBuilder[String]): F[Unit] - def getTField(key: In, field: CaseClassField)(implicit keyBuilder: CacheKeyBuilder[In]): Option[field.Field] + def getTField(key: In, field: CaseClassField)(implicit keyBuilder: CacheKeyBuilder[In]): F[Option[field.Field]] } diff --git a/smt-cache/src/main/scala/org/bitlap/cache/CacheType.scala b/smt-cache/src/main/scala/org/bitlap/cache/CacheStrategy.scala similarity index 88% rename from smt-cache/src/main/scala/org/bitlap/cache/CacheType.scala rename to smt-cache/src/main/scala/org/bitlap/cache/CacheStrategy.scala index e2f6be3681ef0bd40a6600d817d5decf45890279..7fe661171dd0e40546d75082a27281b419e7e697 100644 --- a/smt-cache/src/main/scala/org/bitlap/cache/CacheType.scala +++ b/smt-cache/src/main/scala/org/bitlap/cache/CacheStrategy.scala @@ -25,10 +25,10 @@ package org.bitlap.cache * 梦境迷离 * @version 1.0,6/8/22 */ -trait CacheType +trait CacheStrategy -object CacheType { +object CacheStrategy { - case class Lru(maxSize: Int = 1000) extends CacheType - case object Normal extends CacheType + case class Lru(maxSize: Int = 1000) extends CacheStrategy + case object Normal extends CacheStrategy } diff --git a/smt-cache/src/main/scala/org/bitlap/cache/GenericCache.scala b/smt-cache/src/main/scala/org/bitlap/cache/GenericCache.scala index 9e060de37f7fb675b755baeedcd6593c34b350c5..4e23aa12765ebc2be482ff9b29820d2245142b1e 100644 --- a/smt-cache/src/main/scala/org/bitlap/cache/GenericCache.scala +++ b/smt-cache/src/main/scala/org/bitlap/cache/GenericCache.scala @@ -20,38 +20,85 @@ */ package org.bitlap.cache +import java.util +import java.util.Collections +import scala.concurrent.{ ExecutionContext, Future } +import scala.jdk.CollectionConverters._ /** @author * 梦境迷离 * @version 1.0,6/8/22 */ -sealed trait GenericCache[K] { +sealed trait GenericCache[K, F[_]] { type Out <: Product - def get(key: K)(implicit keyBuilder: CacheKeyBuilder[K]): Option[Out] + def get(key: K)(implicit keyBuilder: CacheKeyBuilder[K]): F[Option[Out]] - def put(key: K, value: Out)(implicit keyBuilder: CacheKeyBuilder[K]): Unit + def put(key: K, value: Out)(implicit keyBuilder: CacheKeyBuilder[K]): F[Unit] -} + def putAll(map: Map[K, Out])(implicit keyBuilder: CacheKeyBuilder[K]): F[Unit] +} object GenericCache { - type Aux[K, Out0] = GenericCache[K] { type Out = Out0 } + type Aux[K, Out0, F[_]] = GenericCache[K, F] { type Out = Out0 } - def apply[K, Out0 <: Product](cacheType: CacheType): Aux[K, Out0] = new GenericCache[K] { - private val typedCache = cacheType match { - case CacheType.Lru(maxSize) => new java.util.LinkedHashMap[String, Out0](maxSize, 0.75f, true) - case CacheType.Normal => new java.util.LinkedHashMap[String, Out0]() - } + def apply[K, Out0 <: Product](cacheStrategy: CacheStrategy): Aux[K, Out0, Identity] = new GenericCache[K, Identity] { + private val typedCache = getCacheByStrategy[Out0](cacheStrategy) override type Out = Out0 - override def get(key: K)(implicit keyBuilder: CacheKeyBuilder[K]): Option[Out] = { + override def get( + key: K + )(implicit + keyBuilder: CacheKeyBuilder[K] + ): Identity[Option[Out]] = { val v = typedCache.get(keyBuilder.generateKey(key)) if (v == null) None else Option(v) } - override def put(key: K, value: Out)(implicit keyBuilder: CacheKeyBuilder[K]): Unit = + override def put(key: K, value: Out)(implicit + keyBuilder: CacheKeyBuilder[K] + ): Identity[Unit] = typedCache.put(keyBuilder.generateKey(key), value) + + override def putAll(map: Map[K, Out0])(implicit keyBuilder: CacheKeyBuilder[K]): Identity[Unit] = + typedCache.putAll(map.map(kv => keyBuilder.generateKey(kv._1) -> kv._2).asJava) } + + def apply[K, Out0 <: Product]( + cacheStrategy: CacheStrategy, + executionContext: ExecutionContext + ): Aux[K, Out0, Future] = + new GenericCache[K, Future] { + implicit val ec = executionContext + private val typedCache = getCacheByStrategy[Out0](cacheStrategy) + override type Out = Out0 + + override def get(key: K)(implicit keyBuilder: CacheKeyBuilder[K]): Future[Option[Out]] = + Future { + val v = typedCache.get(keyBuilder.generateKey(key)) + println(s"key => $key | value => $v") + if (v == null) None else Option(v) + } + + def put(key: K, value: Out)(implicit keyBuilder: CacheKeyBuilder[K]): Future[Unit] = + Future { + typedCache.put(keyBuilder.generateKey(key), value) + }.map(_ => ()) + + override def putAll(map: Map[K, Out0])(implicit keyBuilder: CacheKeyBuilder[K]): Future[Unit] = + Future { + println(s"all map => ${map.mkString(" | ")}") + typedCache.putAll(map.map(kv => keyBuilder.generateKey(kv._1) -> kv._2).asJava) + } + } + + private def getCacheByStrategy[Out0](cacheType: CacheStrategy): util.Map[String, Out0] = + cacheType match { + case CacheStrategy.Lru(maxSize) => + Collections.synchronizedMap(new java.util.LinkedHashMap[String, Out0](maxSize, 0.75f, true)) + case CacheStrategy.Normal => new java.util.concurrent.ConcurrentHashMap[String, Out0]() + // TODO other cache, redis cache, caffeine cache + } } diff --git a/smt-cache/src/main/scala/org/bitlap/cache/package.scala b/smt-cache/src/main/scala/org/bitlap/cache/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..7658d365d7462642b4f420adbb5f241c21f5c58e --- /dev/null +++ b/smt-cache/src/main/scala/org/bitlap/cache/package.scala @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 bitlap + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.bitlap + +/** @author + * 梦境迷离 + * @version 1.0,6/10/22 + */ +package object cache { + type Identity[O] = O +} diff --git a/smt-cache/src/test/scala/org/bitlap/cache/CacheImplicits.scala b/smt-cache/src/test/scala/org/bitlap/cache/CacheImplicits.scala index 9ebc8aeebd0195a9d5e953fb140ca1060f785e89..d3c875956d2859cc9c50d442acdbed78093a9a9d 100644 --- a/smt-cache/src/test/scala/org/bitlap/cache/CacheImplicits.scala +++ b/smt-cache/src/test/scala/org/bitlap/cache/CacheImplicits.scala @@ -20,10 +20,16 @@ */ package org.bitlap.cache +import scala.concurrent.ExecutionContext object CacheImplicits { - implicit lazy val testEntityCache: GenericCache.Aux[String, TestEntity] = - GenericCache[String, TestEntity](CacheType.Normal) + // NOTE:These are two completely different caches and the data is not shared + // If you don't use global, you should also make sure this place the same as getAsyncCache's + implicit lazy val testEntitySyncCache = + GenericCache[String, TestEntity](CacheStrategy.Normal) + + implicit lazy val testEntityAsyncCache = + GenericCache[String, TestEntity](CacheStrategy.Normal, ExecutionContext.Implicits.global) } diff --git a/smt-cache/src/test/scala/org/bitlap/cache/CacheSpec.scala b/smt-cache/src/test/scala/org/bitlap/cache/CacheSpec.scala index 72ea8e0f30fd428451ed56199c3a12ecefcd30a7..c04f4e44662a8ec18488741aa622b5bce2a9d209 100644 --- a/smt-cache/src/test/scala/org/bitlap/cache/CacheSpec.scala +++ b/smt-cache/src/test/scala/org/bitlap/cache/CacheSpec.scala @@ -24,6 +24,9 @@ package org.bitlap.cache import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.bitlap.cache.CacheImplicits._ +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.DurationInt /** @author * 梦境迷离 @@ -37,14 +40,14 @@ class CacheSpec extends AnyFlatSpec with Matchers { ) "cache1" should "get entity from cache successfully" in { - val cache: CacheRef[String, TestEntity] = Cache.getCache[TestEntity] + val cache = Cache.getSyncCache[TestEntity] cache.init(data) val result: Option[TestEntity] = cache.getT("etc") result shouldBe data.get("etc") } "cache2" should "get entity's field from cache successfully" in { - val cache: CacheRef[String, TestEntity] = Cache.getCache[TestEntity] + val cache = Cache.getSyncCache[TestEntity] cache.init(data) val result: Option[String] = cache.getTField("etc", TestEntity.key) result shouldBe Some("world2") @@ -54,7 +57,7 @@ class CacheSpec extends AnyFlatSpec with Matchers { } "cache3" should "get entity's field after refresh" in { - val cache: CacheRef[String, TestEntity] = Cache.getCache[TestEntity] + val cache = Cache.getSyncCache[TestEntity] cache.init(data) val newData = Map( "btc" -> TestEntity("btc", "hello1", "world1"), @@ -66,4 +69,27 @@ class CacheSpec extends AnyFlatSpec with Matchers { val result: Option[TestEntity] = cache.getT("btc-zh-cn") result shouldBe newData.get("btc-zh-cn") } + + "cache4" should "async cache" in { + val newData = Map( + "btc" -> TestEntity("btc", "id123", "btc_key123"), + "btc-zh-cn" -> TestEntity("btc", "id123", "btc_zh_key123") + ) + val newData2 = Map( + "btc" -> TestEntity("btc", "id456", "bt_key456"), + "btc-zh-cn" -> TestEntity("btc", "id456", "btc_zh_key456"), + "eth" -> TestEntity("btc", "id456", "eth_key456") + ) + val cache = Cache.getAsyncCache[TestEntity] + + val ret = for { + _ <- cache.init(newData) + btcKey <- cache.getTField("btc", TestEntity.key) + _ <- cache.putTAll(newData2) + ethKey <- cache.getTField("eth", TestEntity.key) + } yield btcKey -> ethKey + + Await.result(ret, 3.seconds) shouldBe Option("btc_key123") -> Option("eth_key456") + + } } diff --git a/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala b/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala index 6a75e720f5ea380a5281395e9a637548f22bdc69..cad02169ceca1df553c0022f29f58f85213d2d66 100644 --- a/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala +++ b/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala @@ -27,6 +27,7 @@ import scala.reflect.macros.whitebox import scala.reflect.ClassTag import scala.reflect.runtime.{ universe => ru } import scala.reflect.runtime.universe._ +import scala.util.{ Failure, Success } /** @author * 梦境迷离 @@ -38,11 +39,17 @@ object CaseClassExtractor { classTag: ClassTag[T] ): Option[field.Field] = { val mirror = ru.runtimeMirror(getClass.getClassLoader) - getMethods[T] - .filter(_.name.toTermName.decodedName.toString == field.stringify) - .map(m => mirror.reflect(obj).reflectField(m).get) - .headOption - .map(_.asInstanceOf[field.Field]) + val fieldOption = scala.util.Try( + getMethods[T] + .filter(_.name.toTermName.decodedName.toString == field.stringify) + .map(m => mirror.reflect(obj).reflectField(m).get) + .headOption + .map(_.asInstanceOf[field.Field]) + ) + fieldOption match { + case Success(value) => value + case Failure(exception) => exception.printStackTrace(); None + } } def getMethods[T: ru.TypeTag]: List[ru.MethodSymbol] = typeOf[T].members.collect {