Skip to content

Commit 0826bf7

Browse files
authored
Close Spring transaction in the same thread that opened it even on timeout (#2482)
* Improve transaction rollbacking in Spring in case of timeout * Make `ThreadBasedExecutor` mark timed out threads to call `onPhaseTimeout`
1 parent 1cb8c00 commit 0826bf7

File tree

7 files changed

+41
-4
lines changed

7 files changed

+41
-4
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.utbot.common
22

3+
import java.util.WeakHashMap
34
import java.util.concurrent.ArrayBlockingQueue
45
import java.util.concurrent.TimeUnit
56
import kotlin.concurrent.thread
@@ -24,11 +25,15 @@ class ThreadBasedExecutor {
2425
val threadLocal by threadLocalLazy { ThreadBasedExecutor() }
2526
}
2627

28+
// there's no `WeakHashSet`, so we use `WeakHashMap` with dummy values
29+
private val timedOutThreads = WeakHashMap<Thread, Unit>()
2730
private var thread: Thread? = null
2831

2932
private var requestQueue = ArrayBlockingQueue<() -> Any?>(1)
3033
private var responseQueue = ArrayBlockingQueue<Result<Any?>>(1)
3134

35+
fun isCurrentThreadTimedOut(): Boolean =
36+
Thread.currentThread() in timedOutThreads
3237

3338
/**
3439
* Invoke [action] with timeout.
@@ -59,6 +64,7 @@ class ThreadBasedExecutor {
5964
if (res == null) {
6065
try {
6166
val t = thread ?: return res
67+
timedOutThreads[t] = Unit
6268
t.interrupt()
6369
t.join(10)
6470
if (t.isAlive)

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/InstrumentationContext.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.utbot.framework.plugin.api.ClassId
44
import org.utbot.framework.plugin.api.UtConcreteValue
55
import org.utbot.framework.plugin.api.UtModel
66
import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor
7+
import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhase
78
import java.lang.reflect.Method
89
import java.util.IdentityHashMap
910
import org.utbot.instrumentation.instrumentation.mock.computeKeyForMethod
@@ -34,6 +35,13 @@ interface InstrumentationContext {
3435
*/
3536
fun findUtModelWithCompositeOriginConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor?
3637

38+
/**
39+
* Called when [timedOutedPhase] times out.
40+
* This method is executed in the same thread that [timedOutedPhase] was run in.
41+
* Implementor is expected to only perform some clean up operations (e.g. rollback transactions in Spring).
42+
*/
43+
fun onPhaseTimeout(timedOutedPhase: ExecutionPhase)
44+
3745
object MockGetter {
3846
data class MockContainer(private val values: List<*>) {
3947
private var ptr: Int = 0

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/context/SimpleInstrumentationContext.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.utbot.framework.plugin.api.UtModel
66
import org.utbot.framework.plugin.api.util.jClass
77
import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor
88
import org.utbot.instrumentation.instrumentation.execution.constructors.javaStdLibModelWithCompositeOriginConstructors
9+
import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhase
910

1011
/**
1112
* Simple instrumentation context, that is used for pure JVM projects without
@@ -21,4 +22,6 @@ class SimpleInstrumentationContext : InstrumentationContext {
2122

2223
override fun findUtModelWithCompositeOriginConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor? =
2324
javaStdLibModelWithCompositeOriginConstructors[classId.jClass]?.invoke()
25+
26+
override fun onPhaseTimeout(timedOutedPhase: ExecutionPhase) = Unit
2427
}

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases/PhasesController.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.utbot.instrumentation.instrumentation.execution.phases
22

3+
import com.jetbrains.rd.util.error
4+
import com.jetbrains.rd.util.getLogger
35
import org.utbot.common.StopWatch
46
import org.utbot.common.ThreadBasedExecutor
57
import org.utbot.framework.plugin.api.Coverage
@@ -17,7 +19,7 @@ import org.utbot.instrumentation.instrumentation.execution.context.Instrumentati
1719
import java.security.AccessControlException
1820

1921
class PhasesController(
20-
instrumentationContext: InstrumentationContext,
22+
private val instrumentationContext: InstrumentationContext,
2123
traceHandler: TraceHandler,
2224
delegateInstrumentation: Instrumentation<Result<*>>,
2325
private val timeout: Long
@@ -56,13 +58,23 @@ class PhasesController(
5658
}
5759
}
5860

61+
companion object {
62+
private val logger = getLogger<PhasesController>()
63+
}
64+
5965
fun <T, R : ExecutionPhase> executePhaseInTimeout(phase: R, block: R.() -> T): T = phase.start {
6066
val stopWatch = StopWatch()
6167
val context = UtContext(utContext.classLoader, stopWatch)
6268
val timeoutForCurrentPhase = timeout - currentlyElapsed
63-
val result = ThreadBasedExecutor.threadLocal.invokeWithTimeout(timeout - currentlyElapsed, stopWatch) {
69+
val executor = ThreadBasedExecutor.threadLocal
70+
val result = executor.invokeWithTimeout(timeout - currentlyElapsed, stopWatch) {
6471
withUtContext(context) {
65-
phase.block()
72+
try {
73+
phase.block()
74+
} finally {
75+
if (executor.isCurrentThreadTimedOut())
76+
instrumentationContext.onPhaseTimeout(phase)
77+
}
6678
}
6779
} ?: throw TimeoutException("Timeout $timeoutForCurrentPhase ms for phase ${phase.javaClass.simpleName} elapsed, controller timeout - $timeout")
6880

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringInstrumentationContext.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.utbot.framework.plugin.api.util.isSubtypeOf
1111
import org.utbot.framework.plugin.api.util.utContext
1212
import org.utbot.instrumentation.instrumentation.execution.constructors.UtModelWithCompositeOriginConstructor
1313
import org.utbot.instrumentation.instrumentation.execution.context.InstrumentationContext
14+
import org.utbot.instrumentation.instrumentation.execution.phases.ExecutionPhase
1415
import org.utbot.spring.api.SpringApi
1516
import org.utbot.spring.api.provider.SpringApiProviderFacade
1617
import org.utbot.spring.api.provider.InstantiationSettings
@@ -52,4 +53,7 @@ class SpringInstrumentationContext(
5253
override fun findUtModelWithCompositeOriginConstructor(classId: ClassId): UtModelWithCompositeOriginConstructor? =
5354
if (classId.isSubtypeOf(resultActionsClassId)) UtMockMvcResultActionsModelConstructor()
5455
else delegateInstrumentationContext.findUtModelWithCompositeOriginConstructor(classId)
56+
57+
override fun onPhaseTimeout(timedOutedPhase: ExecutionPhase) =
58+
springApi.afterTestMethod()
5559
}

utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/spring/SpringUtExecutionInstrumentation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class SpringUtExecutionInstrumentation(
6565
arguments: ArgumentList,
6666
parameters: Any?,
6767
phasesWrapper: PhasesController.(invokeBasePhases: () -> UtConcreteExecutionResult) -> UtConcreteExecutionResult
68-
): UtConcreteExecutionResult {
68+
): UtConcreteExecutionResult = synchronized(this) {
6969
getRelevantBeans(clazz).forEach { beanName -> springApi.resetBean(beanName) }
7070
return delegateInstrumentation.invoke(clazz, methodSignature, arguments, parameters) { invokeBasePhases ->
7171
phasesWrapper {

utbot-spring-commons/src/main/kotlin/org/utbot/spring/SpringApiImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ class SpringApiImpl(
152152
}
153153

154154
override fun afterTestMethod() {
155+
if (!isInsideTestMethod) {
156+
logger.warn { "afterTestMethod() was probably called twice, ignoring second call" }
157+
return
158+
}
155159
testContextManager.afterTestExecution(dummyTestClassInstance, dummyTestMethod, null)
156160
testContextManager.afterTestMethod(dummyTestClassInstance, dummyTestMethod, null)
157161
isInsideTestMethod = false

0 commit comments

Comments
 (0)