Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Facia presses 'lite' versions of fronts as well as full versions #18364

Closed
wants to merge 11 commits into from
1 change: 1 addition & 0 deletions common/app/common/metrics.scala
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ object FaciaPressMetrics {
val AuPressLatencyMetric = DurationMetric("au-press-latency", StandardUnit.Milliseconds)
val AllFrontsPressLatencyMetric = DurationMetric("front-press-latency", StandardUnit.Milliseconds)
val FrontPressContentSize = SamplerMetric("front-press-content-size", StandardUnit.Bytes)
val FrontPressContentSizeLite = SamplerMetric("front-press-content-size-lite", StandardUnit.Bytes)
val FrontDecodingLatency = DurationMetric("front-decoding-latency", StandardUnit.Milliseconds)
}

Expand Down
5 changes: 4 additions & 1 deletion common/app/layout/Front.scala
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,10 @@ object Front extends implicits.Collections {
type TrailUrl = String

def itemsVisible(containerDefinition: ContainerDefinition): Int =
containerDefinition.slices.flatMap(_.layout.columns.map(_.numItems)).sum
itemsVisible(containerDefinition.slices)

def itemsVisible(slices: Seq[Slice]): Int =
slices.flatMap(_.layout.columns.map(_.numItems)).sum

// Never de-duplicate snaps.
def participatesInDeduplication(faciaContent: PressedContent): Boolean = faciaContent.properties.embedType.isEmpty
Expand Down
13 changes: 12 additions & 1 deletion common/app/layout/slices/Container.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package layout.slices

import model.pressed.CollectionConfig
import model.pressed.{CollectionConfig, PressedContent}
import common.Logging
import layout.Front
import model.facia.PressedCollection

import scala.collection.immutable.Iterable
Expand Down Expand Up @@ -31,6 +32,16 @@ object Container extends Logging {
def fromConfig(collectionConfig: CollectionConfig): Container =
resolve(collectionConfig.collectionType)

def maxStories(collectionConfig: CollectionConfig, items: Seq[PressedContent]): Option[Int] = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would storiesCount would be a better name for this function?

resolve(collectionConfig.collectionType) match {
case Dynamic(dynamicPackage) => dynamicPackage
.slicesFor(items.map(Story.fromFaciaContent))
.map(Front.itemsVisible)
case Fixed(fixedContainer) => Some(Front.itemsVisible(fixedContainer.slices))
case _ => None
}
}

def fromPressedCollection(pressedCollection: PressedCollection, omitMPU: Boolean, adFree: Boolean): Container = {
val container = resolve(pressedCollection.collectionType)
container match {
Expand Down
28 changes: 28 additions & 0 deletions common/app/model/PressedPage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ object PressedPage {
}
}

sealed trait PressedPageType {
def suffix: String
}

case object FullType extends PressedPageType {
override def suffix = ""
}

case object LiteType extends PressedPageType {
override def suffix = ".lite"
}

case class PressedPageVersions(lite: PressedPage, full: PressedPage)

object PressedPageVersions {
def fromPressedCollections(id: String,
seoData: SeoData,
frontProperties: FrontProperties,
pressedCollections: List[PressedCollectionVersions]): PressedPageVersions = {
PressedPageVersions(
PressedPage(id, seoData, frontProperties, pressedCollections.map(_.lite)),
PressedPage(id, seoData, frontProperties, pressedCollections.map(_.full))
)
}
}

case class PressedCollectionVersions(lite: PressedCollection, full: PressedCollection)

case class PressedPage (
id: String,
seoData: SeoData,
Expand Down
25 changes: 7 additions & 18 deletions common/app/services/S3.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.amazonaws.services.s3.model._
import com.amazonaws.util.StringInputStream
import common.Logging
import conf.Configuration
import model.PressedPageType
import org.joda.time.{DateTime, DateTimeZone}
import play.api.libs.ws.{WSClient, WSRequest}
import sun.misc.BASE64Encoder
Expand Down Expand Up @@ -96,13 +97,8 @@ trait S3 extends Logging {
putGzipped(key, value, contentType, Private)
}

def getGzipped(key: String)(implicit codec: Codec): Option[String] = {
val request = new GetObjectRequest(bucket, key)
client.map { client =>
val result = client.getObject(request)
val gzippedStream = new GZIPInputStream(result.getObjectContent)
Source.fromInputStream(gzippedStream).mkString
}
def getGzipped(key: String)(implicit codec: Codec): Option[String] = withS3Result(key) { result =>
Source.fromInputStream(new GZIPInputStream(result.getObjectContent)).mkString
}

private def putGzipped(key: String, value: String, contentType: String, accessControlList: CannedAccessControlList) {
Expand Down Expand Up @@ -158,19 +154,12 @@ object S3FrontsApi extends S3 {
lazy val stage = Configuration.facia.stage.toUpperCase
val namespace = "frontsapi"
lazy val location = s"$stage/$namespace"
private val filename = "pressed.v2.json"

def getLiveFapiPressedKeyForPath(path: String): String =
s"$location/pressed/live/$path/fapi/$filename"

def getDraftFapiPressedKeyForPath(path: String): String =
s"$location/pressed/draft/$path/fapi/$filename"

def putLiveFapiPressedJson(path: String, json: String): Unit =
putPrivateGzipped(getLiveFapiPressedKeyForPath(path), json, "application/json")
private def putFapiPressedJson(live: String, path: String, json: String, suffix: String): Unit =
putPrivateGzipped(s"$location/pressed/$live/$path/fapi/pressed.v2$suffix.json", json, "application/json")

def putDraftFapiPressedJson(path: String, json: String): Unit =
putPrivateGzipped(getDraftFapiPressedKeyForPath(path), json, "application/json")
def putLiveFapiPressedJson(path: String, json: String, pressedType: PressedPageType): Unit = putFapiPressedJson("live", path, json, pressedType.suffix)
def putDraftFapiPressedJson(path: String, json: String, pressedType: PressedPageType): Unit = putFapiPressedJson("draft", path, json, pressedType.suffix)
}

object S3Archive extends S3 {
Expand Down
1 change: 1 addition & 0 deletions facia-press/app/controllers/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Application(liveFapiFrontPress: LiveFapiFrontPress, draftFapiFrontPress: D

def generateLivePressedFor(path: String): Action[AnyContent] = Action.async { request =>
liveFapiFrontPress.getPressedFrontForPath(path)
.map(_.full)
Copy link
Contributor

@TBonnin TBonnin Nov 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a route like /pressed/live/*path.lite so we can get the lite version of the pressed front as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do, I will add it

.map(Json.toJson(_))
.map(Json.prettyPrint)
.map(Ok.apply(_))
Expand Down
73 changes: 49 additions & 24 deletions facia-press/app/frontpress/FapiFrontPress.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package frontpress

import metrics.SamplerMetric
import com.gu.contentapi.client.ContentApiClientLogic
import com.gu.contentapi.client.model.v1.ItemResponse
import com.gu.contentapi.client.model.{ItemQuery, SearchQuery}
Expand All @@ -14,20 +15,22 @@ import conf.Configuration
import conf.switches.Switches.FaciaInlineEmbeds
import contentapi.{CapiHttpClient, CircuitBreakingContentApiClient, ContentApiClient, QueryDefaults}
import services.fronts.FrontsApi
import model._
import model.{PressedPage, _}
import model.facia.PressedCollection
import model.pressed._
import org.joda.time.DateTime
import play.api.libs.json._
import play.api.libs.ws.WSClient
import services.{ConfigAgent, S3FrontsApi}
import implicits.Booleans._
import layout.slices.Container

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

class LiveFapiFrontPress(val wsClient: WSClient, val capiClientForFrontsSeo: ContentApiClient)(implicit ec: ExecutionContext) extends FapiFrontPress {

override def putPressedJson(path: String, json: String): Unit = S3FrontsApi.putLiveFapiPressedJson(path, json)
override def putPressedJson(path: String, json: String, pressedType: PressedPageType): Unit = S3FrontsApi.putLiveFapiPressedJson(path, json, pressedType)
override def isLiveContent: Boolean = true

override implicit val capiClient: ContentApiClientLogic = CircuitBreakingContentApiClient(
Expand Down Expand Up @@ -55,7 +58,7 @@ class DraftFapiFrontPress(val wsClient: WSClient, val capiClientForFrontsSeo: Co

implicit val fapiClient: ApiClient = FrontsApi.crossAccountClient

override def putPressedJson(path: String, json: String): Unit = S3FrontsApi.putDraftFapiPressedJson(path, json)
override def putPressedJson(path: String, json: String, pressedType: PressedPageType): Unit = S3FrontsApi.putDraftFapiPressedJson(path, json, pressedType)
override def isLiveContent: Boolean = false

override def collectionContentWithSnaps(
Expand All @@ -80,7 +83,7 @@ trait FapiFrontPress extends Logging {
implicit def fapiClient: ApiClient
val capiClientForFrontsSeo: ContentApiClient
val wsClient: WSClient
def putPressedJson(path: String, json: String): Unit
def putPressedJson(path: String, json: String, pressedType: PressedPageType): Unit
def isLiveContent: Boolean

def collectionContentWithSnaps(
Expand Down Expand Up @@ -112,10 +115,9 @@ trait FapiFrontPress extends Logging {
val stopWatch: StopWatch = new StopWatch

val pressFuture = getPressedFrontForPath(path)
.map { pressedFront: PressedPage =>
val json: String = Json.stringify(Json.toJson(pressedFront))
FaciaPressMetrics.FrontPressContentSize.recordSample(json.getBytes.length, new DateTime())
putPressedJson(path, json)
.map { pressedFronts: PressedPageVersions =>
putPressedPage(path, pressedFronts.full, FullType)
putPressedPage(path, pressedFronts.lite, LiteType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happen if one of the put fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the two pressed files will not match and the error will be logged.

This shouldn't be a problem. It's already possible for the pressed page to be updated between the browser retrieving the initial pressed page, and the show-more request.

}.fold(
e => {
StatusNotification.notifyFailedJob(path, isLive = isLiveContent, e)
Expand Down Expand Up @@ -148,7 +150,19 @@ trait FapiFrontPress extends Logging {
pressFuture
}

def generateCollectionJsonFromFapiClient(collectionId: String)(implicit executionContext: ExecutionContext): Response[PressedCollection] =
private def putPressedPage(path: String, pressedFront: PressedPage, pressedType: PressedPageType): Unit = {
val json: String = Json.stringify(Json.toJson(pressedFront))

val metric: SamplerMetric = pressedType match {
case FullType => FaciaPressMetrics.FrontPressContentSize
case LiteType => FaciaPressMetrics.FrontPressContentSizeLite
}

metric.recordSample(json.getBytes.length, new DateTime())
putPressedJson(path, json, pressedType)
}

def generateCollectionJsonFromFapiClient(collectionId: String)(implicit executionContext: ExecutionContext): Response[PressedCollectionVersions] = {
for {
collection <- FAPI.getCollection(collectionId)
curated <- getCurated(collection)
Expand All @@ -160,17 +174,28 @@ trait FapiFrontPress extends Logging {
.contains(collection.collectionConfig.collectionType)
.toOption(curated.length + backfill.length)
.getOrElse(Configuration.facia.collectionCap)
val trimmedCurated = curated.take(maxStories)
val trimmedBackfill = backfill.take(maxStories - trimmedCurated.length)
PressedCollection.fromCollectionWithCuratedAndBackfill(
collection,
trimmedCurated,
trimmedBackfill,
treats

val storyCountLite = Container.maxStories(CollectionConfig.make(collection.collectionConfig), curated ++ backfill).getOrElse(maxStories)

PressedCollectionVersions(
pressCollection(collection, curated, backfill, treats, storyCountLite),
pressCollection(collection, curated, backfill, treats, maxStories)
)
}
}


private def pressCollection(collection: Collection, curated: List[PressedContent], backfill: List[PressedContent], treats: List[PressedContent], storyCount: Int) = {
val trimmedCurated = curated.take(storyCount)
val trimmedBackfill = backfill.take(storyCount - trimmedCurated.length)
PressedCollection.fromCollectionWithCuratedAndBackfill(
collection,
trimmedCurated,
trimmedBackfill,
treats
)
}

private def getCurated(collection: Collection)(implicit executionContext: ExecutionContext): Response[List[PressedContent]] = {
// Map initial PressedContent to enhanced content which contains pre-fetched embed content.
val initialContent = collectionContentWithSnaps(collection, searchApiQuery, itemApiQuery)
Expand Down Expand Up @@ -263,14 +288,14 @@ trait FapiFrontPress extends Logging {
}
}

def getPressedFrontForPath(path: String)(implicit executionContext: ExecutionContext): Response[PressedPage] = {
val collectionIds = getCollectionIdsForPath(path)
collectionIds
.flatMap(c => Response.traverse(c.map(generateCollectionJsonFromFapiClient)))
.flatMap(result =>
Response.Async.Right(getFrontSeoAndProperties(path).map{
case (seoData, frontProperties) => PressedPage(path, seoData, frontProperties, result)
}))
def getPressedFrontForPath(path: String)(implicit executionContext: ExecutionContext): Response[PressedPageVersions] = {
for {
collectionIds <- getCollectionIdsForPath(path)
pressedCollections <- Response.traverse(collectionIds.map(generateCollectionJsonFromFapiClient))
seoWithProperties <- Response.Async.Right(getFrontSeoAndProperties(path))
} yield seoWithProperties match {
case (seoData, frontProperties) => PressedPageVersions.fromPressedCollections(path, seoData, frontProperties, pressedCollections)
}
}

private def getFrontSeoAndProperties(path: String)(implicit executionContext: ExecutionContext): Future[(SeoData, FrontProperties)] =
Expand Down
2 changes: 1 addition & 1 deletion facia/app/controllers/front/FrontJsonFapi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import scala.concurrent.{ExecutionContext, Future}
trait FrontJsonFapi extends Logging {
lazy val stage: String = Configuration.facia.stage.toUpperCase
val bucketLocation: String
val parallelJsonPresses = 16
val parallelJsonPresses = 24
val futureSemaphore = new FutureSemaphore(parallelJsonPresses)

def blockingOperations: BlockingOperations
Expand Down