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