/* * 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 { 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(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 { 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 { 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 { 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 { 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 { 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 { 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 { processQueue() } assertEquals("Worker is not current or already terminated", exception.message) } @Test fun requestTerminationOnWrongThread() { val worker = Worker.start() worker.requestTermination().result val exception = assertFailsWith { 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() (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 { future.result } assertEquals("Future is in an invalid state", exception.message) } }