ScalaCache Documentation

repository·master·Indexed 20 days ago

https://github.com/cb372/scalacache

A facade library providing a unified, idiomatic Scala API for interacting with caching backends including Memcached, Redis, and Caffeine. It features a core Cache trait parameterized by an effect monad F, support for custom binary and JSON (Circe) serialization via the Codec trait, and 'get-or-compute' patterns through caching() and cachingF() methods.

Tokens
9.8K
Snippets
32
Records
44
Agent score
72%

What's inside ScalaCache

  1. Disable cache reads and writes using Flags

    master

    You can temporarily bypass the cache for specific operations by providing an implicit scalacache.Flags instance in the scope of your memoized method. This is useful for scenarios where you need to force a fresh value from a data source (like a database) without updating the cache, or for skipping the cache entirely.

    Flag Behaviors:

    • readsEnabled = false: The cache is ignored. ScalaCache treats the operation as a cache miss, meaning the value is computed (e.g., fetched from a DB) and then subsequently written to the cache.
    • writesEnabled = false: On a cache miss, the value is computed but is not written to the cache.
    • Both false: The cache is neither read from nor written to; the value is always computed and never stored.

    Requirement: Your memoized method must accept an implicit parameter of type Flags. If this parameter is missing, any implicit Flags you provide in the calling scope will be silently ignored.

    import scalacache._
    import cats.effect.IO
    
    // The method must take an implicit Flags parameter
    def getCatWithFlags(id: Int)(implicit flags: Flags): Cat = memoize(None) {
      // Logic to fetch from DB
      Cat(id, s"cat ${id}", "black")
    }.unsafeRunSync()
    
    // Usage: Skipping the cache read
    def getCatMaybeSkippingCache(id: Int, skipCache: Boolean): Cat = {
      implicit val flags = Flags(readsEnabled = !skipCache)
      getCatWithFlags(id)
    }
  2. How cache key generation works for memoization

    master

    ScalaCache uses Scala macros to build cache keys at compile time, avoiding runtime reflection. By default, the cache key is automatically constructed from:

    1. The class or object name.
    2. The name of the enclosing method.
    3. The values of all method parameters.

    For example, a call to Bar.baz(1, "hello")("world") inside an object Bar results in a key like foo.bar.Baz(1, hello)(world).

    If you need to change this behavior, you can provide a custom implementation of MethodCallToStringConverter via CacheConfig.

  3. Use Caffeine as a cache implementation

    master

    To use Caffeine (an in-memory cache), add the scalacache-caffeine dependency to your SBT project.

    You can either use the default settings via CaffeineCache[IO, K, V].unsafeRunSync() or provide a custom com.github.benmanes.caffeine.cache.Cache instance to control parameters like maximumSize.

    // Using default settings
    import scalacache._
    import scalacache.caffeine._
    import cats.effect.{Clock, IO}
    import cats.effect.unsafe.implicits.global
    
    implicit val clock: Clock[IO] = Clock[IO]
    implicit val caffeineCache: Cache[IO, String, String] = CaffeineCache[IO, String, String].unsafeRunSync()
    
    // Using a customized Caffeine instance
    import com.github.benmanes.caffeine.cache.Caffeine
    
    val underlyingCaffeineCache = Caffeine.newBuilder().maximumSize(10000L).build[String, Entry[String]]
    implicit val customisedCaffeineCache: Cache[IO, String, String] = CaffeineCache(underlyingCaffeineCache)
  4. Compress serialized data with GZippingBinaryCodec

    master

    To reduce the size of data sent to your cache, you can decorate an existing codec with GZippingBinaryCodec[A]. This trait automatically applies GZip compression to the encoded Array[Byte] if it exceeds a certain sizeThreshold and handles decompression during retrieval.

    How to use: When defining your codec, extend it with GZippingBinaryCodec[A] as the last trait in the inheritance list.

    Using with standard Java Serialization: To apply GZip compression to the standard ScalaCache binary codec, you can either:

    1. Import scalacache.serialization.gzip.GZippingJavaSerializationCodec._.
    2. Provide an implicit GZippingJavaAnyBinaryCodec at the cache call site.
    // Example of decorating a custom codec with GZip
    // Ensure GZippingBinaryCodec is the right-most extended trait
    trait MyCompressedCodec extends MyBaseCodec with GZippingBinaryCodec[MyType] {
      // implementation
    }
    
    // Or using the built-in Java serialization GZip support
    import scalacache.serialization.gzip.GZippingJavaSerializationCodec._
  5. Create a cache instance

    master

    You must choose a cache implementation (e.g., Caffeine for in-memory, Redis or Memcached for distributed) and define it as an implicit Cache[F, K, V]. The type parameters represent the effect type F, the key type K, and the value type V.

    import scalacache.memcached._
    import scalacache.serialization.binary._
    
    final case class Cat(id: Int, name: String, colour: String)
    
    // The cache must be implicit for the API to find it
    implicit val catsCache: Cache[IO, String, Cat] = MemcachedCache("localhost:11211")
  6. Use Memcached as a cache implementation

    master

    To use Memcached, add the scalacache-memcached dependency to your SBT project. You can initialize a cache using a connection string or by providing a custom MemcachedClient (e.g., from the net.spy.memcached library).

    Key Constraints: Memcached requires ASCII keys with a length $\le$ 250 characters. To handle non-compliant keys, ScalaCache provides two KeySanitizer implementations:

    • ReplaceAndTruncateSanitizer: Replaces non-ASCII characters with underscores and truncates keys to 250 characters. Best for human-readable keys.
    • HashingMemcachedKeySanitizer: Hashes the key to ensure it is valid. This allows any string to be used but makes keys difficult to read during debugging.
    // Using a connection string
    import scalacache._
    import scalacache.memcached._
    import scalacache.serialization.binary._
    import cats.effect.IO
    
    implicit val memcachedCache: Cache[IO, String, String] = MemcachedCache("localhost:11211")
    
    // Using a custom MemcachedClient
    import net.spy.memcached._
    
    val memcachedClient = new MemcachedClient(
      new BinaryConnectionFactory(), 
      AddrUtil.getAddresses("localhost:11211")
    )
    implicit val customisedMemcachedCache: Cache[IO, String, String] = MemcachedCache(memcachedClient)
  7. Use the JSON codec via Circe integration

    master

    To serialize values as JSON, use the scalacache-circe module. This requires adding the dependency to your project and importing the codec.

    Dependency:

    libraryDependencies += "com.github.cb372" %% "scalacache-circe" % "0.28.0"

    Usage: Import the codec and ensure that Circe Encoder[A] and Decoder[A] instances for your types are available in the implicit scope. You can use Circe's automatic derivation or semi-automatic derivation for better performance.

    // 1. Add dependency
    // libraryDependencies += "com.github.cb372" %% "scalacache-circe" % "0.28.0"
    
    // 2. Import codec
    import scalacache.serialization.circe._
    
    // 3. Provide Encoders/Decoders (Example using semi-auto derivation)
    import io.circe._
    import io.circe.generic.semiauto._
    
    case class Cat(name: String)
    implicit val catEncoder: Encoder[Cat] = deriveEncoder[Cat]
    implicit val catDecoder: Decoder[Cat] = deriveDecoder[Cat]
  8. Include class constructor arguments in memoization cache keys

    master

    By default, if a memoized method is inside a class (rather than an object), the cache key only includes the method name and method arguments. It does not include the values passed to the class constructor.

    If your method's logic depends on class constructor parameters, you must configure the cache to include them. You can do this by providing a CacheConfig with MethodCallToStringConverter.includeClassConstructorParams.

    import scalacache.memoization._
    
    // Configure the cache to include constructor arguments in the key
    implicit val cacheConfig: CacheConfig = CacheConfig(
      memoization = MemoizationConfig(MethodCallToStringConverter.includeClassConstructorParams)
    )
    
    class Bar(a: Int) {
      def baz(b: Int): Int = memoizeSync(None) {
        a + b
      }
    }
    
    // With the config above:
    // new Bar(10).baz(42) -> key includes '10' and '42'
    // new Bar(20).baz(42) -> key includes '20' and '42'