1 /*
2  *  Make.org Core API
3  *  Copyright (C) 2018 Make.org
4  *
5  * This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU Affero General Public License as
7  *  published by the Free Software Foundation, either version 3 of the
8  *  License, or (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU Affero General Public License for more details.
14  *
15  *  You should have received a copy of the GNU Affero General Public License
16  *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  *
18  */
19 
20 package org.make.core
21 package job
22 
23 import java.time.ZonedDateTime
24 import java.time.{Duration => JavaDuration}
25 
26 import eu.timepit.refined.api.Refined
27 import eu.timepit.refined.numeric.Interval
28 import eu.timepit.refined.{refineV, W}
29 import io.circe.{Decoder, Encoder, Json}
30 import org.make.core.SprayJsonFormatters._
31 import org.make.core.job.Job.JobStatus.Running
32 import org.make.core.job.Job.{missableHeartbeats, JobId, JobStatus}
33 import spray.json.DefaultJsonProtocol._
34 import spray.json._
35 
36 import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}
37 
38 final case class Job(id: JobId, status: JobStatus, createdAt: Option[ZonedDateTime], updatedAt: Option[ZonedDateTime])
39     extends Timestamped {
40 
41   /** Check whether the job is stuck, given the expected heart rate.
42     *
43     * A job is stuck if it has missed three heartbeats (and did not update otherwise), to allow some leniency if it does
44     * not report itself often and last heartbeats have not yet been received or handled (because e.g. of some network
45     * delay, high load…).
46     */
47   def isStuck(heartRate: Duration): Boolean = {
48     status match {
49       case Running(_) =>
50         updatedAt.forall(JavaDuration.between(_, DateHelper.now()).toMillis > (heartRate.toMillis * missableHeartbeats))
51       case _ => false
52     }
53   }
54 
55 }
56 
57 object Job {
58 
59   val defaultHeartRate: FiniteDuration = 10.seconds
60   val missableHeartbeats: Int = 3
61 
62   type ProgressRefinement = Interval.Closed[W.`0D`.T, W.`100D`.T]
63   type Progress = Double Refined ProgressRefinement
64 
65   implicit val progressJsonFormat: JsonFormat[Progress] = new JsonFormat[Progress] {
66     override def write(obj: Progress): JsValue = JsNumber(obj.value)
67 
68     @SuppressWarnings(Array("org.wartremover.warts.Throw"))
69     override def read(json: JsValue): Progress = json match {
70       case n @ JsNumber(value) =>
71         refineV[ProgressRefinement](value.toDouble) match {
72           case Right(progress) => progress
73           case Left(error)     => throw new IllegalArgumentException(s"Unable to convert $n: $error")
74         }
75       case other => throw new IllegalArgumentException(s"Unable to convert $other")
76     }
77   }
78 
79   implicit val jobJsonFormat: RootJsonFormat[Job] = DefaultJsonProtocol.jsonFormat4(Job.apply)
80 
81   final case class JobId(value: String) extends StringValue
82 
83   object JobId {
84 
85     val Reindex: JobId = JobId("Reindex")
86     val ReindexPosts: JobId = JobId("ReindexPosts")
87     val SyncCrmData: JobId = JobId("SyncCrmData")
88     val AnonymizeInactiveUsers: JobId = JobId("AnonymizeInactiveUsers")
89     val SubmittedAsLanguagePatch: JobId = JobId("SubmittedAsLanguagePatch")
90 
91     final val swaggerAllowableValues = "Reindex,ReindexPosts,SyncCrmData,AnonymizeInactiveUsers"
92 
93     implicit val jobIdFormatter: JsonFormat[JobId] = SprayJsonFormatters.forStringValue(JobId.apply)
94 
95     implicit lazy val jobIdEncoder: Encoder[JobId] = (a: JobId) => Json.fromString(a.value)
96     implicit lazy val jobIdDecoder: Decoder[JobId] = Decoder.decodeString.map(JobId(_))
97   }
98 
99   sealed abstract class JobStatus extends Product with Serializable
100 
101   object JobStatus {
102 
103     final case class Running(progress: Progress) extends JobStatus
104 
105     final case class Finished(outcome: Option[String]) extends JobStatus
106 
107     implicit val statusJsonFormat: JsonFormat[JobStatus] = new JsonFormat[JobStatus] {
108 
109       import SprayJsonFormatters.syntax._
110 
111       private implicit val runningFormat: JsonFormat[Running] = DefaultJsonProtocol.jsonFormat1(Running.apply)
112       private implicit val finishedFormat: JsonFormat[Finished] = DefaultJsonProtocol.jsonFormat1(Finished.apply)
113 
114       override def read(json: JsValue): JobStatus = {
115         json.asJsObject.getFields("kind") match {
116           case Seq(JsString("running"))  => json.as[Running]
117           case Seq(JsString("finished")) => json.as[Finished]
118         }
119       }
120 
121       override def write(obj: JobStatus): JsValue = obj match {
122         case Running(progress) => JsObject("kind" := "running", "progress" := progress)
123         case Finished(outcome) => JsObject("kind" := "finished", "outcome" := outcome)
124       }
125 
126     }
127 
128   }
129 
130 }
Line Stmt Id Pos Tree Symbol Tests Code
48 2981 1855 - 1861 Select org.make.core.job.Job.status org.make.api.technical.job.jobcoordinatorservicetest Job.this.status
50 5648 1920 - 2014 Apply scala.Long.> org.make.api.technical.job.jobcoordinatorservicetest java.time.Duration.between(x$1, DateHelper.now()).toMillis().>(heartRate.toMillis.*(org.make.core.job.Job.missableHeartbeats))
50 2286 1974 - 2013 Apply scala.Long.* org.make.api.technical.job.jobcoordinatorservicetest heartRate.toMillis.*(org.make.core.job.Job.missableHeartbeats)
50 1050 1944 - 1960 Apply org.make.core.DefaultDateHelper.now org.make.api.technical.job.jobcoordinatorservicetest DateHelper.now()
50 4256 1995 - 2013 Select org.make.core.job.Job.missableHeartbeats org.make.api.technical.job.jobcoordinatorservicetest org.make.core.job.Job.missableHeartbeats
50 4611 1903 - 2015 Apply scala.Option.forall org.make.api.technical.job.jobcoordinatorservicetest Job.this.updatedAt.forall(((x$1: java.time.ZonedDateTime) => java.time.Duration.between(x$1, DateHelper.now()).toMillis().>(heartRate.toMillis.*(org.make.core.job.Job.missableHeartbeats))))
51 2551 2032 - 2037 Literal <nosymbol> false
59 635 2107 - 2109 Literal <nosymbol> org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest 10
59 3799 2107 - 2117 Select scala.concurrent.duration.DurationConversions.seconds org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest scala.concurrent.duration.`package`.DurationInt(10).seconds
60 2989 2150 - 2151 Literal <nosymbol> org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest 3
65 573 2330 - 2333 Apply org.make.core.job.Job.$anon.<init> org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest new $anon()
66 724 2415 - 2424 Select eu.timepit.refined.api.Refined.value org.scalatest.testsuite obj.value
66 4190 2406 - 2425 Apply spray.json.JsNumber.apply org.scalatest.testsuite spray.json.JsNumber.apply(obj.value)
71 2087 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.boolean.And.andValidate org.scalatest.testsuite boolean.this.And.andValidate[Double, eu.timepit.refined.numeric.GreaterEqual[Double(0.0)], this.R, eu.timepit.refined.numeric.LessEqual[Double(100.0)], this.R](boolean.this.Not.notValidate[Double, eu.timepit.refined.numeric.Less[Double(0.0)], this.R](numeric.this.Less.lessValidate[Double, Double(0.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(0.0)](Witness.mkWitness[Double(0.0)](0.0.asInstanceOf[Double(0.0)])), math.this.Numeric.DoubleIsFractional)), boolean.this.Not.notValidate[Double, eu.timepit.refined.numeric.Greater[Double(100.0)], this.R](numeric.this.Greater.greaterValidate[Double, Double(100.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(100.0)](Witness.mkWitness[Double(100.0)](100.0.asInstanceOf[Double(100.0)])), math.this.Numeric.DoubleIsFractional)))
71 2299 2619 - 2633 Select scala.math.ScalaNumericAnyConversions.toDouble org.scalatest.testsuite value.toDouble
71 875 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.numeric.Greater.greaterValidate org.scalatest.testsuite numeric.this.Greater.greaterValidate[Double, Double(100.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(100.0)](Witness.mkWitness[Double(100.0)](100.0.asInstanceOf[Double(100.0)])), math.this.Numeric.DoubleIsFractional)
71 4619 2618 - 2618 Select scala.math.Numeric.DoubleIsFractional org.scalatest.testsuite math.this.Numeric.DoubleIsFractional
71 5459 2591 - 2634 ApplyToImplicitArgs eu.timepit.refined.internal.RefinePartiallyApplied.apply org.scalatest.testsuite eu.timepit.refined.`package`.refineV[org.make.core.job.Job.ProgressRefinement].apply[Double](value.toDouble)(boolean.this.And.andValidate[Double, eu.timepit.refined.numeric.GreaterEqual[Double(0.0)], this.R, eu.timepit.refined.numeric.LessEqual[Double(100.0)], this.R](boolean.this.Not.notValidate[Double, eu.timepit.refined.numeric.Less[Double(0.0)], this.R](numeric.this.Less.lessValidate[Double, Double(0.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(0.0)](Witness.mkWitness[Double(0.0)](0.0.asInstanceOf[Double(0.0)])), math.this.Numeric.DoubleIsFractional)), boolean.this.Not.notValidate[Double, eu.timepit.refined.numeric.Greater[Double(100.0)], this.R](numeric.this.Greater.greaterValidate[Double, Double(100.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(100.0)](Witness.mkWitness[Double(100.0)](100.0.asInstanceOf[Double(100.0)])), math.this.Numeric.DoubleIsFractional))))
71 5447 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.internal.WitnessAs.singletonWitnessAs org.scalatest.testsuite internal.this.WitnessAs.singletonWitnessAs[Double, Double(0.0)](Witness.mkWitness[Double(0.0)](0.0.asInstanceOf[Double(0.0)]))
71 2558 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.numeric.Less.lessValidate org.scalatest.testsuite numeric.this.Less.lessValidate[Double, Double(0.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(0.0)](Witness.mkWitness[Double(0.0)](0.0.asInstanceOf[Double(0.0)])), math.this.Numeric.DoubleIsFractional)
71 560 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.boolean.Not.notValidate org.scalatest.testsuite boolean.this.Not.notValidate[Double, eu.timepit.refined.numeric.Less[Double(0.0)], this.R](numeric.this.Less.lessValidate[Double, Double(0.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(0.0)](Witness.mkWitness[Double(0.0)](0.0.asInstanceOf[Double(0.0)])), math.this.Numeric.DoubleIsFractional))
71 4199 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.boolean.Not.notValidate org.scalatest.testsuite boolean.this.Not.notValidate[Double, eu.timepit.refined.numeric.Greater[Double(100.0)], this.R](numeric.this.Greater.greaterValidate[Double, Double(100.0)](internal.this.WitnessAs.singletonWitnessAs[Double, Double(100.0)](Witness.mkWitness[Double(100.0)](100.0.asInstanceOf[Double(100.0)])), math.this.Numeric.DoubleIsFractional))
71 1854 2618 - 2618 Select scala.math.Numeric.DoubleIsFractional org.scalatest.testsuite math.this.Numeric.DoubleIsFractional
71 3810 2618 - 2618 ApplyToImplicitArgs eu.timepit.refined.internal.WitnessAs.singletonWitnessAs org.scalatest.testsuite internal.this.WitnessAs.singletonWitnessAs[Double, Double(100.0)](Witness.mkWitness[Double(100.0)](100.0.asInstanceOf[Double(100.0)]))
73 4492 2720 - 2787 Throw <nosymbol> throw new scala.`package`.IllegalArgumentException(("Unable to convert ".+(n).+(": ").+(error): String))
75 2506 2818 - 2881 Throw <nosymbol> throw new scala.`package`.IllegalArgumentException(("Unable to convert ".+(other): String))
79 3744 2977 - 2986 Apply org.make.core.job.Job.apply org.scalatest.testsuite Job.apply(id, status, createdAt, updatedAt)
79 2093 2976 - 2976 ApplyToImplicitArgs spray.json.StandardFormats.optionFormat org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.optionFormat[java.time.ZonedDateTime](org.make.core.SprayJsonFormatters.zonedDateTimeFormatter)
79 3681 2976 - 2976 ApplyToImplicitArgs spray.json.StandardFormats.optionFormat org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.optionFormat[java.time.ZonedDateTime](org.make.core.SprayJsonFormatters.zonedDateTimeFormatter)
79 2694 2945 - 2987 ApplyToImplicitArgs spray.json.ProductFormatsInstances.jsonFormat4 org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.jsonFormat4[org.make.core.job.Job.JobId, org.make.core.job.Job.JobStatus, Option[java.time.ZonedDateTime], Option[java.time.ZonedDateTime], org.make.core.job.Job](((id: org.make.core.job.Job.JobId, status: org.make.core.job.Job.JobStatus, createdAt: Option[java.time.ZonedDateTime], updatedAt: Option[java.time.ZonedDateTime]) => Job.apply(id, status, createdAt, updatedAt)))(Job.this.JobId.jobIdFormatter, Job.this.JobStatus.statusJsonFormat, spray.json.DefaultJsonProtocol.optionFormat[java.time.ZonedDateTime](org.make.core.SprayJsonFormatters.zonedDateTimeFormatter), spray.json.DefaultJsonProtocol.optionFormat[java.time.ZonedDateTime](org.make.core.SprayJsonFormatters.zonedDateTimeFormatter), (ClassTag.apply[org.make.core.job.Job](classOf[org.make.core.job.Job]): scala.reflect.ClassTag[org.make.core.job.Job]))
79 1046 2976 - 2976 Select org.make.core.job.Job.JobStatus.statusJsonFormat org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobStatus.statusJsonFormat
79 5472 2976 - 2976 Select org.make.core.SprayJsonFormatters.zonedDateTimeFormatter org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest org.make.core.SprayJsonFormatters.zonedDateTimeFormatter
79 4147 2976 - 2976 Select org.make.core.SprayJsonFormatters.zonedDateTimeFormatter org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest org.make.core.SprayJsonFormatters.zonedDateTimeFormatter
79 1866 2976 - 2976 Select org.make.core.job.Job.JobId.jobIdFormatter org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobId.jobIdFormatter
85 508 3093 - 3109 Apply org.make.core.job.Job.JobId.apply org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobId.apply("Reindex")
86 3750 3140 - 3161 Apply org.make.core.job.Job.JobId.apply org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobId.apply("ReindexPosts")
87 1879 3191 - 3211 Apply org.make.core.job.Job.JobId.apply org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobId.apply("SyncCrmData")
88 982 3252 - 3283 Apply org.make.core.job.Job.JobId.apply org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobId.apply("AnonymizeInactiveUsers")
89 4342 3326 - 3359 Apply org.make.core.job.Job.JobId.apply org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.JobId.apply("SubmittedAsLanguagePatch")
91 2045 3400 - 3457 Literal <nosymbol> "Reindex,ReindexPosts,SyncCrmData,AnonymizeInactiveUsers"
93 5396 3547 - 3558 Apply org.make.core.job.Job.JobId.apply org.scalatest.testsuite Job.this.JobId.apply(value)
93 3691 3512 - 3559 Apply org.make.core.SprayJsonFormatters.forStringValue org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest SprayJsonFormatters.forStringValue[org.make.core.job.Job.JobId](((value: String) => Job.this.JobId.apply(value)))
107 3699 4038 - 4041 Apply org.make.core.job.Job.JobStatus.$anon.<init> org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest new $anon()
111 3761 4174 - 4220 ApplyToImplicitArgs spray.json.ProductFormatsInstances.jsonFormat1 org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.jsonFormat1[org.make.core.job.Job.Progress, org.make.core.job.Job.JobStatus.Running](((progress: org.make.core.job.Job.Progress) => JobStatus.this.Running.apply(progress)))(Job.this.progressJsonFormat, (ClassTag.apply[org.make.core.job.Job.JobStatus.Running](classOf[org.make.core.job.Job$$JobStatus$Running]): scala.reflect.ClassTag[org.make.core.job.Job.JobStatus.Running]))
111 555 4205 - 4205 Select org.make.core.job.Job.progressJsonFormat org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest Job.this.progressJsonFormat
111 2621 4206 - 4219 Apply org.make.core.job.Job.JobStatus.Running.apply org.scalatest.testsuite JobStatus.this.Running.apply(progress)
112 2221 4287 - 4334 ApplyToImplicitArgs spray.json.ProductFormatsInstances.jsonFormat1 org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.jsonFormat1[Option[String], org.make.core.job.Job.JobStatus.Finished](((outcome: Option[String]) => JobStatus.this.Finished.apply(outcome)))(spray.json.DefaultJsonProtocol.optionFormat[String](spray.json.DefaultJsonProtocol.StringJsonFormat), (ClassTag.apply[org.make.core.job.Job.JobStatus.Finished](classOf[org.make.core.job.Job$$JobStatus$Finished]): scala.reflect.ClassTag[org.make.core.job.Job.JobStatus.Finished]))
112 1984 4319 - 4333 Apply org.make.core.job.Job.JobStatus.Finished.apply JobStatus.this.Finished.apply(outcome)
112 994 4318 - 4318 Select spray.json.BasicFormats.StringJsonFormat org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.StringJsonFormat
112 4271 4318 - 4318 ApplyToImplicitArgs spray.json.StandardFormats.optionFormat org.make.api.sessionhistory.sessionhistorycoordinatortest,org.make.api.technical.crm.crmservicecomponenttest spray.json.DefaultJsonProtocol.optionFormat[String](spray.json.DefaultJsonProtocol.StringJsonFormat)
115 5406 4398 - 4431 Apply spray.json.JsObject.getFields org.scalatest.testsuite json.asJsObject.getFields("kind")
116 3627 4491 - 4491 Select org.make.core.job.Job.JobStatus.$anon.runningFormat org.scalatest.testsuite $anon.this.runningFormat
116 2632 4484 - 4500 ApplyToImplicitArgs org.make.core.SprayJsonFormatters.syntax.JsonReaderSyntax.as org.scalatest.testsuite SprayJsonFormatters.syntax.JsonReaderSyntax(json).as[org.make.core.job.Job.JobStatus.Running]($anon.this.runningFormat)
117 495 4552 - 4552 Select org.make.core.job.Job.JobStatus.$anon.finishedFormat $anon.this.finishedFormat
117 3897 4545 - 4562 ApplyToImplicitArgs org.make.core.SprayJsonFormatters.syntax.JsonReaderSyntax.as SprayJsonFormatters.syntax.JsonReaderSyntax(json).as[org.make.core.job.Job.JobStatus.Finished]($anon.this.finishedFormat)
122 1994 4689 - 4695 Literal <nosymbol> org.scalatest.testsuite "kind"
122 2571 4710 - 4732 ApplyToImplicitArgs org.make.core.SprayJsonFormatters.syntax.JsFieldSyntax.:= org.scalatest.testsuite SprayJsonFormatters.syntax.JsFieldSyntax("progress").:=[org.make.core.job.Job.Progress](progress)(Job.this.progressJsonFormat)
122 3635 4721 - 4721 Select org.make.core.job.Job.progressJsonFormat org.scalatest.testsuite Job.this.progressJsonFormat
122 5253 4699 - 4708 Literal <nosymbol> org.scalatest.testsuite "running"
122 503 4680 - 4733 Apply spray.json.JsObject.apply org.scalatest.testsuite spray.json.JsObject.apply(SprayJsonFormatters.syntax.JsFieldSyntax("kind").:=[String]("running")(spray.json.DefaultJsonProtocol.StringJsonFormat), SprayJsonFormatters.syntax.JsFieldSyntax("progress").:=[org.make.core.job.Job.Progress](progress)(Job.this.progressJsonFormat))
122 4145 4696 - 4696 Select spray.json.BasicFormats.StringJsonFormat org.scalatest.testsuite spray.json.DefaultJsonProtocol.StringJsonFormat
122 2231 4689 - 4708 ApplyToImplicitArgs org.make.core.SprayJsonFormatters.syntax.JsFieldSyntax.:= org.scalatest.testsuite SprayJsonFormatters.syntax.JsFieldSyntax("kind").:=[String]("running")(spray.json.DefaultJsonProtocol.StringJsonFormat)
122 5383 4710 - 4720 Literal <nosymbol> org.scalatest.testsuite "progress"
123 514 4768 - 4820 Apply spray.json.JsObject.apply spray.json.JsObject.apply(SprayJsonFormatters.syntax.JsFieldSyntax("kind").:=[String]("finished")(spray.json.DefaultJsonProtocol.StringJsonFormat), SprayJsonFormatters.syntax.JsFieldSyntax("outcome").:=[Option[String]](outcome)(spray.json.DefaultJsonProtocol.optionFormat[String](spray.json.DefaultJsonProtocol.StringJsonFormat)))
123 1788 4787 - 4797 Literal <nosymbol> "finished"
123 3441 4809 - 4809 ApplyToImplicitArgs spray.json.StandardFormats.optionFormat spray.json.DefaultJsonProtocol.optionFormat[String](spray.json.DefaultJsonProtocol.StringJsonFormat)
123 2161 4799 - 4808 Literal <nosymbol> "outcome"
123 3907 4777 - 4783 Literal <nosymbol> "kind"
123 5390 4809 - 4809 Select spray.json.BasicFormats.StringJsonFormat spray.json.DefaultJsonProtocol.StringJsonFormat
123 5264 4784 - 4784 Select spray.json.BasicFormats.StringJsonFormat spray.json.DefaultJsonProtocol.StringJsonFormat
123 4154 4777 - 4797 ApplyToImplicitArgs org.make.core.SprayJsonFormatters.syntax.JsFieldSyntax.:= SprayJsonFormatters.syntax.JsFieldSyntax("kind").:=[String]("finished")(spray.json.DefaultJsonProtocol.StringJsonFormat)
123 2458 4799 - 4819 ApplyToImplicitArgs org.make.core.SprayJsonFormatters.syntax.JsFieldSyntax.:= SprayJsonFormatters.syntax.JsFieldSyntax("outcome").:=[Option[String]](outcome)(spray.json.DefaultJsonProtocol.optionFormat[String](spray.json.DefaultJsonProtocol.StringJsonFormat))