@@ -4,6 +4,8 @@ import java.net.URLDecoder
44import java .text .DecimalFormat
55import java .time .{Duration , Instant }
66
7+ import cats .Eval
8+ import cats .effect .IO
79import com .criteo .slab .app .StateService .NotFoundError
810import com .criteo .slab .core .Executor .{FetchBoardHistory , FetchBoardHourlySlo , RunBoard }
911import com .criteo .slab .core ._
@@ -16,7 +18,6 @@ import shapeless.HList
1618import shapeless .poly .Case3
1719
1820import scala .concurrent .{ExecutionContext , Future }
19- import scala .util .Try
2021import scala .util .control .NonFatal
2122
2223/** Slab Web server
@@ -30,7 +31,7 @@ import scala.util.control.NonFatal
3031case class WebServer (
3132 val pollingInterval : Int = 60 ,
3233 val statsDays : Int = 730 ,
33- private val routeGenerator : StateService => PartialFunction [Request , Future [Response ]] = _ => PartialFunction .empty,
34+ private val routeGenerator : StateService => PartialFunction [Request , IO [Response ]] = _ => PartialFunction .empty,
3435 private val executors : List [Executor [_]] = List .empty
3536 )(implicit ec : ExecutionContext ) {
3637 /**
@@ -71,7 +72,7 @@ case class WebServer(
7172 * @param generator A function that takes StateService and returns routes
7273 * @return Web server with the created routes
7374 */
74- def withRoutes (generator : StateService => PartialFunction [Request , Future [Response ]]) = this .copy(routeGenerator = generator)
75+ def withRoutes (generator : StateService => PartialFunction [Request , IO [Response ]]) = this .copy(routeGenerator = generator)
7576
7677 private val logger = LoggerFactory .getLogger(this .getClass)
7778
@@ -89,68 +90,80 @@ case class WebServer(
8990
9091 private lazy val boards = executors.map(_.board)
9192
92- private val routes : PartialFunction [Request , Future [Response ]] = {
93+ private val routes : PartialFunction [Request , IO [Response ]] = {
9394 // Configs of boards
9495 case GET at url " /api/boards " => {
9596 Ok (boards.map { board => BoardConfig (board.title, board.layout, board.links, board.slo) }.toJSON).map(jsonContentType)
9697 }
9798 // Current board view
9899 case GET at url " /api/boards/ $board" => {
99100 val boardName = URLDecoder .decode(board, " UTF-8" )
100- stateService
101- .current(boardName)
102- .map((_ : ReadableView ).toJSON)
103- .map(Ok (_))
104- .map(jsonContentType)
105- .recoverWith(errorHandler)
101+ IO .fromFuture(IO (
102+ stateService
103+ .current(boardName)
104+ .map((_ : ReadableView ).toJSON)
105+ .map(Ok (_))
106+ .map(jsonContentType)
107+ .recover(errorHandler)
108+ ))
106109 }
107110 // Snapshot of the given time point
108111 case GET at url " /api/boards/ $board/snapshot/ $timestamp" => {
109112 val boardName = URLDecoder .decode(board, " UTF-8" )
110- executors.find(_.board.title == boardName).fold(Future .successful(NotFound (s " Board $boardName does not exist " ))) { executor =>
111- Try (timestamp.toLong).map(Instant .ofEpochMilli).toOption.fold(
112- Future .successful(BadRequest (" invalid timestamp" ))
113- ) { dateTime =>
114- executor.apply(Some (Context (dateTime)))
115- .map((_ : ReadableView ).toJSON)
116- .map(Ok (_))
117- .map(jsonContentType)
113+ executors.find(_.board.title == boardName).fold(IO (NotFound (s " Board $boardName does not exist " ))) { executor =>
114+ IO (Instant .ofEpochMilli(timestamp.toLong)).attempt.flatMap {
115+ case Left (_) => IO (BadRequest (" invalid timestamp" ))
116+ case Right (dateTime) =>
117+ IO .fromFuture(IO (
118+ executor.apply(Some (Context (dateTime)))
119+ .map((_ : ReadableView ).toJSON)
120+ .map(Ok (_))
121+ .map(jsonContentType)
122+ .recover(errorHandler)
123+ ))
118124 }
119- }.recoverWith(errorHandler)
125+ }
120126 }
121127 // History of last 24 hours
122128 case GET at url " /api/boards/ $board/history?last " => {
123129 val boardName = URLDecoder .decode(board, " UTF-8" )
124- stateService
125- .history(boardName)
126- .map(h => Ok (h.toJSON))
127- .map(jsonContentType)
128- .recoverWith(errorHandler)
130+ IO .fromFuture(IO (
131+ stateService
132+ .history(boardName)
133+ .map(h => Ok (h.toJSON))
134+ .map(jsonContentType)
135+ .recover(errorHandler)
136+ ))
129137 }
130138 // History of the given range
131139 case GET at url " /api/boards/ $board/history?from= $fromTS&until= $untilTS" => {
132140 val boardName = URLDecoder .decode(board, " UTF-8" )
133- executors.find(_.board.title == boardName).fold(Future .successful(NotFound (s " Board $boardName does not exist " ))) { executor =>
134- val range = for {
135- from <- Try (fromTS.toLong).map(Instant .ofEpochMilli).toOption
136- until <- Try (untilTS.toLong).map(Instant .ofEpochMilli).toOption
137- } yield (from, until)
138- range.fold(Future .successful(BadRequest (" Invalid timestamp" ))) { case (from, until) =>
139- executor.fetchHistory(from, until)
140- .map(_.toMap.mapValues(_.status.name).toJSON)
141- .map(Ok (_))
142- .map(jsonContentType)
141+ executors.find(_.board.title == boardName).fold(IO (NotFound (s " Board $boardName does not exist " ))) { executor =>
142+ IO (
143+ Instant .ofEpochMilli(fromTS.toLong) -> Instant .ofEpochMilli(untilTS.toLong)
144+ ).attempt.flatMap {
145+ case Left (_) => IO (BadRequest (" Invalid timestamp" ))
146+ case Right ((from, until)) =>
147+ IO .fromFuture(IO (
148+ executor.fetchHistory(from, until)
149+ .map(_.toMap.mapValues(_.status.name).toJSON)
150+ .map(Ok (_))
151+ .map(jsonContentType)
152+ .recover(errorHandler)
153+ ))
143154 }
144- }.recoverWith(errorHandler)
155+ }
145156 }
146157 // Stats of the board
147158 case GET at url " /api/boards/ $board/stats " => {
148159 val boardName = URLDecoder .decode(board, " UTF-8" )
149- stateService.stats(boardName)
150- .map(_.mapValues(decimalFormat.format)).map(_.toJSON)
151- .map(Ok (_))
152- .map(jsonContentType)
153- .recoverWith(errorHandler)
160+ IO .fromFuture(IO (
161+ stateService.stats(boardName)
162+ .map(_.mapValues(decimalFormat.format)).map(_.toJSON)
163+ .map(Ok (_))
164+ .map(jsonContentType)
165+ .recover(errorHandler)
166+ ))
154167 }
155168 // Static resources
156169 case GET at url " / $file. $ext" => {
@@ -161,14 +174,14 @@ case class WebServer(
161174 }
162175 }
163176
164- private def notFound : PartialFunction [Request , Future [Response ]] = {
177+ private def notFound : PartialFunction [Request , IO [Response ]] = {
165178 case anyReq => {
166179 logger.info(s " ${anyReq.method.toString} ${anyReq.url} not found " )
167180 Response (404 )
168181 }
169182 }
170183
171- private def errorHandler : PartialFunction [Throwable , Future [ Response ] ] = {
184+ private def errorHandler : PartialFunction [Throwable , Response ] = {
172185 case f : NotFoundError =>
173186 NotFound (f.message)
174187 case NonFatal (e) =>
@@ -178,7 +191,7 @@ case class WebServer(
178191
179192 private def jsonContentType (res : Response ) = res.addHeaders(HttpString (" content-type" ) -> HttpString (" application/json" ))
180193
181- private def routeLogger (router : Request => Future [Response ]) = (request : Request ) => {
194+ private def routeLogger (router : Request => IO [Response ]) = (request : Request ) => {
182195 val start = Instant .now()
183196 router(request) map { res =>
184197 val duration = Duration .between(start, Instant .now)
0 commit comments