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

WIP - Marji/improve naming for top mentions implementations #25133

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions article/app/AppLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ import play.api.routing.Router
import router.Routes
import services.ophan.SurgingContentAgentLifecycle
import services.{NewspaperBooksAndSectionsAutoRefresh, OphanApi, SkimLinksCacheLifeCycle}
import jobs.{StoreNavigationLifecycleComponent, TopMentionsLifecycle}
import topmentions.{TopMentionsS3Client, TopMentionsS3ClientImpl, TopMentionsService}
import jobs.{StoreNavigationLifecycleComponent, TopicLifecycle}
import services.topics.{TopicS3Client, TopicS3ClientImpl, TopicService}

class AppLoader extends FrontendApplicationLoader {
override def buildComponents(context: Context): FrontendComponents =
new BuiltInComponentsFromContext(context) with AppComponents
}

trait TopMentionsServices {
lazy val topMentionsS3Client: TopMentionsS3Client = wire[TopMentionsS3ClientImpl]
lazy val topMentionsService = wire[TopMentionsService]
trait TopicServices {
lazy val topicS3Client: TopicS3Client = wire[TopicS3ClientImpl]
lazy val topicService = wire[TopicService]
}

trait AppComponents extends FrontendComponents with ArticleControllers with TopMentionsServices {
trait AppComponents extends FrontendComponents with ArticleControllers with TopicServices {

lazy val capiHttpClient: HttpClient = wire[CapiHttpClient]
lazy val contentApiClient = wire[ContentApiClient]
Expand All @@ -58,7 +58,7 @@ trait AppComponents extends FrontendComponents with ArticleControllers with TopM
wire[DiscussionExternalAssetsLifecycle],
wire[SkimLinksCacheLifeCycle],
wire[StoreNavigationLifecycleComponent],
wire[TopMentionsLifecycle],
wire[TopicLifecycle],
)

lazy val router: Router = wire[Routes]
Expand Down
6 changes: 3 additions & 3 deletions article/app/controllers/ArticleControllers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import model.ApplicationContext
import play.api.libs.ws.WSClient
import play.api.mvc.ControllerComponents
import renderers.DotcomRenderingService
import services.topics.{TopicS3Client, TopicService}
import services.{NewspaperBookSectionTagAgent, NewspaperBookTagAgent}
import topmentions.{TopMentionsS3Client, TopMentionsService}

trait ArticleControllers {
def contentApiClient: ContentApiClient
def controllerComponents: ControllerComponents
def wsClient: WSClient
def remoteRender: DotcomRenderingService
def topMentionsS3Client: TopMentionsS3Client
def topMentionsService: TopMentionsService
def topicS3Client: TopicS3Client
def topicService: TopicService
implicit def appContext: ApplicationContext
lazy val bookAgent: NewspaperBookTagAgent = wire[NewspaperBookTagAgent]
lazy val bookSectionAgent: NewspaperBookSectionTagAgent = wire[NewspaperBookSectionTagAgent]
Expand Down
49 changes: 25 additions & 24 deletions article/app/controllers/LiveBlogController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import play.twirl.api.Html
import renderers.DotcomRenderingService
import services.CAPILookup
import services.dotcomponents.DotcomponentsLogger
import topmentions.TopMentionsService
import services.topics.TopicService
import views.support.RenderOtherStatus

import scala.concurrent.Future

case class MinutePage(article: Article, related: RelatedContent) extends PageWithStoryPackage
Expand All @@ -30,7 +31,7 @@ class LiveBlogController(
val controllerComponents: ControllerComponents,
ws: WSClient,
remoteRenderer: renderers.DotcomRenderingService = DotcomRenderingService(),
topMentionsService: TopMentionsService,
topicService: TopicService,
)(implicit context: ApplicationContext)
extends BaseController
with GuLogging
Expand All @@ -45,7 +46,7 @@ class LiveBlogController(

def renderEmail(path: String): Action[AnyContent] = {
Action.async { implicit request =>
mapModel(path, ArticleBlocks, topMentionResult = None) {
mapModel(path, ArticleBlocks, topicResult = None) {
case (minute: MinutePage, _) =>
Future.successful(common.renderEmail(ArticleEmailHtmlPage.html(minute), minute))
case (blog: LiveBlogPage, _) => Future.successful(common.renderEmail(LiveBlogHtmlPage.html(blog), blog))
Expand All @@ -62,20 +63,20 @@ class LiveBlogController(
): Action[AnyContent] = {
Action.async { implicit request =>
val filter = shouldFilter(filterKeyEvents)
val topMentions = if (filter) None else getTopMentions(path, topics)
val topicList = topMentionsService.getTopics(path)
val topicResult = if (filter) None else getTopicResult(path, topics)
val allTopics = topicService.getTopics(path)
page.map(ParseBlockId.fromPageParam) match {
case Some(ParsedBlockId(id)) =>
renderWithRange(path, PageWithBlock(id), filter, topMentions, topicList) // we know the id of a block
renderWithRange(path, PageWithBlock(id), filter, topicResult, allTopics) // we know the id of a block
case Some(InvalidFormat) =>
Future.successful(
Cached(10)(WithoutRevalidationResult(NotFound)),
) // page param there but couldn't extract a block id
case None => {
topMentions match {
topicResult match {
case Some(value) =>
renderWithRange(path, TopicsLiveBlog, filter, Some(value), topicList) // no page param
case None => renderWithRange(path, CanonicalLiveBlog, filter, None, topicList) // no page param
renderWithRange(path, TopicsLiveBlog, filter, Some(value), allTopics) // no page param
case None => renderWithRange(path, CanonicalLiveBlog, filter, None, allTopics) // no page param
}
}
}
Expand Down Expand Up @@ -114,12 +115,12 @@ class LiveBlogController(
path: String,
range: BlockRange,
filterKeyEvents: Boolean,
topMentionResult: Option[TopMentionsResult],
topicResult: Option[TopicResult],
topics: Option[Seq[TopicWithCount]],
)(implicit
request: RequestHeader,
): Future[Result] = {
mapModel(path, range, filterKeyEvents, topMentionResult) { (page, blocks) =>
mapModel(path, range, filterKeyEvents, topicResult) { (page, blocks) =>
{
val isAmpSupported = page.article.content.shouldAmplify
val pageType: PageType = PageType(page, request, context)
Expand Down Expand Up @@ -318,13 +319,13 @@ class LiveBlogController(
path: String,
range: BlockRange,
filterKeyEvents: Boolean = false,
topMentionResult: Option[TopMentionsResult],
topicResult: Option[TopicResult],
)(
render: (PageWithStoryPackage, Blocks) => Future[Result],
)(implicit request: RequestHeader): Future[Result] = {
capiLookup
.lookup(path, Some(range))
.map(responseToModelOrResult(range, filterKeyEvents, topMentionResult))
.map(responseToModelOrResult(range, filterKeyEvents, topicResult))
.recover(convertApiExceptions)
.flatMap {
case Left((model, blocks)) => render(model, blocks)
Expand All @@ -335,7 +336,7 @@ class LiveBlogController(
private[this] def responseToModelOrResult(
range: BlockRange,
filterKeyEvents: Boolean,
topMentionResult: Option[TopMentionsResult],
topicResult: Option[TopicResult],
)(response: ItemResponse)(implicit request: RequestHeader): Either[(PageWithStoryPackage, Blocks), Result] = {
val supportedContent: Option[ContentType] = response.content.filter(isSupported).map(Content(_))
val supportedContentResult: Either[ContentType, Result] = ModelOrResult(supportedContent, response)
Expand All @@ -352,7 +353,7 @@ class LiveBlogController(
response,
range,
filterKeyEvents,
topMentionResult,
topicResult,
).left
.map(_ -> blocks)
case unknown =>
Expand All @@ -367,17 +368,17 @@ class LiveBlogController(
filterKeyEvents.getOrElse(false)
}

def getTopMentions(blogId: String, topics: Option[String]) = {
val topMentionsResult = for {
topMentionTopic <- TopMentionsTopic.fromString(topics)
topMentions <- topMentionsService.getTopMentionsByTopic(blogId, topMentionTopic)
} yield topMentions
def getTopicResult(blogId: String, topics: Option[String]) = {
val topicResult = for {
topic <- Topic.fromString(topics)
topicResult <- topicService.getTopicResult(blogId, topic)
} yield topicResult

topMentionsResult match {
case Some(_) => log.info(s"top mention result was successfully retrieved for ${topics.get}")
case None => if (topics.isDefined) log.warn(s"top mention result couldn't be retrieved for ${topics.get}")
topicResult match {
case Some(_) => println(s"top mention result was successfully retrieved for ${topics.get}")
case None => if (topics.isDefined) println(s"top mention result couldn't be retrieved for ${topics.get}")
}

topMentionsResult
topicResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package jobs
import app.LifecycleComponent
import common.{AkkaAsync, JobScheduler}
import play.api.inject.ApplicationLifecycle
import topmentions.TopMentionsService
import services.topics.TopicService

import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}

class TopMentionsLifecycle(
class TopicLifecycle(
appLifeCycle: ApplicationLifecycle,
jobs: JobScheduler,
akkaAsync: AkkaAsync,
topMentionService: TopMentionsService,
topicService: TopicService,
)(implicit ec: ExecutionContext)
extends LifecycleComponent {

Expand All @@ -27,18 +27,18 @@ class TopMentionsLifecycle(

// refresh top mentions when app starts
akkaAsync.after1s {
topMentionService.refreshTopMentions()
topicService.refreshTopicsDetails()
}
}

private def scheduleJobs(): Unit = {
// This job runs every 2 minutes
jobs.scheduleEvery("TopMentionsAgentRefreshJob", 2.minutes) {
topMentionService.refreshTopMentions()
jobs.scheduleEvery("BlogsTopicsDetailsAgentRefreshJob", 2.minutes) {
topicService.refreshTopicsDetails()
}
}

private def descheduleJobs(): Unit = {
jobs.deschedule("TopMentionsAgentRefreshJob")
jobs.deschedule("BlogsTopicsDetailsAgentRefreshJob")
}
}
8 changes: 4 additions & 4 deletions article/app/model/LiveBlogHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object LiveBlogHelpers extends GuLogging {
liveBlog: Article,
range: BlockRange,
filterKeyEvents: Boolean,
topMentionResult: Option[TopMentionsResult],
topicResult: Option[TopicResult],
): Option[LiveBlogCurrentPage] = {

val pageSize = if (liveBlog.content.tags.tags.map(_.id).contains("sport/sport")) 30 else 10
Expand All @@ -51,7 +51,7 @@ object LiveBlogHelpers extends GuLogging {
_,
range,
filterKeyEvents,
topMentionResult,
topicResult,
),
)

Expand All @@ -64,7 +64,7 @@ object LiveBlogHelpers extends GuLogging {
response: ItemResponse,
range: BlockRange,
filterKeyEvents: Boolean,
topMentionResult: Option[TopMentionsResult],
topicResult: Option[TopicResult],
): Either[LiveBlogPage, Status] = {

val pageSize = if (liveBlog.content.tags.tags.map(_.id).contains("sport/sport")) 30 else 10
Expand All @@ -76,7 +76,7 @@ object LiveBlogHelpers extends GuLogging {
blocks,
range,
filterKeyEvents,
topMentionResult,
topicResult,
)
} getOrElse None

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
package topmentions
package services.topics

import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.model.{GetObjectRequest, S3Object}
import com.amazonaws.util.IOUtils
import common.GuLogging
import conf.Configuration
import model.{TopMentionJsonParseException, TopMentionsDetails}
import model.TopicsDetails._
import model.{TopicJsonParseException, TopicsDetails}
import play.api.libs.json.{JsError, JsSuccess, Json}
import services.S3
import topmentions.S3ObjectImplicits.RichS3Object
import model.TopMentionsResponse._
import services.topics.S3ObjectImplicits.RichS3Object

import scala.collection.JavaConverters._
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}

trait TopMentionsS3Client {
trait TopicS3Client {
def getListOfKeys(): Future[List[String]]
def getObject(key: String): Future[TopMentionsDetails]
def getObject(key: String): Future[TopicsDetails]
}

final class TopMentionsS3ClientImpl extends TopMentionsS3Client with S3 with GuLogging {
final class TopicS3ClientImpl extends TopicS3Client with S3 with GuLogging {
lazy val optionalBucket: Option[String] = Configuration.aws.topMentionsStoreBucket

def getListOfKeys(): Future[List[String]] = {
Expand All @@ -39,7 +39,7 @@ final class TopMentionsS3ClientImpl extends TopMentionsS3Client with S3 with GuL
}
}

def getObject(key: String): Future[TopMentionsDetails] = {
def getObject(key: String): Future[TopicsDetails] = {
getClient { client =>
Try {
val request = new GetObjectRequest(getBucket, key)
Expand Down Expand Up @@ -70,18 +70,18 @@ final class TopMentionsS3ClientImpl extends TopMentionsS3Client with S3 with GuL

object S3ObjectImplicits {
implicit class RichS3Object(s3Object: S3Object) extends GuLogging {
def parseToTopMentionsDetails: Try[TopMentionsDetails] = {
def parseToTopMentionsDetails: Try[TopicsDetails] = {
val json = Json.parse(asString(s3Object))

Json.fromJson[TopMentionsDetails](json) match {
Json.fromJson[TopicsDetails](json) match {
case JsSuccess(topMentionResponse, __) =>
log.debug(s"Parsed TopMentionsDetails from S3 for key ${s3Object.getKey}")
Success(topMentionResponse)
case JsError(errors) =>
val errorPaths = errors.map { error => error._1.toString() }.mkString(",")
log.error(s"Error parsing topMentionResponse from S3 for key ${s3Object.getKey} paths: ${errorPaths}")
Failure(
TopMentionJsonParseException(
TopicJsonParseException(
s"could not parse S3 TopMentionsDetails json. Errors paths(s): $errors",
),
)
Expand Down
55 changes: 55 additions & 0 deletions article/app/services/topics/TopicService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package services.topics

import common.{Box, GuLogging}
import model.{Topic, TopicResult, TopicWithCount, TopicsDetails}

import scala.concurrent.{ExecutionContext, Future}

class TopicService(topicsS3Client: TopicS3Client) extends GuLogging {

private val blogsTopicsDetails = Box[Option[Map[String, TopicsDetails]]](None)

def refreshTopicsDetails()(implicit executionContext: ExecutionContext): Future[Unit] = {
val retrievedTopicsDetails =
topicsS3Client.getListOfKeys().map { key => key.map { retrieveAllTopicsDetails(_) } }

retrievedTopicsDetails
.flatMap(Future.sequence(_))
.map(response => {
blogsTopicsDetails send Some(response.toMap)
log.info("successfully refreshed top mentions")
})
.recover {
case e =>
log.error("Could not refresh top mentions", e)
}
}

def getBlogTopicsDetails(blogId: String): Option[TopicsDetails] = {
blogsTopicsDetails.get().flatMap(_.get(blogId))
}

def getTopics(blogId: String): Option[Seq[TopicWithCount]] = {
getBlogTopicsDetails(blogId).map(mentions =>
mentions.results.map(mention => TopicWithCount(mention.`type`, mention.name, mention.count)),
)
}

def getAllTopicsDetails: Option[Map[String, TopicsDetails]] = {
blogsTopicsDetails.get()
}

def getTopicResult(
blogId: String,
topic: Topic,
): Option[TopicResult] = {

getBlogTopicsDetails(blogId).flatMap(_.results.find(result => {
result.`type` == topic.`type` && result.name == topic.value
}))
}

private def retrieveAllTopicsDetails(key: String)(implicit executionContext: ExecutionContext) = {
topicsS3Client.getObject(key).map { res => key -> res }
}
}
Loading