Files
Alexander Shabalin ccba52aae4 [K/N][test] Mitigate a race in WorkerTest.executeAfterOrdering
Mostly affects linux aarch64 because we run it under qemu.
2024-01-18 08:12:22 +00:00

429 lines
14 KiB
Kotlin

/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package test.native.concurrent
import kotlin.concurrent.AtomicInt
import kotlin.native.concurrent.*
import kotlin.test.*
class WorkerTest {
@Test
fun execute() = withWorker {
val future = execute(TransferMode.SAFE, { "Input" }) { "$it processed" }
assertEquals("Input processed", future.result)
}
class MyError : Exception("My error")
@Test
fun executeWithException() = withWorker(errorReporting = false) {
val future = execute(TransferMode.SAFE, {}) { throw MyError() }
// Not `MyError`.
assertFailsWith<IllegalStateException> { future.result }
assertEquals("Still working", execute(TransferMode.SAFE, { "Still" }) { "$it working" }.result)
}
@OptIn(FreezingIsDeprecated::class)
@Test
fun executeWithDetachedObjectGraph() = withWorker {
data class SharedDataMember(val double: Double)
data class SharedData(val string: String, val int: Int, val member: SharedDataMember)
// Here we do rather strange thing. To test object detach API we detach object graph,
// pass detached graph to a worker, where we manually reattached passed value.
val future = execute(TransferMode.SAFE, {
DetachedObjectGraph { SharedData("Hello", 10, SharedDataMember(0.1)) }.asCPointer()
}) {
DetachedObjectGraph<SharedData>(it).attach()
}
assertEquals(SharedData("Hello", 10, SharedDataMember(double = 0.1)), future.result)
}
@Test
fun executeOnWrongThread() {
val worker = Worker.start()
worker.requestTermination().result
val exception = assertFailsWith<IllegalStateException> {
worker.execute(TransferMode.SAFE, {}, {}).result
}
assertEquals("Worker is already terminated", exception.message)
}
@Test
fun waitForMultipleFutures() {
val workers = Array(5) { Worker.start() }
val futures = (1..3).flatMap { attempt ->
workers.mapIndexed { index, worker ->
worker.execute(TransferMode.SAFE, { "$attempt: Input $index" }) { "$it processed" }
}
}
val actual = waitForMultipleFutures(futures, 10000)
val expected = (1..3).flatMap { attempt ->
(0 until 5).map { index -> "$attempt: Input $index processed" }
}.toSet()
// actual cannot be empty.
assertTrue(actual.isNotEmpty())
// Everything in actual must be in expected.
// The reverse is not required to be true: waitForMultipleFutures may return when
// only some futures have completed.
actual.forEach { future ->
// Every actual future is also computed.
assertEquals(FutureState.COMPUTED, future.state)
assertTrue(expected.contains(future.result))
}
workers.forEach {
it.requestTermination().result
}
}
@Test
fun executeWithConcurrentArrayModification() {
val workers = Array(100) { Worker.start() }
val array = Array(workers.size) { it }
val futures = workers.mapIndexed { index, worker ->
worker.execute(TransferMode.SAFE, { array to index }) { (array, index) ->
array[index] += index
}
}
while (waitForMultipleFutures(futures, 10000).size < futures.size) {
}
array.forEachIndexed { index, value ->
assertEquals(index * 2, value)
}
workers.forEach {
it.requestTermination().result
}
}
@Test
fun executeAfter() = withWorker {
val counter = AtomicInt(0)
executeAfter(0) {
assertTrue(Worker.current.park(10_000_000, false))
assertEquals(counter.value, 0)
assertTrue(Worker.current.processQueue())
assertEquals(1, counter.value)
// Let main proceed.
counter.incrementAndGet() // counter becomes 2 here.
assertTrue(Worker.current.park(10_000_000, true))
assertEquals(3, counter.value)
}
executeAfter(0) {
counter.incrementAndGet()
Unit
}
while (counter.value < 2) {
Worker.current.park(1_000)
}
executeAfter(0) {
counter.incrementAndGet()
Unit
}
while (counter.value == 2) {
Worker.current.park(1_000)
}
}
@Test
fun executeAfterNegativeDelay() = withWorker {
assertFailsWith<IllegalArgumentException> { executeAfter(-1) {} }
Unit
}
@Test
fun executeAfterModify() = withWorker {
var v = 1
val done = AtomicInt(0)
executeAfter(0) {
v++
assertEquals(2, v)
done.value = 1
}
while (done.value == 0) {
}
assertEquals(2, v)
}
@Test
fun executeAfterOrdering() = withWorker {
val counter = AtomicInt(0)
val lastTask = AtomicInt(0)
executeAfter(500_000) {
lastTask.value = 1
counter.incrementAndGet()
}
executeAfter(1_000) {
lastTask.value = 2
counter.incrementAndGet()
}
// Wait for both tasks to complete.
while (counter.value != 2) {
}
// Task with id 1 was scheduled to execute later, so it has won.
assertEquals(1, lastTask.value)
}
@Test
fun executeAfterCancelled() {
val worker = Worker.start()
val future = worker.execute(TransferMode.SAFE, {}) {
// Here we processed termination request.
assertEquals(false, Worker.current.processQueue())
}
worker.executeAfter(1_000_000_000L) { error("FAILURE") }
// Request worker to terminate and wait for the request to be processed.
worker.requestTermination(processScheduledJobs = false).result
// Now wait for the worker to complete termination, cleaning up after itself.
waitWorkerTermination(worker)
// `future` is bound to terminated `worker` and so it's not available anymore.
assertFailsWith<IllegalStateException> { future.result }
}
@Test
fun executeAfterOnMain() {
var done = false
Worker.current.executeAfter(0) {
done = true
}
// Not executed immediately.
assertFalse(done)
// The current worker's queue may be filled with other tasks, so we must loop.
while (!done) {
Worker.current.processQueue()
}
}
// This test checks that when multiple `executeAfter` jobs are submitted to `targetWorker` and have the
// same scheduled execution time (in micros since an epoch), nether of them gets lost.
@Test
fun executeAfterScheduledTimeClash() = withWorker {
val targetWorker = this
val mainWorker = Worker.current
// Configuration of the test.
val numberOfSubmitters = 2
val numberOfTasks = 100
val delayInMicroseconds = 100L
val submitters = Array(numberOfSubmitters) { Worker.start() }
try {
val readySubmittersCounter = AtomicInt(0)
val executedTasksCounter = AtomicInt(0)
val finishedBatchesCounter = AtomicInt(0)
submitters.forEach {
it.executeAfter(0L) {
readySubmittersCounter.incrementAndGet()
// Wait for other submitters, to make them all start at the same time:
while (readySubmittersCounter.value != numberOfSubmitters) {
}
// Concurrently submit tasks with matching scheduled execution time:
repeat(numberOfTasks) {
targetWorker.executeAfter(delayInMicroseconds) {
executedTasksCounter.incrementAndGet()
Unit
}
}
// Use larger delay for the task below, to make sure it gets executed after
// the tasks above submitted by the same worker.
// If the order is wrong, the test will fail as well.
// NOTE: the code below was affected by the same problem with clashing times, so despite all the effort
// the test still might hang without a fix.
targetWorker.executeAfter(delayInMicroseconds + 1) {
mainWorker.executeAfter(0L) {
finishedBatchesCounter.incrementAndGet()
Unit
}
}
}
}
while (finishedBatchesCounter.value != numberOfSubmitters) {
// Wait and allow processing the `finishedBatchesCounter.increment()` tasks above:
Worker.current.park(delayInMicroseconds, process = true)
}
// Note: we could have just waited for the condition above to become true,
// but this would mean that the test would hang in case of failure, which is not quite convenient.
assertEquals(numberOfSubmitters * numberOfTasks, executedTasksCounter.value)
} finally {
submitters.forEach { it.requestTermination().result }
}
}
@Test
fun executeAfterOnWrongThread() {
val worker = Worker.start()
worker.requestTermination().result
val exception = assertFailsWith<IllegalStateException> {
worker.executeAfter(0L) {}
}
assertEquals("Worker is already terminated", exception.message)
}
@Test
fun withName() = withWorker(name = "Lumberjack") {
execute(TransferMode.SAFE, {}) {
assertEquals("Lumberjack", Worker.current.name)
}.result
assertEquals("Lumberjack", name)
}
@Test
fun nameOnWrongThread() {
val worker = Worker.start(name = "Lumberjack")
worker.requestTermination().result
val exception = assertFailsWith<IllegalStateException> {
worker.name
}
assertEquals("Worker is already terminated", exception.message)
}
@Test
fun park() = withWorker {
val counter = AtomicInt(0)
val f1 = execute(TransferMode.SAFE, { counter }) { counter ->
Worker.current.park(Long.MAX_VALUE / 1000L, process = true)
counter.incrementAndGet()
}
// wait a bit
Worker.current.park(10_000L)
// submit a task
val f2 = execute(TransferMode.SAFE, { counter }) { counter ->
counter.incrementAndGet()
}
f1.result
f2.result
assertEquals(2, counter.value)
}
@Test
fun parkMain() = withWorker {
val main = Worker.current
val counter = AtomicInt(0)
executeAfter(1000) {
main.executeAfter(1) {
counter.incrementAndGet()
Unit
}
}
assertTrue(main.park(1_000_000_000L, process = true))
assertEquals(1, counter.value)
}
@Test
fun parkOnWrongThread() = withWorker {
val exception = assertFailsWith<IllegalStateException> {
park(1L)
}
assertEquals("Worker is not current or already terminated", exception.message)
}
@Test
fun parkZeroTimeout() {
Worker.current.park(0, process = true)
}
@Test
fun processQueue() = withWorker {
val counter = AtomicInt(0)
val future1 = execute(TransferMode.SAFE, { counter }) { counter ->
assertEquals(0, counter.value)
// Process following request.
while (!Worker.current.processQueue()) {
}
// Ensure it has an effect.
assertEquals(1, counter.value)
// No more non-terminating tasks in this worker queue.
assertEquals(false, Worker.current.processQueue())
}
val future2 = execute(TransferMode.SAFE, { counter }) { counter ->
counter.incrementAndGet()
}
future2.result
future1.result
}
@Test
fun processQueueOnWrongThread() = withWorker {
val exception = assertFailsWith<IllegalStateException> {
processQueue()
}
assertEquals("Worker is not current or already terminated", exception.message)
}
@Test
fun requestTerminationOnWrongThread() {
val worker = Worker.start()
worker.requestTermination().result
val exception = assertFailsWith<IllegalStateException> {
worker.requestTermination()
}
assertEquals("Worker is already terminated", exception.message)
}
@Test
fun activeWorkers() {
val workers = Array(10) { Worker.start() }
val actualWorkers = Worker.activeWorkers.toSet()
assertTrue(actualWorkers.size - workers.size == 1 || actualWorkers.size - workers.size == 2,
"actualWorkers.size = ${actualWorkers.size} workers.size = ${workers.size} actual size must be greater by 1 (main worker) or 2 (cleaners worker)")
workers.forEach {
actualWorkers.contains(it)
}
actualWorkers.contains(Worker.current)
val terminatedWorkers = mutableSetOf<Worker>()
(workers.indices step 2).forEach {
val worker = workers[it]
worker.requestTermination().result
terminatedWorkers.add(worker)
}
val actualWorkersAfterTermination = Worker.activeWorkers.toSet()
assertEquals(terminatedWorkers, actualWorkers - actualWorkersAfterTermination)
(workers.indices step 2).forEach {
workers[it + 1].requestTermination().result
}
}
@Test
fun futureConsumedTwice(): Unit = withWorker {
val future = execute(TransferMode.SAFE, {}) {
42
}
assertEquals(42, future.result)
val exception = assertFailsWith<IllegalStateException> {
future.result
}
assertEquals("Future is in an invalid state", exception.message)
}
}