Skip to content

Instantly share code, notes, and snippets.

@brikis98
Last active August 4, 2017 22:22
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save brikis98/761e4fa7404f6b9803bb to your computer and use it in GitHub Desktop.
Save brikis98/761e4fa7404f6b9803bb to your computer and use it in GitHub Desktop.
An outline of how to de-dupe remote service calls in Play.
// Put this filter early in your filter chain so it can initialize and clean up
// the cache
object CacheFilter extends Filter {
def apply(next: RequestHeader => Future[Result])(request: RequestHeader): Future[Result] = {
def init = RestClient.initCacheForRequest(request)
def cleanup = RestClient.cleanupCacheForRequest(request)
// You have to be very careful with error handling to garauntee the cache gets cleaned
// up, or you'll have a memory leak.
try {
init
next(request).map { result =>
result.body.onDoneEnumerating(cleanup)
}.recover { case t: Throwable =>
cleanup
// Log or re-throw the exception
}
} catch {
case t: Throwable =>
cleanup
// Log or re-throw the exception
}
}
}
object ExampleUsage extends Controller {
def index = Action { implicit request =>
// These two calls should be de-duped, so only one remote call
// is actually made.
val future1 = RestClient.get("http://www.my-site.com/foo")
val future2 = RestClient.get("http://www.my-site.com/foo")
for {
foo1 <- future1
foo2 <- future2
} yield {
// ...
}
}
}
// This is a client you use everywhere in your code to make REST requests.
// It will de-dupe read requests so you never perform the same HTTP GET more
// than once for the same user.
//
// Note that this caching strategy can be used with *any* remote protocol, not
// just HTTP/REST. The only thing you need is:
//
// 1. A way to tell if it's safe to use the cache
// 2. A way to tell if two requests are identical
//
// For example, for REST:
//
// 1. Any GET should be cacheable.
// 2. Two GETs with identical URLs are equal.
//
object RestClient {
// Basicaly a ConcurrentHashMap from request id => a cache of service calls made
// while processing that request.
// For the Cache class, see: https://gist.github.com/brikis98/5843195
private val cache = new Cache[Long, Cache[String, Future[Response]]]()
// Make an HTTP GET request. Assumption: two requests with the same URL are
// identical, so they will be de-duped. The first time there is a unique URL,
// we use WS to actually make the request and store the Future object in the
// cache. The next time we see the same URL, we just return the cached Future.
def get(url: String)(implicit request: RequestHeader): Future[Response] = {
cache.get(request.id).getOrElseUpdate(url, WS.url(url).get())
}
// Initialize the cache for each incoming HTTP request. The best place to call this method
// is from a filter.
def initCacheForRequest(request: RequestHeader): Unit = {
cache.put(request.id, new Cache[String, Future[Response]]())
}
// Once you are doing processing an incoming request, don't forget to clean up the cache,
// or you will have a memory leak. The best place to call this method is from a filter.
def cleanupCacheForRequest(request: RequestHeader): Unit = {
cache.remove(request.id)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment