Skip to main content

Migration Guide: v0.4.0

This guide covers upgrading from v0.3.x to v0.4.0. The release focuses on distributed caching infrastructure with minimal breaking changes.

Before You Start

Back up your build.sbt and take note of any custom CacheStats imports. The only breaking change is a moved type, which is easy to fix with a find-and-replace.

Summary

v0.4.0 introduces:

  • Pluggable cache backends — Memcached support out of the box, extensible for Redis/Caffeine
  • CacheSerde abstraction — Serialization layer for distributed cache backends
  • DistributedCacheBackend base class — Simplifies implementing network-backed caches
  • Unified CacheStats — Single statistics type across compilation and runtime caching
  • LangCompilerBuilder.withCacheBackend — Custom cache backends for compilation results

Breaking Changes

Import Path Changed

The CacheStats type has moved to a new package. If you import it directly, update the import path or your code will fail to compile.

CacheStats Type Location Changed

Impact: Low. Affects code that imports CacheStats by full path.

The CacheStats type has moved from io.constellation.lang.CacheStats to io.constellation.cache.CacheStats.

// Before (v0.3.x)
import io.constellation.lang.CacheStats

// After (v0.4.0)
import io.constellation.cache.CacheStats

Backward compatibility: The old type is removed, but the new CacheStats includes .hitRate and .entries aliases that match the old API. If you only use these fields, your code works without changes after updating the import.

val stats: CacheStats = cache.stats.unsafeRunSync()
stats.hitRate // Works (alias for hitRatio)
stats.entries // Works (alias for size)
stats.hitRatio // Also works (canonical name)
stats.size // Also works (canonical name)

CompilationCache Internal Changes

Impact: None for external users. The CompilationCache internal implementation changed from a local CacheEntry case class to using the runtime CacheEntry[A] through the CacheBackend interface. This is an internal refactoring with no API changes.

New Features

Pluggable Cache Backends

v0.4.0 introduces a cache backend SPI that allows plugging in distributed caches for both compilation results and module execution caching.

Using the Built-in Memcached Backend

Add the optional dependency:

// build.sbt
libraryDependencies += "io.constellation" %% "constellation-cache-memcached" % "0.4.0"

Configure and use:

import io.constellation.cache.memcached.{MemcachedCacheBackend, MemcachedConfig}

// Single server
MemcachedCacheBackend.resource(MemcachedConfig.single()).use { cache =>
// Use for compilation caching
val compiler = LangCompilerBuilder()
.withCacheBackend(cache)
.build()

// Use for runtime caching
val constellation = ConstellationImpl.builder()
.withBackends(ConstellationBackends(cache = Some(cache)))
.build()

// ... run application
}

// Cluster configuration
MemcachedCacheBackend.resource(MemcachedConfig.cluster(
servers = "mc1:11211,mc2:11211,mc3:11211"
)).use { cache =>
// ...
}

See the Cache Backend Integration Guide for Redis and Caffeine implementations.

Implementing a Custom Backend

For in-process caches (Caffeine, Guava):

import io.constellation.cache.{CacheBackend, CacheEntry, CacheStats}

class CaffeineCacheBackend(underlying: Cache[String, CacheEntry[Any]]) extends CacheBackend {
def get[A](key: String): IO[Option[CacheEntry[A]]] = IO {
Option(underlying.getIfPresent(key))
.filter(!_.isExpired)
.map(_.asInstanceOf[CacheEntry[A]])
}

def set[A](key: String, value: A, ttl: FiniteDuration): IO[Unit] = IO {
underlying.put(key, CacheEntry.create(value, ttl).asInstanceOf[CacheEntry[Any]])
}

// ... implement remaining methods
}

For network-backed caches (Redis, DynamoDB), extend DistributedCacheBackend:

import io.constellation.cache.{CacheSerde, DistributedCacheBackend}

class RedisCacheBackend(redis: RedisCommands[IO, String, Array[Byte]], serde: CacheSerde[Any])
extends DistributedCacheBackend(serde) {

override protected def getBytes(key: String): IO[Option[(Array[Byte], Long, Long)]] =
redis.get(key).map(_.map(bytes => (bytes, 0L, Long.MaxValue)))

override protected def setBytes(key: String, bytes: Array[Byte], ttl: FiniteDuration): IO[Unit] =
redis.setEx(key, ttl, bytes)

override protected def deleteKey(key: String): IO[Boolean] =
redis.del(key).map(_ > 0)

override protected def clearAll: IO[Unit] =
redis.flushDb

override protected def getStats: IO[CacheStats] =
IO.pure(CacheStats.empty)
}

CacheSerde for Serialization

Distributed cache backends need to serialize values to bytes. The CacheSerde[A] type class handles this:

trait CacheSerde[A] {
def serialize(value: A): Array[Byte]
def deserialize(bytes: Array[Byte]): A
}

Built-in implementations:

SerdeUse Case
CacheSerde.cvalueSerdeConstellation CValue types (JSON)
CacheSerde.mapCValueSerdeMap[String, CValue] inputs/outputs
CacheSerde.javaSerde[A]Any java.io.Serializable
CacheSerde.anySerdeDefault: JSON for CValue, Java fallback

Custom serde example:

import io.constellation.cache.CacheSerde

given mySerde: CacheSerde[MyType] = new CacheSerde[MyType] {
def serialize(value: MyType): Array[Byte] = value.toProto.toByteArray
def deserialize(bytes: Array[Byte]): MyType = MyProto.parseFrom(bytes).toMyType
}

LangCompilerBuilder.withCacheBackend

Configure custom cache backends for compilation results:

import io.constellation.lang.LangCompilerBuilder

// Before (v0.3.x) - only in-memory caching
val compiler = LangCompilerBuilder()
.withCaching(maxEntries = 1000, ttl = 1.hour)
.build()

// After (v0.4.0) - pluggable backend
val compiler = LangCompilerBuilder()
.withCacheBackend(myRedisCache)
.build()

This enables sharing compilation caches across multiple Constellation instances.

ModuleOptionsExecutor.createWithCacheBackend

Wire a cache backend into the module execution layer:

import io.constellation.lang.compiler.ModuleOptionsExecutor

for {
executor <- ModuleOptionsExecutor.createWithCacheBackend(
cacheBackend = Some(myDistributedCache),
scheduler = myScheduler
)
// Module results cached in distributed backend
} yield ()

Upgrade Steps

Step 1: Update Dependencies

// build.sbt
libraryDependencies ++= Seq(
"io.constellation" %% "constellation-core" % "0.4.0",
"io.constellation" %% "constellation-runtime" % "0.4.0",
"io.constellation" %% "constellation-lang-compiler" % "0.4.0",
// ... other modules
)

Step 2: Fix CacheStats Import (If Applicable)

If you import CacheStats directly:

// Change this:
import io.constellation.lang.CacheStats

// To this:
import io.constellation.cache.CacheStats

Step 3: (Optional) Add Distributed Caching

If you want to share caches across instances:

// Add optional dependency
libraryDependencies += "io.constellation" %% "constellation-cache-memcached" % "0.4.0"

// Configure in your application
import io.constellation.cache.memcached.{MemcachedCacheBackend, MemcachedConfig}

MemcachedCacheBackend.resource(MemcachedConfig.fromEnv()).use { cache =>
val compiler = LangCompilerBuilder()
.withCacheBackend(cache)
.build()

val constellation = ConstellationImpl.builder()
.withBackends(ConstellationBackends(cache = Some(cache)))
.build()

// ... run application
}

Step 4: Verify Tests Pass

make test

Step 5: Monitor Cache Metrics

After deploying v0.4.0 with distributed caching:

# Check cache hit rate
curl http://localhost:8080/metrics | jq '.cache'

# Expected output:
{
"hits": 1234,
"misses": 56,
"evictions": 0,
"size": 89,
"hitRate": 0.956,
"entries": 89
}

Rollback Procedure

Rollback Is Safe

Rolling back to v0.3.x only requires reverting dependency versions and removing v0.4.0-specific API calls. No data migration is needed.

If you need to rollback to v0.3.x:

Step 1: Revert Dependencies

// build.sbt
libraryDependencies ++= Seq(
"io.constellation" %% "constellation-core" % "0.3.0",
// ... other modules at 0.3.0
)

Step 2: Remove v0.4.0 Features

Remove any usage of new v0.4.0 APIs:

// Remove distributed cache backend configuration
// val compiler = LangCompilerBuilder().withCacheBackend(cache).build()

// Revert to in-memory caching
val compiler = LangCompilerBuilder()
.withCaching(maxEntries = 1000, ttl = 1.hour)
.build()

Step 3: Fix CacheStats Import

// Revert to:
import io.constellation.lang.CacheStats

Step 4: Rebuild and Deploy

make clean
make compile
make test
make assembly

API Compatibility Matrix

APIv0.3.xv0.4.0Notes
CacheBackend traitAvailableAvailableNo changes
InMemoryCacheBackendAvailableAvailableNo changes
CachingLangCompilerAvailableAvailableNo changes
CacheStatsio.constellation.langio.constellation.cacheMoved
CacheSerdeN/ANewSerialization abstraction
DistributedCacheBackendN/ANewBase for network caches
MemcachedCacheBackendN/ANewOptional module
LangCompilerBuilder.withCacheBackendN/ANewCustom compilation cache
ModuleOptionsExecutor.createWithCacheBackendN/ANewCustom execution cache