Skip to content

Commit

Permalink
add separate switch for dcr circuit breaker and split by rendering app
Browse files Browse the repository at this point in the history
  • Loading branch information
cemms1 committed Feb 27, 2024
1 parent 75d5845 commit 34affae
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 23 deletions.
10 changes: 10 additions & 0 deletions common/app/conf/switches/PerformanceSwitches.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ trait PerformanceSwitches {
exposeClientSide = false,
)

val DCRCircuitBreakerSwitch = Switch(
SwitchGroup.Performance,
"dcr-circuit-breaker",
"If this switch is switched on then the DCR circuit breaker will be operational",
owners = Seq(Owner.withEmail("dotcom.platform@theguardian.com")),
safeState = On,
sellByDate = never,
exposeClientSide = false,
)

val AutoRefreshSwitch = Switch(
SwitchGroup.Performance,
"auto-refresh",
Expand Down
83 changes: 60 additions & 23 deletions common/app/renderers/DotcomRenderingService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.gu.contentapi.client.model.v1.{Block, Blocks, Content}
import common.{DCRMetrics, GuLogging}
import concurrent.CircuitBreakerRegistry
import conf.Configuration
import conf.switches.Switches.CircuitBreakerSwitch
import conf.switches.Switches.DCRCircuitBreakerSwitch
import crosswords.CrosswordPageWithContent
import http.{HttpPreconnections, ResultWithPreconnectPreload}
import model.Cached.{RevalidatableResult, WithoutRevalidationResult}
Expand All @@ -27,6 +27,7 @@ import model.{
Topic,
TopicResult,
}
import org.apache.pekko.pattern.CircuitBreaker
import play.api.libs.ws.{WSClient, WSResponse}
import play.api.mvc.Results.{InternalServerError, NotFound}
import play.api.mvc.{RequestHeader, Result}
Expand All @@ -48,13 +49,42 @@ case class DCRRenderingException(message: String) extends IllegalStateException(

class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload {

private[this] val circuitBreaker = CircuitBreakerRegistry.withConfig(
name = "dotcom-rendering-client",
system = PekkoActorSystem("dotcom-rendering-client-circuit-breaker"),
maxFailures = Configuration.rendering.circuitBreakerMaxFailures,
callTimeout = Configuration.rendering.timeout.plus(200.millis),
resetTimeout = Configuration.rendering.timeout * 4,
)
private[this] val getCircuitBreaker = (name: String) => {
val circuitBreakerName = s"dcr-$name-client"
CircuitBreakerRegistry.withConfig(
name = circuitBreakerName,
system = PekkoActorSystem(s"$circuitBreakerName-circuit-breaker"),
maxFailures = Configuration.rendering.circuitBreakerMaxFailures,
callTimeout = Configuration.rendering.timeout.plus(200.millis),
resetTimeout = Configuration.rendering.timeout * 4,
)
}

private[this] val circuitBreakerRendering = getCircuitBreaker("rendering")
private[this] val circuitBreakerArticleRendering = getCircuitBreaker("article-rendering")
private[this] val circuitBreakerFaciaRendering = getCircuitBreaker("facia-rendering")
private[this] val circuitBreakerInteractiveRendering = getCircuitBreaker("interactive-rendering")

/**
* Decides which of the rendering services to use.
* Returns a tuple of (baseURL, circuitBreaker)
*/
private[this] def decideService(path: String): (String, CircuitBreaker) = {
path match {
case "/Article" | "/AppsArticle" | "/AMPArticle" | "/Blocks" =>
(Configuration.rendering.articleBaseURL, circuitBreakerArticleRendering)

case "/Front" | "/TagPage" =>
(Configuration.rendering.faciaBaseURL, circuitBreakerFaciaRendering)

case "/Interactive" | "/AppsInteractive" | "/AMPInteractive" =>
(Configuration.rendering.interactiveBaseURL, circuitBreakerInteractiveRendering)

case "/EmailNewsletters" | _ =>
(Configuration.rendering.baseURL, circuitBreakerRendering)

}
}

private[this] def postWithoutHandler(
ws: WSClient,
Expand Down Expand Up @@ -96,10 +126,13 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
private[this] def post(
ws: WSClient,
payload: String,
endpoint: String,
path: String,
cacheTime: CacheTime,
timeout: Duration = Configuration.rendering.timeout,
)(implicit request: RequestHeader): Future[Result] = {
val (baseUrl, circuitBreaker) = decideService(path)
val endpoint = s"$baseUrl$path"

def handler(response: WSResponse): Result = {
response.status match {
case 200 =>
Expand Down Expand Up @@ -133,7 +166,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
}
}

if (CircuitBreakerSwitch.isSwitchedOn) {
if (DCRCircuitBreakerSwitch.isSwitchedOn) {
circuitBreaker.withCircuitBreaker(postWithoutHandler(ws, payload, endpoint, timeout)).map(handler)
} else {
postWithoutHandler(ws, payload, endpoint, timeout).map(handler)
Expand Down Expand Up @@ -233,7 +266,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
}

val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.articleBaseURL + path, page.metadata.cacheTime)
post(ws, json, path, page.metadata.cacheTime)
}

def getBlocks(
Expand All @@ -244,7 +277,11 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
val dataModel = DotcomBlocksRenderingDataModel(page, request, blocks)
val json = DotcomBlocksRenderingDataModel.toJson(dataModel)

postWithoutHandler(ws, json, Configuration.rendering.articleBaseURL + "/Blocks")
val path = "/Blocks"
val (_, baseUrl) = decideService(path)
val endpoint = s"$baseUrl$path"

postWithoutHandler(ws, json, endpoint)
.flatMap(response => {
if (response.status == 200)
Future.successful(response.body)
Expand Down Expand Up @@ -277,7 +314,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
)

val json = DotcomFrontsRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.faciaBaseURL + "/Front", CacheTime.Facia)
post(ws, json, "/Front", CacheTime.Facia)
}

def getTagPage(
Expand All @@ -292,7 +329,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
)

val json = DotcomTagPagesRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.faciaBaseURL + "/TagPage", CacheTime.Facia)
post(ws, json, "/TagPage", CacheTime.Facia)
}

def getInteractive(
Expand All @@ -308,7 +345,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
// Nb. interactives have a longer timeout because some of them are very
// large unfortunately. E.g.
// https://www.theguardian.com/education/ng-interactive/2018/may/29/university-guide-2019-league-table-for-computer-science-information.
post(ws, json, Configuration.rendering.interactiveBaseURL + "/Interactive", page.metadata.cacheTime, 4.seconds)
post(ws, json, "/Interactive", page.metadata.cacheTime, 4.seconds)
}

def getAMPInteractive(
Expand All @@ -320,7 +357,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload

val dataModel = DotcomRenderingDataModel.forInteractive(page, blocks, request, pageType)
val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.interactiveBaseURL + "/AMPInteractive", page.metadata.cacheTime)
post(ws, json, "/AMPInteractive", page.metadata.cacheTime)
}

def getAppsInteractive(
Expand All @@ -336,7 +373,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
// Nb. interactives have a longer timeout because some of them are very
// large unfortunately. E.g.
// https://www.theguardian.com/education/ng-interactive/2018/may/29/university-guide-2019-league-table-for-computer-science-information.
post(ws, json, Configuration.rendering.interactiveBaseURL + "/AppsInteractive", page.metadata.cacheTime, 4.seconds)
post(ws, json, "/AppsInteractive", page.metadata.cacheTime, 4.seconds)
}

def getEmailNewsletters(
Expand All @@ -347,7 +384,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload

val dataModel = DotcomNewslettersPageRenderingDataModel.apply(page, newsletters, request)
val json = DotcomNewslettersPageRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.baseURL + "/EmailNewsletters", CacheTime.Facia)
post(ws, json, "/EmailNewsletters", CacheTime.Facia)
}

def getImageContent(
Expand All @@ -358,7 +395,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
)(implicit request: RequestHeader): Future[Result] = {
val dataModel = DotcomRenderingDataModel.forImageContent(imageContent, request, pageType, mainBlock)
val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.articleBaseURL + "/Article", CacheTime.Facia)
post(ws, json, "/Article", CacheTime.Facia)
}

def getAppsImageContent(
Expand All @@ -369,7 +406,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
)(implicit request: RequestHeader): Future[Result] = {
val dataModel = DotcomRenderingDataModel.forImageContent(imageContent, request, pageType, mainBlock)
val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.articleBaseURL + "/AppsArticle", CacheTime.Facia)
post(ws, json, "/AppsArticle", CacheTime.Facia)
}

def getMedia(
Expand All @@ -381,7 +418,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
val dataModel = DotcomRenderingDataModel.forMedia(mediaPage, request, pageType, blocks)

val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.articleBaseURL + "/Article", CacheTime.Facia)
post(ws, json, "/Article", CacheTime.Facia)
}

def getGallery(
Expand All @@ -393,7 +430,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
val dataModel = DotcomRenderingDataModel.forGallery(gallery, request, pageType, blocks)

val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.articleBaseURL + "/Article", CacheTime.Facia)
post(ws, json, "/Article", CacheTime.Facia)
}

def getCrossword(
Expand All @@ -403,7 +440,7 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload
)(implicit request: RequestHeader): Future[Result] = {
val dataModel = DotcomRenderingDataModel.forCrossword(crosswordPage, request, pageType)
val json = DotcomRenderingDataModel.toJson(dataModel)
post(ws, json, Configuration.rendering.articleBaseURL + "/Article", CacheTime.Facia)
post(ws, json, "/Article", CacheTime.Facia)
}
}

Expand Down

0 comments on commit 34affae

Please sign in to comment.