All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lucuma.itc.service.ItcCacheOrRemote.scala Maven / Gradle / Ivy

// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package lucuma.itc.service

import boopickle.DefaultBasic.*
import buildinfo.BuildInfo
import cats.*
import cats.data.NonEmptyChain
import cats.effect.kernel.Clock
import cats.syntax.all.*
import dev.profunktor.redis4cats.algebra.Flush
import dev.profunktor.redis4cats.algebra.StringCommands
import lucuma.core.enums.Band
import lucuma.itc.*
import lucuma.itc.service.redis.given
import lucuma.itc.service.requests.*
import natchez.Trace
import org.typelevel.log4cats.Logger

import java.nio.ByteBuffer
import java.nio.charset.Charset
import scala.concurrent.duration.*

/**
 * Methods to check if a values is on the cache and if not retrieve them from old itc and store them
 * in the cache
 *
 * redis keys are formed with a prefix and a hash of the request redis values are stored in binary
 * via boopickle
 */
trait ItcCacheOrRemote extends Version:
  val KeyCharset = Charset.forName("UTF8")
  // Time to live for entries
  val TTL        = FiniteDuration(10, DAYS)
  val VersionKey = "itc:version".getBytes(KeyCharset)

  /**
   * Generic method to stora a value on redis or request it locally
   */
  private def cacheOrRemote[F[_]: MonadThrow: Logger: Trace: Clock, A: Hash, B: Pickler](
    a:       A,
    request: A => F[B]
  )(
    prefix:  String,
    redis:   StringCommands[F, Array[Byte], Array[Byte]]
  ): F[B] = {
    val hash        = Hash[A].hash(a)
    val redisKeyStr = s"$prefix:$hash"
    val redisKey    = redisKeyStr.getBytes(KeyCharset)
    val redisKey2   = s"$redisKeyStr 1".getBytes(KeyCharset)
    val L           = Logger[F]

    Trace[F].span("redis-cache-read") {
      for
        _         <- Trace[F].put("redis.key" -> redisKeyStr)
        _         <- L.debug(s"Read key $redisKeyStr")
        fromRedis <- redis
                       .get(redisKey2)
                       .handleErrorWith(e => L.error(e)(s"Error reading $redisKey") *> none.pure[F])
        decoded   <-
          fromRedis
            .flatMap(b => Either.catchNonFatal(Unpickle[B].fromBytes(ByteBuffer.wrap(b))).toOption)
            .pure[F]
        _         <- L.debug(s"$hash found on redis").unlessA(fromRedis.isEmpty && decoded.isEmpty)
        r         <- decoded.map(_.pure[F]).getOrElse(Trace[F].span("request-call")(request(a)))
        _         <-
          redis
            .setEx(redisKey, Pickle.intoBytes(r).compact().array(), TTL)
            .handleErrorWith(L.error(_)(s"Error writing $redisKey"))
            .whenA(fromRedis.isEmpty || decoded.isEmpty)
      yield r
    }
  }

  private def requestGraph[F[_]: Functor](itc: Itc[F])(
    request: TargetGraphRequest
  ): F[(TargetGraphsCalcResult, Band)] =
    val band: Band = request.target.bandFor(request.wavelength)
    itc
      .calculateGraph(
        request.target,
        band,
        request.specMode,
        request.constraints,
        request.expTime,
        request.exp,
        request.signalToNoiseAt
      )
      .map((_, band))

  /**
   * Request a graph
   */
  def graphFromCacheOrRemote[F[_]: MonadThrow: Logger: Trace: Clock](
    request: TargetGraphRequest
  )(
    itc:     Itc[F],
    redis:   StringCommands[F, Array[Byte], Array[Byte]]
  ): F[(TargetGraphsCalcResult, Band)] =
    cacheOrRemote(request, requestGraph(itc))("itc:graph:spec", redis)

  private def requestSpecTimeCalc[F[_]: Functor](itc: Itc[F])(
    calcRequest: TargetSpectroscopyTimeRequest
  ): F[(NonEmptyChain[IntegrationTime], Band)] =
    val band: Band = calcRequest.target.bandFor(calcRequest.wavelength)
    itc
      .calculateIntegrationTime(
        calcRequest.target,
        band,
        calcRequest.specMode,
        calcRequest.constraints,
        calcRequest.signalToNoise,
        calcRequest.signalToNoiseAt
      )
      .map((_, band))

  /**
   * Request exposure time calculation for spectroscopy
   */
  def specTimeFromCacheOrRemote[F[_]: MonadThrow: Logger: Trace: Clock](
    calcRequest: TargetSpectroscopyTimeRequest
  )(
    itc:         Itc[F],
    redis:       StringCommands[F, Array[Byte], Array[Byte]]
  ): F[(NonEmptyChain[IntegrationTime], Band)] =
    cacheOrRemote(calcRequest, requestSpecTimeCalc(itc))(
      "itc:calc:spec",
      redis
    )

  private def requestImgTimeCalc[F[_]: Functor](itc: Itc[F])(
    calcRequest: TargetImagingTimeRequest
  ): F[(NonEmptyChain[IntegrationTime], Band)] =
    val band: Band = calcRequest.target.bandFor(calcRequest.wavelength)
    itc
      .calculateIntegrationTime(
        calcRequest.target,
        band,
        calcRequest.imagingMode,
        calcRequest.constraints,
        calcRequest.signalToNoise,
        none
      )
      .map((_, band))

  /**
   * Request exposure time calculation for imaging
   */
  def imgTimeFromCacheOrRemote[F[_]: MonadThrow: Logger: Trace: Clock](
    calcRequest: TargetImagingTimeRequest
  )(
    itc:         Itc[F],
    redis:       StringCommands[F, Array[Byte], Array[Byte]]
  ): F[(NonEmptyChain[IntegrationTime], Band)] =
    cacheOrRemote(calcRequest, requestImgTimeCalc(itc))(
      "itc:calc:img",
      redis
    )

  /**
   * This method will get the version from the remote itc and compare it with the one on redis. If
   * there is none in redis we just store it If the remote is different than the local flush the
   * cache
   */
  def checkVersionToPurge[F[_]: MonadThrow: Logger](
    redis: StringCommands[F, Array[Byte], Array[Byte]] & Flush[F, Array[Byte]],
    itc:   Itc[F]
  ): F[Unit] = {
    val L      = Logger[F]
    val result = for
      _              <- L.info("Check for stale cache")
      _              <- L.info(s"Current itc data checksum ${BuildInfo.ocslibHash}")
      fromRedis      <- redis.get(VersionKey)
      versionOnRedis <- fromRedis.map(v => String(v, KeyCharset)).pure[F]
      _              <- L.info(s"itc data checksum on redis $versionOnRedis")
      // if the version changes flush redis
      _              <- (L.info(
                          s"Flush redis cache on itc version change, set to ${BuildInfo.ocslibHash}"
                        ) *> redis.flushAll)
                          .whenA(versionOnRedis.exists(_ =!= BuildInfo.ocslibHash))
      _              <- redis.setEx(VersionKey, BuildInfo.ocslibHash.getBytes(KeyCharset), TTL)
    yield ()
    result.handleErrorWith(e => L.error(e)("Error doing version check to purge"))
  }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy