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:
@@ -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))
|
||||
|
||||
+7
-48
@@ -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
|
||||
|
||||
Vendored
+10
-68
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-59
@@ -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)
|
||||
}
|
||||
|
||||
+3
-80
@@ -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()
|
||||
}
|
||||
|
||||
+21
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user