On Github MateuszKubuszok / OptimizingHeavyWebServicePresentation
-- 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, ... }, ... ]