Last active
December 6, 2019 14:30
-
-
Save ShahOdin/b63bf3a322f0806305f6fd0bf165ee82 to your computer and use it in GitHub Desktop.
reconstructing state
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.itv.sif.db | |
import cats.Show | |
import cats.data.{NonEmptyList, NonEmptySet, Validated} | |
import cats.syntax.apply._ | |
import cats.syntax.either._ | |
import cats.syntax.validated._ | |
import cats.syntax.show._ | |
import cats.instances.all._ | |
import cats.kernel.Semigroup | |
//This demo showcases a system with two entities that can be independently updated. | |
//we can either aggregate them dynamically (in-memory) and reconstruct a history of the | |
//evolution of the aggregate model. | |
//alternatively, we can record the aggregate model and update its value at every update so that one can | |
//read a static list of values for the aggregate model. | |
//Or we can do both, as the two give us different set of information and can be different if | |
//our pipeline isn't updating the aggregate model correctly. | |
object CoolDemo extends App { | |
sealed trait Color | |
object Color { | |
case object Green extends Color | |
case object Red extends Color | |
} | |
sealed trait Hobby | |
object Hobby { | |
case object BirdWatching extends Hobby | |
case object Sports extends Hobby | |
} | |
type TimeCount = Long | |
case class Failed(reasons: NonEmptyList[String], failedAt: TimeCount, sources: NonEmptySet[String]) | |
object Failed{ | |
def one(reason: String, failureTime: TimeCount, sources: NonEmptySet[String]) = Failed(NonEmptyList.one(reason), failureTime, sources) | |
implicit val semiGroup: Semigroup[Failed] = (x: Failed, y: Failed) => Failed(x.reasons ::: y.reasons, math.max(x.failedAt, y.failedAt), (x.sources ++ y.sources)) | |
} | |
type ValueOrWhyNot[A] = Either[Failed, A] | |
case class Profile(favoriteColor: Color, wantsKids: Boolean, favoriteHobby: Hobby) | |
object Profile { | |
def fromPersonRecord(personRecord: PersonRecord, attemptTime: TimeCount): Validated[Failed, Profile] = { | |
( | |
personRecord.favoriteColor.toValidated, | |
personRecord.wantsKids.toValidated, | |
personRecord.favoriteHobby.toValidated | |
).mapN{ | |
case (c, wk, h) => Profile(c, wk, h) | |
}.leftMap( | |
_.copy(failedAt = attemptTime) | |
) | |
} | |
} | |
case class PersonRecord(favoriteColor: ValueOrWhyNot[Color], wantsKids: ValueOrWhyNot[Boolean], favoriteHobby: ValueOrWhyNot[Hobby], updateTime: TimeCount) | |
case class Match(p1: Profile, p2: Profile, matchedAt: TimeCount) | |
object Match { | |
def fromProfiles(p1: Profile, p2: Profile, matchAttemptTime: TimeCount): Validated[Failed, Match] = { | |
if (matchScore(p1, p2) >= 2) { | |
Match(p1, p2, matchAttemptTime).valid | |
} else { | |
Failed.one("Profiles complete but the two do not have enough in common.", matchAttemptTime, NonEmptySet.of("Alice", "Bob")).invalid | |
} | |
} | |
def matchScore(p1: Profile, p2: Profile): Int = { | |
implicit class convertBooleanToInt(b: Boolean) { | |
def toInt = if(b) 1 else 0 | |
} | |
NonEmptyList.of( | |
p1.wantsKids == p2.wantsKids, | |
p1.favoriteColor == p2.favoriteColor, | |
p1.favoriteHobby == p2.favoriteHobby | |
).map(_.toInt) | |
.reduce | |
} | |
} | |
var alice: NonEmptyList[PersonRecord] = NonEmptyList.one( | |
PersonRecord( | |
favoriteColor = Failed.one("Alice Color: N/A", failureTime = 0, NonEmptySet.one("Alice")).asLeft, | |
wantsKids = Failed.one("Alice Wants Kids: N/A", failureTime = 0, NonEmptySet.one("Alice")).asLeft, | |
favoriteHobby = Failed.one("Alice Hobby: N/A", failureTime = 0, NonEmptySet.one("Alice")).asLeft, | |
updateTime = 0 | |
) | |
) | |
var bob: NonEmptyList[PersonRecord] = NonEmptyList.one( | |
PersonRecord( | |
favoriteColor = Failed.one("Bob Color: N/A", failureTime = 0, NonEmptySet.one("Bob")).asLeft, | |
wantsKids = Failed.one("Bob Wants Kids: N/A", failureTime = 0, NonEmptySet.one("Bob")).asLeft, | |
favoriteHobby = Failed.one("Bob Hobby: N/A", failureTime = 0, NonEmptySet.one("Bob")).asLeft, | |
updateTime = 0 | |
) | |
) | |
var matchHistory: List[Either[Failed, Match]] = Nil | |
def updateMatchStatus(timeCount: TimeCount): Unit = { | |
val latestMatchStatus = matchRecords( | |
alice.head, | |
bob.head, | |
timeCount | |
) | |
.andThen( _ => Failed.one(reason = "Computer says no.", timeCount, NonEmptySet.of("Bob", "Alice")).invalid[Match]) | |
.toEither | |
matchHistory = latestMatchStatus :: matchHistory | |
} | |
def updateAliceState(record: PersonRecord): Unit = { | |
alice = record :: alice | |
updateMatchStatus(record.updateTime) | |
} | |
def updateBobState(record: PersonRecord): Unit = { | |
bob = record :: bob | |
updateMatchStatus(record.updateTime) | |
} | |
def updateAliceHobby(maybeHobby: ValueOrWhyNot[Hobby], count: TimeCount): Unit = { | |
updateAliceState(alice.head.copy(favoriteHobby = maybeHobby, updateTime = count)) | |
} | |
def updateAliceColor(maybeColor: ValueOrWhyNot[Color], count: TimeCount): Unit = { | |
updateAliceState(alice.head.copy(favoriteColor = maybeColor, updateTime = count)) | |
} | |
def updateAliceWantsKids(maybeWantsKids: ValueOrWhyNot[Boolean], count: TimeCount): Unit = { | |
updateAliceState(alice.head.copy(wantsKids = maybeWantsKids, updateTime = count)) | |
} | |
def updateBobHobby(maybeHobby: ValueOrWhyNot[Hobby], count: TimeCount): Unit = { | |
updateBobState(bob.head.copy(favoriteHobby = maybeHobby, updateTime = count)) | |
} | |
def updateBobColor(maybeColor: ValueOrWhyNot[Color], count: TimeCount): Unit = { | |
updateBobState(bob.head.copy(favoriteColor = maybeColor, updateTime = count)) | |
} | |
def updateBobWantsKids(maybeWantsKids: ValueOrWhyNot[Boolean], count: TimeCount): Unit = { | |
updateBobState(bob.head.copy(wantsKids = maybeWantsKids, updateTime = count)) | |
} | |
def aliceRecordAtTime(count: TimeCount): PersonRecord = alice | |
.filter(_.updateTime <= count) | |
.maxBy(_.updateTime) | |
def bobRecordAtTime(count: TimeCount): PersonRecord = bob | |
.filter(_.updateTime <= count) | |
.maxBy(_.updateTime) | |
def updateEntryTimes: NonEmptyList[TimeCount] = { | |
alice.map(_.updateTime) ::: bob.map(_.updateTime) | |
}.distinct | |
def aliceAndBobRecords: NonEmptyList[(PersonRecord, PersonRecord, TimeCount)] = updateEntryTimes.map { t => | |
( | |
aliceRecordAtTime(t), | |
bobRecordAtTime(t), | |
t | |
) | |
} | |
def matchRecords(record1: PersonRecord, record2: PersonRecord, timeCount: TimeCount): Validated[Failed, Match] = ( | |
Profile.fromPersonRecord(record1, timeCount), | |
Profile.fromPersonRecord(record2, timeCount) | |
).mapN { | |
case (profile1, profile2) => | |
Match.fromProfiles(profile1, profile2, timeCount) | |
}.andThen(identity) | |
def recordMatchability(nel: NonEmptyList[PersonRecord]): NonEmptyList[(Boolean, TimeCount)] = nel.map{ | |
r => | |
val ready = r.favoriteColor.isRight && r.favoriteHobby.isRight && r.wantsKids.isRight | |
(ready, r.updateTime) | |
} | |
case class MatchResult(value: Either[Failed, Match]) extends AnyVal | |
object MatchResult { | |
implicit val show: Show[MatchResult] = { | |
matchResult => | |
matchResult.value match { | |
case Left(failed) => s"Failed because of: ${failed.sources.toNonEmptyList.toList.mkString(",")}, detailed: ${failed.reasons.toList.mkString(",")}, updated at: ${failed.failedAt}" | |
case Right(matched) => matched.toString | |
} | |
} | |
} | |
def reconstructedAliceAndBobCompatibilityOverTime: NonEmptyList[MatchResult] = { | |
aliceAndBobRecords.map { | |
case (aliceRecord, bobRecord, t) => | |
matchRecords(aliceRecord, bobRecord, t).toEither | |
}.map( | |
MatchResult.apply | |
).sortBy( | |
_.value | |
.map(_.matchedAt) | |
.valueOr(_.failedAt) | |
) | |
} | |
def recordedAliceAndBobCompatibilityOverTime: List[MatchResult] = matchHistory | |
.map(MatchResult.apply) | |
.sortBy( | |
_.value | |
.map(_.matchedAt) | |
.valueOr(_.failedAt) | |
) | |
updateMatchStatus(0) | |
updateAliceHobby(Hobby.BirdWatching.asRight, 1) | |
updateBobHobby(Hobby.Sports.asRight, 2) | |
updateAliceColor(Color.Red.asRight, 3) | |
updateAliceWantsKids(false.asRight, 4) | |
updateBobWantsKids(true.asRight, 6) | |
updateBobColor(Color.Red.asRight, 7) | |
updateBobWantsKids(false.asRight, 9) | |
updateAliceWantsKids(Failed.one("Alice wants kids: It's complicated", 12, NonEmptySet.one("Alice")).asLeft, 11) | |
updateAliceWantsKids(true.asRight, 12) | |
println("\n***recorded info over time. shows the bug at the outermost level: `computer says no` ***\n") | |
recordedAliceAndBobCompatibilityOverTime.map(r => println(r.show)) | |
println("\n***alice ready to be matched over time***\n") | |
recordMatchability(alice).sortBy(_._2).map(println) | |
println("\n***bob ready to be matched over time***\n") | |
recordMatchability(bob).sortBy(_._2).map(println) | |
println("\n***reconstructed info over time. ***\n") | |
reconstructedAliceAndBobCompatibilityOverTime.map(r => println(r.show)) | |
} | |
// | |
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Alice Hobby: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 0 | |
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 1 | |
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 2 | |
//Failed because of: Alice,Bob, detailed: Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 3 | |
//Failed because of: Bob, detailed: Bob Color: N/A,Bob Wants Kids: N/A, updated at: 4 | |
//Failed because of: Bob, detailed: Bob Color: N/A, updated at: 6 | |
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 7 | |
//Failed because of: Alice,Bob, detailed: Computer says no., updated at: 9 | |
//Failed because of: Alice, detailed: Alice wants kids: It's complicated, updated at: 11 | |
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 12 | |
// | |
//***alice ready to be matched over time*** | |
// | |
//(false,0) | |
//(false,1) | |
//(false,3) | |
//(true,4) | |
//(false,11) | |
//(true,12) | |
// | |
//***bob ready to be matched over time*** | |
// | |
//(false,0) | |
//(false,2) | |
//(false,6) | |
//(true,7) | |
//(true,9) | |
// | |
//***reconstructed info over time. *** | |
// | |
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Alice Hobby: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 0 | |
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A,Bob Hobby: N/A, updated at: 1 | |
//Failed because of: Alice,Bob, detailed: Alice Color: N/A,Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 2 | |
//Failed because of: Alice,Bob, detailed: Alice Wants Kids: N/A,Bob Color: N/A,Bob Wants Kids: N/A, updated at: 3 | |
//Failed because of: Bob, detailed: Bob Color: N/A,Bob Wants Kids: N/A, updated at: 4 | |
//Failed because of: Bob, detailed: Bob Color: N/A, updated at: 6 | |
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 7 | |
//Match(Profile(Red,false,BirdWatching),Profile(Red,false,Sports),9) | |
//Failed because of: Alice, detailed: Alice wants kids: It's complicated, updated at: 11 | |
//Failed because of: Alice,Bob, detailed: Profiles complete but the two do not have enough in common., updated at: 12 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
added source of failure to make it easier to read.