mirror of
https://github.com/corda/corda.git
synced 2025-02-01 00:45:59 +00:00
Add AffinityExecutor: a tool for making it easier to perform thread assertions.
This commit is contained in:
parent
d3011817ed
commit
f6ef1c8071
173
src/main/kotlin/core/utilities/AffinityExecutor.kt
Normal file
173
src/main/kotlin/core/utilities/AffinityExecutor.kt
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package core.utilities
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions.checkState
|
||||||
|
import com.google.common.util.concurrent.Uninterruptibles
|
||||||
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.*
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extended executor interface that supports thread affinity assertions and short circuiting. This can be useful
|
||||||
|
* for ensuring code runs on the right thread, and also for unit testing.
|
||||||
|
*/
|
||||||
|
interface AffinityExecutor : Executor {
|
||||||
|
/** Returns true if the current thread is equal to the thread this executor is backed by. */
|
||||||
|
val isOnThread: Boolean
|
||||||
|
|
||||||
|
/** Throws an IllegalStateException if the current thread is equal to the thread this executor is backed by. */
|
||||||
|
fun checkOnThread()
|
||||||
|
|
||||||
|
/** If isOnThread() then runnable is invoked immediately, otherwise the closure is queued onto the backing thread. */
|
||||||
|
fun executeASAP(runnable: () -> Unit)
|
||||||
|
|
||||||
|
/** Terminates any backing thread (pool) without waiting for tasks to finish. */
|
||||||
|
fun shutdownNow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given function on the executor, blocking until the result is available. Be careful not to deadlock this
|
||||||
|
* way! Make sure the executor can't possibly be waiting for the calling thread.
|
||||||
|
*/
|
||||||
|
fun <T> fetchFrom(fetcher: () -> T): T {
|
||||||
|
if (isOnThread)
|
||||||
|
return fetcher()
|
||||||
|
else
|
||||||
|
return CompletableFuture.supplyAsync(Supplier { fetcher() }, this).get()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseAffinityExecutor protected constructor() : AffinityExecutor {
|
||||||
|
protected val exceptionHandler: Thread.UncaughtExceptionHandler
|
||||||
|
|
||||||
|
init {
|
||||||
|
exceptionHandler = Thread.currentThread().uncaughtExceptionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract override val isOnThread: Boolean
|
||||||
|
|
||||||
|
override fun checkOnThread() {
|
||||||
|
checkState(isOnThread, "On wrong thread: %s", Thread.currentThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun executeASAP(runnable: () -> Unit) {
|
||||||
|
val command = {
|
||||||
|
try {
|
||||||
|
runnable()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
exceptionHandler.uncaughtException(Thread.currentThread(), throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isOnThread)
|
||||||
|
command()
|
||||||
|
else {
|
||||||
|
execute(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must comply with the Executor definition w.r.t. exceptions here.
|
||||||
|
abstract override fun execute(command: Runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An executor backed by thread pool (which may often have a single thread) which makes it easy to schedule
|
||||||
|
* tasks in the future and verify code is running on the executor.
|
||||||
|
*/
|
||||||
|
class ServiceAffinityExecutor(threadName: String, numThreads: Int) : BaseAffinityExecutor() {
|
||||||
|
protected val threads = Collections.synchronizedSet(HashSet<Thread>())
|
||||||
|
|
||||||
|
private val handler = Thread.currentThread().uncaughtExceptionHandler
|
||||||
|
val service: ScheduledThreadPoolExecutor
|
||||||
|
|
||||||
|
init {
|
||||||
|
val threadFactory = fun(runnable: Runnable): Thread {
|
||||||
|
val thread = object : Thread() {
|
||||||
|
override fun run() {
|
||||||
|
runnable.run()
|
||||||
|
threads -= this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread.isDaemon = true
|
||||||
|
thread.name = threadName
|
||||||
|
threads += thread
|
||||||
|
return thread
|
||||||
|
}
|
||||||
|
// The scheduled variant of the JDK thread pool doesn't do automatic calibration of the thread pool size,
|
||||||
|
// it always uses the 'core size'. So there is no point in allowing separate specification of core and max
|
||||||
|
// numbers of threads.
|
||||||
|
service = ScheduledThreadPoolExecutor(numThreads, threadFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isOnThread: Boolean get() = Thread.currentThread() in threads
|
||||||
|
|
||||||
|
override fun execute(command: Runnable) {
|
||||||
|
service.execute {
|
||||||
|
try {
|
||||||
|
command.run()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (handler != null)
|
||||||
|
handler.uncaughtException(Thread.currentThread(), e)
|
||||||
|
else
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> executeIn(time: Duration, command: () -> T): ScheduledFuture<T> {
|
||||||
|
return service.schedule(Callable {
|
||||||
|
try {
|
||||||
|
command()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (handler != null)
|
||||||
|
handler.uncaughtException(Thread.currentThread(), e)
|
||||||
|
else
|
||||||
|
e.printStackTrace()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}, time.toMillis(), TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdownNow() {
|
||||||
|
service.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val logger = loggerFor<ServiceAffinityExecutor>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An executor useful for unit tests: allows the current thread to block until a command arrives from another
|
||||||
|
* thread, which is then executed. Inbound closures/commands stack up until they are cleared by looping.
|
||||||
|
*
|
||||||
|
* @param alwaysQueue If true, executeASAP will never short-circuit and will always queue up.
|
||||||
|
*/
|
||||||
|
class Gate(private val alwaysQueue: Boolean = false) : BaseAffinityExecutor() {
|
||||||
|
private val thisThread = Thread.currentThread()
|
||||||
|
private val commandQ = LinkedBlockingQueue<Runnable>()
|
||||||
|
|
||||||
|
override val isOnThread: Boolean
|
||||||
|
get() = !alwaysQueue && Thread.currentThread() === thisThread
|
||||||
|
|
||||||
|
override fun execute(command: Runnable) {
|
||||||
|
Uninterruptibles.putUninterruptibly(commandQ, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun waitAndRun() {
|
||||||
|
val runnable = Uninterruptibles.takeUninterruptibly(commandQ)
|
||||||
|
runnable.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
val taskQueueSize: Int get() = commandQ.size
|
||||||
|
|
||||||
|
override fun shutdownNow() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val SAME_THREAD: AffinityExecutor = object : BaseAffinityExecutor() {
|
||||||
|
override val isOnThread: Boolean get() = true
|
||||||
|
override fun execute(command: Runnable) = command.run()
|
||||||
|
override fun shutdownNow() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
src/test/kotlin/core/utilities/AffinityExecutorTests.kt
Normal file
81
src/test/kotlin/core/utilities/AffinityExecutorTests.kt
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package core.utilities
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
class AffinityExecutorTests {
|
||||||
|
@Test fun `direct thread executor works`() {
|
||||||
|
val thisThread = Thread.currentThread()
|
||||||
|
AffinityExecutor.SAME_THREAD.execute { assertEquals(thisThread, Thread.currentThread()) }
|
||||||
|
AffinityExecutor.SAME_THREAD.executeASAP { assertEquals(thisThread, Thread.currentThread()) }
|
||||||
|
assert(AffinityExecutor.SAME_THREAD.isOnThread)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `single threaded affinity executor works`() {
|
||||||
|
val thisThread = Thread.currentThread()
|
||||||
|
val executor = AffinityExecutor.ServiceAffinityExecutor("test thread", 1)
|
||||||
|
assert(!executor.isOnThread)
|
||||||
|
assertFails { executor.checkOnThread() }
|
||||||
|
|
||||||
|
var thread: Thread? = null
|
||||||
|
executor.execute {
|
||||||
|
assertNotEquals(thisThread, Thread.currentThread())
|
||||||
|
executor.checkOnThread()
|
||||||
|
thread = Thread.currentThread()
|
||||||
|
}
|
||||||
|
executor.execute {
|
||||||
|
assertEquals(thread, Thread.currentThread())
|
||||||
|
executor.checkOnThread()
|
||||||
|
}
|
||||||
|
executor.fetchFrom { } // Serialize
|
||||||
|
|
||||||
|
executor.service.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `pooled executor works`() {
|
||||||
|
val executor = AffinityExecutor.ServiceAffinityExecutor("test2", 3)
|
||||||
|
assert(!executor.isOnThread)
|
||||||
|
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val threads = Collections.synchronizedList(ArrayList<Thread>())
|
||||||
|
|
||||||
|
fun blockAThread() {
|
||||||
|
executor.execute {
|
||||||
|
assert(executor.isOnThread)
|
||||||
|
threads += Thread.currentThread()
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockAThread()
|
||||||
|
blockAThread()
|
||||||
|
executor.fetchFrom { } // Serialize
|
||||||
|
assertEquals(2, threads.size)
|
||||||
|
executor.fetchFrom {
|
||||||
|
assert(executor.isOnThread)
|
||||||
|
threads += Thread.currentThread()
|
||||||
|
assertEquals(3, threads.distinct().size)
|
||||||
|
}
|
||||||
|
latch.countDown()
|
||||||
|
executor.fetchFrom { } // Serialize
|
||||||
|
executor.service.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Volatile var exception: Throwable? = null
|
||||||
|
@Test fun exceptions() {
|
||||||
|
// Run in a separate thread to avoid messing with any default exception handlers in the unit test thread.
|
||||||
|
thread {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> exception = throwable }
|
||||||
|
val executor = AffinityExecutor.ServiceAffinityExecutor("test3", 1)
|
||||||
|
executor.execute {
|
||||||
|
throw Exception("foo")
|
||||||
|
}
|
||||||
|
executor.fetchFrom { } // Serialize
|
||||||
|
assertEquals("foo", exception!!.message)
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user