diff --git a/build.sbt b/build.sbt index 6ff8620..51f1fe0 100644 --- a/build.sbt +++ b/build.sbt @@ -82,6 +82,7 @@ lazy val oni = project ), libraryDependencies ++= Seq( auth0, + caffeine, circeCore, circeGeneric, circeParser, diff --git a/oni/src/main/scala/org/mbari/oni/endpoints/ConceptEndpoints.scala b/oni/src/main/scala/org/mbari/oni/endpoints/ConceptEndpoints.scala index e8d668c..0f8f2b3 100644 --- a/oni/src/main/scala/org/mbari/oni/endpoints/ConceptEndpoints.scala +++ b/oni/src/main/scala/org/mbari/oni/endpoints/ConceptEndpoints.scala @@ -15,7 +15,7 @@ import sttp.tapir.server.ServerEndpoint import org.mbari.oni.domain.{ConceptCreate, ConceptDelete, ConceptMetadata, ConceptUpdate, ErrorMsg, ServerError} import org.mbari.oni.etc.circe.CirceCodecs.given import org.mbari.oni.etc.jwt.JwtService -import org.mbari.oni.services.{ConceptNameService, ConceptService} +import org.mbari.oni.services.{ConceptCache, ConceptNameService, ConceptService} import sttp.shared.Identity import scala.concurrent.{ExecutionContext, Future} @@ -24,6 +24,7 @@ class ConceptEndpoints(entityManagerFactory: EntityManagerFactory)(using jwtServ private val service = ConceptService(entityManagerFactory) private val conceptNameService = ConceptNameService(entityManagerFactory) + private val conceptCache = ConceptCache(service, conceptNameService) private val base = "concept" private val tag = "Concept" @@ -38,7 +39,7 @@ class ConceptEndpoints(entityManagerFactory: EntityManagerFactory)(using jwtServ val allEndpointImpl: ServerEndpoint[Any, Future] = allEndpoint.serverLogic { _ => val limit = 10000 val offset = 0 - handleErrorsAsync(conceptNameService.findAllNames(limit, offset)) + handleErrorsAsync(conceptCache.findAllNames(limit, offset)) } val createEndpoint: Endpoint[Option[String], ConceptCreate, ErrorMsg, ConceptMetadata, Any] = secureEndpoint @@ -109,7 +110,7 @@ class ConceptEndpoints(entityManagerFactory: EntityManagerFactory)(using jwtServ .tag(tag) val findByNameImpl: ServerEndpoint[Any, Future] = findByName.serverLogic { name => - handleErrorsAsync(service.findByName(name)) + handleErrorsAsync(conceptCache.findByName(name)) } val findByNameContaining: Endpoint[Unit, String, ErrorMsg, Seq[ConceptMetadata], Any] = openEndpoint diff --git a/oni/src/main/scala/org/mbari/oni/etc/jdk/CloseableLock.scala b/oni/src/main/scala/org/mbari/oni/etc/jdk/CloseableLock.scala new file mode 100644 index 0000000..287d8f0 --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/etc/jdk/CloseableLock.scala @@ -0,0 +1,21 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.etc.jdk + +import java.util.concurrent.locks.ReentrantLock + +class CloseableLock extends ReentrantLock with AutoCloseable { + + def lockAndGet: CloseableLock = { + lock(); + this + } + + override def close(): Unit = unlock() + +} diff --git a/oni/src/main/scala/org/mbari/oni/jdbc/FastPhylogenyService.scala b/oni/src/main/scala/org/mbari/oni/jdbc/FastPhylogenyService.scala index f744dbe..34f1202 100644 --- a/oni/src/main/scala/org/mbari/oni/jdbc/FastPhylogenyService.scala +++ b/oni/src/main/scala/org/mbari/oni/jdbc/FastPhylogenyService.scala @@ -75,15 +75,17 @@ class FastPhylogenyService(entityManagerFactory: EntityManagerFactory): lock.lock() log.atDebug.log("Loading cache ...") - val cache = executeQuery() - if cache.nonEmpty then - val lu = cache.maxBy(_.lastUpdate.toEpochMilli) - lastUpdate = lu.lastUpdate - - val r = MutableConcept.toTree(cache) - rootNode = r._1 - allNodes = r._2 - lock.unlock() + try + val cache = executeQuery() + if cache.nonEmpty then + val lu = cache.maxBy(_.lastUpdate.toEpochMilli) + lastUpdate = lu.lastUpdate + + val r = MutableConcept.toTree(cache) + rootNode = r._1 + allNodes = r._2 + finally + lock.unlock() def findLastUpdate(): Instant = val attempt = Using(entityManagerFactory.createEntityManager) { entityManager => diff --git a/oni/src/main/scala/org/mbari/oni/services/ConceptCache.scala b/oni/src/main/scala/org/mbari/oni/services/ConceptCache.scala new file mode 100644 index 0000000..61d35be --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/services/ConceptCache.scala @@ -0,0 +1,64 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.services + +import com.github.benmanes.caffeine.cache.{Cache, Caffeine} +import org.mbari.oni.domain.ConceptMetadata +import org.mbari.oni.etc.jdk.Loggers.{*, given} + +import java.util.concurrent.TimeUnit + +class ConceptCache(conceptService: ConceptService, conceptNameService: ConceptNameService) { + + private val log = System.getLogger(getClass.getName) + + private val nameCache: Cache[String, ConceptMetadata] = Caffeine + .newBuilder() + .expireAfterWrite(15, TimeUnit.MINUTES) + .build[String, ConceptMetadata]() + + private val allNamesCache: Cache[String, Seq[String]] = Caffeine + .newBuilder() + .expireAfterWrite(15, TimeUnit.MINUTES) + .build[String, Seq[String]]() + + def findByName(name: String): Either[Throwable, ConceptMetadata] = { + Option(nameCache.getIfPresent(name)) match { + case Some(node) => Right(node) + case None => + conceptService.findByName(name) match + case Left(e) => + log.atInfo.withCause(e).log(s"Failed to find concept by name: $name") + Left(e) + case Right(conceptNode) => + nameCache.put(name, conceptNode) + Right(conceptNode) + } + } + + def findAllNames(limit: Int, offset: Int): Either[Throwable, Seq[String]] = { + val allNames = Option(allNamesCache.getIfPresent(ConceptCache.AllNamesCacheKey)) + if (allNames.isDefined && allNames.get.nonEmpty) { + Right(allNames.get.slice(offset, offset + limit)) + } + else { + conceptNameService.findAllNames(1000000, 0) match + case Left(e) => + log.atError.withCause(e).log("Failed to find all concept names") + Left(e) + case Right(names) => + allNamesCache.put(ConceptCache.AllNamesCacheKey, names) + Right(names.slice(offset, offset + limit)) + } + } + +} + +object ConceptCache { + val AllNamesCacheKey = "all-names" +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 6397bfe..ba31ef3 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,6 +2,7 @@ import sbt.* object Dependencies { lazy val auth0 = "com.auth0" % "java-jwt" % "4.4.0" + lazy val caffeine = "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8" val circeVersion = "0.14.9" lazy val circeCore = "io.circe" %% "circe-core" % circeVersion