Compare commits
No commits in common. "fb54b388191e4e86357031086d7ba3be641b7a52" and "8060ded9475664981f08a2a5eae3de04d14027df" have entirely different histories.
fb54b38819
...
8060ded947
29
.drone.yml
29
.drone.yml
|
@ -1,29 +0,0 @@
|
||||||
---
|
|
||||||
global-variables:
|
|
||||||
gradle-image: &gradle-image gradle:7.1-jdk11
|
|
||||||
|
|
||||||
kind: pipeline
|
|
||||||
name: default
|
|
||||||
type: docker
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: gradle-test
|
|
||||||
image: *gradle-image
|
|
||||||
commands:
|
|
||||||
- gradle test
|
|
||||||
when:
|
|
||||||
branch: develop
|
|
||||||
|
|
||||||
- name: publish
|
|
||||||
image: *gradle-image
|
|
||||||
environment:
|
|
||||||
MAVEN_REPOSITORY_URL: https://archiva.fyloz.dev/repository/internal/
|
|
||||||
MAVEN_REPOSITORY_USERNAME:
|
|
||||||
from_secret: maven_repository_username
|
|
||||||
MAVEN_REPOSITORY_PASSWORD:
|
|
||||||
from_secret: maven_repository_password
|
|
||||||
commands:
|
|
||||||
- gradle publish -Pversion=${DRONE_TAG}
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- tag
|
|
|
@ -1,8 +1,8 @@
|
||||||
group = "dev.fyloz"
|
group = "dev.fyloz"
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.6.10"
|
id("org.jetbrains.kotlin.jvm") version "1.6.10"
|
||||||
id("maven-publish")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -21,39 +21,6 @@ dependencies {
|
||||||
testImplementation("io.mockk:mockk:1.12.2")
|
testImplementation("io.mockk:mockk:1.12.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
tasks.withType<Test> {
|
||||||
publications {
|
|
||||||
create<MavenPublication>("memory-cache") {
|
|
||||||
from(components["kotlin"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
val repoUrl = System.getenv("MAVEN_REPOSITORY_URL")
|
|
||||||
val repoUsername = System.getenv("MAVEN_REPOSITORY_USERNAME")
|
|
||||||
val repoPassword = System.getenv("MAVEN_REPOSITORY_PASSWORD")
|
|
||||||
val repoName = System.getenv("MAVEN_REPOSITORY_NAME") ?: "Archiva"
|
|
||||||
|
|
||||||
if (repoUrl != null && repoUsername != null && repoPassword != null) {
|
|
||||||
url = uri(repoUrl)
|
|
||||||
name = repoName
|
|
||||||
|
|
||||||
credentials {
|
|
||||||
username = repoUsername
|
|
||||||
password = repoPassword
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("Some maven repository credentials were not configured, publishing is not configured")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|
||||||
testLogging {
|
|
||||||
events("failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,2 +1 @@
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
version=dev
|
|
|
@ -1,129 +0,0 @@
|
||||||
package dev.fyloz.memorycache
|
|
||||||
|
|
||||||
import dev.fyloz.memorycache.utils.removeFirst
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A key-value memory cache allowing for expiring keys.
|
|
||||||
*
|
|
||||||
* The key's expiration can be setup with two methods:
|
|
||||||
* - After a number of access to the cache
|
|
||||||
* - After a duration
|
|
||||||
*
|
|
||||||
* Either methods can be used, or both. But at least one need to be configured.
|
|
||||||
* Note that using a single method is more optimized, since the keys can be sorted easily.
|
|
||||||
*
|
|
||||||
* For both methods, the lifetime of the key will be renewed after it has been set or accessed.
|
|
||||||
*
|
|
||||||
* @param K The type of the keys.
|
|
||||||
* @param V The type of the values.
|
|
||||||
* @property maxAccessCount The number of access to the cache before a key expires.
|
|
||||||
* @property maxLifetimeSeconds The duration of a key before its expiration in seconds.
|
|
||||||
*/
|
|
||||||
class ExpiringMemoryCache<K, V>(
|
|
||||||
private val maxAccessCount: Long = -1,
|
|
||||||
private val maxLifetimeSeconds: Long = -1
|
|
||||||
) : BaseMemoryCache<K, V>() {
|
|
||||||
private val keys: TreeSet<ExpiringKey<K>>
|
|
||||||
private var accessCount = 0L
|
|
||||||
|
|
||||||
private val maxLifetimeMillis = maxLifetimeSeconds * 1000
|
|
||||||
private val expireByAccessCount: Boolean = maxAccessCount > 0
|
|
||||||
private val expireByAccessTime: Boolean = maxLifetimeSeconds > 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Determine the correct key comparator to use
|
|
||||||
val keysComparator: Comparator<ExpiringKey<*>> = if (expireByAccessCount) {
|
|
||||||
ExpiringKey.accessCountComparator
|
|
||||||
} else if (expireByAccessTime) {
|
|
||||||
ExpiringKey.timeComparator
|
|
||||||
} else {
|
|
||||||
throw InvalidExpiringCacheOptionsException()
|
|
||||||
}
|
|
||||||
|
|
||||||
keys = TreeSet(keysComparator)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun get(key: K): V? {
|
|
||||||
accessCount++
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
renewKey(key)
|
|
||||||
|
|
||||||
return super.get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun set(key: K, value: V) {
|
|
||||||
super.set(key, value)
|
|
||||||
|
|
||||||
renewKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove(key: K) {
|
|
||||||
super.remove(key)
|
|
||||||
keys.removeFirst { it.key == key }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clear() {
|
|
||||||
super.clear()
|
|
||||||
keys.clear()
|
|
||||||
|
|
||||||
accessCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renewKey(key: K) {
|
|
||||||
keys.removeFirst { it.key == key }
|
|
||||||
keys.add(getNewExpiringKey(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup() {
|
|
||||||
with(keys.iterator()) {
|
|
||||||
while (hasNext()) {
|
|
||||||
val expiringKey = next()
|
|
||||||
if (expireByAccessCount) {
|
|
||||||
if (accessCount - expiringKey.lastAccessCount > maxAccessCount) {
|
|
||||||
removeExpiringKey(expiringKey)
|
|
||||||
} else if (!expireByAccessTime) {
|
|
||||||
break // Keys are sorted, so if this key is not expired, the next ones are not
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expireByAccessTime) {
|
|
||||||
if (getCurrentTime() - expiringKey.lastAccessMillis > maxLifetimeMillis) {
|
|
||||||
removeExpiringKey(expiringKey)
|
|
||||||
} else if (!expireByAccessCount) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MutableIterator<ExpiringKey<K>>.removeExpiringKey(expiringKey: ExpiringKey<K>) {
|
|
||||||
remove()
|
|
||||||
super.remove(expiringKey.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNewExpiringKey(key: K) =
|
|
||||||
ExpiringKey(key, accessCount, getCurrentTime())
|
|
||||||
|
|
||||||
private fun getCurrentTime() =
|
|
||||||
Instant.now().toEpochMilli()
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ExpiringKey<K>(val key: K, var lastAccessCount: Long, var lastAccessMillis: Long) {
|
|
||||||
companion object {
|
|
||||||
internal val accessCountComparator: Comparator<ExpiringKey<*>> =
|
|
||||||
Comparator.comparingLong<ExpiringKey<*>> { it.lastAccessCount }
|
|
||||||
.thenComparingLong { it.lastAccessMillis }
|
|
||||||
.thenComparingInt { it.key.hashCode() }
|
|
||||||
|
|
||||||
internal val timeComparator: Comparator<ExpiringKey<*>> =
|
|
||||||
Comparator.comparingLong<ExpiringKey<*>> { it.lastAccessMillis }
|
|
||||||
.thenComparingLong { it.lastAccessCount }
|
|
||||||
.thenComparingInt { it.key.hashCode() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class InvalidExpiringCacheOptionsException :
|
|
||||||
RuntimeException("An expiration cache must have at least one expiration method configured")
|
|
|
@ -1,41 +0,0 @@
|
||||||
package dev.fyloz.memorycache.utils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the first element matching the given [predicate] in this [MutableIterable].
|
|
||||||
* Returns true if any element was removed.
|
|
||||||
*/
|
|
||||||
inline fun <T> MutableIterable<T>.removeFirst(predicate: (T) -> Boolean): Boolean {
|
|
||||||
with(iterator()) {
|
|
||||||
while (hasNext()) {
|
|
||||||
if (predicate(next())) {
|
|
||||||
remove()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets the nth item in this [Iterable]. */
|
|
||||||
inline fun <T> Iterable<T>.nth(n: Int, predicate: (T) -> Boolean): T? {
|
|
||||||
var matches = 0
|
|
||||||
|
|
||||||
for (item in this) {
|
|
||||||
if (!predicate(item)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
matches++
|
|
||||||
|
|
||||||
if (matches == n) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is this [Int] is even or not. */
|
|
||||||
val Int.isEven: Boolean
|
|
||||||
get() = this % 2 == 0
|
|
|
@ -1,98 +0,0 @@
|
||||||
package dev.fyloz.memorycache
|
|
||||||
|
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
|
||||||
import io.kotest.core.spec.style.ShouldSpec
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class ExpiringMemoryCacheTest : ShouldSpec({
|
|
||||||
context("expiration by access count") {
|
|
||||||
val maxAccessCount = 5L
|
|
||||||
val cache = ExpiringMemoryCache<Int, String>(maxAccessCount = maxAccessCount)
|
|
||||||
|
|
||||||
val accessedKey = 0
|
|
||||||
val expiringKey = Int.MAX_VALUE
|
|
||||||
|
|
||||||
should("removes expired keys") {
|
|
||||||
// Arrange
|
|
||||||
cache[accessedKey] = accessedKey.toString()
|
|
||||||
cache[expiringKey] = expiringKey.toString()
|
|
||||||
|
|
||||||
// Act
|
|
||||||
for (i in 0..maxAccessCount) {
|
|
||||||
cache[accessedKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
val containsUnusedKey = expiringKey in cache
|
|
||||||
containsUnusedKey shouldBe false
|
|
||||||
}
|
|
||||||
|
|
||||||
should("renew keys on access") {
|
|
||||||
// Arrange
|
|
||||||
cache[accessedKey] = accessedKey.toString()
|
|
||||||
cache[expiringKey] = expiringKey.toString()
|
|
||||||
|
|
||||||
for (i in 0 until maxAccessCount - 1) {
|
|
||||||
cache[accessedKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act
|
|
||||||
cache[expiringKey]
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
val containsUnusedKey = expiringKey in cache
|
|
||||||
containsUnusedKey shouldBe true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("expiration by access time") {
|
|
||||||
val maxLifetimeSeconds = 2L
|
|
||||||
val cache = ExpiringMemoryCache<Int, String>(maxLifetimeSeconds = maxLifetimeSeconds)
|
|
||||||
|
|
||||||
val key = Int.MAX_VALUE
|
|
||||||
|
|
||||||
suspend fun wait(seconds: Long) =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
Thread.sleep(seconds * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
should("removes expired keys") {
|
|
||||||
// Arrange
|
|
||||||
cache[key] = key.toString()
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wait(maxLifetimeSeconds)
|
|
||||||
cache[key]
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
val containsKey = key in cache
|
|
||||||
containsKey shouldBe false
|
|
||||||
}
|
|
||||||
|
|
||||||
should("renew keys on access") {
|
|
||||||
// Arrange
|
|
||||||
cache[key] = key.toString()
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wait(maxLifetimeSeconds / 2)
|
|
||||||
cache[key] // Access key before expiration
|
|
||||||
wait(maxLifetimeSeconds / 2)
|
|
||||||
cache[key]
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
val containsKey = key in cache
|
|
||||||
containsKey shouldBe true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("no expiration method defined") {
|
|
||||||
should("throws InvalidExpiringCacheOptionsException") {
|
|
||||||
// Arrange
|
|
||||||
// Act
|
|
||||||
// Assert
|
|
||||||
shouldThrow<InvalidExpiringCacheOptionsException> { ExpiringMemoryCache<Int, String>() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,42 +0,0 @@
|
||||||
package dev.fyloz.memorycache.utils
|
|
||||||
|
|
||||||
import io.kotest.core.spec.style.ShouldSpec
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
|
|
||||||
class CollectionsTest : ShouldSpec({
|
|
||||||
context("removeFirst") {
|
|
||||||
val collection = setOf(1, 2, 3, 4, 5)
|
|
||||||
|
|
||||||
fun getMutableCollection() = mutableSetOf(*collection.toTypedArray())
|
|
||||||
|
|
||||||
var mutableCollection = getMutableCollection()
|
|
||||||
|
|
||||||
beforeEach {
|
|
||||||
mutableCollection = getMutableCollection()
|
|
||||||
}
|
|
||||||
|
|
||||||
should("remove first item matching predicate from collection") {
|
|
||||||
// Arrange
|
|
||||||
val firstEvenItem = mutableCollection.first { it.isEven }
|
|
||||||
|
|
||||||
// Act
|
|
||||||
mutableCollection.removeFirst { it.isEven }
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
val contains = firstEvenItem in mutableCollection
|
|
||||||
contains shouldBe false
|
|
||||||
}
|
|
||||||
|
|
||||||
should("does not remove second item matching predicate from collection") {
|
|
||||||
// Arrange
|
|
||||||
val secondEvenItem = mutableCollection.nth(2) { it.isEven }
|
|
||||||
|
|
||||||
// Act
|
|
||||||
mutableCollection.removeFirst { it.isEven }
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
val contains = secondEvenItem in mutableCollection
|
|
||||||
contains shouldBe true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
Loading…
Reference in New Issue