From 907841ee5083d58b455a060aae33418fc0625550 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 17 Apr 2024 13:42:47 +0400 Subject: [PATCH 1/6] Add configurable cache size and element expiration time --- .../zephyr/cfg/CachesConfiguration.kt | 31 ++++++++++++++ .../zephyr/cfg/EventProcessorCfg.kt | 5 +++ .../zephyr/impl/cache/LRUCache.kt | 42 +++++++++++++++++++ .../scale/ZephyrScaleEventProcessorImpl.kt | 7 +++- .../zephyr/impl/cache/LRUCacheTest.kt | 40 ++++++++++++++++++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt create mode 100644 src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt create mode 100644 src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt new file mode 100644 index 0000000..7c446b2 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.dataprocessor.zephyr.cfg + +import java.util.concurrent.TimeUnit + +class CachesConfiguration( + val cycles: CacheConfiguration = CacheConfiguration( + expireAfterSeconds = TimeUnit.DAYS.toSeconds(1), + size = 100, + ) +) + +class CacheConfiguration( + val expireAfterSeconds: Long, + val size: Int = 100, +) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/EventProcessorCfg.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/EventProcessorCfg.kt index a47d7c8..416c446 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/EventProcessorCfg.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/EventProcessorCfg.kt @@ -80,6 +80,11 @@ class EventProcessorCfg( * Mapping between custom field to set in execution and the value it should have */ val customFields: Map = emptyMap(), + + /** + * Configuration that will be applied to the internal caches inside the zephyr processors (e.g. cycles cache) + */ + val cachesConfiguration: CachesConfiguration = CachesConfiguration(), ) { val issueRegexp: Regex = issueFormat.toPattern().toRegex() init { diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt new file mode 100644 index 0000000..5948de6 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.dataprocessor.zephyr.impl.cache + +import org.apache.commons.collections4.map.LRUMap +import java.util.function.LongSupplier + +internal class LRUCache( + size: Int, + private val expireAfterMillis: Long, + private val timeSource: LongSupplier = LongSupplier(System::currentTimeMillis), +) { + init { + require(size > 0) { "size must be positive but was $size" } + require(expireAfterMillis > 0) { "expireAfterMillis must be positive but was $expireAfterMillis" } + } + private class TimestampedValue(val value: T, val createdAt: Long) + + private val cache = LRUMap>(size) + + operator fun get(key: K): V? = cache[key]?.takeUnless { + timeSource.asLong - it.createdAt > expireAfterMillis + }?.value + + operator fun set(key: K, value: V) { + cache[key] = TimestampedValue(value, timeSource.asLong) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt index 56e7a85..c4add39 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt @@ -20,6 +20,7 @@ import com.exactpro.th2.common.grpc.EventStatus import com.exactpro.th2.dataprocessor.zephyr.cfg.EventProcessorCfg import com.exactpro.th2.dataprocessor.zephyr.cfg.TestExecutionMode import com.exactpro.th2.dataprocessor.zephyr.impl.AbstractZephyrProcessor +import com.exactpro.th2.dataprocessor.zephyr.impl.cache.LRUCache import com.exactpro.th2.dataprocessor.zephyr.impl.scale.extractors.CustomValueExtractor import com.exactpro.th2.dataprocessor.zephyr.impl.scale.extractors.ExtractionContext import com.exactpro.th2.dataprocessor.zephyr.impl.scale.extractors.createCustomValueExtractors @@ -38,7 +39,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import mu.KotlinLogging -import org.apache.commons.collections4.map.LRUMap import javax.annotation.concurrent.GuardedBy class ZephyrScaleEventProcessorImpl( @@ -77,7 +77,9 @@ class ZephyrScaleEventProcessorImpl( private val lock = Mutex() @GuardedBy("lock") - private val cycleCache = LRUMap(100) + private val cycleCaches: Map> = configurations.associate { + it.destination to it.cachesConfiguration.cycles.run { LRUCache(size, expireAfterSeconds * 1_000) } + } override suspend fun EventProcessorContext.processEvent( eventName: String, @@ -167,6 +169,7 @@ class ZephyrScaleEventProcessorImpl( // We cache the result because the search for cycle by name takes a lot of time val cacheKey = CycleCacheKey(project.id, version.name, cycleName) return lock.withLock { + val cycleCache = cycleCaches.getValue(configuration.destination) val cachedCycle = cycleCache[cacheKey] cachedCycle?.apply { LOGGER.trace { "Cycle cache hit. Key: $cacheKey, Value: $key ($name)" } } ?: zephyr.getCycle(project, version, folder = null, cycleName)?.also { cycleCache[cacheKey] = it } diff --git a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt new file mode 100644 index 0000000..7d34cac --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt @@ -0,0 +1,40 @@ +package com.exactpro.th2.dataprocessor.zephyr.impl.cache + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.util.function.LongSupplier + +class LRUCacheTest { + @Test + fun `returns null if not value`() { + val cache = createCache() + assertNull(cache["key"], "unexpected value") + } + + @Test + fun `returns value if not expired yet`() { + val cache = createCache() + cache["key"] = 42 + assertEquals(42, cache["key"], "unexpected value") + } + + @Test + fun `does not return value if it is expired`() { + val expireAfterMillis: Long = 1000 + val time = System.currentTimeMillis() + val timeSource = mock { + on { asLong } doReturn time doReturn time + expireAfterMillis + 1 + } + val cache = LRUCache(size = 10, expireAfterMillis = expireAfterMillis, timeSource) + cache["key"] = 42 + assertNull(cache["key"], "value should be expired") + } + + private fun createCache(size: Int = 10, expireAfterMillis: Long = 1000): LRUCache { + val cache = LRUCache(size = size, expireAfterMillis = expireAfterMillis) + return cache + } +} \ No newline at end of file From f95c747cd8a39bd0969bdb5087b5f88282e652a3 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 17 Apr 2024 13:47:10 +0400 Subject: [PATCH 2/6] Update readme and version --- README.md | 4 ++++ gradle.properties | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 784d583..ae6f325 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,10 @@ Contains parameters for synchronization with Zephyr type: jira extract: VERSION ``` ++ **cachesConfiguration** - configuration that will be applied to the internal caches inside the zephyr processors (e.g. cycles cache) + + **cycle** - configuration for cycle caching + + **size** - cache size. Default value is 100. + + **expireAfterSeconds** - element expiration time in seconds. Default value is 86400 (1 day). ##### Strategies (only for Zephyr Squad) diff --git a/gradle.properties b/gradle.properties index eeec85c..133a1bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ kotlin.code.style=official kotlin_version=1.6.21 -release_version=0.3.0 +release_version=0.4.0 description = 'th2 data processor for Zephyr synchronization' vcs_url=https://github.com/th2-net/th2-data-processor-zephyr.git \ No newline at end of file From 557a2b4ae9218b2adccff378af8e9f6fea9bae35 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 17 Apr 2024 13:48:59 +0400 Subject: [PATCH 3/6] Add missing copyright --- .../zephyr/impl/cache/LRUCacheTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt index 7d34cac..bfdc1df 100644 --- a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt +++ b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.exactpro.th2.dataprocessor.zephyr.impl.cache import org.junit.jupiter.api.Assertions.assertEquals From 6fbe8b56d96f4c0a10d269388215fe0430f5a360 Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 13 May 2024 17:06:51 +0400 Subject: [PATCH 4/6] Add invalidateAt parameter to cache configuration --- README.md | 2 + build.gradle | 2 + .../zephyr/cfg/CachesConfiguration.kt | 2 + .../zephyr/cfg/ZephyrSynchronizationCfg.kt | 2 + .../zephyr/impl/cache/LRUCache.kt | 57 +++++++++++++++--- .../scale/ZephyrScaleEventProcessorImpl.kt | 4 +- .../cfg/TestZephyrSynchronizationCfg.kt | 60 +++++++++++++++++++ .../zephyr/impl/cache/LRUCacheTest.kt | 30 ++++++++-- 8 files changed, 147 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ae6f325..b3048c3 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,8 @@ Contains parameters for synchronization with Zephyr + **cycle** - configuration for cycle caching + **size** - cache size. Default value is 100. + **expireAfterSeconds** - element expiration time in seconds. Default value is 86400 (1 day). + + **invalidateAt** - time in UTC (e.g. 00:00:00) when all values in cache should be invalidated. Repeats every day. + By default, `null` meaning no scheduled invalidation is configured ##### Strategies (only for Zephyr Squad) diff --git a/build.gradle b/build.gradle index 0323593..1f6b103 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,8 @@ dependencies { implementation("org.apache.commons:commons-collections4") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + // Idiomatic logging for Kotlin. Wraps slf4j implementation 'io.github.microutils:kotlin-logging:3.0.0' diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt index 7c446b2..b975024 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/CachesConfiguration.kt @@ -16,6 +16,7 @@ package com.exactpro.th2.dataprocessor.zephyr.cfg +import java.time.LocalTime import java.util.concurrent.TimeUnit class CachesConfiguration( @@ -28,4 +29,5 @@ class CachesConfiguration( class CacheConfiguration( val expireAfterSeconds: Long, val size: Int = 100, + val invalidateAt: LocalTime? = null, ) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/ZephyrSynchronizationCfg.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/ZephyrSynchronizationCfg.kt index d26161b..8901837 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/ZephyrSynchronizationCfg.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/ZephyrSynchronizationCfg.kt @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.client.features.logging.LogLevel @@ -36,6 +37,7 @@ class ZephyrSynchronizationCfg( } companion object { val MAPPER: ObjectMapper = jacksonObjectMapper() + .registerModules(JavaTimeModule()) .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) } } diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt index 5948de6..5a98eea 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt @@ -16,27 +16,70 @@ package com.exactpro.th2.dataprocessor.zephyr.impl.cache +import mu.KotlinLogging import org.apache.commons.collections4.map.LRUMap -import java.util.function.LongSupplier +import java.time.Clock +import java.time.Duration +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneOffset +import java.time.temporal.ChronoUnit internal class LRUCache( size: Int, private val expireAfterMillis: Long, - private val timeSource: LongSupplier = LongSupplier(System::currentTimeMillis), + private val timeSource: Clock = Clock.systemUTC(), + invalidateAt: LocalTime? = null, ) { + private var nextInvalidateAt: Instant init { require(size > 0) { "size must be positive but was $size" } require(expireAfterMillis > 0) { "expireAfterMillis must be positive but was $expireAfterMillis" } + nextInvalidateAt = if (invalidateAt == null) { + Instant.MAX + } else { + val now = timeSource.instant() + val next = invalidateAt.atDate(LocalDate.ofInstant(now, ZoneOffset.UTC)) + .toInstant(ZoneOffset.UTC) + if (next <= now) { + next.plus(1, ChronoUnit.DAYS) + } else { + next + } + } } - private class TimestampedValue(val value: T, val createdAt: Long) + private class TimestampedValue(val value: T, val createdAt: Instant) private val cache = LRUMap>(size) - operator fun get(key: K): V? = cache[key]?.takeUnless { - timeSource.asLong - it.createdAt > expireAfterMillis - }?.value + operator fun get(key: K): V? { + val now = timeSource.instant() + if (now >= nextInvalidateAt) { + invalidateAll() + } + return cache[key]?.takeUnless { + Duration.between(it.createdAt, now).toMillis() > expireAfterMillis + }?.value + } operator fun set(key: K, value: V) { - cache[key] = TimestampedValue(value, timeSource.asLong) + cache[key] = TimestampedValue(value, timeSource.instant()) + } + + private fun invalidateAll() { + cache.clear() + val currentInvalidateAt = nextInvalidateAt + var next: Instant = nextInvalidateAt + val now = timeSource.instant() + do { + next = next.plus(1, ChronoUnit.DAYS) + } while (next < now) + nextInvalidateAt = next + LOGGER.info { "Cache invalidated. Now: $now, Invalidate at: $currentInvalidateAt, Next invalidate at: $nextInvalidateAt" } + } + + private companion object { + private val LOGGER = KotlinLogging.logger { } } } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt index c4add39..db417ef 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/scale/ZephyrScaleEventProcessorImpl.kt @@ -78,7 +78,9 @@ class ZephyrScaleEventProcessorImpl( @GuardedBy("lock") private val cycleCaches: Map> = configurations.associate { - it.destination to it.cachesConfiguration.cycles.run { LRUCache(size, expireAfterSeconds * 1_000) } + it.destination to it.cachesConfiguration.cycles.run { + LRUCache(size, expireAfterSeconds * 1_000, invalidateAt = invalidateAt) + } } override suspend fun EventProcessorContext.processEvent( diff --git a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/TestZephyrSynchronizationCfg.kt b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/TestZephyrSynchronizationCfg.kt index 8485bec..ac58554 100644 --- a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/TestZephyrSynchronizationCfg.kt +++ b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/cfg/TestZephyrSynchronizationCfg.kt @@ -24,6 +24,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import io.ktor.client.features.logging.LogLevel import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import java.time.LocalTime class TestZephyrSynchronizationCfg { private val mapper: ObjectMapper = ZephyrSynchronizationCfg.MAPPER @@ -251,6 +252,65 @@ class TestZephyrSynchronizationCfg { assertEquals(LogLevel.ALL, level) } } + + @Test + fun `deserialize cache configuration`() { + val data = """ + { + "connection": { + "baseUrl": "https://your.jira.address.com", + "jira": { + "username": "jira-user", + "key": "your password" + } + }, + "dataService": { + "name": "ZephyrService", + "versionMarker": "0.0.1" + }, + "syncParameters": { + "issueFormat": "QAP_\\d+", + "delimiter": "|", + "statusMapping": { + "SUCCESS": "PASS", + "FAILED": "WIP" + }, + "jobAwaitTimeout": 1000, + "cachesConfiguration": { + "cycles": { + "expireAfterSeconds": 86400, + "size": 200, + "invalidateAt": "00:00:00" + } + } + }, + "httpLogging": { + "level": "ALL" + } + } + """.trimIndent() + val cfg = mapper.readValue(data) + assertEquals(1, cfg.syncParameters.size) + with(cfg.syncParameters.first()) { + assertEquals("QAP_\\d+", issueRegexp.pattern) + assertEquals('|', delimiter) + assertEquals("PASS", statusMapping[EventStatus.SUCCESS]) + assertEquals("WIP", statusMapping[EventStatus.FAILED]) + assertEquals(1000, jobAwaitTimeout) + assertEquals( + 86400, + cachesConfiguration.cycles.expireAfterSeconds, + ) + assertEquals( + 200, + cachesConfiguration.cycles.size, + ) + assertEquals( + LocalTime.MIDNIGHT, + cachesConfiguration.cycles.invalidateAt, + ) + } + } } private fun Map.assertKey(key: K): V { diff --git a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt index bfdc1df..d3fef73 100644 --- a/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt +++ b/src/test/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCacheTest.kt @@ -20,8 +20,15 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively import org.mockito.kotlin.mock -import java.util.function.LongSupplier +import org.mockito.kotlin.whenever +import java.time.Clock +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneOffset +import java.time.temporal.ChronoUnit class LRUCacheTest { @Test @@ -37,12 +44,27 @@ class LRUCacheTest { assertEquals(42, cache["key"], "unexpected value") } + @Test + fun `invalidates all keys after specified time`() { + val invalidateAt = LocalTime.of(12, 0,0) + val time = invalidateAt.atDate(LocalDate.now()).toInstant(ZoneOffset.UTC).plusSeconds(42) + val timeSource = mock() + whenever(timeSource.instant()) doReturn time + val cache = LRUCache(size = 10, expireAfterMillis = 1000, timeSource, invalidateAt) + cache["key"] = 42 + assertEquals(42, cache["key"], "unexpected value") + // next day + whenever(timeSource.instant()) doReturnConsecutively listOf(time.plus(1, ChronoUnit.DAYS)) + + assertNull(cache["key"], "unexpected value") + } + @Test fun `does not return value if it is expired`() { val expireAfterMillis: Long = 1000 - val time = System.currentTimeMillis() - val timeSource = mock { - on { asLong } doReturn time doReturn time + expireAfterMillis + 1 + val time = Instant.now() + val timeSource = mock { + on { instant() } doReturn time doReturn time.plusMillis(expireAfterMillis + 1) } val cache = LRUCache(size = 10, expireAfterMillis = expireAfterMillis, timeSource) cache["key"] = 42 From ce3d8874f494673262b5c0871cf4f7a859e70154 Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 14 May 2024 11:22:47 +0400 Subject: [PATCH 5/6] Add volatile modifier for inner variable. Update readme --- README.md | 8 +++++++- .../th2/dataprocessor/zephyr/impl/cache/LRUCache.kt | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3048c3..6cf3224 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Zephyr data processor (0.3.0) +# Zephyr data processor (0.4.0) Zephyr data processor synchronizes the test in th2 with Zephyr Squad and Zephyr Scale. It searches for events that match format in the configuration and updates test executions. @@ -277,6 +277,12 @@ spec: # Changes +## v0.4.0 + +### Added + ++ Parameters to configure when cycle cache for Zephyr Scale processor is invalidated. Please refer to [configuration block](#configuration). + ## v0.3.0 ### Added diff --git a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt index 5a98eea..b3c54d0 100644 --- a/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt +++ b/src/main/kotlin/com/exactpro/th2/dataprocessor/zephyr/impl/cache/LRUCache.kt @@ -32,6 +32,7 @@ internal class LRUCache( private val timeSource: Clock = Clock.systemUTC(), invalidateAt: LocalTime? = null, ) { + @Volatile private var nextInvalidateAt: Instant init { require(size > 0) { "size must be positive but was $size" } From 7636844dc79f111ae2aa986933ed05b3cd58ef96 Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 14 May 2024 12:06:27 +0400 Subject: [PATCH 6/6] Correct mistake in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cf3224..2db640a 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Contains parameters for synchronization with Zephyr extract: VERSION ``` + **cachesConfiguration** - configuration that will be applied to the internal caches inside the zephyr processors (e.g. cycles cache) - + **cycle** - configuration for cycle caching + + **cycles** - configuration for cycle caching + **size** - cache size. Default value is 100. + **expireAfterSeconds** - element expiration time in seconds. Default value is 86400 (1 day). + **invalidateAt** - time in UTC (e.g. 00:00:00) when all values in cache should be invalidated. Repeats every day.