-- Donald Knuth
trait Service[Request, Response] {
def apply(request: Request): Future[Response]
}
object Service {
def apply[Request, Response](body: Request => Future[Response]) =
new Service[Request, Response] {
override def apply(request: Request) = body(request)
}
}
def monitored[Request, Response](name: String)
(service: Service[Request, Response]) =
new Service[Request, Response] {
override def apply(request: Request) = {
NewRelic.incrementCounter(name)
val start = System.currentTimeMillis
val response = service(request)
response onComplete { case _ =>
val time = System.currentTimeMillis - start
NewRelic.recordResponseTimeMetric(name, time)
}
response onError { case ex => NewRelic.noticeError(ex) }
response
}
}
val getUsersBillings: Service[UsersBillingsReq, UsersBillingsRes] =
monitored("UserServices.getUserBillings") {
Service { request =>
Future {
val formattedBillings = for {
user <- userRepository.fetchUsers(request.userIds)
contract <- user.contracts
billings <- contract.billings
} yield formatBilling(user, contract, billing)
UsersBillingsRes(formattedBillings)
}
}
}
// avg. user has 3 contacts
// avg. contract has 2 billings
val formattedBillings = for {
user <- userRepository.fetchUsers(request.userIds)
// 1 DB query
contract <- user.contracts
// next 3 DB queries on avg.
billing <- contracts.billings
// next 3*2=6 DB queries on avg.
} yield formatBilling(user, contract, billing)
// total 10 queries on avg.
// avg. user has 3 contacts
// avg. contract has 2 billings
val users = userRepository.fetchUsers(request.userIds)
.map(user => (user.id, user)).toMap
val contracts = contractRepository.findForUserIds(users.keys)
.map(contract => (contract.id, contract)).toMap
val billings = billingRepository.findByContractIds(contracts.keys)
// 3 queries in total
val formattedBillings = for {
billing <- billings
contract <- contract.get(billing.contractId).toSeq
user <- users.get(contract.userId).toSeq
} yield formatBilling(user, contract, billing)
Future {
val users = userRepository.fetchUsers(request.userIds)
.map(user => (user.id, user)).toMap
val contracts = contractRepository.findForUserIds(users.keys)
.map(contract => (contract.id, contract)).toMap
val billings = billingRepository.findByContractIds(contracts.keys)
val formattedBillings = for {
billing <- billings
contract <- contract.get(billing.contractId).toSeq
user <- users.get(contract.userId).toSeq
} yield formatBilling(user, contract, billing)
UsersBillingsRes(formattedBillings)
}
{
"EntityKey(user, 1)" : {
"user-billings-1,2,5": "",
...
},
"EntityKey(user, 2)" : {
"user-billings-1,2,5": "",
...
},
...
"user-billings-1,2,5": [serialized value],
...
}
case class EntityKey(type: String, id: String)
trait CacheContext[T] {
def entityKeys(value: T): Seq[EntityKey] // entities result depends on
def serializer: Serializer[T] // T -> String
def deserializer: Deserializer[T] // String -> T
}
trait CacheHandler {
def getOrPut[T](valueKey: String, ttl: Duration)
(valueF => Future[T]))
(implicit cacheContext: CacheContext[T],
executionContext: ExecutionContext): Future[T]
def invalidate(entityKeys: EntityKey*)
}
def get[T](key: String)
(implicit cacheContext: CacheContext[T],
executionContext: ExecutionContext): Future[T] =
redisClient.mget(key) map { gets =>
gets.headOption map (_.toArray) map cacheContext.deserializer
}
def put[T](valueKey: String, value: T, ttl: Duration)
(implicit cacheContext: CacheContext[T],
executionContext: ExecutionContext): Future[Unit] = {
val entityKeys = cacheContext.entityKeys(value)
val bytes = cacheContext.serializer(value).bytes
val transaction = redisClient.multi()
transaction.setex(valueKey, ttl.toSeconds, bytes)
entityKeys map (_.toString) map { entityKey =>
transaction.hmset(entityKey, Map(valueKey -> Array[Bytes]()))
transaction.expire(entityKey, maxTtl.toSeconds)
}
transaction.exec() map (())
}
def getOrPut[T](valueKey: String, ttl: Duration)
(valueF: => Future[T]))
(implicit cacheContext: CacheContext[T],
executionContext: ExecutionContext): Future[T] =
get[T](valueKey) flatMap { optionValue =>
optionValue map Future.successful getOrElse {
for {
value <- valueF
_ <- put[T](valueKey, value, ttl)
} yield value
}
}
val transaction = redisClient.multi()
val keys = entityKeys map (_.toString) map { key =>
key -> transaction.hgetall(key) }
for {
_ <- transaction.exec()
invTransaction = redisClient.multi()
allInvalidatedKeys <- Future.sequence(keys map { case (key, keyMapF) =>
keyMapF map { keyMap =>
val invKeys = keyMap.keys
invTransaction.hdel(key, invKeys:_*)
invKeys
}
}) map (_.flatten)
_ = invTransaction.del(allInvalidatedKeys:_*)
_ <- invTransaction.exec()
} yield ()
implicit val userBillingsContext = new CacheContext[UsersBillingsRes] {
def entityKeys(value: UsersBillingsRes): Seq[EntityKey] = ...
def serializer: Serializer[UsersBillingsRes] = ...
def deserializer: Deserializer[UsersBillingsRes] = ...
}
cacheHandler.getOrPut[UsersBillingsRes](
s"user-billings-${request.userIds.mkString}", 10 minutes) {
Future {
// ...
UsersBillingsRes(formattedBillings)
}
}
trait UsersControllerImpl extends UsersController {
def getBillingsForCurrentUserAndContractor(userId: Long) =
authenticatedRequest { request =>
val observedEntityId = userId
val observedEntityType = "user"
val currentUserId = currentUser.id
for {
result <- userServices.getContractBillingsForPair(
ContractBillingsForPairReuqest(userId, currentUserId))
} yield {
// create JSON from user
}
}
}
def getOrPut[T](valueKey: String, ttl: Duration, request: Request)
(valueF => Future[T]))
(implicit cacheContext: RequestCacheContext[T],
executionContext: ExecutionContext): Future[T] = {
val uri = request.uri
val header = request.headers.get(Headers.Authentication)
.getOrElse("")
val params = request.params.map { case (name, value) =>
name + ":" + value.sorted.toString
}.toSeq.sorted
val requestSuffix = s"$uri-$header-${params.mkString}"
getOrPut[T](s"$value-$requestSuffix", ttl)(valueF)
}
def getBillingsForCurrentUserAndContractor(userId: Long) =
authenticatedRequest { request =>
val observedEntityId = userId
val currentUserId = currentUser.id
cacheHandler.getOrPut[BillingJsonResult](
"api-billings-for-$userId", 10 minutes, request) {
for {
result <- userServices.getContractBillingsForPair(
ContractBillingsForPairReuqest(userId, currentUserId))
} yield {
// create JSON from user
}
}
}
val users = userRepository.fetchUsers(request.userIds)
.map(user => (user.id, user)).toMap
val contracts = contractRepository.findForUserIds(users.keys)
.map(contract => (contract.id, contract)).toMap
val billings = billingRepository.findByContractIds(contracts.keys)
val formattedBillings = for {
billing <- billings
contract <- contract.get(billing.contractId).toSeq
user <- users.get(contract.userId).toSeq
} yield formatBilling(user, contract, billing)
UsersBillingsRes(formattedBillings)
[
{
"id": 5543,
"date": "2015-10-23",
...
"_links": ...,
"_embedded": {
"user": {
"id": 1,
"name": "John",
"surname": "Smith",
...
},
"contract": {
"id": 646,
...
},
"billings": {
"id": 766,
...
}
}
},
...
]
No!
We actually needed:
// UserId -> Billings
val billingsByUsers: Map[Long, Seq[Billing]] =
billingRepository.findByUserIds(request.userIds)
val formattedBillings = for {
(userId, billings) <- billingsByUsers
billing <- billings
contractId = billing.contractId
} yield formatBilling(userId, contractId, billing)
UsersBillingsResponse(formattedBillings)
[
{
"id": 5543,
"date": "2015-10-23",
"userId": 1,
"contractId": 646,
...
},
...
]