refactor(infra-cache): Refine module with Kotlin idioms and robust tests

This commit introduces a comprehensive refactoring of the cache module to improve code consistency, API ergonomics, and test robustness.

Code Refinements & Improvements
Standardized on kotlin.time: Replaced all usages of java.time.Instant and java.time.Duration with their kotlin.time counterparts (Instant, Duration). This aligns the module with the project-wide standard established in the core module and avoids type conversions.

Added Idiomatic Kotlin API: Introduced inline extension functions with reified type parameters for get() and multiGet(). This allows for a cleaner, more type-safe call syntax (e.g., cache.get<User>("key")) for Kotlin consumers.

Code Cleanup: Removed redundant @OptIn(ExperimentalTime::class) annotations from data classes by setting the compiler option at the module level in cache-api/build.gradle.kts.

Testing Enhancements
Stabilized Offline-Mode Tests: Re-implemented the previously disabled offline capability tests. The new approach uses MockK to simulate RedisConnectionFailureException instead of trying to stop/start the Testcontainer. This allows for reliable and robust testing of the "dirty key" synchronization logic.

Fixed Compilation Errors: Resolved various compilation errors in the test suite that arose from the type refactoring and incorrect mock setups.
This commit is contained in:
stefan
2025-08-09 14:57:44 +02:00
parent 4f67379b42
commit 7592adfbb5
10 changed files with 207 additions and 579 deletions
+7
View File
@@ -5,6 +5,13 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
// Erlaubt die Verwendung der kotlin.time API im gesamten Modul
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
}
}
dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
implementation(platform(projects.platform.platformBom))
@@ -1,68 +1,27 @@
package at.mocode.infrastructure.cache.api
import java.time.Duration
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
/**
* Configuration for the distributed cache.
*/
interface CacheConfiguration {
/**
* Default time-to-live for cache entries.
* If null, entries do not expire by default.
*/
val defaultTtl: Duration?
/**
* Maximum number of entries to store in the local cache.
* If null, there is no limit.
*/
val localCacheMaxSize: Int?
/**
* Whether to enable offline mode.
* If true, the cache will store entries locally when offline
* and synchronize them when online.
*/
val offlineModeEnabled: Boolean
/**
* How often to attempt synchronization in offline mode.
*/
val synchronizationInterval: Duration
/**
* Maximum age of entries to keep in the local cache when offline.
* If null, entries do not expire when offline.
*/
val offlineEntryMaxAge: Duration?
/**
* Prefix to add to all cache keys.
* This can be used to namespace cache entries.
*/
val keyPrefix: String
/**
* Whether to compress cache entries.
*/
val compressionEnabled: Boolean
/**
* Threshold in bytes above which to compress cache entries.
* Only used if compressionEnabled is true.
*/
val compressionThreshold: Int
}
/**
* Default implementation of CacheConfiguration.
*/
data class DefaultCacheConfiguration(
override val defaultTtl: Duration? = Duration.ofHours(1),
override val defaultTtl: Duration? = 1.hours,
override val localCacheMaxSize: Int? = 10000,
override val offlineModeEnabled: Boolean = true,
override val synchronizationInterval: Duration = Duration.ofMinutes(5),
override val offlineEntryMaxAge: Duration? = Duration.ofDays(7),
override val synchronizationInterval: Duration = 5.minutes,
override val offlineEntryMaxAge: Duration? = 7.days,
override val keyPrefix: String = "",
override val compressionEnabled: Boolean = true,
override val compressionThreshold: Int = 1024
@@ -1,96 +1,38 @@
package at.mocode.infrastructure.cache.api
import java.time.Instant
import kotlin.time.Clock
import kotlin.time.Instant
/**
* Represents an entry in the cache with metadata for offline capability.
*
* @param key The key of the cache entry
* @param value The value stored in the cache
* @param createdAt When the entry was created
* @param expiresAt When the entry expires, or null if it doesn't expire
* @param lastModifiedAt When the entry was last modified
* @param isDirty Whether the entry has been modified locally and needs to be synchronized
* @param isLocal Whether the entry is only stored locally (not yet synchronized)
*/
data class CacheEntry<T : Any>(
val key: String,
val value: T,
val createdAt: Instant = Instant.now(),
val createdAt: Instant = Clock.System.now(),
val expiresAt: Instant? = null,
val lastModifiedAt: Instant = Instant.now(),
val lastModifiedAt: Instant = Clock.System.now(),
val isDirty: Boolean = false,
val isLocal: Boolean = false
) {
/**
* Checks if the entry is expired.
*
* @return true if the entry is expired, false otherwise
*/
fun isExpired(): Boolean {
return expiresAt?.isBefore(Instant.now()) ?: false
return expiresAt?.let { it < Clock.System.now() } ?: false
}
/**
* Creates a new entry with the isDirty flag set to true.
*
* @return A new CacheEntry with isDirty set to true
*/
fun markDirty(): CacheEntry<T> {
return copy(
isDirty = true,
lastModifiedAt = Instant.now()
)
return copy(isDirty = true, lastModifiedAt = Clock.System.now())
}
/**
* Creates a new entry with the isDirty flag set to false.
*
* @return A new CacheEntry with isDirty set to false
*/
fun markClean(): CacheEntry<T> {
return copy(
isDirty = false,
isLocal = false,
lastModifiedAt = Instant.now()
)
return copy(isDirty = false, isLocal = false, lastModifiedAt = Clock.System.now())
}
/**
* Creates a new entry with the isLocal flag set to true.
*
* @return A new CacheEntry with isLocal set to true
*/
fun markLocal(): CacheEntry<T> {
return copy(
isLocal = true,
lastModifiedAt = Instant.now()
)
return copy(isLocal = true, lastModifiedAt = Clock.System.now())
}
/**
* Creates a new entry with an updated value.
*
* @param newValue The new value
* @return A new CacheEntry with the updated value
*/
fun updateValue(newValue: T): CacheEntry<T> {
return copy(
value = newValue,
lastModifiedAt = Instant.now()
)
return copy(value = newValue, lastModifiedAt = Clock.System.now())
}
/**
* Creates a new entry with an updated expiration time.
*
* @param newExpiresAt The new expiration time
* @return A new CacheEntry with the updated expiration time
*/
fun updateExpiration(newExpiresAt: Instant?): CacheEntry<T> {
return copy(
expiresAt = newExpiresAt,
lastModifiedAt = Instant.now()
)
return copy(expiresAt = newExpiresAt, lastModifiedAt = Clock.System.now())
}
}
@@ -1,76 +1,19 @@
package at.mocode.infrastructure.cache.api
import java.time.Instant
import kotlin.time.Instant
/**
* Represents the connection status of the cache.
*/
enum class ConnectionState {
/**
* The cache is connected to the remote server.
*/
CONNECTED,
/**
* The cache is disconnected from the remote server.
*/
DISCONNECTED,
/**
* The cache is attempting to reconnect to the remote server.
*/
RECONNECTING
CONNECTED, DISCONNECTED, RECONNECTING
}
/**
* Interface for tracking the connection status of the cache.
*/
interface ConnectionStatusTracker {
/**
* Gets the current connection state.
*
* @return The current connection state
*/
fun getConnectionState(): ConnectionState
/**
* Gets the time when the connection state last changed.
*
* @return The time when the connection state last changed
*/
fun getLastStateChangeTime(): Instant
/**
* Registers a listener to be notified when the connection state changes.
*
* @param listener The listener to register
*/
fun registerConnectionListener(listener: ConnectionStateListener)
/**
* Unregisters a connection state listener.
*
* @param listener The listener to unregister
*/
fun unregisterConnectionListener(listener: ConnectionStateListener)
/**
* Checks if the cache is currently connected.
*
* @return true if the cache is connected, false otherwise
*/
fun isConnected(): Boolean = getConnectionState() == ConnectionState.CONNECTED
}
/**
* Listener for connection state changes.
*/
interface ConnectionStateListener {
/**
* Called when the connection state changes.
*
* @param newState The new connection state
* @param timestamp The time when the state changed
*/
fun onConnectionStateChanged(newState: ConnectionState, timestamp: Instant)
}
@@ -1,94 +1,17 @@
package at.mocode.infrastructure.cache.api
import java.time.Duration
import kotlin.time.Duration
/**
* Interface for a distributed cache that supports offline capability.
* This cache can be used to store and retrieve data across multiple instances
* and provides mechanisms for offline operation.
*/
interface DistributedCache {
/**
* Retrieves a value from the cache.
*
* @param key The key to retrieve
* @return The value associated with the key, or null if not found
*/
fun <T : Any> get(key: String, clazz: Class<T>): T?
/**
* Stores a value in the cache with an optional time-to-live.
*
* @param key The key to store the value under
* @param value The value to store
* @param ttl Optional time-to-live for the cache entry
*/
fun <T : Any> set(key: String, value: T, ttl: Duration? = null)
/**
* Removes a value from the cache.
*
* @param key The key to remove
*/
fun <T : Any> set(key: String, value: T, ttl: Duration? = null) // Geändert
fun delete(key: String)
/**
* Checks if a key exists in the cache.
*
* @param key The key to check
* @return true if the key exists, false otherwise
*/
fun exists(key: String): Boolean
/**
* Retrieves multiple values from the cache.
*
* @param keys The keys to retrieve
* @return A map of keys to values, with missing keys omitted
*/
fun <T : Any> multiGet(keys: Collection<String>, clazz: Class<T>): Map<String, T>
/**
* Stores multiple values in the cache with an optional time-to-live.
*
* @param entries The key-value pairs to store
* @param ttl Optional time-to-live for the cache entries
*/
fun <T : Any> multiSet(entries: Map<String, T>, ttl: Duration? = null)
/**
* Removes multiple values from the cache.
*
* @param keys The keys to remove
*/
fun <T : Any> multiSet(entries: Map<String, T>, ttl: Duration? = null) // Geändert
fun multiDelete(keys: Collection<String>)
/**
* Synchronizes the local cache with the distributed cache.
* This is used to ensure that the local cache is up-to-date with the distributed cache
* after being offline.
*
* @param keys Optional collection of keys to synchronize. If null, all keys are synchronized.
*/
fun synchronize(keys: Collection<String>? = null)
/**
* Marks a key as dirty, indicating that it has been modified locally
* and needs to be synchronized with the distributed cache.
*
* @param key The key to mark as dirty
*/
fun markDirty(key: String)
/**
* Gets all keys that have been marked as dirty.
*
* @return A collection of dirty keys
*/
fun getDirtyKeys(): Collection<String>
/**
* Clears all entries from the cache.
*/
fun clear()
}
@@ -0,0 +1,21 @@
package at.mocode.infrastructure.cache.api
/**
* Kotlin-idiomatic extension function to retrieve a value from the cache
* using reified types.
*
* Example: `val user = cache.get<User>("user:123")`
*/
inline fun <reified T : Any> DistributedCache.get(key: String): T? {
return this.get(key, T::class.java)
}
/**
* Kotlin-idiomatic extension function to retrieve multiple values from the cache
* using reified types.
*
* Example: `val users = cache.multiGet<User>(listOf("user:123", "user:124"))`
*/
inline fun <reified T : Any> DistributedCache.multiGet(keys: Collection<String>): Map<String, T> {
return this.multiGet(keys, T::class.java)
}