Skip to content

Commit 188f682

Browse files
authored
Make thread stopping in ThreadBasedExecutor more safe (#2503)
Make `ThreadBasedExecutor` avoid race condition and not call `Thread.stop()` during while executing clean up callback
1 parent 0312936 commit 188f682

File tree

2 files changed

+43
-15
lines changed
  • utbot-core/src/main/kotlin/org/utbot/common
  • utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/instrumentation/execution/phases

2 files changed

+43
-15
lines changed

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

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package org.utbot.common
22

3-
import java.util.WeakHashMap
43
import java.util.concurrent.ArrayBlockingQueue
54
import java.util.concurrent.TimeUnit
5+
import java.util.concurrent.locks.ReentrantLock
66
import kotlin.concurrent.thread
7+
import kotlin.concurrent.withLock
78
import kotlin.properties.ReadOnlyProperty
9+
import kotlin.random.Random
810
import kotlin.reflect.KProperty
911

1012

@@ -25,15 +27,34 @@ class ThreadBasedExecutor {
2527
val threadLocal by threadLocalLazy { ThreadBasedExecutor() }
2628
}
2729

28-
// there's no `WeakHashSet`, so we use `WeakHashMap` with dummy values
29-
private val timedOutThreads = WeakHashMap<Thread, Unit>()
30+
/**
31+
* Used to avoid calling [Thread.stop] during clean up.
32+
*
33+
* @see runCleanUpIfTimedOut
34+
*/
35+
private val timeOutCleanUpLock = ReentrantLock()
36+
37+
/**
38+
* `null` when either:
39+
* - no tasks have yet been run
40+
* - current task timed out, and we are waiting for its thread to die
41+
*/
42+
@Volatile
3043
private var thread: Thread? = null
3144

3245
private var requestQueue = ArrayBlockingQueue<() -> Any?>(1)
3346
private var responseQueue = ArrayBlockingQueue<Result<Any?>>(1)
3447

35-
fun isCurrentThreadTimedOut(): Boolean =
36-
Thread.currentThread() in timedOutThreads
48+
/**
49+
* Can be called from lambda passed to [invokeWithTimeout].
50+
* [ThreadBasedExecutor] guarantees that it won't attempt to terminate [cleanUpBlock] with [Thread.stop].
51+
*/
52+
fun runCleanUpIfTimedOut(cleanUpBlock: () -> Unit) {
53+
timeOutCleanUpLock.withLock {
54+
if (thread == null)
55+
cleanUpBlock()
56+
}
57+
}
3758

3859
/**
3960
* Invoke [action] with timeout.
@@ -64,16 +85,22 @@ class ThreadBasedExecutor {
6485
if (res == null) {
6586
try {
6687
val t = thread ?: return res
67-
timedOutThreads[t] = Unit
88+
thread = null
6889
t.interrupt()
6990
t.join(10)
70-
if (t.isAlive)
71-
@Suppress("DEPRECATION")
72-
t.stop()
91+
// to avoid race condition we need to wait for `t` to die
92+
while (t.isAlive) {
93+
timeOutCleanUpLock.withLock {
94+
@Suppress("DEPRECATION")
95+
t.stop()
96+
}
97+
// If somebody catches `ThreadDeath`, for now we
98+
// just wait for at most 10s and throw another one.
99+
//
100+
// A better approach may be to kill instrumented process.
101+
t.join(10_000)
102+
}
73103
} catch (_: Throwable) {}
74-
75-
thread = null
76-
77104
}
78105
return res
79106
}
@@ -90,9 +117,9 @@ class ThreadBasedExecutor {
90117
requestQueue = ArrayBlockingQueue<() -> Any?>(1)
91118
responseQueue = ArrayBlockingQueue<Result<Any?>>(1)
92119

93-
thread = thread(name = "executor", isDaemon = true) {
120+
thread = thread(name = "executor @${Random.nextInt(10_000)}", isDaemon = true) {
94121
try {
95-
while (true) {
122+
while (thread === Thread.currentThread()) {
96123
val next = requestQueue.take()
97124
responseQueue.offer(kotlin.runCatching { next() })
98125
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ class PhasesController(
7272
try {
7373
phase.block()
7474
} finally {
75-
if (executor.isCurrentThreadTimedOut())
75+
executor.runCleanUpIfTimedOut {
7676
instrumentationContext.onPhaseTimeout(phase)
77+
}
7778
}
7879
}
7980
} ?: throw TimeoutException("Timeout $timeoutForCurrentPhase ms for phase ${phase.javaClass.simpleName} elapsed, controller timeout - $timeout")

0 commit comments

Comments
 (0)