Skip to content

Commit eaf4b55

Browse files
Redesign for Concrete Executor timeout (#1728)
* Create ResultAndErrorHandlingApiOfTheInstrumentedProcess.md * [utbot-java] 1. New timeouts system for contest estimator. 2. Concrete execution phases redesign Fix #1726 Possible fix #1718 * [utbot-java] If docker image for monitoring is not available - CI should continue even without monitoring * [utbot-java] Mockito warmup as it takes too much time in tests to initialize it * [utbot-java] Providing additional auxiliary log information when running in docker * Fix: document * [utbot-java] Review fix Co-authored-by: Sergey Pospelov <sergeypospelov59@gmail.com>
1 parent 1cccc07 commit eaf4b55

File tree

35 files changed

+1381
-1303
lines changed

35 files changed

+1381
-1303
lines changed

.github/workflows/build-and-run-tests-from-branch.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ jobs:
9999
- name: Run monitoring
100100
# secret uploaded using base64 encoding to have one-line output:
101101
# cat file | base64 -w 0
102+
continue-on-error: true
102103
run: |
103104
chmod +x ./scripts/project/monitoring.sh
104105
./scripts/project/monitoring.sh "${PUSHGATEWAY_HOSTNAME}" "${{ secrets.PUSHGATEWAY_USER }}" "${{ secrets.PUSHGATEWAY_PASSWORD }}"

build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ allprojects {
5353
}
5454
}
5555
withType<Test> {
56+
// uncomment if you want to see loggers output in console
57+
// this is useful if you debug in docker
58+
// testLogging.showStandardStreams = true
59+
// testLogging.showStackTraces = true
5660
// set heap size for the test JVM(s)
5761
minHeapSize = "128m"
5862
maxHeapSize = "3072m"
@@ -68,6 +72,9 @@ allprojects {
6872
override fun beforeTest(testDescriptor: TestDescriptor) {}
6973
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {
7074
println("[$testDescriptor.classDisplayName] [$testDescriptor.displayName]: $result.resultType, length - ${(result.endTime - result.startTime) / 1000.0} sec")
75+
if (result.resultType == TestResult.ResultType.FAILURE) {
76+
println("Exception: " + result.exception?.stackTraceToString())
77+
}
7178
}
7279

7380
override fun afterSuite(testDescriptor: TestDescriptor, result: TestResult) {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Result & Error Handling API of the Instrumented Process
2+
3+
## Terminology
4+
5+
- The _instrumented process_ is an external process used for the isolated invocation.
6+
- The `ConcreteExecutor` is a class which provides smooth and concise interaction with the _instrumented process_. It works in the _main process_.
7+
- A client is an object which directly uses the `ConcreteExecutor`, so it works in the _main process_ as well.
8+
- An _Instrumentation_ is an object which has to be passed to the `ConcreteExecutor`. It defines the logic of invocation and bytecode instrumentation in the _instrumented process_.
9+
10+
## Common
11+
12+
Basically, if any exception happens inside the _instrumented process_, it is rethrown to the client process via RD.
13+
- Errors which do not cause the termination of the _instrumented process_ are wrapped in `InstrumentedProcessError`. Process won't be restarted, so client's requests will be handled by the same process. We believe, that the state of the _instrumented process_ is consistent, but in some tricky situations it **may be not**. Such situations should be reported as bugs.
14+
- Some of the errors lead to the instant death of the _instrumented process_. Such errors are wrapped in `InstrumentedProcessDeathException`. Before processing the next request, the _instrumented process_ will be restarted automatically, but it can take some time.
15+
16+
The extra logic of error and result handling depends on the provided instrumentation.
17+
18+
## UtExecutionInstrumentation
19+
20+
The next sections are related only to the `UtExecutionInstrumentation` passed to the _instrumented process_.
21+
22+
The calling of `ConcreteExecutor::executeAsync` instantiated by the `UtExecutionInstrumentation` can lead to the three possible situations:
23+
- `InstrumentedProcessDeathException` occurs. Usually, this situation means there is an internal issue in the _instrumented process_, but, nevertheless, this exception should be handled by the client.
24+
- `InstrumentedProcessError` occurs. It also means an internal issue and should be handled by the client. Sometimes it happens because the client provided the wrong configuration or parameters, but the _instrumented process_ **can't determine exactly** what's wrong with the client's data. The cause contains the description of the phase which threw the exception.
25+
- No exception occurs, so the `UtConcreteExecutionResult` is returned. It means that everything went well during the invocation or something broke down because of the wrong input, and the _instrumented process_ **knows exactly** what's wrong with the client's input. The _instrumented process_ guarantees that the state **is consistent**. The exact reason of failure is a `UtConcreteExecutionResult::result` field. It includes:
26+
- `UtSandboxFailure` --- violation of permissions.
27+
- `UtTimeoutException` --- the test execution time exceeds the provided time limit (`UtConcreteExecutionData::timeout`).
28+
- `UtExecutionSuccess` --- the test executed successfully.
29+
- `UtExplicitlyThrownException` --- the target method threw exception explicitly (via `throw` instruction).
30+
- `UtImplicitlyThrownException` --- the target method threw exception implicitly (`NPE`, `OOB`, etc. or it was thrown inside the system library)
31+
- etc.
32+
33+
### How the error handling works
34+
35+
The pipeline of the `UtExecutionInstrumentation::invoke` consists of 6 phases:
36+
- `ValueConstructionPhase` --- constructs values from the models.
37+
- `PreparationPhase` --- prepares statics, etc.
38+
- `InvocationPhase` --- invokes the target method.
39+
- `StatisticsCollectionPhase` --- collects the coverage and execution-related data.
40+
- `ModelConstructionPhase` --- constructs the result models from the heap objects (`Any?`).
41+
- `PostprocessingPhase` --- restores statics, clear mocks, etc.
42+
43+
Each phase can throw two kinds of exceptions:
44+
- `ExecutionPhaseStop` --- indicates that the phase want to stop the invocation of the pipeline completely, because it's already has a result. The returned result is the `ExecutionPhaseStop::result` field.
45+
- `ExecutionPhaseError` --- indicates that an unexpected error happened inside the phase execution, so it's rethrown to the main process.
46+
47+
The `PhasesController::computeConcreteExecutionResult` then matches on the exception type and rethrows the exception if it's an `ExecutionPhaseError`, and returns the result if it's an `ExecutionPhaseStop`.
48+
49+
### Timeout
50+
51+
There is a time limit on the concrete execution, so the `UtExecutionInstrumentation::invoke` method must respect it. We wrap phases which can take a long time with the `executePhaseInTimeout` block, which internally keeps and updates the elapsed time. If any wrapped phase exceeds the timeout, we return the `TimeoutException` as the result of that phase.
52+
53+
The clients cannot depend that cancellation request immediately breaks the invocation inside the _instrumented process_. The invocation is guaranteed to finish in the time of passed timeout. It may or **may not** finish earlier. Already started query in instrumented process is **uncancellable** - this is by design.
54+
55+
Even if the `TimeoutException` occurs, the _instrumented process_ is ready to process new requests.

utbot-core/src/main/kotlin/org/utbot/common/StopWatch.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class StopWatch {
5252
}
5353
}
5454

55-
fun get(unit: TimeUnit) = lock.withLockInterruptibly {
55+
fun get(unit: TimeUnit = TimeUnit.MILLISECONDS) = lock.withLockInterruptibly {
5656
unsafeUpdate()
5757
unit.convert(elapsedMillis, TimeUnit.MILLISECONDS)
5858
}

utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ object UtSettings : AbstractSettings(logger, defaultKeyForSettingsPath, defaultS
277277
/**
278278
* Timeout for specific concrete execution (in milliseconds).
279279
*/
280-
var concreteExecutionTimeoutInInstrumentedProcess: Long by getLongProperty(
280+
var concreteExecutionDefaultTimeoutInInstrumentedProcessMillis: Long by getLongProperty(
281281
DEFAULT_EXECUTION_TIMEOUT_IN_INSTRUMENTED_PROCESS_MS
282282
)
283283

Lines changed: 108 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,108 @@
1-
package org.utbot.framework.plugin.api
2-
3-
import org.utbot.framework.plugin.api.visible.UtStreamConsumingException
4-
import java.io.File
5-
import java.util.LinkedList
6-
7-
sealed class UtExecutionResult
8-
9-
data class UtExecutionSuccess(val model: UtModel) : UtExecutionResult() {
10-
override fun toString() = "$model"
11-
}
12-
13-
sealed class UtExecutionFailure : UtExecutionResult() {
14-
abstract val exception: Throwable
15-
16-
/**
17-
* Represents the most inner exception in the failure.
18-
* Often equals to [exception], but is wrapped exception in [UtStreamConsumingException].
19-
*/
20-
open val rootCauseException: Throwable
21-
get() = exception
22-
}
23-
24-
data class UtOverflowFailure(
25-
override val exception: Throwable,
26-
) : UtExecutionFailure()
27-
28-
data class UtSandboxFailure(
29-
override val exception: Throwable
30-
) : UtExecutionFailure()
31-
32-
data class UtStreamConsumingFailure(
33-
override val exception: UtStreamConsumingException,
34-
) : UtExecutionFailure() {
35-
override val rootCauseException: Throwable
36-
get() = exception.innerExceptionOrAny
37-
}
38-
39-
/**
40-
* unexpectedFail (when exceptions such as NPE, IOBE, etc. appear, but not thrown by a user, applies both for function under test and nested calls )
41-
* expectedCheckedThrow (when function under test or nested call explicitly says that checked exception could be thrown and throws it)
42-
* expectedUncheckedThrow (when there is a throw statement for unchecked exception inside of function under test)
43-
* unexpectedUncheckedThrow (in case when there is unchecked exception thrown from nested call)
44-
*/
45-
data class UtExplicitlyThrownException(
46-
override val exception: Throwable,
47-
val fromNestedMethod: Boolean
48-
) : UtExecutionFailure()
49-
50-
data class UtImplicitlyThrownException(
51-
override val exception: Throwable,
52-
val fromNestedMethod: Boolean
53-
) : UtExecutionFailure()
54-
55-
class TimeoutException(s: String) : Exception(s)
56-
57-
data class UtTimeoutException(override val exception: TimeoutException) : UtExecutionFailure()
58-
59-
/**
60-
* Indicates failure in concrete execution.
61-
* For now it is explicitly throwing by ConcreteExecutor in case instrumented process death.
62-
*/
63-
class ConcreteExecutionFailureException(cause: Throwable, errorFile: File, val processStdout: List<String>) :
64-
Exception(
65-
buildString {
66-
appendLine()
67-
appendLine("----------------------------------------")
68-
appendLine("The instrumented process is dead")
69-
appendLine("Cause:\n${cause.message}")
70-
appendLine("Last 1000 lines of the error log ${errorFile.absolutePath}:")
71-
appendLine("----------------------------------------")
72-
errorFile.useLines { lines ->
73-
val lastLines = LinkedList<String>()
74-
for (line in lines) {
75-
lastLines.add(line)
76-
if (lastLines.size > 1000) {
77-
lastLines.removeFirst()
78-
}
79-
}
80-
lastLines.forEach { appendLine(it) }
81-
}
82-
appendLine("----------------------------------------")
83-
},
84-
cause
85-
)
86-
87-
data class UtConcreteExecutionFailure(override val exception: ConcreteExecutionFailureException) : UtExecutionFailure()
88-
89-
val UtExecutionResult.isSuccess: Boolean
90-
get() = this is UtExecutionSuccess
91-
92-
val UtExecutionResult.isFailure: Boolean
93-
get() = this is UtExecutionFailure
94-
95-
inline fun UtExecutionResult.onSuccess(action: (model: UtModel) -> Unit): UtExecutionResult {
96-
if (this is UtExecutionSuccess) action(model)
97-
return this
98-
}
99-
100-
inline fun UtExecutionResult.onFailure(action: (exception: Throwable) -> Unit): UtExecutionResult {
101-
if (this is UtExecutionFailure) action(rootCauseException)
102-
return this
103-
}
104-
105-
fun UtExecutionResult.exceptionOrNull(): Throwable? = when (this) {
106-
is UtExecutionFailure -> rootCauseException
107-
is UtExecutionSuccess -> null
108-
}
1+
package org.utbot.framework.plugin.api
2+
3+
import org.utbot.framework.plugin.api.visible.UtStreamConsumingException
4+
import java.io.File
5+
import java.util.LinkedList
6+
7+
sealed class UtExecutionResult
8+
9+
data class UtExecutionSuccess(val model: UtModel) : UtExecutionResult() {
10+
override fun toString() = "$model"
11+
}
12+
13+
sealed class UtExecutionFailure : UtExecutionResult() {
14+
abstract val exception: Throwable
15+
16+
/**
17+
* Represents the most inner exception in the failure.
18+
* Often equals to [exception], but is wrapped exception in [UtStreamConsumingException].
19+
*/
20+
open val rootCauseException: Throwable
21+
get() = exception
22+
}
23+
24+
data class UtOverflowFailure(
25+
override val exception: Throwable,
26+
) : UtExecutionFailure()
27+
28+
data class UtSandboxFailure(
29+
override val exception: Throwable
30+
) : UtExecutionFailure()
31+
32+
data class UtStreamConsumingFailure(
33+
override val exception: UtStreamConsumingException,
34+
) : UtExecutionFailure() {
35+
override val rootCauseException: Throwable
36+
get() = exception.innerExceptionOrAny
37+
}
38+
39+
/**
40+
* unexpectedFail (when exceptions such as NPE, IOBE, etc. appear, but not thrown by a user, applies both for function under test and nested calls )
41+
* expectedCheckedThrow (when function under test or nested call explicitly says that checked exception could be thrown and throws it)
42+
* expectedUncheckedThrow (when there is a throw statement for unchecked exception inside of function under test)
43+
* unexpectedUncheckedThrow (in case when there is unchecked exception thrown from nested call)
44+
*/
45+
data class UtExplicitlyThrownException(
46+
override val exception: Throwable,
47+
val fromNestedMethod: Boolean
48+
) : UtExecutionFailure()
49+
50+
data class UtImplicitlyThrownException(
51+
override val exception: Throwable,
52+
val fromNestedMethod: Boolean
53+
) : UtExecutionFailure()
54+
55+
class TimeoutException(s: String) : Exception(s)
56+
57+
data class UtTimeoutException(override val exception: TimeoutException) : UtExecutionFailure()
58+
59+
/**
60+
* Indicates failure in concrete execution.
61+
* For now it is explicitly throwing by ConcreteExecutor in case instrumented process death.
62+
*/
63+
class InstrumentedProcessDeathException(cause: Throwable, errorFile: File, val processStdout: List<String>) :
64+
Exception(
65+
buildString {
66+
appendLine()
67+
appendLine("----------------------------------------")
68+
appendLine("The instrumented process is dead")
69+
appendLine("Cause:\n${cause.message}")
70+
appendLine("Last 1000 lines of the error log ${errorFile.absolutePath}:")
71+
appendLine("----------------------------------------")
72+
errorFile.useLines { lines ->
73+
val lastLines = LinkedList<String>()
74+
for (line in lines) {
75+
lastLines.add(line)
76+
if (lastLines.size > 1000) {
77+
lastLines.removeFirst()
78+
}
79+
}
80+
lastLines.forEach { appendLine(it) }
81+
}
82+
appendLine("----------------------------------------")
83+
},
84+
cause
85+
)
86+
87+
data class UtConcreteExecutionFailure(override val exception: InstrumentedProcessDeathException) : UtExecutionFailure()
88+
89+
val UtExecutionResult.isSuccess: Boolean
90+
get() = this is UtExecutionSuccess
91+
92+
val UtExecutionResult.isFailure: Boolean
93+
get() = this is UtExecutionFailure
94+
95+
inline fun UtExecutionResult.onSuccess(action: (model: UtModel) -> Unit): UtExecutionResult {
96+
if (this is UtExecutionSuccess) action(model)
97+
return this
98+
}
99+
100+
inline fun UtExecutionResult.onFailure(action: (exception: Throwable) -> Unit): UtExecutionResult {
101+
if (this is UtExecutionFailure) action(rootCauseException)
102+
return this
103+
}
104+
105+
fun UtExecutionResult.exceptionOrNull(): Throwable? = when (this) {
106+
is UtExecutionFailure -> rootCauseException
107+
is UtExecutionSuccess -> null
108+
}

utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import kotlinx.collections.immutable.persistentSetOf
77
import kotlinx.collections.immutable.toPersistentList
88
import kotlinx.collections.immutable.toPersistentMap
99
import kotlinx.collections.immutable.toPersistentSet
10+
import mu.KotlinLogging
1011
import org.utbot.common.WorkaroundReason.HACK
1112
import org.utbot.framework.UtSettings.ignoreStaticsFromTrustedLibraries
1213
import org.utbot.common.WorkaroundReason.IGNORE_STATICS_FROM_TRUSTED_LIBRARIES
@@ -224,6 +225,7 @@ import java.lang.reflect.TypeVariable
224225
import java.lang.reflect.WildcardType
225226

226227
private val CAUGHT_EXCEPTION = LocalVariable("@caughtexception")
228+
private val logger = KotlinLogging.logger {}
227229

228230
class Traverser(
229231
private val methodUnderTest: ExecutableId,

0 commit comments

Comments
 (0)