Skip to content

Commit

Permalink
Quantitative relaxation erased
Browse files Browse the repository at this point in the history
  • Loading branch information
SokolovaMaria authored and ndkoval committed Oct 13, 2019
1 parent 1a408e6 commit ab80a19
Show file tree
Hide file tree
Showing 24 changed files with 365 additions and 1,656 deletions.
67 changes: 1 addition & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ Table of contents
* [Linearizability](#linearizability)
* [Serializability](#serializability)
* [Quiescent consistency](#quiescent-consistency)
* [Quantitative relaxation](#quantitative-relaxation)
* [Blocking data structures](#blocking-data-structures)
* [Configuration via options](#configuration-via-options)
* [Sample](#sample)
Expand Down Expand Up @@ -138,7 +137,7 @@ Unfortunately, this feature is disabled in **javac** compiler by default. Use `-
By default, **lincheck** sequentially uses the testing data structure to define the correct specification.
However, it is sometimes better to define it explicitly, by writing a simple sequential implementation, and be sure that it is correct.
Thus, **lincheck** can test that the testing data structure is correct even without parallelism.
This sequential specification class should have the same methods as the testing data structure; however, some verifiers require additional parameters for these methods, see *QuantitativelyRelaxedLinearizabilityVerifier* as an example.
This sequential specification class should have the same methods as the testing data structure.
The specification class can be defined via `sequentialSpecification` parameter in both `Options` instances and the corresponding annotations.

## Run test
Expand Down Expand Up @@ -260,70 +259,6 @@ public class QuiescentQueueTest extends VerifierState {
}
```


## Quantitative relaxation
One more trade-off contract is suggested by T. Henzinger et al., which relaxes a data structure semantics. Instead of allowing some reorderings, they suggest to allow "illegal" by the data structure specification results, but with a penalty. This penalty is called a transition cost and then is used to count a path cost, which should be less then a relaxation factor if an execution is correct.

> Look at this paper for details: Henzinger, Thomas A., et al. "Quantitative relaxation of concurrent data structures." ACM SIGPLAN Notices. Vol. 48. No. 1. ACM, 2013.
In order to describe the contract of a testing data structure, the transition costs, the path cost function and the relaxation factor should be specified.

At first, all relaxed operations should be annotated with `@QuantitativeRelaxed`. The current version of **lincheck** counts a path cost using all relaxed operations, but grouping is going to be introduced later.

Then the special cost counter class have to be defined. This class represents a current data structure state and has the same methods as testing operations, but with an additional `Result` parameter and another return type. If an operation is not relaxed this cost counter should check that the operation result is correct and return the next state (which is a cost counter too) or `null` in case the result is incorrect. Otherwise, if a corresponding operation is relaxed (annotated with `@QuantitativeRelaxed`), the method should return a list of all possible next states with their transition cost. For this purpose, a special `CostWithNextCostCounter` class should be used. This class contains the next state and the transition cost with the predicate value, which are defined in accordance with the original paper. Thus, `List<CostWithNextCostCounter>` should be returned by these methods and an empty list should be returned in case no transitions are possible. In order to restrict the number of possible transitions, the relaxation factor should be used. It is provided via a constructor, so **lincheck** uses the `(int relaxationFactor)` constructor for the first instance creation.

The last thing to do is to provide the relaxation factor, the cost counter class, and the path cost function to the verifier. For this purpose, the test class should have an `@QuantitativeRelaxationVerifierConf(...)` annotation, which is then used by `QuantitativeRelaxationVerifier`. As for the path cost function, `MAX`, `PHI_INTERVAL`, and `PHI_INTERVAL_RESTRICTED_MAX` are implemented in `PathCostFunction` class in accordance with the past cost functions in the original paper.

Here is an example for k-stack with relaxed `pop()` operation and normal `push` one:

### Test example

```kotlin
@StressCTest(sequentialSpecification = KRelaxedPopStackTest.CostCounter::class,
verifier = QuantitativeRelaxationVerifier::class)
class KRelaxedPopStackTest {
private val s = KRelaxedPopStack<Int>(K)

@Operation
fun push(x: Int) = s.push(x)

@QuantitativeRelaxed
@Operation
fun pop(): Int? = s.pop()

@Test
fun test() = LinChecker.check(KRelaxedPopStackTest::class.java)

@QuantitativeRelaxationVerifierConf(factor = K, pathCostFunc = MAX)
data class CostCounter @JvmOverloads constructor(
private val k: Int,
private val s: List<Int> = emptyList()
) {
fun push(value: Int, result: Result): CostCounter {
check(result.type == VOID)
val sNew = ArrayList(s)
sNew.add(0, value)
return CostCounter(k, sNew)
}

fun pop(result: Result): List<CostWithNextCostCounter<CostCounter>> {
if (result.value == null) {
return if (s.isEmpty())
listOf(CostWithNextCostCounter(this, 0))
else emptyList()
}
return (0..(k - 1).coerceAtMost(s.size - 1))
.filter { i -> s[i] == result.value }
.map { i ->
val sNew = ArrayList(s)
sNew.removeAt(i)
CostWithNextCostCounter(CostCounter(k, sNew), i)
}
}
}
}
```

# Blocking data structures

**Lincheck** supports blocking operations implemented with `suspend` functions from Kotlin language. The examples of such data structures from the Kotlin Coroutines library are mutexes, semaphores, and channels; see the [corresponding guide](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html) to understand what these data structures are.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.jetbrains.kotlinx.lincheck.execution.ExecutionGenerator;
import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario;
import org.jetbrains.kotlinx.lincheck.verifier.Verifier;
import org.jetbrains.kotlinx.lincheck.verifier.quantitative.QuantitativelyRelaxedLinearizabilityVerifier;

import static org.jetbrains.kotlinx.lincheck.ReporterKt.DEFAULT_LOG_LEVEL;

Expand Down Expand Up @@ -158,8 +157,6 @@ public OPT logLevel(LoggingLevel logLevel) {
* The specified class defines the sequential behavior of the testing data structure;
* it is used by {@link Verifier} to build a labeled transition system,
* and should have the same methods as the testing data structure.
* However, some verifiers require additional parameters for these methods,
* see {@link QuantitativelyRelaxedLinearizabilityVerifier} as an example.
*
* By default, the provided concurrent implementation is used in a sequential way.
*/
Expand Down
21 changes: 3 additions & 18 deletions lincheck/src/main/java/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,18 @@ fun createCostCounterInstance(costCounter: Class<*>, relaxationFactor: Int): Any
}

internal fun executeActor(testInstance: Any, actor: Actor) = executeActor(testInstance, actor, null)
internal fun executeActor(testInstance: Any, actor: Actor, completion: Continuation<Any?>?) =
executeActor(testInstance, actor, completion, null)

/**
* Executes the specified actor on the sequential specification instance and returns its result.
*/
internal fun executeActor(
specification: Any,
actor: Actor,
completion: Continuation<Any?>?,
result: Result?
completion: Continuation<Any?>?
): Result {
try {
val m = if (result == null) getMethod(specification, actor)
else getRelaxedMethod(specification, actor)
val args = when {
actor.isSuspendable && result != null -> actor.arguments + result + completion
actor.isSuspendable -> actor.arguments + completion
result != null -> actor.arguments + result
else -> actor.arguments
}
val m = getMethod(specification, actor)
val args = if (actor.isSuspendable) actor.arguments + completion else actor.arguments
val res = m.invoke(specification, *args.toTypedArray())
return if (m.returnType.isAssignableFrom(Void.TYPE)) VoidResult else createLinCheckResult(res)
} catch (invE: Throwable) {
Expand Down Expand Up @@ -113,12 +104,6 @@ private fun getMethod(instance: Any, actor: Actor): Method = methodsCache
instance.javaClass.getMethod(actor.method.name, *actor.method.parameterTypes)
}

private fun getRelaxedMethod(instance: Any, actor: Actor): Method = methodsCache
.computeIfAbsent(instance.javaClass) { hashMapOf() }
.computeIfAbsent(actor.method) {
instance.javaClass.getMethod(actor.method.name, *(actor.method.parameterTypes + Result::class.java))
}

/**
* Creates [Result] of corresponding type from any given value.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.jetbrains.kotlinx.lincheck.verifier.DummySequentialSpecification;
import org.jetbrains.kotlinx.lincheck.verifier.linearizability.LinearizabilityVerifier;
import org.jetbrains.kotlinx.lincheck.verifier.Verifier;
import org.jetbrains.kotlinx.lincheck.verifier.quantitative.QuantitativelyRelaxedLinearizabilityVerifier;

import java.lang.annotation.*;

Expand Down Expand Up @@ -121,8 +120,6 @@
* The specified class defines the sequential behavior of the testing data structure;
* it is used by {@link Verifier} to build a labeled transition system,
* and should have the same methods as the testing data structure.
* However, some verifiers require additional parameters for these methods,
* see {@link QuantitativelyRelaxedLinearizabilityVerifier} as an example.
*
* By default, the provided concurrent implementation is used in a sequential way.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.jetbrains.kotlinx.lincheck.verifier.DummySequentialSpecification;
import org.jetbrains.kotlinx.lincheck.verifier.linearizability.LinearizabilityVerifier;
import org.jetbrains.kotlinx.lincheck.verifier.Verifier;
import org.jetbrains.kotlinx.lincheck.verifier.quantitative.QuantitativelyRelaxedLinearizabilityVerifier;

import java.lang.annotation.*;

Expand Down Expand Up @@ -121,8 +120,6 @@
* The specified class defines the sequential behavior of the testing data structure;
* it is used by {@link Verifier} to build a labeled transition system,
* and should have the same methods as the testing data structure.
* However, some verifiers require additional parameters for these methods,
* see {@link QuantitativelyRelaxedLinearizabilityVerifier} as an example.
*
* By default, the provided concurrent implementation is used in a sequential way.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*-
* #%L
* Lincheck
* %%
* Copyright (C) 2019 JetBrains s.r.o.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/lgpl-3.0.html>.
* #L%
*/
package org.jetbrains.kotlinx.lincheck.verifier

import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.execution.*

/**
* An abstraction for verifiers which use the labeled transition system (LTS) under the hood.
* The main idea of such verifiers is finding a path in LTS, which starts from the initial
* LTS state (see [LTS.initialState]) and goes through all actors with the specified results.
* To determine which transitions are possible from the current state, we store related
* to the current path prefix information in the special [VerifierContext], which determines
* the next possible transitions using [VerifierContext.nextContext] function. This verifier
* uses depth-first search to find a proper path.
*/
abstract class AbstractLTSVerifier(protected val scenario: ExecutionScenario, protected val sequentialSpecification: Class<*>) : CachedVerifier() {
abstract val lts: LTS
abstract fun createInitialContext(results: ExecutionResult): VerifierContext

override fun verifyResultsImpl(results: ExecutionResult) = createInitialContext(results).verify()

private fun VerifierContext.verify(): Boolean {
// Check if a possible path is found.
if (completed) return true
// Traverse through next possible transitions using depth-first search (DFS). Note that
// initial and post parts are represented as threads with ids `0` and `threads + 1` respectively.
for (threadId in threads) {
val nextContext = nextContext(threadId)
if (nextContext !== null && nextContext.verify()) return true
}
return false
}

override fun checkStateEquivalenceImplementation() = lts.checkStateEquivalenceImplementation()
}

/**
* Reflects the current path prefix information and stores the current LTS state
* (which essentially indicates the data structure state) for a single step of a legal path search
* in LTS-based verifiers. It counts next possible transitions via [nextContext] function.
*/
abstract class VerifierContext(
/**
* Current execution scenario.
*/
protected val scenario: ExecutionScenario,
/**
* Expected execution results
*/
protected val results: ExecutionResult,
/**
* LTS state of this context
*/
val state: LTS.State,
/**
* Number of executed actors in each thread. Note that initial and post parts
* are represented as threads with ids `0` and `threads + 1` respectively.
*/
protected val executed: IntArray = IntArray(scenario.threads + 2),
/**
* For every scenario thread stores whether it is suspended or not.
*/
protected val suspended: BooleanArray = BooleanArray(scenario.threads + 2),
/**
* For every thread it stores a ticket assigned to the last executed actor by [LTS].
* A ticket is assigned from the range (0 .. threads) to an actor that suspends it's execution,
* after being resumed the actor is invoked with this ticket to complete it's execution.
* If an actor does not suspend, the assigned ticket equals `-1`.
*/
protected val tickets: IntArray = IntArray(scenario.threads + 2) { NO_TICKET }
) {
/**
* Counts next possible states and the corresponding contexts if the specified thread is executed.
*/
abstract fun nextContext(threadId: Int): VerifierContext?

/**
* Returns `true` if all actors in the specified thread are executed.
*/
fun isCompleted(threadId: Int) = executed[threadId] == scenario[threadId].size

/**
* Returns `true` if the initial part is completed.
*/
val initCompleted: Boolean get() = isCompleted(0)

/**
* Returns `true` if all actors from the parallel scenario part are executed.
*/
val parallelCompleted: Boolean get() = completedThreads(1..scenario.threads) == scenario.threads

/**
* Returns `true` if all threads completed their execution.
*/
val completed: Boolean get() = completedThreads + suspendedThreads == scenario.threads + 2

/**
* Range for all threads
*/
val threads: IntRange get() = 0..(scenario.threads + 1)

/**
* The number of threads that expectedly suspended their execution.
*/
private val suspendedThreads: Int get() = threads.count { t -> suspended[t] && results[t][executed[t]] === Suspended }

/**
* Returns the number of completed threads from the specified range.
*/
private fun completedThreads(range: IntRange) = range.count { t -> isCompleted(t) }

/**
* Returns the number of completed scenario threads.
*/
private val completedThreads: Int get() = completedThreads(threads)
}
Loading

0 comments on commit ab80a19

Please sign in to comment.