From 61531ab3a9f227049ba9fbaf4339aabadf9e576d Mon Sep 17 00:00:00 2001 From: Ivan Subotic <400790+subotic@users.noreply.github.com> Date: Tue, 15 Jun 2021 18:21:42 +0200 Subject: [PATCH] refactor(cache-service): add in-memory implementation (#1870) --- README.md | 3 +- WORKSPACE | 2 - third_party/dependencies.bzl | 2 + webapi/BUILD.bazel | 5 + .../knora/webapi/app/ApplicationActor.scala | 23 +- .../scala/org/knora/webapi/app/BUILD.bazel | 4 + .../knora/webapi/exceptions/Exceptions.scala | 4 +- .../org/knora/webapi/messages/BUILD.bazel | 2 + ...esponderUtil.scala => ResponderData.scala} | 7 +- .../search/gravsearch/GravsearchParser.scala | 2 +- .../org/knora/webapi/responders/BUILD.bazel | 2 + .../knora/webapi/responders/Responder.scala | 8 +- .../webapi/responders/ResponderManager.scala | 10 +- .../admin/ProjectsResponderADM.scala | 4 +- .../responders/admin/UsersResponderADM.scala | 4 +- .../org/knora/webapi/settings/BUILD.bazel | 1 + .../knora/webapi/settings/KnoraSettings.scala | 9 +- .../scala/org/knora/webapi/store/BUILD.bazel | 1 + .../org/knora/webapi/store/StoreManager.scala | 14 +- .../webapi/store/cacheservice/BUILD.bazel | 52 +++ .../store/cacheservice/CacheService.scala | 42 ++ .../cacheservice/CacheServiceExceptions.scala | 29 ++ .../cacheservice/CacheServiceManager.scala | 383 ++-------------- .../store/cacheservice/inmem/BUILD.bazel | 24 + .../inmem/CacheServiceInMemImpl.scala | 192 ++++++++ .../store/cacheservice/redis/BUILD.bazel | 27 ++ .../redis/CacheServiceRedisImpl.scala | 423 ++++++++++++++++++ .../cacheservice/serialization/BUILD.bazel | 50 +++ .../CacheSerialization.scala | 28 +- .../store/cacheservice/settings/BUILD.bazel | 15 + .../settings/CacheServiceSettings.scala | 32 ++ .../test/scala/org/knora/webapi/BUILD.bazel | 2 + .../scala/org/knora/webapi/CoreSpec.scala | 13 +- .../org/knora/webapi/KnoraFakeCore.scala | 2 +- .../knora/webapi/ManagersWithMockedSipi.scala | 25 +- .../test/scala/org/knora/webapi/R2RSpec.scala | 4 +- .../org/knora/webapi/RedisTestContainer.scala | 45 ++ .../org/knora/webapi/TestContainers.scala | 18 +- .../scala/org/knora/webapi/UnitSpec.scala | 61 +++ .../scala/org/knora/webapi/e2e/v1/BUILD.bazel | 10 + .../webapi/e2e/v1/ResourcesV1R2RSpec.scala | 2 +- .../responders/MockableResponderManager.scala | 7 +- .../v1/ResourcesResponderV1Spec.scala | 4 +- .../webapi/store/MockableStoreManager.scala | 5 +- .../webapi/store/cacheservice/BUILD.bazel | 18 - .../CacheServiceManagerSpec.scala | 21 +- .../store/cacheservice/inmem/BUILD.bazel | 34 ++ .../inmem/CacheServiceInMemImplSpec.scala | 85 ++++ .../store/cacheservice/redis/BUILD.bazel | 40 ++ .../redis/CacheServiceRedisImplSpec.scala | 90 ++++ .../cacheservice/serialization/BUILD.bazel | 32 ++ .../CacheSerializationSpec.scala | 29 +- 52 files changed, 1488 insertions(+), 463 deletions(-) rename webapi/src/main/scala/org/knora/webapi/messages/util/{ResponderUtil.scala => ResponderData.scala} (74%) create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/BUILD.bazel create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheService.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceExceptions.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImpl.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImpl.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel rename webapi/src/main/scala/org/knora/webapi/store/cacheservice/{ => serialization}/CacheSerialization.scala (71%) create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/BUILD.bazel create mode 100644 webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/CacheServiceSettings.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/RedisTestContainer.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/UnitSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel create mode 100644 webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImplSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel create mode 100644 webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImplSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel rename webapi/src/test/scala/org/knora/webapi/store/cacheservice/{ => serialization}/CacheSerializationSpec.scala (58%) diff --git a/README.md b/README.md index d772c7bb6f..0a13a1e534 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,8 @@ Lukas Rosenthaler `` ## Commit Message Schema -When writing commit messages, we follow the [Conventional Commit messages](https://www.conventionalcommits.org/) rules. Get more information in our official [DSP Contribution Documentation](https://docs.dasch.swiss/developers/dsp/contribution/#git-commit-guidelines) +When writing commit messages, we follow the [Conventional Commit messages](https://www.conventionalcommits.org/) rules. +Get more information in our official [DSP Contribution Documentation](https://docs.dasch.swiss/developers/dsp/contribution/#git-commit-guidelines) ## Release Versioning Convention diff --git a/WORKSPACE b/WORKSPACE index 2bc677cd9e..1982b02232 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -289,8 +289,6 @@ http_archive( url = "https://github.com/bazelbuild/buildtools/archive/master.zip", ) - - ##################################### # rules_pkg - basic packaging rules # ##################################### diff --git a/third_party/dependencies.bzl b/third_party/dependencies.bzl index 157a90001e..76b9392c0c 100644 --- a/third_party/dependencies.bzl +++ b/third_party/dependencies.bzl @@ -164,6 +164,8 @@ ALL_WEBAPI_MAIN_DEPENDENCIES = [ "//webapi/src/main/scala/org/knora/webapi/routing", "//webapi/src/main/scala/org/knora/webapi/settings", "//webapi/src/main/scala/org/knora/webapi/store", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis", "//webapi/src/main/scala/org/knora/webapi/util", "//webapi/src/main/scala/org/knora/webapi/util/cache", ] diff --git a/webapi/BUILD.bazel b/webapi/BUILD.bazel index 1839855532..eded0335cd 100644 --- a/webapi/BUILD.bazel +++ b/webapi/BUILD.bazel @@ -30,6 +30,7 @@ scala_library( "//webapi/scripts:fuseki_repository_config_ttl_template", "//webapi/src/main/resources", ], + scalacopts = ["-deprecation"], unused_dependency_checker_mode = "warn", runtime_deps = [ "@maven//:ch_qos_logback_logback_classic", @@ -140,6 +141,7 @@ scala_library( "//sipi/config", "//webapi/src/test/resources", ], + scalacopts = ["-deprecation"], unused_dependency_checker_mode = "warn", runtime_deps = [ "@maven//:ch_qos_logback_logback_classic", @@ -161,6 +163,9 @@ scala_library( "//webapi/src/main/scala/org/knora/webapi/routing", "//webapi/src/main/scala/org/knora/webapi/settings", "//webapi/src/main/scala/org/knora/webapi/store", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings", "//webapi/src/main/scala/org/knora/webapi/util", # Logging "@maven//:com_typesafe_scala_logging_scala_logging_2_13", diff --git a/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala b/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala index 9d198065f6..1f90bee9f6 100644 --- a/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala +++ b/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala @@ -50,7 +50,7 @@ import org.knora.webapi.messages.store.cacheservicemessages.{ } import org.knora.webapi.messages.store.sipimessages.{IIIFServiceGetStatus, IIIFServiceStatusNOK, IIIFServiceStatusOK} import org.knora.webapi.messages.store.triplestoremessages._ -import org.knora.webapi.messages.util.KnoraSystemInstances +import org.knora.webapi.messages.util.{KnoraSystemInstances, ResponderData} import org.knora.webapi.messages.v1.responder.KnoraRequestV1 import org.knora.webapi.messages.v2.responder.ontologymessages.LoadOntologiesRequestV2 import org.knora.webapi.messages.v2.responder.{KnoraRequestV2, SuccessResponseV2} @@ -61,6 +61,8 @@ import org.knora.webapi.routing.v1._ import org.knora.webapi.routing.v2._ import org.knora.webapi.settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl, _} import org.knora.webapi.store.StoreManager +import org.knora.webapi.store.cacheservice.inmem.CacheServiceInMemImpl +import org.knora.webapi.store.cacheservice.settings.CacheServiceSettings import org.knora.webapi.util.cache.CacheUtil import redis.clients.jedis.exceptions.JedisConnectionException @@ -69,6 +71,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} trait Managers { + implicit val system: ActorSystem val responderManager: ActorRef val storeManager: ActorRef } @@ -80,7 +83,7 @@ trait LiveManagers extends Managers { * The actor that forwards messages to actors that deal with persistent storage. */ lazy val storeManager: ActorRef = context.actorOf( - Props(new StoreManager(self) with LiveActorMaker) + Props(new StoreManager(appActor = self, cs = CacheServiceInMemImpl) with LiveActorMaker) .withDispatcher(KnoraDispatchers.KnoraActorDispatcher), name = StoreManagerActorName ) @@ -89,7 +92,14 @@ trait LiveManagers extends Managers { * The actor that forwards messages to responder actors to handle API requests. */ lazy val responderManager: ActorRef = context.actorOf( - Props(new ResponderManager(self) with LiveActorMaker) + Props( + new ResponderManager( + appActor = self, + responderData = ResponderData(system = context.system, + appActor = self, + knoraSettings = KnoraSettings(system), + cacheServiceSettings = new CacheServiceSettings(system.settings.config)) + ) with LiveActorMaker) .withDispatcher(KnoraDispatchers.KnoraActorDispatcher), name = RESPONDER_MANAGER_ACTOR_NAME ) @@ -115,6 +125,11 @@ class ApplicationActor extends Actor with Stash with LazyLogging with AroundDire */ implicit val knoraSettings: KnoraSettingsImpl = KnoraSettings(system) + /** + * The Cache Service's configuration. + */ + implicit val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(system.settings.config) + /** * The default feature factory configuration, which is used during startup. */ @@ -169,7 +184,7 @@ class ApplicationActor extends Actor with Stash with LazyLogging with AroundDire private var printConfigState = false private var ignoreRepository = true private var withIIIFService = true - private val withCacheService = knoraSettings.cacheServiceEnabled + private val withCacheService = cacheServiceSettings.cacheServiceEnabled /** * Startup of the ApplicationActor is a two step process: diff --git a/webapi/src/main/scala/org/knora/webapi/app/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/app/BUILD.bazel index a561fa19d1..00ed4ee205 100644 --- a/webapi/src/main/scala/org/knora/webapi/app/BUILD.bazel +++ b/webapi/src/main/scala/org/knora/webapi/app/BUILD.bazel @@ -19,6 +19,10 @@ scala_library( "//webapi/src/main/scala/org/knora/webapi/routing", "//webapi/src/main/scala/org/knora/webapi/settings", "//webapi/src/main/scala/org/knora/webapi/store", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings", "//webapi/src/main/scala/org/knora/webapi/util/cache", "@maven//:ch_megard_akka_http_cors_2_13", "@maven//:com_github_swagger_akka_http_swagger_akka_http_2_13", diff --git a/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala b/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala index 82eb3dde5a..bca702d4d7 100644 --- a/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala +++ b/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala @@ -383,11 +383,11 @@ object InvalidApiJsonException { } /** - * Indicates that the during caching with Redis something went wrong. + * Indicates that the during caching with the [[org.knora.webapi.store.cacheservice.CacheService]] something went wrong. * * @param message a description of the error. */ -abstract class RedisException(message: String) extends InternalServerException(message) +abstract class CacheServiceException(message: String) extends InternalServerException(message) /** * Indicates that an application lock could not be acquired. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel index d12410c2fc..5efc64ca71 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel +++ b/webapi/src/main/scala/org/knora/webapi/messages/BUILD.bazel @@ -5,6 +5,7 @@ load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") scala_library( name = "messages", srcs = glob(["**/*.scala"]) + ["//webapi/src/main/twirl:twirl_sources"], + scalacopts = ["-deprecation"], unused_dependency_checker_mode = "warn", deps = [ "//webapi/src/main/scala/org/knora/webapi", @@ -12,6 +13,7 @@ scala_library( "//webapi/src/main/scala/org/knora/webapi/exceptions", "//webapi/src/main/scala/org/knora/webapi/feature", "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings", "//webapi/src/main/scala/org/knora/webapi/util", "//webapi/src/main/scala/org/knora/webapi/util/cache", "@maven//:com_apicatalog_titanium_json_ld", diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala similarity index 74% rename from webapi/src/main/scala/org/knora/webapi/messages/util/ResponderUtil.scala rename to webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala index 15df597589..3ed7b16d45 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala @@ -20,6 +20,8 @@ package org.knora.webapi.messages.util import akka.actor.{ActorRef, ActorSystem} +import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.store.cacheservice.settings.CacheServiceSettings /** * Data needed to be passed to each responder. @@ -27,4 +29,7 @@ import akka.actor.{ActorRef, ActorSystem} * @param system the actor system. * @param appActor the main application actor. */ -case class ResponderData(system: ActorSystem, appActor: ActorRef) +case class ResponderData(system: ActorSystem, + appActor: ActorRef, + knoraSettings: KnoraSettingsImpl, + cacheServiceSettings: CacheServiceSettings) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala index 7212d656b9..2bf9115a2b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala @@ -172,7 +172,7 @@ object GravsearchParser { wherePatterns.toSeq } - private def unsupported(node: algebra.QueryModelNode) { + private def unsupported(node: algebra.QueryModelNode): Unit = { throw GravsearchException(s"SPARQL feature not supported in Gravsearch query: $node") } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/responders/BUILD.bazel index c8027d1a2c..de680f45a3 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/BUILD.bazel +++ b/webapi/src/main/scala/org/knora/webapi/responders/BUILD.bazel @@ -5,6 +5,7 @@ load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") scala_library( name = "responders", srcs = glob(["**/*.scala"]), + scalacopts = ["-deprecation"], unused_dependency_checker_mode = "warn", deps = [ "//webapi/src/main/scala/org/knora/webapi", @@ -15,6 +16,7 @@ scala_library( "//webapi/src/main/scala/org/knora/webapi/instrumentation", "//webapi/src/main/scala/org/knora/webapi/messages", "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings", "//webapi/src/main/scala/org/knora/webapi/util", "//webapi/src/main/scala/org/knora/webapi/util/cache", "@maven//:com_typesafe_akka_akka_actor_2_13", diff --git a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala index 4bde0a8767..b993a235c9 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -33,6 +33,7 @@ import akka.http.scaladsl.util.FastFuture import akka.pattern._ import akka.util.Timeout import com.typesafe.scalalogging.{LazyLogging, Logger} +import org.knora.webapi.store.cacheservice.settings.CacheServiceSettings import scala.concurrent.{ExecutionContext, Future} import scala.language.postfixOps @@ -75,7 +76,12 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging { /** * The application settings. */ - protected val settings: KnoraSettingsImpl = KnoraSettings(system) + protected val settings: KnoraSettingsImpl = responderData.knoraSettings + + /** + * The Cache Service settings. + */ + protected val cacheServiceSettings: CacheServiceSettings = responderData.cacheServiceSettings /** * The main application actor. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/ResponderManager.scala b/webapi/src/main/scala/org/knora/webapi/responders/ResponderManager.scala index cfb920da4e..25657af132 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/ResponderManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/ResponderManager.scala @@ -60,7 +60,7 @@ import scala.concurrent.ExecutionContext * * @param appActor the main application actor. */ -class ResponderManager(appActor: ActorRef) extends Actor with ActorLogging { +class ResponderManager(appActor: ActorRef, responderData: ResponderData) extends Actor with ActorLogging { this: ActorMaker => /** @@ -74,14 +74,6 @@ class ResponderManager(appActor: ActorRef) extends Actor with ActorLogging { protected implicit val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - /** - * The responder data. - */ - private val responderData = ResponderData( - system = system, - appActor = appActor - ) - // A subclass can replace the standard responders with custom responders, e.g. for testing. To do this, it must // override one or more of the protected val members below representing responder classes. To construct a default // responder, a subclass can call one of the protected methods below. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index 29d744c599..6817c509b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -1153,7 +1153,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo private def getProjectFromCacheOrTriplestore( identifier: ProjectIdentifierADM, featureFactoryConfig: FeatureFactoryConfig): Future[Option[ProjectADM]] = { - if (settings.cacheServiceEnabled) { + if (cacheServiceSettings.cacheServiceEnabled) { // caching enabled getProjectFromCache(identifier) .flatMap { @@ -1405,7 +1405,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo * Removes the project from cache. */ private def invalidateCachedProjectADM(maybeProject: Option[ProjectADM]): Future[Boolean] = { - if (settings.cacheServiceEnabled) { + if (cacheServiceSettings.cacheServiceEnabled) { val keys: Set[String] = Seq(maybeProject.map(_.id), maybeProject.map(_.shortname), maybeProject.map(_.shortcode)).flatten.toSet // only send to Redis if keys are not empty diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala index 0ad6432b50..00c1dafd67 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala @@ -1626,7 +1626,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde */ private def getUserFromCacheOrTriplestore(identifier: UserIdentifierADM, featureFactoryConfig: FeatureFactoryConfig): Future[Option[UserADM]] = { - if (settings.cacheServiceEnabled) { + if (cacheServiceSettings.cacheServiceEnabled) { // caching enabled getUserFromCache(identifier) .flatMap { @@ -1981,7 +1981,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * Removes the user from cache. */ private def invalidateCachedUserADM(maybeUser: Option[UserADM]): Future[Boolean] = { - if (settings.cacheServiceEnabled) { + if (cacheServiceSettings.cacheServiceEnabled) { val keys: Set[String] = Seq(maybeUser.map(_.id), maybeUser.map(_.email), maybeUser.map(_.username)).flatten.toSet // only send to Redis if keys are not empty if (keys.nonEmpty) { diff --git a/webapi/src/main/scala/org/knora/webapi/settings/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/settings/BUILD.bazel index b70cdcc03f..995c612376 100644 --- a/webapi/src/main/scala/org/knora/webapi/settings/BUILD.bazel +++ b/webapi/src/main/scala/org/knora/webapi/settings/BUILD.bazel @@ -5,6 +5,7 @@ load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") scala_library( name = "settings", srcs = glob(["*.scala"]), + scalacopts = ["-deprecation"], unused_dependency_checker_mode = "warn", deps = [ "//webapi/src/main/scala/org/knora/webapi/exceptions", diff --git a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala index 6659b1cf81..62011d7f41 100644 --- a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala +++ b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala @@ -270,11 +270,6 @@ class KnoraSettingsImpl(config: Config, log: LoggingAdapter) extends Extension { val bcryptPasswordStrength: Int = config.getInt("app.bcrypt-password-strength") - // Cache Service - val cacheServiceEnabled: Boolean = config.getBoolean("app.cache-service.enabled") - val cacheServiceRedisHost: String = config.getString("app.cache-service.redis.host") - val cacheServiceRedisPort: Int = config.getInt("app.cache-service.redis.port") - // Client test data service val collectClientTestData: Boolean = if (config.hasPath("app.client-test-data-service.collect-client-test-data")) { @@ -297,8 +292,8 @@ class KnoraSettingsImpl(config: Config, log: LoggingAdapter) extends Extension { private def getFiniteDuration(path: String, underlying: Config): FiniteDuration = Duration(underlying.getString(path)) match { - case x: FiniteDuration ⇒ x - case _ ⇒ throw new ConfigurationException(s"Config setting '$path' must be a finite duration") + case x: FiniteDuration => x + case _ => throw new ConfigurationException(s"Config setting '$path' must be a finite duration") } val prometheusEndpoint: Boolean = config.getBoolean("app.monitoring.prometheus-endpoint") diff --git a/webapi/src/main/scala/org/knora/webapi/store/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/store/BUILD.bazel index f0a8499218..bde0466e83 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/BUILD.bazel +++ b/webapi/src/main/scala/org/knora/webapi/store/BUILD.bazel @@ -15,6 +15,7 @@ scala_library( "//webapi/src/main/scala/org/knora/webapi/messages", "//webapi/src/main/scala/org/knora/webapi/routing", "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", "//webapi/src/main/scala/org/knora/webapi/util", "@maven//:com_twitter_chill_2_13", "@maven//:com_typesafe_akka_akka_actor_2_13", diff --git a/webapi/src/main/scala/org/knora/webapi/store/StoreManager.scala b/webapi/src/main/scala/org/knora/webapi/store/StoreManager.scala index f2bafb1c23..8f0725d380 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/StoreManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/StoreManager.scala @@ -28,7 +28,7 @@ import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceRequest import org.knora.webapi.messages.store.sipimessages.IIIFRequest import org.knora.webapi.messages.store.triplestoremessages.TriplestoreRequest import org.knora.webapi.settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl, _} -import org.knora.webapi.store.cacheservice.CacheServiceManager +import org.knora.webapi.store.cacheservice.{CacheService, CacheServiceManager} import org.knora.webapi.store.iiif.IIIFManager import org.knora.webapi.store.triplestore.TriplestoreManager @@ -42,7 +42,7 @@ import scala.concurrent.ExecutionContext * * @param appActor a reference to the main application actor. */ -class StoreManager(appActor: ActorRef) extends Actor with ActorLogging { +class StoreManager(appActor: ActorRef, cs: CacheService) extends Actor with ActorLogging { this: ActorMaker => /** @@ -89,14 +89,14 @@ class StoreManager(appActor: ActorRef) extends Actor with ActorLogging { /** * Instantiates the Redis Manager */ - protected lazy val redisManager: ActorRef = makeActor( - Props(new CacheServiceManager).withDispatcher(KnoraDispatchers.KnoraActorDispatcher), + protected lazy val cacheServiceManager: ActorRef = makeActor( + Props(new CacheServiceManager(cs)).withDispatcher(KnoraDispatchers.KnoraActorDispatcher), RedisManagerActorName) def receive: Receive = LoggingReceive { - case tripleStoreMessage: TriplestoreRequest => triplestoreManager forward tripleStoreMessage - case iiifMessages: IIIFRequest => iiifManager forward iiifMessages - case redisMessages: CacheServiceRequest => redisManager forward redisMessages + case tripleStoreMessage: TriplestoreRequest => triplestoreManager forward tripleStoreMessage + case iiifMessages: IIIFRequest => iiifManager forward iiifMessages + case cacheServiceMessages: CacheServiceRequest => cacheServiceManager forward cacheServiceMessages case other => sender ! Status.Failure(UnexpectedMessageException(s"StoreManager received an unexpected message: $other")) } diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/BUILD.bazel new file mode 100644 index 0000000000..607d0205ad --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/BUILD.bazel @@ -0,0 +1,52 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") + +scala_library( + name = "cacheservice", + srcs = [ + "CacheService.scala", + "CacheServiceExceptions.scala", + "CacheServiceManager.scala", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/core", + "//webapi/src/main/scala/org/knora/webapi/exceptions", + "//webapi/src/main/scala/org/knora/webapi/feature", + "//webapi/src/main/scala/org/knora/webapi/instrumentation", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/routing", + "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/util", + "@maven//:com_twitter_chill_2_13", + "@maven//:com_typesafe_akka_akka_actor_2_13", + "@maven//:com_typesafe_akka_akka_http_2_13", + "@maven//:com_typesafe_akka_akka_http_core_2_13", + "@maven//:com_typesafe_akka_akka_http_spray_json_2_13", + "@maven//:com_typesafe_akka_akka_stream_2_13", + "@maven//:com_typesafe_config", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:commons_cli_commons_cli", + "@maven//:commons_codec_commons_codec", + "@maven//:commons_io_commons_io", + "@maven//:io_spray_spray_json_2_13", + "@maven//:org_apache_commons_commons_csv", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_apache_commons_commons_pool2", + "@maven//:org_apache_httpcomponents_httpclient", + "@maven//:org_apache_httpcomponents_httpclient_cache", + "@maven//:org_apache_httpcomponents_httpcore", + "@maven//:org_apache_jena_apache_jena_libs", + "@maven//:org_apache_jena_jena_arq", + "@maven//:org_apache_jena_jena_base", + "@maven//:org_apache_jena_jena_core", + "@maven//:org_apache_jena_jena_tdb", + "@maven//:org_apache_jena_jena_text", + "@maven//:org_apache_lucene_lucene_core", + "@maven//:org_slf4j_slf4j_api", + "@maven//:redis_clients_jedis", + ], +) diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheService.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheService.scala new file mode 100644 index 0000000000..f339427102 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheService.scala @@ -0,0 +1,42 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice + +import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectADM, ProjectIdentifierADM} +import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM} +import org.knora.webapi.messages.store.cacheservicemessages.{CacheServiceFlushDBACK, CacheServiceStatusResponse} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Cache Service Interface + */ +trait CacheService { + def putUserADM(value: UserADM)(implicit ec: ExecutionContext): Future[Boolean] + def getUserADM(identifier: UserIdentifierADM)(implicit ec: ExecutionContext): Future[Option[UserADM]] + def putProjectADM(value: ProjectADM)(implicit ec: ExecutionContext): Future[Boolean] + def getProjectADM(identifier: ProjectIdentifierADM)(implicit ec: ExecutionContext): Future[Option[ProjectADM]] + def writeStringValue(key: String, value: String)(implicit ec: ExecutionContext): Future[Boolean] + def getStringValue(maybeKey: Option[String])(implicit ec: ExecutionContext): Future[Option[String]] + def removeValues(keys: Set[String])(implicit ec: ExecutionContext): Future[Boolean] + def flushDB(requestingUser: UserADM)(implicit ec: ExecutionContext): Future[CacheServiceFlushDBACK] + def ping()(implicit ec: ExecutionContext): Future[CacheServiceStatusResponse] +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceExceptions.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceExceptions.scala new file mode 100644 index 0000000000..e641e75955 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceExceptions.scala @@ -0,0 +1,29 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice + +import org.knora.webapi.exceptions.CacheServiceException + +case class EmptyKey(message: String) extends CacheServiceException(message) + +case class EmptyValue(message: String) extends CacheServiceException(message) + +case class UnsupportedValueType(message: String) extends CacheServiceException(message) diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceManager.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceManager.scala index eb5304b2e8..b258ab71a3 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheServiceManager.scala @@ -20,30 +20,22 @@ package org.knora.webapi.store.cacheservice import akka.actor.{Actor, ActorLogging, ActorSystem, Status} -import akka.http.scaladsl.util.FastFuture -import com.typesafe.scalalogging.{LazyLogging, Logger} -import org.knora.webapi.exceptions.{ForbiddenException, RedisException, UnexpectedMessageException} +import com.typesafe.scalalogging.LazyLogging +import org.knora.webapi.exceptions.UnexpectedMessageException import org.knora.webapi.instrumentation.InstrumentationSupport -import org.knora.webapi.messages.admin.responder.projectsmessages.{ - ProjectADM, - ProjectIdentifierADM, - ProjectIdentifierType -} -import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM, UserIdentifierType} +import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectADM, ProjectIdentifierADM} +import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM} import org.knora.webapi.messages.store.cacheservicemessages._ -import org.knora.webapi.settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl} +import org.knora.webapi.settings.KnoraDispatchers import org.knora.webapi.util.ActorUtil.future2Message -import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} import scala.concurrent.{ExecutionContext, Future} -case class EmptyKey(message: String) extends RedisException(message) - -case class EmptyValue(message: String) extends RedisException(message) - -case class UnsupportedValueType(message: String) extends RedisException(message) - -class CacheServiceManager extends Actor with ActorLogging with LazyLogging with InstrumentationSupport { +class CacheServiceManager(cs: CacheService) + extends Actor + with ActorLogging + with LazyLogging + with InstrumentationSupport { /** * The Knora Akka actor system. @@ -55,38 +47,18 @@ class CacheServiceManager extends Actor with ActorLogging with LazyLogging with */ protected implicit val ec: ExecutionContext = context.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - /** - * The Knora settings. - */ - protected val s: KnoraSettingsImpl = KnoraSettings(context.system) - - /** - * The Redis Client Pool - */ - val pool: JedisPool = new JedisPool(new JedisPoolConfig(), s.cacheServiceRedisHost, s.cacheServiceRedisPort, 20999) - - // this is needed for time measurements using 'org.knora.webapi.Timing' - - implicit val l: Logger = logger - - // close the redis client pool - override def postStop(): Unit = { - logger.info("CacheServiceManager - shutdown in progress, initiating post stop cleanup.") - pool.close() - } - def receive = { - case CacheServicePutUserADM(value) => future2Message(sender(), redisPutUserADM(value), log) - case CacheServiceGetUserADM(identifier) => future2Message(sender(), redisGetUserADM(identifier), log) - case CacheServicePutProjectADM(value) => future2Message(sender(), redisPutProjectADM(value), log) - case CacheServiceGetProjectADM(identifier) => future2Message(sender(), redisGetProjectADM(identifier), log) + case CacheServicePutUserADM(value) => future2Message(sender(), putUserADM(value), log) + case CacheServiceGetUserADM(identifier) => future2Message(sender(), getUserADM(identifier), log) + case CacheServicePutProjectADM(value) => future2Message(sender(), putProjectADM(value), log) + case CacheServiceGetProjectADM(identifier) => future2Message(sender(), getProjectADM(identifier), log) case CacheServicePutString(key, value) => future2Message(sender(), writeStringValue(key, value), log) case CacheServiceGetString(key) => future2Message(sender(), getStringValue(key), log) case CacheServiceRemoveValues(keys) => future2Message(sender(), removeValues(keys), log) case CacheServiceFlushDB(requestingUser) => future2Message(sender(), flushDB(requestingUser), log) case CacheServiceGetStatus => future2Message(sender(), ping(), log) case other => - sender ! Status.Failure(UnexpectedMessageException(s"RedisManager received an unexpected message: $other")) + sender() ! Status.Failure(UnexpectedMessageException(s"RedisManager received an unexpected message: $other")) } /** @@ -99,23 +71,8 @@ class CacheServiceManager extends Actor with ActorLogging with LazyLogging with * * @param value the stored value */ - private def redisPutUserADM(value: UserADM): Future[Boolean] = tracedFuture("redis-write-user") { - - val resultFuture = for { - bytes: Array[Byte] <- CacheSerialization.serialize(value) - result: Boolean <- writeBytesValue(value.id, bytes) - // additionally store the IRI under the username and email key - _ = writeStringValue(value.username, value.id) - _ = writeStringValue(value.email, value.id) - } yield result - - val recoverableResultFuture = resultFuture.recover { - case e: Exception => - logger.warn("Aborting writing 'UserADM' to Redis - {}", e.getMessage) - false - } - - recoverableResultFuture + private def putUserADM(value: UserADM): Future[Boolean] = tracedFuture("caches-service-write-user") { + cs.putUserADM(value) } /** @@ -124,49 +81,9 @@ class CacheServiceManager extends Actor with ActorLogging with LazyLogging with * * @param identifier the project identifier. */ - private def redisGetUserADM(identifier: UserIdentifierADM): Future[Option[UserADM]] = - tracedFuture("redis-read-user") { - - // The data is stored under the IRI key. - // Additionally, the SHORTNAME and SHORTCODE keys point to the IRI key - val resultFuture: Future[Option[UserADM]] = identifier.hasType match { - case UserIdentifierType.IRI => - for { - maybeBytes: Option[Array[Byte]] <- getBytesValue(identifier.toIriOption) - maybeUser: Option[UserADM] <- maybeBytes match { - case Some(bytes) => CacheSerialization.deserialize[UserADM](bytes) - case None => FastFuture.successful(None) - } - } yield maybeUser - - case UserIdentifierType.USERNAME => - for { - maybeIriKey: Option[String] <- getStringValue(identifier.toUsernameOption) - maybeBytes: Option[Array[Byte]] <- getBytesValue(maybeIriKey) - maybeUser: Option[UserADM] <- maybeBytes match { - case Some(bytes) => CacheSerialization.deserialize[UserADM](bytes) - case None => FastFuture.successful(None) - } - } yield maybeUser - - case UserIdentifierType.EMAIL => - for { - maybeIriKey: Option[String] <- getStringValue(identifier.toEmailOption) - maybeBytes: Option[Array[Byte]] <- getBytesValue(maybeIriKey) - maybeUser: Option[UserADM] <- maybeBytes match { - case Some(bytes) => CacheSerialization.deserialize[UserADM](bytes) - case None => FastFuture.successful(None) - } - } yield maybeUser - } - - val recoverableResultFuture = resultFuture.recover { - case e: Exception => - logger.warn("Aborting reading 'UserADM' from Redis - {}", e.getMessage) - None - } - - recoverableResultFuture + private def getUserADM(identifier: UserIdentifierADM): Future[Option[UserADM]] = + tracedFuture("cache-service-read-user") { + cs.getUserADM(identifier) } /** @@ -179,285 +96,65 @@ class CacheServiceManager extends Actor with ActorLogging with LazyLogging with * * @param value the stored value */ - private def redisPutProjectADM(value: ProjectADM): Future[Boolean] = tracedFuture("redis-write-project") { - - val resultFuture = for { - bytes: Array[Byte] <- CacheSerialization.serialize(value) - result: Boolean <- writeBytesValue(value.id, bytes) - _ = writeStringValue(value.shortcode, value.id) - _ = writeStringValue(value.shortname, value.id) - } yield result - - val recoverableResultFuture = resultFuture.recover { - case e: Exception => - logger.warn("Aborting writing 'ProjectADM' to Redis - {}", e.getMessage) - false + private def putProjectADM(value: ProjectADM)(implicit ec: ExecutionContext): Future[Boolean] = + tracedFuture("cache-service-write-project") { + cs.putProjectADM(value) } - recoverableResultFuture - } - /** * Retrieves the project stored under the identifier (either iri, shortname, or shortcode). * * @param identifier the project identifier. */ - private def redisGetProjectADM(identifier: ProjectIdentifierADM): Future[Option[ProjectADM]] = + private def getProjectADM(identifier: ProjectIdentifierADM)( + implicit ec: ExecutionContext): Future[Option[ProjectADM]] = tracedFuture("redis-read-project") { - - // The data is stored under the IRI key. - // Additionally, the SHORTNAME and SHORTCODE keys point to the IRI key - val resultFuture: Future[Option[ProjectADM]] = identifier.hasType match { - case ProjectIdentifierType.IRI => - for { - maybeBytes <- getBytesValue(identifier.toIriOption) - maybeProject <- maybeBytes match { - case Some(bytes) => CacheSerialization.deserialize[ProjectADM](bytes) - case None => FastFuture.successful(None) - } - } yield maybeProject - case ProjectIdentifierType.SHORTCODE => - for { - maybeIriKey <- getStringValue(identifier.toShortcodeOption) - maybeBytes <- getBytesValue(maybeIriKey) - maybeProject: Option[ProjectADM] <- maybeBytes match { - case Some(bytes) => CacheSerialization.deserialize[ProjectADM](bytes) - case None => FastFuture.successful(None) - } - } yield maybeProject - case ProjectIdentifierType.SHORTNAME => - for { - maybeIriKey <- getStringValue(identifier.toShortnameOption) - maybeBytes <- getBytesValue(maybeIriKey) - maybeProject: Option[ProjectADM] <- maybeBytes match { - case Some(bytes) => CacheSerialization.deserialize[ProjectADM](bytes) - case None => FastFuture.successful(None) - } - } yield maybeProject - } - - val recoverableResultFuture = resultFuture.recover { - case e: Exception => - logger.warn("Aborting reading 'ProjectADM' from Redis - {}", e.getMessage) - None - } - - recoverableResultFuture - } - - /** - * Get value stored under the key as a byte array. If no value is found - * under the key, then a [[None]] is returned.. - * - * @param maybeKey the key. - */ - private def getBytesValue(maybeKey: Option[String]): Future[Option[Array[Byte]]] = { - - val operationFuture: Future[Option[Array[Byte]]] = maybeKey match { - case Some(key) => - Future { - val conn = pool.getResource - try { - Option(conn.get(key.getBytes)) - } finally { - conn.close() - } - } - case None => - FastFuture.successful(None) + cs.getProjectADM(identifier) } - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - // Log any errors. - logger.warn("Reading byte array from Redis failed - {}", e.getMessage) - None - } - - recoverableOperationFuture - } - - /** - * Store string or byte array value under key. - * - * @param key the key. - * @param value the value. - */ - private def writeBytesValue(key: String, value: Array[Byte]): Future[Boolean] = { - - if (key.isEmpty) - throw EmptyKey("The key under which the value should be written is empty. Aborting writing to redis.") - - if (value.isEmpty) - throw EmptyValue("The byte array value is empty. Aborting writing to redis.") - - val operationFuture: Future[Boolean] = Future { - val conn: Jedis = pool.getResource - try { - conn.set(key.getBytes, value) - true - } finally { - conn.close() - } - } - - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - // Log any errors. - logger.warn("Writing to Redis failed - {}", e.getMessage) - false - } - - recoverableOperationFuture - } - /** * Get value stored under the key as a string. * * @param maybeKey the key. */ - private def getStringValue(maybeKey: Option[String]): Future[Option[String]] = { - - val operationFuture: Future[Option[String]] = maybeKey match { - case Some(key) => - Future { - val conn: Jedis = pool.getResource - try { - Option(conn.get(key)) - } finally { - conn.close() - } - } - case None => - FastFuture.successful(None) - } - - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - // Log any errors. - logger.warn("Reading string from Redis failed, {}", e) - None + private def getStringValue(maybeKey: Option[String]): Future[Option[String]] = + tracedFuture("cache-service-get-string") { + cs.getStringValue(maybeKey) } - recoverableOperationFuture - } - /** * Store string or byte array value under key. * * @param key the key. * @param value the value. */ - private def writeStringValue(key: String, value: String): Future[Boolean] = { - - if (key.isEmpty) - throw EmptyKey("The key under which the value should be written is empty. Aborting writing to redis.") - - if (value.isEmpty) - throw EmptyValue("The string value is empty. Aborting writing to redis.") - - val operationFuture: Future[Boolean] = Future { - - val conn: Jedis = pool.getResource - try { - conn.set(key, value) - true - } finally { - conn.close() - } - - } - - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - // Log any errors. - logger.warn("Writing to Redis failed - {}", e.getMessage) - false + private def writeStringValue(key: String, value: String): Future[Boolean] = + tracedFuture("cache-service-write-string") { + cs.writeStringValue(key, value) } - recoverableOperationFuture - } - /** * Removes values for the provided keys. Any invalid keys are ignored. * * @param keys the keys. */ - private def removeValues(keys: Set[String]): Future[Boolean] = tracedFuture("redis-remove-values") { - - logger.debug("removeValues - {}", keys) - - val operationFuture: Future[Boolean] = Future { - // del takes a vararg so I nee to convert the set to a swq and then to vararg - val conn: Jedis = pool.getResource - try { - conn.del(keys.toSeq: _*) - true - } finally { - conn.close() - } + private def removeValues(keys: Set[String]): Future[Boolean] = + tracedFuture("redis-remove-values") { + cs.removeValues(keys) } - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - // Log any errors. - logger.warn("Removing keys from Redis failed.", e.getMessage) - false - } - - recoverableOperationFuture - } - /** * Flushes (removes) all stored content from the Redis store. */ - private def flushDB(requestingUser: UserADM): Future[CacheServiceFlushDBACK] = tracedFuture("redis-flush-db") { - - if (!requestingUser.isSystemUser) { - throw ForbiddenException("Only the system user is allowed to perform this operation.") - } - - val operationFuture: Future[CacheServiceFlushDBACK] = Future { - - val conn: Jedis = pool.getResource - try { - conn.flushDB() - CacheServiceFlushDBACK() - } finally { - conn.close() - } - } - - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - // Log any errors. - logger.warn("Flushing DB failed", e.getMessage) - throw e + private def flushDB(requestingUser: UserADM): Future[CacheServiceFlushDBACK] = + tracedFuture("redis-flush-db") { + cs.flushDB(requestingUser) } - recoverableOperationFuture - } - /** - * Pings the Redis store to see if it is available. + * Pings the cache service to see if it is available. */ private def ping(): Future[CacheServiceStatusResponse] = { - val operationFuture: Future[CacheServiceStatusResponse] = Future { - - val conn: Jedis = pool.getResource - try { - conn.ping("test") - CacheServiceStatusOK - } finally { - conn.close() - } - } - - val recoverableOperationFuture = operationFuture.recover { - case e: Exception => - CacheServiceStatusNOK - } - - recoverableOperationFuture + cs.ping() } } diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel new file mode 100644 index 0000000000..a3310bda75 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel @@ -0,0 +1,24 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") + +scala_library( + name = "inmem", + srcs = [ + "CacheServiceInMemImpl.scala", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/exceptions", + "//webapi/src/main/scala/org/knora/webapi/instrumentation", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "@maven//:com_typesafe_akka_akka_actor_2_13", + "@maven//:com_typesafe_akka_akka_http_core_2_13", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:org_slf4j_slf4j_api", + ], +) diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImpl.scala new file mode 100644 index 0000000000..b4621e9d55 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImpl.scala @@ -0,0 +1,192 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice.inmem + +import akka.http.scaladsl.util.FastFuture +import com.typesafe.scalalogging.LazyLogging +import org.knora.webapi.messages.admin.responder.projectsmessages.{ + ProjectADM, + ProjectIdentifierADM, + ProjectIdentifierType +} +import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM, UserIdentifierType} +import org.knora.webapi.messages.store.cacheservicemessages.{ + CacheServiceFlushDBACK, + CacheServiceStatusOK, + CacheServiceStatusResponse +} +import org.knora.webapi.store.cacheservice.{CacheService, EmptyKey, EmptyValue} + +import scala.concurrent.{ExecutionContext, Future} + +object CacheServiceInMemImpl extends CacheService with LazyLogging { + + private var cache: scala.collection.mutable.Map[Any, Any] = + scala.collection.mutable.Map[Any, Any]() + + /** + * Stores the user under the IRI and additionally the IRI under the keys of + * USERNAME and EMAIL: + * + * IRI -> byte array + * username -> IRI + * email -> IRI + * + * @param value the stored value + */ + def putUserADM(value: UserADM)(implicit ec: ExecutionContext): Future[Boolean] = { + cache(value.id) = value + cache(value.username) = value.id + cache(value.email) = value.id + FastFuture.successful(true) + } + + /** + * Retrieves the user stored under the identifier (either iri, username, + * or email). + * + * @param identifier the user identifier. + */ + def getUserADM(identifier: UserIdentifierADM)(implicit ec: ExecutionContext): Future[Option[UserADM]] = { + // The data is stored under the IRI key. + // Additionally, the USERNAME and EMAIL keys point to the IRI key + val resultFuture: Future[Option[UserADM]] = identifier.hasType match { + case UserIdentifierType.IRI => FastFuture.successful(cache.get(identifier.toIri).map(_.asInstanceOf[UserADM])) + case UserIdentifierType.USERNAME => { + cache.get(identifier.toUsername) match { + case Some(iriKey) => FastFuture.successful(cache.get(iriKey).map(_.asInstanceOf[UserADM])) + case None => FastFuture.successful(None) + } + } + case UserIdentifierType.EMAIL => + cache.get(identifier.toEmail) match { + case Some(iriKey) => FastFuture.successful(cache.get(iriKey).map(_.asInstanceOf[UserADM])) + case None => FastFuture.successful(None) + } + } + resultFuture + } + + /** + * Stores the project under the IRI and additionally the IRI under the keys + * of SHORTCODE and SHORTNAME: + * + * IRI -> byte array + * shortname -> IRI + * shortcode -> IRI + * + * @param value the stored value + */ + def putProjectADM(value: ProjectADM)(implicit ec: ExecutionContext): Future[Boolean] = { + cache(value.id) = value + cache(value.shortcode) = value.id + cache(value.shortname) = value.id + FastFuture.successful(true) + } + + /** + * Retrieves the project stored under the identifier (either iri, shortname, or shortcode). + * + * @param identifier the project identifier. + */ + def getProjectADM(identifier: ProjectIdentifierADM)(implicit ec: ExecutionContext): Future[Option[ProjectADM]] = { + // The data is stored under the IRI key. + // Additionally, the SHORTNAME and SHORTCODE keys point to the IRI key + val resultFuture: Future[Option[ProjectADM]] = identifier.hasType match { + case ProjectIdentifierType.IRI => + FastFuture.successful(cache.get(identifier.toIri).map(_.asInstanceOf[ProjectADM])) + case ProjectIdentifierType.SHORTCODE => + cache.get(identifier.toShortcode) match { + case Some(iriKey) => FastFuture.successful(cache.get(iriKey).map(_.asInstanceOf[ProjectADM])) + case None => FastFuture.successful(None) + } + case ProjectIdentifierType.SHORTNAME => + cache.get(identifier.toShortname) match { + case Some(iriKey) => FastFuture.successful(cache.get(iriKey).map(_.asInstanceOf[ProjectADM])) + case None => FastFuture.successful(None) + } + } + resultFuture + } + + /** + * Store string or byte array value under key. + * + * @param key the key. + * @param value the value. + */ + def writeStringValue(key: String, value: String)(implicit ec: ExecutionContext): Future[Boolean] = { + + if (key.isEmpty) + throw EmptyKey("The key under which the value should be written is empty. Aborting writing to in-memory cache.") + + if (value.isEmpty) + throw EmptyValue("The string value is empty. Aborting writing to in-memory cache.") + + cache(key) = value + FastFuture.successful(true) + } + + /** + * Get value stored under the key as a string. + * + * @param maybeKey the key. + */ + def getStringValue(maybeKey: Option[String])(implicit ec: ExecutionContext): Future[Option[String]] = { + maybeKey match { + case Some(key) => + FastFuture.successful(cache.get(key).map(_.asInstanceOf[String])) + case None => + FastFuture.successful(None) + } + } + + /** + * Removes values for the provided keys. Any invalid keys are ignored. + * + * @param keys the keys. + */ + def removeValues(keys: Set[String])(implicit ec: ExecutionContext): Future[Boolean] = { + + logger.debug("removeValues - {}", keys) + keys foreach { key => + cache remove key + } + + FastFuture.successful(true) + } + + /** + * Flushes (removes) all stored content from the in-memory cache. + */ + def flushDB(requestingUser: UserADM)(implicit ec: ExecutionContext): Future[CacheServiceFlushDBACK] = { + cache = scala.collection.mutable.Map[Any, Any]() + FastFuture.successful(CacheServiceFlushDBACK()) + } + + /** + * Pings the in-memory cache to see if it is available. + */ + def ping()(implicit ec: ExecutionContext): Future[CacheServiceStatusResponse] = { + FastFuture.successful(CacheServiceStatusOK) + } + +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel new file mode 100644 index 0000000000..db7e971ebb --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel @@ -0,0 +1,27 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") + +scala_library( + name = "redis", + srcs = [ + "CacheServiceRedisImpl.scala", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/exceptions", + "//webapi/src/main/scala/org/knora/webapi/instrumentation", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings", + "@maven//:com_typesafe_akka_akka_actor_2_13", + "@maven//:com_typesafe_akka_akka_http_core_2_13", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:org_apache_commons_commons_pool2", + "@maven//:org_slf4j_slf4j_api", + "@maven//:redis_clients_jedis", + ], +) diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImpl.scala new file mode 100644 index 0000000000..da0e2e7b2a --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImpl.scala @@ -0,0 +1,423 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice.redis + +import akka.http.scaladsl.util.FastFuture +import com.typesafe.scalalogging.{LazyLogging, Logger} +import org.knora.webapi.exceptions.ForbiddenException +import org.knora.webapi.messages.admin.responder.projectsmessages.{ + ProjectADM, + ProjectIdentifierADM, + ProjectIdentifierType +} +import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM, UserIdentifierType} +import org.knora.webapi.messages.store.cacheservicemessages.{ + CacheServiceFlushDBACK, + CacheServiceStatusNOK, + CacheServiceStatusOK, + CacheServiceStatusResponse +} +import org.knora.webapi.store.cacheservice.serialization.CacheSerialization +import org.knora.webapi.store.cacheservice.settings.CacheServiceSettings +import org.knora.webapi.store.cacheservice.{CacheService, EmptyKey, EmptyValue} +import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} + +import scala.concurrent.{ExecutionContext, Future} + +class CacheServiceRedisImpl(s: CacheServiceSettings) extends CacheService with LazyLogging { + + /** + * The Redis Client Pool + */ + val pool: JedisPool = new JedisPool(new JedisPoolConfig(), s.cacheServiceRedisHost, s.cacheServiceRedisPort, 20999) + + // this is needed for time measurements using 'org.knora.webapi.Timing' + + implicit val l: Logger = logger + + /** + * Stores the user under the IRI and additionally the IRI under the keys of + * USERNAME and EMAIL: + * + * IRI -> byte array + * username -> IRI + * email -> IRI + * + * @param value the stored value + */ + def putUserADM(value: UserADM)(implicit ec: ExecutionContext): Future[Boolean] = { + val resultFuture = for { + bytes: Array[Byte] <- CacheSerialization.serialize(value) + result: Boolean <- writeBytesValue(value.id, bytes) + // additionally store the IRI under the username and email key + _ = writeStringValue(value.username, value.id) + _ = writeStringValue(value.email, value.id) + } yield result + + val recoverableResultFuture = resultFuture.recover { + case e: Exception => + logger.warn("Aborting writing 'UserADM' to Redis - {}", e.getMessage) + false + } + + recoverableResultFuture + } + + /** + * Retrieves the user stored under the identifier (either iri, username, + * or email). + * + * @param identifier the user identifier. + */ + def getUserADM(identifier: UserIdentifierADM)(implicit ec: ExecutionContext): Future[Option[UserADM]] = { + // The data is stored under the IRI key. + // Additionally, the USERNAME and EMAIL keys point to the IRI key + val resultFuture: Future[Option[UserADM]] = identifier.hasType match { + case UserIdentifierType.IRI => + for { + maybeBytes: Option[Array[Byte]] <- getBytesValue(identifier.toIriOption) + maybeUser: Option[UserADM] <- maybeBytes match { + case Some(bytes) => CacheSerialization.deserialize[UserADM](bytes) + case None => FastFuture.successful(None) + } + } yield maybeUser + + case UserIdentifierType.USERNAME => + for { + maybeIriKey: Option[String] <- getStringValue(identifier.toUsernameOption) + maybeBytes: Option[Array[Byte]] <- getBytesValue(maybeIriKey) + maybeUser: Option[UserADM] <- maybeBytes match { + case Some(bytes) => CacheSerialization.deserialize[UserADM](bytes) + case None => FastFuture.successful(None) + } + } yield maybeUser + + case UserIdentifierType.EMAIL => + for { + maybeIriKey: Option[String] <- getStringValue(identifier.toEmailOption) + maybeBytes: Option[Array[Byte]] <- getBytesValue(maybeIriKey) + maybeUser: Option[UserADM] <- maybeBytes match { + case Some(bytes) => CacheSerialization.deserialize[UserADM](bytes) + case None => FastFuture.successful(None) + } + } yield maybeUser + } + + val recoverableResultFuture = resultFuture.recover { + case e: Exception => + logger.warn("Aborting reading 'UserADM' from Redis - {}", e.getMessage) + None + } + + recoverableResultFuture + } + + /** + * Stores the project under the IRI and additionally the IRI under the keys + * of SHORTCODE and SHORTNAME: + * + * IRI -> byte array + * shortname -> IRI + * shortcode -> IRI + * + * @param value the stored value + */ + def putProjectADM(value: ProjectADM)(implicit ec: ExecutionContext): Future[Boolean] = { + val resultFuture = for { + bytes: Array[Byte] <- CacheSerialization.serialize(value) + result: Boolean <- writeBytesValue(value.id, bytes) + _ = writeStringValue(value.shortcode, value.id) + _ = writeStringValue(value.shortname, value.id) + } yield result + + val recoverableResultFuture = resultFuture.recover { + case e: Exception => + logger.warn("Aborting writing 'ProjectADM' to Redis - {}", e.getMessage) + false + } + + recoverableResultFuture + } + + /** + * Retrieves the project stored under the identifier (either iri, shortname, or shortcode). + * + * @param identifier the project identifier. + */ + def getProjectADM(identifier: ProjectIdentifierADM)(implicit ec: ExecutionContext): Future[Option[ProjectADM]] = { + + // The data is stored under the IRI key. + // Additionally, the SHORTNAME and SHORTCODE keys point to the IRI key + val resultFuture: Future[Option[ProjectADM]] = identifier.hasType match { + case ProjectIdentifierType.IRI => + for { + maybeBytes <- getBytesValue(identifier.toIriOption) + maybeProject <- maybeBytes match { + case Some(bytes) => CacheSerialization.deserialize[ProjectADM](bytes) + case None => FastFuture.successful(None) + } + } yield maybeProject + case ProjectIdentifierType.SHORTCODE => + for { + maybeIriKey <- getStringValue(identifier.toShortcodeOption) + maybeBytes <- getBytesValue(maybeIriKey) + maybeProject: Option[ProjectADM] <- maybeBytes match { + case Some(bytes) => CacheSerialization.deserialize[ProjectADM](bytes) + case None => FastFuture.successful(None) + } + } yield maybeProject + case ProjectIdentifierType.SHORTNAME => + for { + maybeIriKey <- getStringValue(identifier.toShortnameOption) + maybeBytes <- getBytesValue(maybeIriKey) + maybeProject: Option[ProjectADM] <- maybeBytes match { + case Some(bytes) => CacheSerialization.deserialize[ProjectADM](bytes) + case None => FastFuture.successful(None) + } + } yield maybeProject + } + + val recoverableResultFuture = resultFuture.recover { + case e: Exception => + logger.warn("Aborting reading 'ProjectADM' from Redis - {}", e.getMessage) + None + } + + recoverableResultFuture + } + + /** + * Store string or byte array value under key. + * + * @param key the key. + * @param value the value. + */ + def writeStringValue(key: String, value: String)(implicit ec: ExecutionContext): Future[Boolean] = { + + if (key.isEmpty) + throw EmptyKey("The key under which the value should be written is empty. Aborting writing to redis.") + + if (value.isEmpty) + throw EmptyValue("The string value is empty. Aborting writing to redis.") + + val operationFuture: Future[Boolean] = Future { + + val conn: Jedis = pool.getResource + try { + conn.set(key, value) + true + } finally { + conn.close() + } + + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + // Log any errors. + logger.warn("Writing to Redis failed - {}", e.getMessage) + false + } + + recoverableOperationFuture + } + + /** + * Get value stored under the key as a string. + * + * @param maybeKey the key. + */ + def getStringValue(maybeKey: Option[String])(implicit ec: ExecutionContext): Future[Option[String]] = { + + val operationFuture: Future[Option[String]] = maybeKey match { + case Some(key) => + Future { + val conn: Jedis = pool.getResource + try { + Option(conn.get(key)) + } finally { + conn.close() + } + } + case None => + FastFuture.successful(None) + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + // Log any errors. + logger.warn("Reading string from Redis failed, {}", e) + None + } + + recoverableOperationFuture + } + + /** + * Removes values for the provided keys. Any invalid keys are ignored. + * + * @param keys the keys. + */ + def removeValues(keys: Set[String])(implicit ec: ExecutionContext): Future[Boolean] = { + + logger.debug("removeValues - {}", keys) + + val operationFuture: Future[Boolean] = Future { + // del takes a vararg so I nee to convert the set to a swq and then to vararg + val conn: Jedis = pool.getResource + try { + conn.del(keys.toSeq: _*) + true + } finally { + conn.close() + } + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + // Log any errors. + logger.warn("Removing keys from Redis failed.", e.getMessage) + false + } + + recoverableOperationFuture + } + + /** + * Flushes (removes) all stored content from the Redis store. + */ + def flushDB(requestingUser: UserADM)(implicit ec: ExecutionContext): Future[CacheServiceFlushDBACK] = { + + if (!requestingUser.isSystemUser) { + throw ForbiddenException("Only the system user is allowed to perform this operation.") + } + + val operationFuture: Future[CacheServiceFlushDBACK] = Future { + + val conn: Jedis = pool.getResource + try { + conn.flushDB() + CacheServiceFlushDBACK() + } finally { + conn.close() + } + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + // Log any errors. + logger.warn("Flushing DB failed", e.getMessage) + throw e + } + + recoverableOperationFuture + } + + /** + * Pings the Redis store to see if it is available. + */ + def ping()(implicit ec: ExecutionContext): Future[CacheServiceStatusResponse] = { + val operationFuture: Future[CacheServiceStatusResponse] = Future { + + val conn: Jedis = pool.getResource + try { + conn.ping("test") + CacheServiceStatusOK + } finally { + conn.close() + } + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + CacheServiceStatusNOK + } + + recoverableOperationFuture + } + + /** + * Store string or byte array value under key. + * + * @param key the key. + * @param value the value. + */ + private def writeBytesValue(key: String, value: Array[Byte])(implicit ec: ExecutionContext): Future[Boolean] = { + + if (key.isEmpty) + throw EmptyKey("The key under which the value should be written is empty. Aborting writing to redis.") + + if (value.isEmpty) + throw EmptyValue("The byte array value is empty. Aborting writing to redis.") + + val operationFuture: Future[Boolean] = Future { + val conn: Jedis = pool.getResource + try { + conn.set(key.getBytes, value) + true + } finally { + conn.close() + } + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + // Log any errors. + logger.warn("Writing to Redis failed - {}", e.getMessage) + false + } + + recoverableOperationFuture + } + + /** + * Get value stored under the key as a byte array. If no value is found + * under the key, then a [[None]] is returned.. + * + * @param maybeKey the key. + */ + private def getBytesValue(maybeKey: Option[String])(implicit ec: ExecutionContext): Future[Option[Array[Byte]]] = { + + val operationFuture: Future[Option[Array[Byte]]] = maybeKey match { + case Some(key) => + Future { + val conn = pool.getResource + try { + Option(conn.get(key.getBytes)) + } finally { + conn.close() + } + } + case None => + FastFuture.successful(None) + } + + val recoverableOperationFuture = operationFuture.recover { + case e: Exception => + // Log any errors. + logger.warn("Reading byte array from Redis failed - {}", e.getMessage) + None + } + + recoverableOperationFuture + } + +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel new file mode 100644 index 0000000000..8cef4a7af3 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel @@ -0,0 +1,50 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") + +scala_library( + name = "serialization", + srcs = [ + "CacheSerialization.scala", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/core", + "//webapi/src/main/scala/org/knora/webapi/exceptions", + "//webapi/src/main/scala/org/knora/webapi/feature", + "//webapi/src/main/scala/org/knora/webapi/instrumentation", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/routing", + "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/util", + "@maven//:com_twitter_chill_2_13", + "@maven//:com_typesafe_akka_akka_actor_2_13", + "@maven//:com_typesafe_akka_akka_http_2_13", + "@maven//:com_typesafe_akka_akka_http_core_2_13", + "@maven//:com_typesafe_akka_akka_http_spray_json_2_13", + "@maven//:com_typesafe_akka_akka_stream_2_13", + "@maven//:com_typesafe_config", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:commons_cli_commons_cli", + "@maven//:commons_codec_commons_codec", + "@maven//:commons_io_commons_io", + "@maven//:io_spray_spray_json_2_13", + "@maven//:org_apache_commons_commons_csv", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_apache_commons_commons_pool2", + "@maven//:org_apache_httpcomponents_httpclient", + "@maven//:org_apache_httpcomponents_httpclient_cache", + "@maven//:org_apache_httpcomponents_httpcore", + "@maven//:org_apache_jena_apache_jena_libs", + "@maven//:org_apache_jena_jena_arq", + "@maven//:org_apache_jena_jena_base", + "@maven//:org_apache_jena_jena_core", + "@maven//:org_apache_jena_jena_tdb", + "@maven//:org_apache_jena_jena_text", + "@maven//:org_apache_lucene_lucene_core", + "@maven//:org_slf4j_slf4j_api", + "@maven//:redis_clients_jedis", + ], +) diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheSerialization.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/CacheSerialization.scala similarity index 71% rename from webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheSerialization.scala rename to webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/CacheSerialization.scala index b2a23dff1a..eec7f3238c 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/CacheSerialization.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization/CacheSerialization.scala @@ -1,33 +1,33 @@ /* * Copyright © 2015-2021 the contributors (see Contributors.md). * - * This file is part of Knora. + * This file is part of the DaSCH Service Platform. * - * Knora is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. * - * Knora is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public - * License along with Knora. If not, see . + * License along with the DaSCH Service Platform. If not, see + * . */ -package org.knora.webapi.store.cacheservice - -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream} +package org.knora.webapi.store.cacheservice.serialization import com.twitter.chill.MeatLocker -import org.knora.webapi.exceptions.RedisException +import org.knora.webapi.exceptions.CacheServiceException import org.knora.webapi.instrumentation.InstrumentationSupport +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream} import scala.concurrent.{ExecutionContext, Future} -case class EmptyByteArray(message: String) extends RedisException(message) +case class EmptyByteArray(message: String) extends CacheServiceException(message) object CacheSerialization extends InstrumentationSupport { diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/BUILD.bazel new file mode 100644 index 0000000000..23044ebd20 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/BUILD.bazel @@ -0,0 +1,15 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") + +scala_library( + name = "settings", + srcs = [ + "CacheServiceSettings.scala", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "@maven//:com_typesafe_config", + ], +) diff --git a/webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/CacheServiceSettings.scala b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/CacheServiceSettings.scala new file mode 100644 index 0000000000..be1235b5a9 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings/CacheServiceSettings.scala @@ -0,0 +1,32 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice.settings + +import com.typesafe.config.Config + +/** + * Holds the Cache Service specific settings. + */ +class CacheServiceSettings(config: Config) { + val cacheServiceEnabled: Boolean = config.getBoolean("app.cache-service.enabled") + val cacheServiceRedisHost: String = config.getString("app.cache-service.redis.host") + val cacheServiceRedisPort: Int = config.getInt("app.cache-service.redis.port") +} diff --git a/webapi/src/test/scala/org/knora/webapi/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/BUILD.bazel index 70144af980..ab9bc242d7 100644 --- a/webapi/src/test/scala/org/knora/webapi/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/BUILD.bazel @@ -13,8 +13,10 @@ filegroup( "KnoraFakeCore.scala", "ManagersWithMockedSipi.scala", "R2RSpec.scala", + "RedisTestContainer.scala", "TestContainers.scala", "TestProbeMaker.scala", + "UnitSpec.scala", "//webapi/src/test/scala/org/knora/webapi/e2e:srcs", "//webapi/src/test/scala/org/knora/webapi/responders:srcs", "//webapi/src/test/scala/org/knora/webapi/sharedtestdata:srcs", diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index 5d6a60bba0..03fc01918d 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -30,6 +30,7 @@ import messages.util.rdf.RdfFeatureFactory import messages.util.{KnoraSystemInstances, ResponderData} import messages.v2.responder.ontologymessages.LoadOntologiesRequestV2 import settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl, _} +import store.cacheservice.settings.CacheServiceSettings import util.StartupUtils import akka.actor.{ActorRef, ActorSystem, Props} @@ -63,8 +64,8 @@ object CoreSpec { val s = (Thread.currentThread.getStackTrace map (_.getClassName) drop 1) .dropWhile(_ matches "(java.lang.Thread|.*CoreSpec.?$)") val reduced = s.lastIndexWhere(_ == clazz.getName) match { - case -1 ⇒ s - case z ⇒ s drop (z + 1) + case -1 => s + case z => s drop (z + 1) } reduced.head.replaceFirst(""".*\.""", "").replaceAll("[^a-zA-Z_0-9]", "_") } @@ -122,7 +123,9 @@ abstract class CoreSpec(_system: ActorSystem) val responderData: ResponderData = ResponderData( system = system, - appActor = appActor + appActor = appActor, + knoraSettings = settings, + cacheServiceSettings = new CacheServiceSettings(system.settings.config) ) protected val defaultFeatureFactoryConfig: FeatureFactoryConfig = new TestFeatureFactoryConfig( @@ -130,7 +133,7 @@ abstract class CoreSpec(_system: ActorSystem) parent = new KnoraSettingsFeatureFactoryConfig(settings) ) - final override def beforeAll() { + final override def beforeAll(): () = { // set allow reload over http appActor ! SetAllowReloadOverHTTPState(true) @@ -145,7 +148,7 @@ abstract class CoreSpec(_system: ActorSystem) // memusage() } - final override def afterAll() { + final override def afterAll(): () = { appActor ! AppStop() // memusage() } diff --git a/webapi/src/test/scala/org/knora/webapi/KnoraFakeCore.scala b/webapi/src/test/scala/org/knora/webapi/KnoraFakeCore.scala index 8b2ec42ff7..7aa0c72916 100644 --- a/webapi/src/test/scala/org/knora/webapi/KnoraFakeCore.scala +++ b/webapi/src/test/scala/org/knora/webapi/KnoraFakeCore.scala @@ -66,7 +66,7 @@ trait KnoraFakeCore { * Starts the Faked Knora API server. */ def startService(): Unit = { - Http().bindAndHandle(Route.handlerFlow(apiRoutes), settings.internalKnoraApiHost, settings.internalKnoraApiPort) + Http().newServerAt(settings.internalKnoraApiHost, settings.internalKnoraApiPort).bindFlow(Route.toFlow(apiRoutes)) println( s"Faked Knora API Server started at http://${settings.internalKnoraApiHost}:${settings.internalKnoraApiPort}.") } diff --git a/webapi/src/test/scala/org/knora/webapi/ManagersWithMockedSipi.scala b/webapi/src/test/scala/org/knora/webapi/ManagersWithMockedSipi.scala index f0a3471e9a..664afd4f0a 100644 --- a/webapi/src/test/scala/org/knora/webapi/ManagersWithMockedSipi.scala +++ b/webapi/src/test/scala/org/knora/webapi/ManagersWithMockedSipi.scala @@ -22,9 +22,12 @@ package org.knora.webapi import akka.actor.{Actor, ActorRef, Props} import org.knora.webapi.app.Managers import org.knora.webapi.core.LiveActorMaker +import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.responders.MockableResponderManager import org.knora.webapi.settings._ import org.knora.webapi.store.MockableStoreManager +import org.knora.webapi.store.cacheservice.inmem.CacheServiceInMemImpl +import org.knora.webapi.store.cacheservice.settings.CacheServiceSettings import org.knora.webapi.store.iiif.MockSipiConnector /** @@ -38,9 +41,23 @@ trait ManagersWithMockedSipi extends Managers { lazy val mockResponders: Map[String, ActorRef] = Map.empty[String, ActorRef] lazy val storeManager: ActorRef = context.actorOf( - Props(new MockableStoreManager(mockStoreConnectors = mockStoreConnectors, appActor = self) with LiveActorMaker), - name = StoreManagerActorName) + Props( + new MockableStoreManager(mockStoreConnectors = mockStoreConnectors, appActor = self, cs = CacheServiceInMemImpl) + with LiveActorMaker + ), + name = StoreManagerActorName + ) + lazy val responderManager: ActorRef = context.actorOf( - Props(new MockableResponderManager(mockRespondersOrStoreConnectors = mockResponders, appActor = self)), - name = RESPONDER_MANAGER_ACTOR_NAME) + Props( + new MockableResponderManager( + mockRespondersOrStoreConnectors = mockResponders, + appActor = self, + responderData = ResponderData(system, + self, + knoraSettings = KnoraSettings(system), + cacheServiceSettings = new CacheServiceSettings(system.settings.config)) + )), + name = RESPONDER_MANAGER_ACTOR_NAME + ) } diff --git a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala index 00b7cf58e9..e9fbcd7edd 100644 --- a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala @@ -108,7 +108,7 @@ class R2RSpec val log: LoggingAdapter = akka.event.Logging(system, this.getClass) - override def beforeAll() { + override def beforeAll(): () = { // set allow reload over http appActor ! SetAllowReloadOverHTTPState(true) @@ -121,7 +121,7 @@ class R2RSpec loadTestData(rdfDataObjects) } - override def afterAll() { + override def afterAll(): () = { /* Stop the server when everything else has finished */ appActor ! AppStop() } diff --git a/webapi/src/test/scala/org/knora/webapi/RedisTestContainer.scala b/webapi/src/test/scala/org/knora/webapi/RedisTestContainer.scala new file mode 100644 index 0000000000..652203c7d7 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/RedisTestContainer.scala @@ -0,0 +1,45 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi + +import com.typesafe.config.{Config, ConfigFactory} +import org.testcontainers.containers.GenericContainer +import org.testcontainers.utility.DockerImageName + +import scala.jdk.CollectionConverters._ + +/** + * Provides all containers necessary for running tests. + */ +object RedisTestContainer { + + val RedisImageName: DockerImageName = DockerImageName.parse("redis:5") + val RedisContainer = new GenericContainer(RedisImageName) + RedisContainer.withExposedPorts(6379) + RedisContainer.start() + + private val portMap = Map( + "app.cache-service.redis.port" -> RedisContainer.getFirstMappedPort + ).asJava + + // all tests need to be configured with these ports. + val PortConfig: Config = + ConfigFactory.parseMap(portMap, "Ports from RedisTestContainer").withFallback(ConfigFactory.load()) +} diff --git a/webapi/src/test/scala/org/knora/webapi/TestContainers.scala b/webapi/src/test/scala/org/knora/webapi/TestContainers.scala index f42fa41e83..8b1727b56f 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestContainers.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestContainers.scala @@ -64,18 +64,22 @@ object TestContainers { val sipiIp: String = SipiContainer.getHost val sipiPort: Int = SipiContainer.getFirstMappedPort - val RedisImageName: DockerImageName = DockerImageName.parse("redis:5") - val RedisContainer = new GenericContainer(RedisImageName) - RedisContainer.withExposedPorts(6379) - RedisContainer.start() + + // The new default is the inmem cache implementation, so no need + // for a container + // + // val RedisImageName: DockerImageName = DockerImageName.parse("redis:5") + // val RedisContainer = new GenericContainer(RedisImageName) + // RedisContainer.withExposedPorts(6379) + // RedisContainer.start() private val portMap = Map( "app.triplestore.fuseki.port" -> FusekiContainer.getFirstMappedPort, "app.sipi.internal-host" -> sipiIp, - "app.sipi.internal-port" -> sipiPort, - "app.cache-service.redis.port" -> RedisContainer.getFirstMappedPort + "app.sipi.internal-port" -> sipiPort + // "app.cache-service.redis.port" -> RedisContainer.getFirstMappedPort ).asJava // all tests need to be configured with these ports. - val PortConfig: Config = ConfigFactory.parseMap(portMap, "Ports from ContainerizedSpec") + val PortConfig: Config = ConfigFactory.parseMap(portMap, "Ports from TestContainers") } diff --git a/webapi/src/test/scala/org/knora/webapi/UnitSpec.scala b/webapi/src/test/scala/org/knora/webapi/UnitSpec.scala new file mode 100644 index 0000000000..c8c66ba929 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/UnitSpec.scala @@ -0,0 +1,61 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi + +import messages.StringFormatter + +import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.scalalogging.LazyLogging +import org.scalatest.BeforeAndAfterAll +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike + +import scala.language.postfixOps + +object UnitSpec { + + /* + Loads the following (first-listed are higher priority): + - system properties (e.g., -Dconfig.resource=fuseki.conf) + - test/resources/application.conf + - main/resources/application.conf + */ + val defaultConfig: Config = ConfigFactory.load() + + /* Copied from: akka/akka-testkit/src/test/scala/akka/testkit/AkkaSpec.scala */ + def getCallerName(clazz: Class[_]): String = { + val s = (Thread.currentThread.getStackTrace map (_.getClassName) drop 1) + .dropWhile(_ matches "(java.lang.Thread|.*UnitSpec.?$)") + val reduced = s.lastIndexWhere(_ == clazz.getName) match { + case -1 => s + case z => s drop (z + 1) + } + reduced.head.replaceFirst(""".*\.""", "").replaceAll("[^a-zA-Z_0-9]", "_") + } +} + +abstract class UnitSpec(_config: Config) extends AnyWordSpecLike with Matchers with BeforeAndAfterAll with LazyLogging { + + def this() = + this(UnitSpec.defaultConfig) + + // needs to be initialized early on + StringFormatter.initForTest() +} diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/e2e/v1/BUILD.bazel index 8d5285d056..20b4bbf883 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/BUILD.bazel @@ -16,6 +16,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -34,6 +35,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -53,6 +55,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -71,6 +74,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -90,6 +94,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -129,6 +134,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -147,6 +153,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -165,6 +172,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -185,6 +193,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", @@ -203,6 +212,7 @@ scala_test( ], jvm_flags = ["-Dconfig.resource=fuseki.conf"], # unused_dependency_checker_mode = "warn", + scalacopts = ["-deprecation"], deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ "//webapi:main_library", "//webapi:test_library", diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala index 70a8156187..3fa6caf073 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala @@ -1715,7 +1715,7 @@ class ResourcesV1R2RSpec extends R2RSpec { "create 10,000 anything:Thing resources with random contents" in { def maybeAppendValue(random: Random, xmlStringBuilder: StringBuilder, value: String): Unit = { - if (random.nextBoolean) { + if (random.nextBoolean()) { xmlStringBuilder.append(value) } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/MockableResponderManager.scala b/webapi/src/test/scala/org/knora/webapi/responders/MockableResponderManager.scala index 5f5c51f2ce..1e11a86bf0 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/MockableResponderManager.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/MockableResponderManager.scala @@ -21,6 +21,7 @@ package org.knora.webapi.responders import akka.actor._ import org.knora.webapi.core.{ActorMaker, LiveActorMaker} +import org.knora.webapi.messages.util.ResponderData /** * A subclass of [[ResponderManager]] that allows tests to substitute custom responders for the standard ones. @@ -30,8 +31,10 @@ import org.knora.webapi.core.{ActorMaker, LiveActorMaker} * used as the key in the map. * @param appActor the main application actor. */ -class MockableResponderManager(mockRespondersOrStoreConnectors: Map[String, ActorRef], appActor: ActorRef) - extends ResponderManager(appActor) +class MockableResponderManager(mockRespondersOrStoreConnectors: Map[String, ActorRef], + appActor: ActorRef, + responderData: ResponderData) + extends ResponderManager(appActor, responderData) with LiveActorMaker { this: ActorMaker => diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala index c98b1884b9..e4dceffe2c 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala @@ -675,11 +675,11 @@ class ResourcesResponderV1Spec extends CoreSpec(ResourcesResponderV1Spec.config) case (expectedProp: PropertyV1, receivedProp: PropertyV1) => // sort property attributes val expectedPropWithSortedAttr = expectedProp.copy( - attributes = expectedProp.attributes.sorted + attributes = expectedProp.attributes.toSeq.sorted.unwrap ) val receivedPropWithSortedAttr = receivedProp.copy( - attributes = receivedProp.attributes.sorted + attributes = receivedProp.attributes.toSeq.sorted.unwrap ) assert( diff --git a/webapi/src/test/scala/org/knora/webapi/store/MockableStoreManager.scala b/webapi/src/test/scala/org/knora/webapi/store/MockableStoreManager.scala index 3ce43e5440..7a64febed2 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/MockableStoreManager.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/MockableStoreManager.scala @@ -22,10 +22,11 @@ package org.knora.webapi.store import akka.actor.{ActorRef, Props} import org.knora.webapi.core.LiveActorMaker import org.knora.webapi.settings.{KnoraDispatchers, _} +import org.knora.webapi.store.cacheservice.CacheService import org.knora.webapi.store.iiif.MockableIIIFManager -class MockableStoreManager(mockStoreConnectors: Map[String, ActorRef], appActor: ActorRef) - extends StoreManager(appActor) +class MockableStoreManager(mockStoreConnectors: Map[String, ActorRef], appActor: ActorRef, cs: CacheService) + extends StoreManager(appActor, cs) with LiveActorMaker { /** diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/BUILD.bazel index ff0d569489..edcb77f463 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/BUILD.bazel @@ -3,24 +3,6 @@ package(default_visibility = ["//visibility:public"]) load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") load("//third_party:dependencies.bzl", "ALL_WEBAPI_MAIN_DEPENDENCIES", "BASE_TEST_DEPENDENCIES", "BASE_TEST_DEPENDENCIES_WITH_JSON", "BASE_TEST_DEPENDENCIES_WITH_JSON_LD") -scala_test( - name = "CacheSerializationSpec", - size = "small", # 60s - srcs = [ - "CacheSerializationSpec.scala", - ], - data = [ - "//knora-ontologies", - "//test_data", - ], - jvm_flags = ["-Dconfig.resource=fuseki.conf"], - # unused_dependency_checker_mode = "warn", - deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ - "//webapi:main_library", - "//webapi:test_library", - ] + BASE_TEST_DEPENDENCIES, -) - scala_test( name = "CacheServiceManagerSpec", size = "small", # 60s diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheServiceManagerSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheServiceManagerSpec.scala index 57108883e4..c171010723 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheServiceManagerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheServiceManagerSpec.scala @@ -1,20 +1,21 @@ /* * Copyright © 2015-2021 the contributors (see Contributors.md). * - * This file is part of Knora. + * This file is part of the DaSCH Service Platform. * - * Knora is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. * - * Knora is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public - * License along with Knora. If not, see . + * License along with the DaSCH Service Platform. If not, see + * . */ package org.knora.webapi.store.cacheservice @@ -40,7 +41,7 @@ object CacheServiceManagerSpec { } /** - * This spec is used to test [[org.knora.webapi.store.cacheservice.CacheSerialization]]. + * This spec is used to test [[org.knora.webapi.store.cacheservice.serialization.CacheSerialization]]. */ class CacheServiceManagerSpec extends CoreSpec(CacheServiceManagerSpec.config) { diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel new file mode 100644 index 0000000000..23767f367b --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/BUILD.bazel @@ -0,0 +1,34 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") +load("//third_party:dependencies.bzl", "ALL_WEBAPI_MAIN_DEPENDENCIES", "BASE_TEST_DEPENDENCIES", "BASE_TEST_DEPENDENCIES_WITH_JSON", "BASE_TEST_DEPENDENCIES_WITH_JSON_LD") + +scala_test( + name = "CacheServiceInMemImplSpec", + size = "small", # 60s + srcs = [ + "CacheServiceInMemImplSpec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi:test_library", + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/inmem", + "@maven//:com_typesafe_config", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:org_scalactic_scalactic_2_13", + "@maven//:org_scalatest_scalatest_compatible", + "@maven//:org_scalatest_scalatest_core_2_13", + "@maven//:org_scalatest_scalatest_matchers_core_2_13", + "@maven//:org_scalatest_scalatest_shouldmatchers_2_13", + "@maven//:org_scalatest_scalatest_wordspec_2_13", + ], +) diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImplSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImplSpec.scala new file mode 100644 index 0000000000..c0dba213d0 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/inmem/CacheServiceInMemImplSpec.scala @@ -0,0 +1,85 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice.inmem + +import org.knora.webapi.UnitSpec +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectADM, ProjectIdentifierADM} +import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM} +import org.knora.webapi.sharedtestdata.SharedTestDataADM + +/** + * This spec is used to test [[org.knora.webapi.store.cacheservice.inmem.CacheServiceInMemImpl]]. + */ +class CacheServiceInMemImplSpec extends UnitSpec() { + + implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global + + + private val user: UserADM = SharedTestDataADM.imagesUser01 + private val project: ProjectADM = SharedTestDataADM.imagesProject + + private val inMemCache: CacheServiceInMemImpl.type = CacheServiceInMemImpl + + "The CacheServiceInMemImpl" should { + + "successfully store a user" in { + val resFuture = inMemCache.putUserADM(user) + resFuture map {res => res should equal(true)} + } + + "successfully retrieve a user by IRI" in { + val resFuture = inMemCache.getUserADM(UserIdentifierADM(maybeIri = Some(user.id))) + resFuture map {res => res should equal(Some(user))} + } + + "successfully retrieve a user by USERNAME" in { + val resFuture = inMemCache.getUserADM(UserIdentifierADM(maybeUsername = Some(user.username))) + resFuture map {res => res should equal(Some(user))} + } + + "successfully retrieve a user by EMAIL" in { + val resFuture = inMemCache.getUserADM(UserIdentifierADM(maybeEmail = Some(user.email))) + resFuture map {res => res should equal(Some(user))} + } + + "successfully store a project" in { + val resFuture = inMemCache.putProjectADM(project) + resFuture map { res => res should equal(true)} + } + + "successfully retrieve a project by IRI" in { + val resFuture = inMemCache.getProjectADM(ProjectIdentifierADM(maybeIri = Some(project.id))) + resFuture map { res => res should equal(Some(project))} + } + + "successfully retrieve a project by SHORTNAME" in { + val resFuture = inMemCache.getProjectADM(ProjectIdentifierADM(maybeShortname = Some(project.shortname))) + resFuture map { res => res should equal(Some(project))} + } + + "successfully retrieve a project by SHORTCODE" in { + val resFuture = inMemCache.getProjectADM(ProjectIdentifierADM(maybeShortcode = Some(project.shortcode))) + resFuture map { res => res should equal(Some(project))} + } + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel new file mode 100644 index 0000000000..71f71fc93b --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/BUILD.bazel @@ -0,0 +1,40 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") +load("//third_party:dependencies.bzl", "ALL_WEBAPI_MAIN_DEPENDENCIES", "BASE_TEST_DEPENDENCIES", "BASE_TEST_DEPENDENCIES_WITH_JSON", "BASE_TEST_DEPENDENCIES_WITH_JSON_LD") + +scala_test( + name = "CacheServiceRedisImplSpec", + size = "small", # 60s + srcs = [ + "CacheServiceRedisImplSpec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + resources = [ + "//webapi/src/main/resources", + "//webapi/src/test/resources", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi:test_library", + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/redis", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/settings", + "@maven//:com_typesafe_config", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:org_scalactic_scalactic_2_13", + "@maven//:org_scalatest_scalatest_2_13", + "@maven//:org_scalatest_scalatest_compatible", + "@maven//:org_scalatest_scalatest_core_2_13", + "@maven//:org_scalatest_scalatest_matchers_core_2_13", + "@maven//:org_scalatest_scalatest_shouldmatchers_2_13", + "@maven//:org_scalatest_scalatest_wordspec_2_13", + ], +) diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImplSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImplSpec.scala new file mode 100644 index 0000000000..276bf8f6d2 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/redis/CacheServiceRedisImplSpec.scala @@ -0,0 +1,90 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.store.cacheservice.redis + +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectADM, ProjectIdentifierADM} +import org.knora.webapi.messages.admin.responder.usersmessages.{UserADM, UserIdentifierADM} +import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.store.cacheservice.settings.CacheServiceSettings +import org.knora.webapi.{RedisTestContainer, UnitSpec} + +import scala.concurrent.ExecutionContext + +/** + * This spec is used to test [[org.knora.webapi.store.cacheservice.redis.CacheServiceRedisImplSpec]]. + * Adding the [[RedisTestContainer.PortConfig]] config will start the Redis container and make it + * available to the test. + */ +class CacheServiceRedisImplSpec extends UnitSpec(RedisTestContainer.PortConfig) { + + implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + implicit val ec: ExecutionContext = ExecutionContext.global + + private val user: UserADM = SharedTestDataADM.imagesUser01 + private val project: ProjectADM = SharedTestDataADM.imagesProject + + private val redisCache: CacheServiceRedisImpl = new CacheServiceRedisImpl( + new CacheServiceSettings(RedisTestContainer.PortConfig)) + + "The CacheServiceRedisImpl" should { + + "successfully store a user" in { + val resFuture = redisCache.putUserADM(user) + resFuture map {res => res should equal(true)} + } + + "successfully retrieve a user by IRI" in { + val resFuture = redisCache.getUserADM(UserIdentifierADM(maybeIri = Some(user.id))) + resFuture map {res => res should equal(Some(user))} + } + + "successfully retrieve a user by USERNAME" in { + val resFuture = redisCache.getUserADM(UserIdentifierADM(maybeUsername = Some(user.username))) + resFuture map {res => res should equal(Some(user))} + } + + "successfully retrieve a user by EMAIL" in { + val resFuture = redisCache.getUserADM(UserIdentifierADM(maybeEmail = Some(user.email))) + resFuture map {res => res should equal(Some(user))} + } + + "successfully store a project" in { + val resFuture = redisCache.putProjectADM(project) + resFuture map { res => res should equal(true)} + } + + "successfully retrieve a project by IRI" in { + val resFuture = redisCache.getProjectADM(ProjectIdentifierADM(maybeIri = Some(project.id))) + resFuture map { res => res should equal(Some(project))} + } + + "successfully retrieve a project by SHORTNAME" in { + val resFuture = redisCache.getProjectADM(ProjectIdentifierADM(maybeShortname = Some(project.shortname))) + resFuture map { res => res should equal(Some(project))} + } + + "successfully retrieve a project by SHORTCODE" in { + val resFuture = redisCache.getProjectADM(ProjectIdentifierADM(maybeShortcode = Some(project.shortcode))) + resFuture map { res => res should equal(Some(project))} + } + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel new file mode 100644 index 0000000000..2a87201970 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/BUILD.bazel @@ -0,0 +1,32 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") + +scala_test( + name = "CacheSerializationSpec", + size = "small", # 60s + srcs = [ + "CacheSerializationSpec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi:test_library", + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/instrumentation", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/store/cacheservice/serialization", + "@maven//:com_typesafe_config", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:org_scalactic_scalactic_2_13", + "@maven//:org_scalatest_scalatest_compatible", + "@maven//:org_scalatest_scalatest_core_2_13", + "@maven//:org_scalatest_scalatest_matchers_core_2_13", + "@maven//:org_scalatest_scalatest_shouldmatchers_2_13", + "@maven//:org_scalatest_scalatest_wordspec_2_13", + ], +) diff --git a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheSerializationSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/CacheSerializationSpec.scala similarity index 58% rename from webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheSerializationSpec.scala rename to webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/CacheSerializationSpec.scala index f83086aac5..769c22511b 100644 --- a/webapi/src/test/scala/org/knora/webapi/store/cacheservice/CacheSerializationSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/store/cacheservice/serialization/CacheSerializationSpec.scala @@ -1,26 +1,27 @@ /* * Copyright © 2015-2021 the contributors (see Contributors.md). * - * This file is part of Knora. + * This file is part of the DaSCH Service Platform. * - * Knora is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. * - * Knora is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public - * License along with Knora. If not, see . + * License along with the DaSCH Service Platform. If not, see + * . */ -package org.knora.webapi.store.cacheservice +package org.knora.webapi.store.cacheservice.serialization import com.typesafe.config.ConfigFactory -import org.knora.webapi._ +import org.knora.webapi.UnitSpec import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.sharedtestdata.SharedTestDataADM @@ -33,9 +34,11 @@ object CacheSerializationSpec { } /** - * This spec is used to test [[org.knora.webapi.store.cacheservice.CacheSerialization]]. + * This spec is used to test [[CacheSerialization]]. */ -class CacheSerializationSpec extends CoreSpec(CacheSerializationSpec.config) { +class CacheSerializationSpec extends UnitSpec(CacheSerializationSpec.config) { + + implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global "serialize and deserialize" should {