Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execution results are presented in case of any failure #314

Merged
merged 7 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 175 additions & 99 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class ExecutionResult(
* Results of the initial sequential part of the execution.
* @see ExecutionScenario.initExecution
*/
val initResults: List<Result>,
val initResults: List<Result?>,
/**
* State representation at the end of the init part.
*/
Expand All @@ -38,13 +38,13 @@ data class ExecutionResult(
* Results of the last sequential part of the execution.
* @see ExecutionScenario.postExecution
*/
val postResults: List<Result>,
val postResults: List<Result?>,
/**
* State representation at the end of the scenario.
*/
val afterPostStateRepresentation: String?
) {
constructor(initResults: List<Result>, parallelResultsWithClock: List<List<ResultWithClock>>, postResults: List<Result>) :
constructor(initResults: List<Result?>, parallelResultsWithClock: List<List<ResultWithClock>>, postResults: List<Result?>) :
this(initResults, null, parallelResultsWithClock, null, postResults, null)

/**
Expand Down Expand Up @@ -139,10 +139,10 @@ val ExecutionResult.withEmptyClocks: ExecutionResult get() = ExecutionResult(
this.afterPostStateRepresentation
)

val ExecutionResult.parallelResults: List<List<Result>> get() =
val ExecutionResult.parallelResults: List<List<Result?>> get() =
parallelResultsWithClock.map { it.map { r -> r.result } }

val ExecutionResult.threadsResults: List<List<Result>> get() =
val ExecutionResult.threadsResults: List<List<Result?>> get() =
threadsResultsWithClock.map { it.map { r -> r.result } }

// for tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ data class HBClock(val clock: IntArray) {
fun emptyClock(size: Int) = HBClock(emptyClockArray(size))
fun emptyClockArray(size: Int) = IntArray(size) { 0 }

data class ResultWithClock(val result: Result, val clockOnStart: HBClock)
data class ResultWithClock(val result: Result?, val clockOnStart: HBClock)

fun Result.withEmptyClock(threads: Int) = ResultWithClock(this, emptyClock(threads))
fun List<Result>.withEmptyClock(threads: Int): List<ResultWithClock> = map { it.withEmptyClock(threads) }
fun List<ResultWithClock>.withEmptyClock() = map { it.result.withEmptyClock(it.clockOnStart.threads) }
fun List<ResultWithClock>.withEmptyClock() = mapNotNull { it.result?.withEmptyClock(it.clockOnStart.threads) }
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,22 @@ class CompletedInvocationResult(
/**
* Indicates that the invocation has run into deadlock or livelock found by [ManagedStrategy].
*/
data object ManagedDeadlockInvocationResult : InvocationResult()
class ManagedDeadlockInvocationResult(val results: ExecutionResult) : InvocationResult()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that all invocation results contain results field, let's propagate this field to the parent class InvocationResult.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, take a look at
data object SpinCycleFoundAndReplayRequired: InvocationResult():
it doesn't contain results

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So let's change it to also contain results?)

Then it would be uniform, no special cases, no needs to do type-casts and type-checks (i.e. result is CompletedInvocationResult) when we want to access results field, etc.


/**
* The invocation was not completed after timeout and runner halted the execution.
*/
class RunnerTimeoutInvocationResult(
val threadDump: Map<Thread, Array<StackTraceElement>>,
val results: ExecutionResult
): InvocationResult()

/**
* The invocation has completed with an unexpected exception.
*/
class UnexpectedExceptionInvocationResult(
val exception: Throwable
val exception: Throwable,
val results: ExecutionResult
) : InvocationResult()

/**
Expand All @@ -50,18 +52,20 @@ class UnexpectedExceptionInvocationResult(
*/
class ValidationFailureInvocationResult(
val scenario: ExecutionScenario,
val exception: Throwable
val exception: Throwable,
val results: ExecutionResult
) : InvocationResult()

/**
* Obstruction freedom check is requested,
* but an invocation that hangs has been found.
*/
class ObstructionFreedomViolationInvocationResult(
val reason: String
val reason: String,
val results: ExecutionResult
) : InvocationResult()

/**
* Indicates that spin-cycle has been found for the first time and replay of current interleaving is required.
*/
object SpinCycleFoundAndReplayRequired: InvocationResult()
data object SpinCycleFoundAndReplayRequired: InvocationResult()
Original file line number Diff line number Diff line change
Expand Up @@ -308,35 +308,46 @@ internal open class ParallelThreadsRunner(
executor.submitAndAwait(arrayOf(validationPart), timeout)
val validationResult = validationPart.results.single()
if (validationResult is ExceptionResult) {
return ValidationFailureInvocationResult(scenario, validationResult.throwable)
return ValidationFailureInvocationResult(scenario, validationResult.throwable, collectExecutionResults())
}
}
// Combine the results and convert them for the standard class loader (if they are of non-primitive types).
// We do not want the transformed code to be reachable outside of the runner and strategy classes.
return CompletedInvocationResult(
ExecutionResult(
initResults = initialPartExecution?.results?.toList().orEmpty(),
parallelResultsWithClock = parallelPartExecutions.map { execution ->
execution.results.zip(execution.clocks).map {
ResultWithClock(it.first, HBClock(it.second.clone()))
}
},
postResults = postPartExecution?.results?.toList().orEmpty(),
afterInitStateRepresentation = afterInitStateRepresentation,
afterParallelStateRepresentation = afterParallelStateRepresentation,
afterPostStateRepresentation = afterPostStateRepresentation
)
)
return CompletedInvocationResult(collectExecutionResults(afterInitStateRepresentation, afterParallelStateRepresentation, afterPostStateRepresentation))
} catch (e: TimeoutException) {
val threadDump = collectThreadDump(this)
return RunnerTimeoutInvocationResult(threadDump)
return RunnerTimeoutInvocationResult(threadDump, collectExecutionResults())
} catch (e: ExecutionException) {
return UnexpectedExceptionInvocationResult(e.cause!!)
return UnexpectedExceptionInvocationResult(e.cause!!, collectExecutionResults())
} finally {
resetState()
}
}

/**
* This method is called when we have some execution result other than [CompletedInvocationResult].
*/
fun collectExecutionResults(): ExecutionResult {
return collectExecutionResults(null, null, null)
}

private fun collectExecutionResults(
afterInitStateRepresentation: String?,
afterParallelStateRepresentation: String?,
afterPostStateRepresentation: String?
) = ExecutionResult(
initResults = initialPartExecution?.results?.toList().orEmpty(),
parallelResultsWithClock = parallelPartExecutions.map { execution ->
execution.results.zip(execution.clocks).map {
ResultWithClock(it.first, HBClock(it.second.clone()))
}
},
postResults = postPartExecution?.results?.toList().orEmpty(),
afterInitStateRepresentation = afterInitStateRepresentation,
afterParallelStateRepresentation = afterParallelStateRepresentation,
afterPostStateRepresentation = afterPostStateRepresentation
)


private fun createInitialPartExecution() =
if (scenario.initExecution.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,59 @@ import org.jetbrains.kotlinx.lincheck.strategy.managed.*

sealed class LincheckFailure(
val scenario: ExecutionScenario,
val results: ExecutionResult,
val trace: Trace?
) {
override fun toString() = StringBuilder().appendFailure(this).toString()
}

internal class IncorrectResultsFailure(
scenario: ExecutionScenario,
val results: ExecutionResult,
results: ExecutionResult,
trace: Trace? = null
) : LincheckFailure(scenario, trace)
) : LincheckFailure(scenario, results, trace)

internal class DeadlockOrLivelockFailure(
internal class ManagedDeadlockFailure(
scenario: ExecutionScenario,
// Thread dump is not present in case of model checking
val threadDump: Map<Thread, Array<StackTraceElement>>?,
results: ExecutionResult,
trace: Trace? = null
) : LincheckFailure(scenario, trace)
) : LincheckFailure(scenario,results, trace)

internal class TimeoutFailure(
scenario: ExecutionScenario,
results: ExecutionResult,
val threadDump: Map<Thread, Array<StackTraceElement>>,
) : LincheckFailure(scenario,results, null)

internal class UnexpectedExceptionFailure(
scenario: ExecutionScenario,
results: ExecutionResult,
val exception: Throwable,
trace: Trace? = null
) : LincheckFailure(scenario, trace)
) : LincheckFailure(scenario,results, trace)

internal class ValidationFailure(
scenario: ExecutionScenario,
results: ExecutionResult,
val exception: Throwable,
trace: Trace? = null
) : LincheckFailure(scenario, trace) {
) : LincheckFailure(scenario,results, trace) {
val validationFunctionName: String = scenario.validationFunction!!.method.name
}

internal class ObstructionFreedomViolationFailure(
scenario: ExecutionScenario,
results: ExecutionResult,
val reason: String,
trace: Trace? = null
) : LincheckFailure(scenario, trace)
) : LincheckFailure(scenario, results, trace)

internal fun InvocationResult.toLincheckFailure(scenario: ExecutionScenario, trace: Trace? = null) = when (this) {
is ManagedDeadlockInvocationResult -> DeadlockOrLivelockFailure(scenario, threadDump = null, trace)
is RunnerTimeoutInvocationResult -> DeadlockOrLivelockFailure(scenario, threadDump, trace = null)
is UnexpectedExceptionInvocationResult -> UnexpectedExceptionFailure(scenario, exception, trace)
is ValidationFailureInvocationResult -> ValidationFailure(scenario, exception, trace)
is ObstructionFreedomViolationInvocationResult -> ObstructionFreedomViolationFailure(scenario, reason, trace)
is ManagedDeadlockInvocationResult -> ManagedDeadlockFailure(scenario, results, trace)
is RunnerTimeoutInvocationResult -> TimeoutFailure(scenario, results, threadDump)
is UnexpectedExceptionInvocationResult -> UnexpectedExceptionFailure(scenario, results, exception, trace)
is ValidationFailureInvocationResult -> ValidationFailure(scenario, results, exception, trace)
is ObstructionFreedomViolationInvocationResult -> ObstructionFreedomViolationFailure(scenario, results, reason, trace)
is CompletedInvocationResult -> IncorrectResultsFailure(scenario, results, trace)
else -> error("Unexpected invocation result type: ${this.javaClass.simpleName}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,13 @@ abstract class ManagedStrategy(
}

private fun failDueToDeadlock(): Nothing {
suddenInvocationResult = ManagedDeadlockInvocationResult
suddenInvocationResult = ManagedDeadlockInvocationResult(runner.collectExecutionResults())
// Forcibly finish the current execution by throwing an exception.
throw ForcibleExecutionFinishError
}

private fun failDueToLivelock(lazyMessage: () -> String): Nothing {
suddenInvocationResult = ObstructionFreedomViolationInvocationResult(lazyMessage())
suddenInvocationResult = ObstructionFreedomViolationInvocationResult(lazyMessage(), runner.collectExecutionResults())
// Forcibly finish the current execution by throwing an exception.
throw ForcibleExecutionFinishError
}
Expand Down Expand Up @@ -440,7 +440,7 @@ abstract class ManagedStrategy(
// the managed strategy can construct a trace to reproduce this failure.
// Let's then store the corresponding failing result and construct the trace.
if (exception === ForcibleExecutionFinishError) return // not a forcible execution finish
suddenInvocationResult = UnexpectedExceptionInvocationResult(exception)
suddenInvocationResult = UnexpectedExceptionInvocationResult(exception, runner.collectExecutionResults())
}

override fun onActorStart(iThread: Int) = runInIgnoredSection {
Expand Down Expand Up @@ -499,7 +499,7 @@ abstract class ManagedStrategy(
val nextThread = (0 until nThreads).firstOrNull { !finished[it] && isSuspended[it] }
if (nextThread == null) {
// must switch not to get into a deadlock, but there are no threads to switch.
suddenInvocationResult = ManagedDeadlockInvocationResult
suddenInvocationResult = ManagedDeadlockInvocationResult(runner.collectExecutionResults())
// forcibly finish execution by throwing an exception.
throw ForcibleExecutionFinishError
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed
import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.ExecutionPart
import org.jetbrains.kotlinx.lincheck.strategy.DeadlockOrLivelockFailure
import org.jetbrains.kotlinx.lincheck.strategy.LincheckFailure
import org.jetbrains.kotlinx.lincheck.strategy.*
import org.jetbrains.kotlinx.lincheck.strategy.ManagedDeadlockFailure
import org.jetbrains.kotlinx.lincheck.strategy.ObstructionFreedomViolationFailure
import org.jetbrains.kotlinx.lincheck.strategy.TimeoutFailure
import org.jetbrains.kotlinx.lincheck.strategy.ValidationFailure
import java.util.*
import kotlin.math.*

@Synchronized // we should avoid concurrent executions to keep `objectNumeration` consistent
internal fun StringBuilder.appendTrace(
failure: LincheckFailure,
results: ExecutionResult?,
results: ExecutionResult,
trace: Trace,
exceptionStackTraces: Map<Throwable, ExceptionNumberAndStacktrace>
) {
Expand All @@ -48,7 +50,7 @@ private fun StringBuilder.appendShortTrace(
val traceRepresentation = traceGraphToRepresentationList(sectionsFirstNodes, false)
appendLine(TRACE_TITLE)
appendTraceRepresentation(failure.scenario, traceRepresentation)
if (failure is DeadlockOrLivelockFailure) {
if (failure is ManagedDeadlockFailure || failure is TimeoutFailure) {
appendLine(ALL_UNFINISHED_THREADS_IN_DEADLOCK_MESSAGE)
}
appendLine()
Expand All @@ -64,7 +66,7 @@ private fun StringBuilder.appendDetailedTrace(
appendLine(DETAILED_TRACE_TITLE)
val traceRepresentationVerbose = traceGraphToRepresentationList(sectionsFirstNodes, true)
appendTraceRepresentation(failure.scenario, traceRepresentationVerbose)
if (failure is DeadlockOrLivelockFailure) {
if (failure is ManagedDeadlockFailure || failure is TimeoutFailure) {
appendLine(ALL_UNFINISHED_THREADS_IN_DEADLOCK_MESSAGE)
}
}
Expand Down Expand Up @@ -127,7 +129,7 @@ class TableSectionColumnsRepresentation(
*/
internal fun constructTraceGraph(
failure: LincheckFailure,
results: ExecutionResult?,
results: ExecutionResult,
trace: Trace,
exceptionStackTraces: Map<Throwable, ExceptionNumberAndStacktrace>
): List<TraceNode> {
Expand Down Expand Up @@ -177,8 +179,7 @@ internal fun constructTraceGraph(
last = lastNode,
callDepth = 0,
actorRepresentation = actorRepresentations[iThread][nextActor],
resultRepresentation = resultProvider[iThread, nextActor]
?.let { actorNodeResultRepresentation(it, exceptionStackTraces) }
resultRepresentation = actorNodeResultRepresentation(resultProvider[iThread, nextActor], failure, exceptionStackTraces)
)
}
actorNodes[iThread][nextActor] = actorNode
Expand Down Expand Up @@ -220,7 +221,7 @@ internal fun constructTraceGraph(
last = lastNode,
callDepth = 0,
actorRepresentation = actorRepresentations[iThread][actorId],
resultRepresentation = actorNodeResultRepresentation(actorResult, exceptionStackTraces)
resultRepresentation = actorNodeResultRepresentation(actorResult, failure, exceptionStackTraces)
)
actorNodes[iThread][actorId] = actorNode
traceGraphNodes += actorNode
Expand Down Expand Up @@ -251,6 +252,20 @@ internal fun constructTraceGraph(
return traceGraphNodesSections.map { it.first() }
}

private fun actorNodeResultRepresentation(result: Result?, failure: LincheckFailure, exceptionStackTraces: Map<Throwable, ExceptionNumberAndStacktrace>): String? {
// We don't mark actors that violated obstruction freedom as hung.
if (result == null && failure is ObstructionFreedomViolationFailure) return null
return when (result) {
null -> "<hung>"
is ExceptionResult -> {
val exceptionNumberRepresentation = exceptionStackTraces[result.throwable]?.let { " #${it.number}" } ?: ""
"$result$exceptionNumberRepresentation"
}
is VoidResult -> null // don't print
else -> result.toString()
}
}

/**
* Helper class to provider execution results, including a validation function result
*/
Expand All @@ -259,24 +274,21 @@ private class ExecutionResultsProvider(result: ExecutionResult?, failure: Linche
/**
* A map of type Map<(threadId, actorId) -> Result>
*/
private val threadNumberToActorResultMap: Map<Pair<Int, Int>, Result> = when {
// If the results of the failure are present, then just collect them to a map.
// In that case, we know that the failure reason is not validation function, so we ignore it.
(result != null) -> {
result.threadsResults
private val threadNumberToActorResultMap: Map<Pair<Int, Int>, Result?>

init {
val results = hashMapOf<Pair<Int, Int>, Result?>()
if (result != null) {
results += result.threadsResults
.flatMapIndexed { tId, actors -> actors.flatMapIndexed { actorId, result ->
listOf((tId to actorId) to result)
}}
.toMap()
}

// If validation function is the reason if the failure then the only result we're interested in
// is the validation function exception.
failure is ValidationFailure -> {
mapOf((0 to firstThreadActorCount(failure)) to ExceptionResult.create(failure.exception, false))
if (failure is ValidationFailure) {
results[0 to firstThreadActorCount(failure)] = ExceptionResult.create(failure.exception, false)
}

else -> emptyMap()
threadNumberToActorResultMap = results
}

operator fun get(iThread: Int, actorId: Int): Result? {
Expand Down
Loading