common/app/services/ConfigAgentTrait.scala (122 lines of code) (raw):

package services import app.LifecycleComponent import com.gu.facia.api.models.{Front, _} import com.gu.facia.client.ApiClient import com.gu.facia.client.models.{ConfigJson, FrontJson} import com.madgag.scala.collection.decorators.MapDecorator import common._ import conf.Configuration import fronts.FrontsApi import model.pressed.CollectionConfig import model.{ApplicationContext, FrontProperties, SeoDataJson} import org.apache.pekko.util.Timeout import play.api.inject.ApplicationLifecycle import play.api.libs.json.Json import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} case class CollectionConfigWithId(id: String, config: CollectionConfig) /** ConfigAgent is a cache for Fronts config. * * It is the metadata overview for all fronts on www.theguardian.com. It describes the fronts on website along with the * collections they contain. The data is pulled from a single file in S3 in the CMS Fronts AWS account. * * Note, it is pre 'pressing', which means that collections do not contain CAPI article data for backfill etc. Instead, * only the metadata is stored - e.g. if the backfill is CAPI-driven and, if so, the query to use. Full data can then * be retrieved from the (pressed) Fronts API, which again is really a wrapper over an S3 bucket. */ object ConfigAgent extends GuLogging { implicit lazy val alterTimeout: Timeout = Configuration.faciatool.configBeforePressTimeout.millis private lazy val configAgent = Box[Option[ConfigJson]](None) def isLoaded(): Boolean = configAgent.get().isDefined def getClient(implicit ec: ExecutionContext): ApiClient = FrontsApi.crossAccountClient def refresh(implicit ec: ExecutionContext): Future[Unit] = { val futureConfig = getClient.config futureConfig.onComplete { case Success(_) => log.info(s"Successfully got config") case Failure(t) => log.error(s"Getting config failed with $t", t) } futureConfig.map(Option.apply).map(configAgent.send) } def refreshWith(config: ConfigJson): Future[Option[ConfigJson]] = { configAgent.alter(Option(config)) } def refreshAndReturn(implicit ec: ExecutionContext): Future[Unit] = getClient.config .map(config => configAgent.send(Option(config))) .fallbackTo { log.warn("Falling back to current ConfigAgent contents on refreshAndReturn") Future.successful(()) } def getPathIds: List[String] = { val config = configAgent.get() config.map(_.fronts.keys.toList).getOrElse(Nil) } def getConfigCollectionMap: Map[String, Seq[String]] = { val config = configAgent.get() config.map(_.fronts.mapV(_.collections)).getOrElse(Map.empty) } def getConfigsUsingCollectionId(id: String): Seq[String] = { (getConfigCollectionMap collect { case (configId, collectionIds) if collectionIds.contains(id) => configId }).toSeq } def getConfig(id: String): Option[CollectionConfig] = configAgent.get().flatMap(_.collections.get(id).map(CollectionConfig.make)) def contentsAsJsonString: String = Json.prettyPrint(Json.toJson(configAgent.get())) def getSeoDataJsonFromConfig(path: String): SeoDataJson = { val config = configAgent.get() val frontOption = config.flatMap(_.fronts.get(path)) SeoDataJson( path, navSection = frontOption.flatMap(_.navSection).filter(_.nonEmpty), webTitle = frontOption.flatMap(_.webTitle).filter(_.nonEmpty), title = frontOption.flatMap(_.title).filter(_.nonEmpty), description = frontOption.flatMap(_.description).filter(_.nonEmpty), ) } def getFrontPriorityFromConfig(pageId: String): Option[FrontPriority] = { configAgent.get() flatMap { _.fronts.get(pageId) map { frontJson => Front.fromFrontJson(pageId, frontJson).priority } } } def getFrontProperties(id: String): FrontProperties = { val frontOption: Option[FrontJson] = configAgent.get().flatMap(_.fronts.get(id)) frontOption .map(frontJson => FrontProperties( onPageDescription = frontJson.onPageDescription, imageUrl = frontJson.imageUrl, imageWidth = frontJson.imageWidth.map(_.toString), imageHeight = frontJson.imageHeight.map(_.toString), isImageDisplayed = frontJson.isImageDisplayed.getOrElse(false), editorialType = None, // value found in Content API commercial = None, // value found in Content API priority = frontJson.priority, ), ) .getOrElse(FrontProperties.empty) } def isFrontHidden(id: String): Boolean = configAgent.get().exists(_.fronts.get(id).flatMap(_.isHidden).exists(identity)) def isEmailFront(id: String): Boolean = getFrontPriorityFromConfig(id).contains(EmailPriority) // email fronts are only served if the email-friendly format has been specified in the request def shouldServeFront(id: String)(implicit context: ApplicationContext): Boolean = getPathIds.contains(id) && (context.isPreview || !isFrontHidden(id)) def frontExistsInConfig(id: String)(implicit context: ApplicationContext): Boolean = getPathIds.contains(id) def shouldServeEditionalisedFront(edition: Edition, id: String)(implicit context: ApplicationContext): Boolean = { shouldServeFront(s"${edition.id.toLowerCase}/$id") } } class ConfigAgentLifecycle(appLifecycle: ApplicationLifecycle, jobs: JobScheduler, pekkoAsync: PekkoAsync)(implicit ec: ExecutionContext, ) extends LifecycleComponent { appLifecycle.addStopHook { () => Future { jobs.deschedule("ConfigAgentJob") } } override def start(): Unit = { jobs.deschedule("ConfigAgentJob") jobs.schedule("ConfigAgentJob", "18 * * * * ?") { ConfigAgent.refresh } pekkoAsync.after1s { ConfigAgent.refresh } } }