diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCache.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCache.kt index e2711c45962..22a50f1f337 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCache.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCache.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.caches import com.intellij.openapi.application.ApplicationManager +import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals import java.lang.ref.ReferenceQueue import java.lang.ref.SoftReference import java.util.concurrent.ConcurrentHashMap @@ -20,7 +21,8 @@ import java.util.concurrent.ConcurrentHashMap * The cleaner may be invoked multiple times by the cache, in any thread. Implementations of [SoftValueCleaner] must ensure that the * operation is repeatable and thread-safe. */ -internal fun interface SoftValueCleaner { +@LLFirInternals +fun interface SoftValueCleaner { /** * Cleans up after [value] has been removed from the cache or garbage-collected. * @@ -43,7 +45,8 @@ internal fun interface SoftValueCleaner { * @param getCleaner Returns the [SoftValueCleaner] that should be invoked after [V] has been collected or removed from the cache. The * function will be invoked once when the value is added to the cache. */ -internal class CleanableSoftValueCache( +@LLFirInternals +class CleanableSoftValueCache( private val getCleaner: (V) -> SoftValueCleaner, ) { private val backingMap = ConcurrentHashMap>() diff --git a/analysis/low-level-api-fir/tests-jdk11/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCacheLincheckTest.kt b/analysis/low-level-api-fir/tests-jdk11/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCacheLincheckTest.kt new file mode 100644 index 00000000000..c0ca5e11406 --- /dev/null +++ b/analysis/low-level-api-fir/tests-jdk11/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/CleanableSoftValueCacheLincheckTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.analysis.low.level.api.fir.caches + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.strategy.managed.forClasses +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import org.jetbrains.kotlinx.lincheck.strategy.stress.StressOptions +import org.junit.jupiter.api.Test + +/** + * Verifies the linearizability of [CleanableSoftValueCache]. + * + * This test cannot rely on garbage collector and soft reference queue semantics, so we associate no cleaning operations with it. + * + * Various functions of the cache are not checked by Lincheck: + * + * - [CleanableSoftValueCache.clear] is not checked because it must be executed in a write action, which guarantees single-threadedness. + * - [CleanableSoftValueCache.size] and [CleanableSoftValueCache.isEmpty] are not checked because the underlying `ConcurrentHashMap`'s + * implementation of these properties isn't guaranteed to immediately take effect after map operations (see the `ConcurrentHashMap` + * section in the book "Java Concurrency in Practice"). + * - [CleanableSoftValueCache.keys] is not checked for linearizability because it is only weakly consistent via the underlying concurrent + * hash map implementation. + */ +class CleanableSoftValueCacheLincheckTest { + private val cache = CleanableSoftValueCache { SoftValueCleaner { } } + + @Operation + fun get(key: Int): Int? = cache.get(key) + + @Operation + fun computeIfAbsent(key: Int, value: Int): Int = cache.computeIfAbsent(key) { value } + + @Operation + fun compute(key: Int, offset: Int): Int? = cache.compute(key) { _, base -> (base ?: 0) + offset } + + @Operation + fun put(key: Int, value: Int): Int? = cache.put(key, value) + + @Operation + fun remove(key: Int): Int? = cache.remove(key) + + /** + * The guarantee for [ConcurrentHashMap][java.util.concurrent.ConcurrentHashMap] is required for model checking to succeed because + * `ConcurrentHashMap` doesn't pass Lincheck model checking itself. + */ + @Test + fun modelCheckingTest() = ModelCheckingOptions() + .addGuarantee(forClasses("java.util.concurrent.ConcurrentHashMap").allMethods().treatAsAtomic()) + .check(this::class) + + @Test + fun stressTest() = StressOptions().check(this::class) +} diff --git a/analysis/low-level-api-fir/tests-jdk11/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/ExampleLincheckTest.kt b/analysis/low-level-api-fir/tests-jdk11/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/ExampleLincheckTest.kt deleted file mode 100644 index 53e745c3eb2..00000000000 --- a/analysis/low-level-api-fir/tests-jdk11/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/caches/ExampleLincheckTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.analysis.low.level.api.fir.caches - -import org.jetbrains.kotlinx.lincheck.annotations.Operation -import org.jetbrains.kotlinx.lincheck.check -import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions -import org.jetbrains.kotlinx.lincheck.strategy.stress.StressOptions -import org.junit.jupiter.api.Test -import java.util.concurrent.atomic.AtomicInteger - -// This is an example Lincheck test to ensure that the test infrastructure is working. It will be replaced with proper "cleanable soft value -// cache" tests in the scope of KT-62136. - -class ExampleLincheckTest { - private val c = AtomicInteger(0) - - @Operation - fun inc(): Int = c.incrementAndGet() - - @Operation - fun get(): Int = c.get() - - @Test - fun stressTest() = StressOptions().check(this::class) - - @Test - fun modelCheckingTest() = ModelCheckingOptions().check(this::class) -}