Skip to content

Commit

Permalink
Update README. Cleanup imports, etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
cb372 committed Nov 16, 2013
1 parent a12ca0e commit 1d31943
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 56 deletions.
115 changes: 101 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,115 @@
# Cacheable

An experimental attempt to recreate the behaviour of Spring's `@Cacheable` annotation, using Scala macros instead of AOP.
A simple and handy library for adding caching to any Scala app with the minimum of fuss.

Example usage:
Cacheable is ideal for caching DB lookups, API calls, or anything else that takes a long time to compute.

The following cache implementations are supported, and it's super-easy to plugin your own implementation:
* Google Guava
* Memcached
* Redis (TODO)
* Ehcache (TODO)

## How to use

```scala
import cacheable._

// Configuration: the cache implementation to use, and how to generate cache keys
implicit val cacheConfig = CacheConfig(new MyCache(), KeyGenerator.defaultGenerator)

def getUser(id: Int): User = {
cacheable {
// Do DB lookup here...
User(id, s"user${id}")
}
def getUser(id: Int): User = cacheable {
// Do DB lookup here...
User(id, s"user${id}")
}
```

This will memoize the result of the `cacheable` block in a cache of your choice.
Did you spot the magic word 'cacheable' in the 'getUser' method? Just adding this keyword will cause the result of the method to be memoized to a cache, so the next time you call the method the result will be retrieved from the cache.

## How it works

Like Spring Cache and similar frameworks, Cacheable automatically builds a cache key based on the method being called. However, it does *not* use AOP. Instead it makes use of Scala macros, so the cache key generation is performed at compile time. Runtime performance overhead is zero, and there is no need to fiddle with AOP configuration.

### Cache key generation

The cache key is built automatically from the class name, the name of the enclosing method (`getUser`), and the values of all of the method's parameters.

## TODO
For example, given the following method:

```scala
package foo

object Bar {
def baz(a: Int, b: String)(c: String): Int = cacheable {
// Reticulating splines...
123
}
}
```

the result of the method call
```scala
val result = Bar.baz(1, "hello")("world")
```

would be cached with the key: `foo.bar.Baz(1, hello)(world)`.

Note that the cache key generation logic is customizable.

## Cache implementations

### Google Guava

SBT:

```
libraryDependencies += "com.github.cb372" %% "cacheable-guava" % "0.1"
```

Usage:

```scala
import cacheable._
import guava._

implicit val cacheConfig = CacheConfig(GuavaCache())
```

This will build a Guava cache with all the default settings. If you want to customize your Guava cache, then build it yourself and pass it to `GuavaCache` like this:

```scala
import cacheable._
import guava._
import com.google.common.cache.CacheBuilder

val underlyingGuavaCache = CacheBuilder.newBuilder().maximumSize(10000L).build[String, Object]
implicit val cacheConfig = CacheConfig(GuavaCache(underlyingGuavaCache))
```

### Memcached

SBT:

* <del>If possible include full package name in cache key</del>
* <del>Write some tests</del>
* Provide a few useful cache implementations: <del>Guava,</del> <del>Memached</del>, Redis, Ehcache
* <del>Add per-entry TTL support<del>
* Improve error messages, expand README
```
libraryDependencies += "com.github.cb372" %% "cacheable-memcached" % "0.1"
```

Usage:

```scala
import cacheable._
import memcached._

implicit val cacheConfig = CacheConfig(MemcachedCache("host:port"))
```

or provide your own Memcached client, like this:

```scala
import cacheable._
import memcached._
import net.spy.memcached.MemcachedClient

val memcachedClient = new MemcachedClient(...)
implicit val cacheConfig = CacheConfig(MemcachedCache(memcachedClient))
```
2 changes: 1 addition & 1 deletion core/src/main/scala/cacheable/CacheConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ package cacheable
*/
case class CacheConfig (
cache: Cache,
keyGenerator: KeyGenerator
keyGenerator: KeyGenerator = KeyGenerator.defaultGenerator
)

Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package cacheable

import scala.language.experimental.macros
import scala.concurrent.duration._

object Cacheable {
package object cacheable {

/**
* Perform the given operation and memoize its result to a cache before returning it.
Expand Down
3 changes: 1 addition & 2 deletions core/src/test/scala/cacheable/CacheKeySpec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cacheable

import org.scalatest._
import cacheable.Cacheable._

/**
*
Expand All @@ -12,7 +11,7 @@ class CacheKeySpec extends FlatSpec with ShouldMatchers with BeforeAndAfter {

behavior of "cache key generation"

val cache = new SimpleCache
val cache = new MockCache
implicit val cacheConfig = CacheConfig(cache, KeyGenerator.defaultGenerator)

before {
Expand Down
1 change: 0 additions & 1 deletion core/src/test/scala/cacheable/CacheableSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cacheable
import org.scalatest.ShouldMatchers
import org.scalatest.FlatSpec

import Cacheable.cacheable
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.duration._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package cacheable
import scala.concurrent.duration.Duration

/**
* A mock cache for use in Sample.scala.
* A mock cache for use in tests and samples.
* Does not support TTL.
*
* Author: c-birchall
* Date: 13/11/07
*/
class SimpleCache extends Cache {
class MockCache extends Cache {

val mmap = collection.mutable.Map[String, Any]()

Expand Down
26 changes: 0 additions & 26 deletions core/src/test/scala/cacheable/Sample.scala

This file was deleted.

2 changes: 0 additions & 2 deletions core/src/test/scala/cacheable/pkg/package.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package cacheable

import cacheable.Cacheable._

/**
*
* Author: c-birchall
Expand Down
30 changes: 30 additions & 0 deletions core/src/test/scala/sample/Sample.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package sample

import cacheable._
import scala.concurrent.duration._
import language.postfixOps

case class User(id: Int, name: String)

/**
* Sample showing how to use cacheable.
*/
object Sample extends App {

class UserRepository {
implicit val cacheConfig = CacheConfig(new MockCache(), KeyGenerator.defaultGenerator)

def getUser(id: Int): User = cacheable {
// Do DB lookup here...
User(id, s"user${id}")
}

def withExpiry(id: Int): User = cacheable(60 seconds) {
// Do DB lookup here...
User(id, s"user${id}")
}

}

}

2 changes: 1 addition & 1 deletion guava/src/main/scala/cacheable/guava/GuavaCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object GuavaCache {
/**
* Create a new Guava cache
*/
def apply: GuavaCache = apply(GCacheBuilder.newBuilder().build[String, Object]())
def apply(): GuavaCache = apply(GCacheBuilder.newBuilder().build[String, Object]())

/**
* Create a new cache utilizing the given underlying Guava cache.
Expand Down
2 changes: 1 addition & 1 deletion guava/src/test/scala/cacheable/guava/GuavaCacheSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import scala.concurrent.duration._
*/
class GuavaCacheSpec extends FlatSpec with ShouldMatchers with BeforeAndAfter {

def newGCache = CacheBuilder.newBuilder().build[String, Object]
def newGCache = CacheBuilder.newBuilder.build[String, Object]

behavior of "get"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ class MemcachedCache(client: MemcachedClient) extends Cache with MemcachedTTLCon
object MemcachedCache {

/**
* Create a Memcached client connecting to localhost:11211 and use that for caching
* Create a Memcached client connecting to localhost:11211 and use it for caching
*/
def apply: MemcachedCache =
apply(new MemcachedClient(new BinaryConnectionFactory(), AddrUtil.getAddresses("localhost:11211")))
def apply(): MemcachedCache = apply("localhost:11211")

/**
* Create a Memcached client connecting to the given host(s) and use it for caching
* @param addressString Address string, with addresses separated by spaces, e.g. "host1:11211 host2:22322"
*/
def apply(addressString: String): MemcachedCache =
apply(new MemcachedClient(new BinaryConnectionFactory(), AddrUtil.getAddresses(addressString)))

/**
* Create a cache that uses the given Memcached client
Expand Down

0 comments on commit 1d31943

Please sign in to comment.