From ea8d564b584b5a90eb92423ed2a47742ddf0056d Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Sat, 18 Jul 2020 23:52:05 -0700 Subject: [PATCH 1/8] Add debounce selector --- .../common/src/flow/operators/Delay.kt | 99 +++++++++++++------ .../test/flow/operators/DebounceTest.kt | 49 ++++++++- 2 files changed, 114 insertions(+), 34 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 5f3c900c1b..377e0179ac 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -16,7 +16,7 @@ import kotlin.time.* /** * Returns a flow that mirrors the original flow, but filters out values - * that are followed by the newer values within the given [timeout][timeoutMillis]. + * that are followed by the newer values within the given [timeoutMillis]. * The latest value is always emitted. * * Example: @@ -37,39 +37,45 @@ import kotlin.time.* * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. + * @param timeoutMillis must be positive */ @FlowPreview -public fun Flow.debounce(timeoutMillis: Long): Flow { - require(timeoutMillis > 0) { "Debounce timeout should be positive" } - return scopedFlow { downstream -> - // Actually Any, KT-30796 - val values = produce(capacity = Channel.CONFLATED) { - collect { value -> send(value ?: NULL) } - } - var lastValue: Any? = null - while (lastValue !== DONE) { - select { - // Should be receiveOrClosed when boxing issues are fixed - values.onReceiveOrNull { - if (it == null) { - if (lastValue != null) downstream.emit(NULL.unbox(lastValue)) - lastValue = DONE - } else { - lastValue = it - } - } +public fun Flow.debounce(timeoutMillis: Long): Flow = debounceInternal { timeoutMillis } - lastValue?.let { value -> - // set timeout when lastValue != null - onTimeout(timeoutMillis) { - lastValue = null // Consume the value - downstream.emit(NULL.unbox(value)) - } - } - } - } +/** + * A variation of [debounce] that allows specifying the timeout value dynamically. + * + * Example: + * ``` + * flow { + * emit(1) + * delay(100) + * emit(2) + * delay(100) + * emit(3) + * delay(1000) + * emit(4) + * delay(100) + * emit(5) + * delay(100) + * emit(6) + * }.debounce { + * if (it == 4) { + * 1L + * } else { + * 300L + * } + * } + * ``` + * produces `3, 4, 6`. + * + * @param timeoutMillisSelector [T] is the emitted value and the return value must be a positive timeout in milliseconds + */ +@FlowPreview +public fun Flow.debounce(timeoutMillisSelector: (T) -> Long): Flow = + debounceInternal { emittedItem -> + timeoutMillisSelector(emittedItem) } -} /** * Returns a flow that mirrors the original flow, but filters out values @@ -99,6 +105,39 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { @FlowPreview public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.toDelayMillis()) +private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : Flow = + scopedFlow { downstream -> + // Actually Any, KT-30796 + val values = produce(capacity = Channel.CONFLATED) { + collect { value -> send(value ?: NULL) } + } + var lastValue: Any? = null + while (lastValue !== DONE) { + select { + // Should be receiveOrClosed when boxing issues are fixed + values.onReceiveOrNull { + if (it == null) { + if (lastValue != null) downstream.emit(NULL.unbox(lastValue)) + lastValue = DONE + } else { + lastValue = it + } + } + + lastValue?.let { value -> + val unboxedValue: T = NULL.unbox(value) + val timeoutMillis = timeoutMillisSelector(unboxedValue) + require(timeoutMillis > 0) { "Debounce timeout should be positive" } + // set timeout when lastValue != null + onTimeout(timeoutMillis) { + lastValue = null // Consume the value + downstream.emit(unboxedValue) + } + } + } + } + } + /** * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis]. * diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index 4065671e3d..baec61d87d 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -11,7 +11,7 @@ import kotlin.time.* class DebounceTest : TestBase() { @Test - public fun testBasic() = withVirtualTime { + fun testBasic() = withVirtualTime { expect(1) val flow = flow { expect(3) @@ -175,7 +175,6 @@ class DebounceTest : TestBase() { expect(2) yield() throw TestException() - it } assertFailsWith(flow) @@ -193,7 +192,6 @@ class DebounceTest : TestBase() { expect(2) yield() throw TestException() - it } assertFailsWith(flow) @@ -202,7 +200,7 @@ class DebounceTest : TestBase() { @ExperimentalTime @Test - public fun testDurationBasic() = withVirtualTime { + fun testDurationBasic() = withVirtualTime { expect(1) val flow = flow { expect(3) @@ -223,4 +221,47 @@ class DebounceTest : TestBase() { assertEquals(listOf("A", "D", "E"), result) finish(5) } + + @Test + fun testDebounceSelector() = withVirtualTime { + expect(1) + val flow = flow { + expect(3) + emit("A") + delay(1500) + emit("B") + delay(500) + emit("C") + delay(250) + emit("D") + delay(800) + emit("E") + expect(4) + } + + expect(2) + val result = flow.debounce { + if (it == "C") { + 1 + } else { + 1000 + } + }.toList() + + assertEquals(listOf("A", "C", "E"), result) + finish(5) + } + + @Test + fun testZeroDebounceTime() = withVirtualTime { + expect(1) + val flow = flow { + expect(2) + emit("A") + delay(10) + emit("B") + } + assertFailsWith(flow.debounce(0)) + finish(3) + } } From 2dc4968733eb2787bff3a004725b3f0675a15cec Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Sun, 19 Jul 2020 10:12:41 -0700 Subject: [PATCH 2/8] Allow 0 debounce time only for the selector --- .../common/src/flow/operators/Delay.kt | 14 +++++++++----- .../common/test/flow/operators/DebounceTest.kt | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 377e0179ac..028ddb40c4 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -40,7 +40,10 @@ import kotlin.time.* * @param timeoutMillis must be positive */ @FlowPreview -public fun Flow.debounce(timeoutMillis: Long): Flow = debounceInternal { timeoutMillis } +public fun Flow.debounce(timeoutMillis: Long): Flow { + require(timeoutMillis > 0) { "Debounce timeout should be positive" } + return debounceInternal { timeoutMillis } +} /** * A variation of [debounce] that allows specifying the timeout value dynamically. @@ -61,7 +64,7 @@ public fun Flow.debounce(timeoutMillis: Long): Flow = debounceInternal * emit(6) * }.debounce { * if (it == 4) { - * 1L + * 0L * } else { * 300L * } @@ -69,12 +72,14 @@ public fun Flow.debounce(timeoutMillis: Long): Flow = debounceInternal * ``` * produces `3, 4, 6`. * - * @param timeoutMillisSelector [T] is the emitted value and the return value must be a positive timeout in milliseconds + * @param timeoutMillisSelector [T] is the emitted value and the return value is timeout in milliseconds. 0ms timeout is allowed. */ @FlowPreview public fun Flow.debounce(timeoutMillisSelector: (T) -> Long): Flow = debounceInternal { emittedItem -> - timeoutMillisSelector(emittedItem) + val timeoutMillis = timeoutMillisSelector(emittedItem) + require(timeoutMillis >= 0) { "Debounce timeout should not be negative" } + timeoutMillis } /** @@ -127,7 +132,6 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F lastValue?.let { value -> val unboxedValue: T = NULL.unbox(value) val timeoutMillis = timeoutMillisSelector(unboxedValue) - require(timeoutMillis > 0) { "Debounce timeout should be positive" } // set timeout when lastValue != null onTimeout(timeoutMillis) { lastValue = null // Consume the value diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index baec61d87d..7e4dc28394 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -232,7 +232,7 @@ class DebounceTest : TestBase() { emit("B") delay(500) emit("C") - delay(250) + delay(1) emit("D") delay(800) emit("E") @@ -242,7 +242,7 @@ class DebounceTest : TestBase() { expect(2) val result = flow.debounce { if (it == "C") { - 1 + 0 } else { 1000 } @@ -253,15 +253,21 @@ class DebounceTest : TestBase() { } @Test - fun testZeroDebounceTime() = withVirtualTime { + fun testZeroDebounceTime() = runTest { + assertFailsWith { + flowOf().debounce(0) + } + } + + @Test + fun testNegativeDebounceTimeSelector() = withVirtualTime { expect(1) val flow = flow { expect(2) emit("A") - delay(10) - emit("B") + delay(1) } - assertFailsWith(flow.debounce(0)) + assertFailsWith(flow.debounce { -1 }) finish(3) } } From 59751daf254291b17f9fdffb254384d43a15931e Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Mon, 20 Jul 2020 19:08:10 -0700 Subject: [PATCH 3/8] Remove <= 0ms illegal argument exceptions --- .../common/src/flow/operators/Delay.kt | 9 ++---- .../test/flow/operators/DebounceTest.kt | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 028ddb40c4..9cc3c228ec 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -37,11 +37,10 @@ import kotlin.time.* * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. - * @param timeoutMillis must be positive */ @FlowPreview public fun Flow.debounce(timeoutMillis: Long): Flow { - require(timeoutMillis > 0) { "Debounce timeout should be positive" } + if (timeoutMillis <= 0) return this return debounceInternal { timeoutMillis } } @@ -72,14 +71,12 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * ``` * produces `3, 4, 6`. * - * @param timeoutMillisSelector [T] is the emitted value and the return value is timeout in milliseconds. 0ms timeout is allowed. + * @param timeoutMillisSelector [T] is the emitted value and the return value is timeout in milliseconds. */ @FlowPreview public fun Flow.debounce(timeoutMillisSelector: (T) -> Long): Flow = debounceInternal { emittedItem -> - val timeoutMillis = timeoutMillisSelector(emittedItem) - require(timeoutMillis >= 0) { "Debounce timeout should not be negative" } - timeoutMillis + timeoutMillisSelector(emittedItem) } /** diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index 7e4dc28394..0569eb383a 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -223,7 +223,7 @@ class DebounceTest : TestBase() { } @Test - fun testDebounceSelector() = withVirtualTime { + fun testDebounceSelectorBasic() = withVirtualTime { expect(1) val flow = flow { expect(3) @@ -253,21 +253,36 @@ class DebounceTest : TestBase() { } @Test - fun testZeroDebounceTime() = runTest { - assertFailsWith { - flowOf().debounce(0) + fun testZeroDebounceTime() = withVirtualTime { + expect(1) + val flow = flow { + expect(2) + emit("A") + emit("B") + emit("C") + expect(3) } + + val result = flow.debounce(0).toList() + + assertEquals(listOf("A", "B", "C"), result) + finish(4) } @Test - fun testNegativeDebounceTimeSelector() = withVirtualTime { + fun testZeroDebounceTimeSelector() = withVirtualTime { expect(1) val flow = flow { expect(2) emit("A") delay(1) + emit("B") + expect(3) } - assertFailsWith(flow.debounce { -1 }) - finish(3) + + val result = flow.debounce { 0 }.toList() + + assertEquals(listOf("A", "B"), result) + finish(4) } } From f18831698fefa789ca41746efc0e6b2ef7721540 Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Fri, 25 Sep 2020 22:56:58 -0700 Subject: [PATCH 4/8] Add ExperimentalTime, throw IAE on negative, 0 capacity --- kotlinx-coroutines-core/common/src/flow/operators/Delay.kt | 7 +++++-- .../common/test/flow/operators/DebounceTest.kt | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 9cc3c228ec..bebeaca499 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -40,7 +40,8 @@ import kotlin.time.* */ @FlowPreview public fun Flow.debounce(timeoutMillis: Long): Flow { - if (timeoutMillis <= 0) return this + require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } + if (timeoutMillis == 0L) return this return debounceInternal { timeoutMillis } } @@ -73,6 +74,7 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * * @param timeoutMillisSelector [T] is the emitted value and the return value is timeout in milliseconds. */ +@ExperimentalTime @FlowPreview public fun Flow.debounce(timeoutMillisSelector: (T) -> Long): Flow = debounceInternal { emittedItem -> @@ -110,7 +112,7 @@ public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.t private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : Flow = scopedFlow { downstream -> // Actually Any, KT-30796 - val values = produce(capacity = Channel.CONFLATED) { + val values = produce(capacity = 0) { collect { value -> send(value ?: NULL) } } var lastValue: Any? = null @@ -129,6 +131,7 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F lastValue?.let { value -> val unboxedValue: T = NULL.unbox(value) val timeoutMillis = timeoutMillisSelector(unboxedValue) + require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } // set timeout when lastValue != null onTimeout(timeoutMillis) { lastValue = null // Consume the value diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index 0569eb383a..6d95b043dd 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -159,7 +159,7 @@ class DebounceTest : TestBase() { expect(2) throw TestException() }.flowOn(NamedDispatchers("source")).debounce(Long.MAX_VALUE).map { - expectUnreached() + expectUnreached() } assertFailsWith(flow) finish(3) @@ -222,6 +222,7 @@ class DebounceTest : TestBase() { finish(5) } + @ExperimentalTime @Test fun testDebounceSelectorBasic() = withVirtualTime { expect(1) @@ -269,6 +270,7 @@ class DebounceTest : TestBase() { finish(4) } + @ExperimentalTime @Test fun testZeroDebounceTimeSelector() = withVirtualTime { expect(1) From 3f108cbfbc49680a88010b80b477130f67a3e473 Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Fri, 16 Oct 2020 10:54:35 +0330 Subject: [PATCH 5/8] Update shared-mutable-state-and-concurrency.md (#2309) --- docs/shared-mutable-state-and-concurrency.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/shared-mutable-state-and-concurrency.md b/docs/shared-mutable-state-and-concurrency.md index 316d56e5bc..8b83ad0b20 100644 --- a/docs/shared-mutable-state-and-concurrency.md +++ b/docs/shared-mutable-state-and-concurrency.md @@ -24,7 +24,7 @@ but others are unique. ### The problem -Let us launch a hundred coroutines all doing the same action thousand times. +Let us launch a hundred coroutines all doing the same action a thousand times. We'll also measure their completion time for further comparisons:
@@ -102,7 +102,7 @@ increment the `counter` concurrently from multiple threads without any synchroni ### Volatiles are of no help -There is common misconception that making a variable `volatile` solves concurrency problem. Let us try it: +There is a common misconception that making a variable `volatile` solves concurrency problem. Let us try it: @@ -158,7 +158,7 @@ do not provide atomicity of larger actions (increment in our case). ### Thread-safe data structures The general solution that works both for threads and for coroutines is to use a thread-safe (aka synchronized, -linearizable, or atomic) data structure that provides all the necessarily synchronization for the corresponding +linearizable, or atomic) data structure that provides all the necessary synchronization for the corresponding operations that needs to be performed on a shared state. In the case of a simple counter we can use `AtomicInteger` class which has atomic `incrementAndGet` operations: From 8e8feb7bd63aed3bff1a56bab3d55aae678420a0 Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Sat, 17 Oct 2020 21:38:17 -0700 Subject: [PATCH 6/8] Correct kdoc, optimize 0ms timeout, fix wrong annotation --- .../common/src/flow/operators/Delay.kt | 109 +++++++++++++----- .../test/flow/operators/DebounceTest.kt | 70 ++++++++--- 2 files changed, 133 insertions(+), 46 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index bebeaca499..d5d4278a2e 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -11,12 +11,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.internal.* import kotlinx.coroutines.selects.* +import kotlin.coroutines.* import kotlin.jvm.* import kotlin.time.* /** * Returns a flow that mirrors the original flow, but filters out values - * that are followed by the newer values within the given [timeoutMillis]. + * that are followed by the newer values within the given [timeout][timeoutMillis]. * The latest value is always emitted. * * Example: @@ -46,39 +47,43 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { } /** + * Returns a flow that mirrors the original flow, but filters out values + * that are followed by the newer values within the given [timeout][timeoutMillis]. + * The latest value is always emitted. + * * A variation of [debounce] that allows specifying the timeout value dynamically. * * Example: * ``` * flow { * emit(1) - * delay(100) + * delay(90) * emit(2) - * delay(100) + * delay(90) * emit(3) - * delay(1000) + * delay(1010) * emit(4) - * delay(100) + * delay(1010) * emit(5) - * delay(100) - * emit(6) * }.debounce { - * if (it == 4) { + * if (it == 1) { * 0L * } else { - * 300L + * 1000L * } * } * ``` - * produces `3, 4, 6`. + * produces `1, 3, 4, 5`. + * + * Note that the resulting flow does not emit anything as long as the original flow emits + * items faster than every [timeoutMillis] milliseconds. * - * @param timeoutMillisSelector [T] is the emitted value and the return value is timeout in milliseconds. + * @param timeoutMillis [T] is the emitted value and the return value is timeout in milliseconds. */ -@ExperimentalTime @FlowPreview -public fun Flow.debounce(timeoutMillisSelector: (T) -> Long): Flow = +public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = debounceInternal { emittedItem -> - timeoutMillisSelector(emittedItem) + timeoutMillis(emittedItem) } /** @@ -107,7 +112,48 @@ public fun Flow.debounce(timeoutMillisSelector: (T) -> Long): Flow = */ @ExperimentalTime @FlowPreview -public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.toDelayMillis()) +public fun Flow.debounceWithDuration(timeout: Duration): Flow = debounce(timeout.toDelayMillis()) + +/** + * Returns a flow that mirrors the original flow, but filters out values + * that are followed by the newer values within the given [timeout]. + * The latest value is always emitted. + * + * A variation of [debounceWithDuration] that allows specifying the timeout value dynamically. + * + * Example: + * ``` + * flow { + * emit(1) + * delay(90.milliseconds) + * emit(2) + * delay(90.milliseconds) + * emit(3) + * delay(1010.milliseconds) + * emit(4) + * delay(1010.milliseconds) + * emit(5) + * }.debounce { + * if (it == 1) { + * 0.milliseconds + * } else { + * 1000.milliseconds + * } + * } + * ``` + * produces `1, 3, 4, 5`. + * + * Note that the resulting flow does not emit anything as long as the original flow emits + * items faster than every [timeout] milliseconds. + * + * @param timeout [T] is the emitted value and the return value is timeout in [Duration]. + */ +@ExperimentalTime +@FlowPreview +public fun Flow.debounceWithDuration(timeout: (T) -> Duration): Flow = + debounceInternal { emittedItem -> + timeout(emittedItem).toDelayMillis() + } private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : Flow = scopedFlow { downstream -> @@ -118,6 +164,26 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F var lastValue: Any? = null while (lastValue !== DONE) { select { + // Give a chance to consume lastValue first before onReceiveOrNull receives a new value + lastValue?.let { value -> + val unboxedValue: T = NULL.unbox(value) + val timeoutMillis = timeoutMillisSelector(unboxedValue) + require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } + + if (timeoutMillis == 0L) { + lastValue = null + runBlocking { + launch { downstream.emit(unboxedValue) } + } + } else { + // Set timeout when lastValue != null + onTimeout(timeoutMillis) { + lastValue = null // Consume the value + downstream.emit(unboxedValue) + } + } + } + // Should be receiveOrClosed when boxing issues are fixed values.onReceiveOrNull { if (it == null) { @@ -127,17 +193,6 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F lastValue = it } } - - lastValue?.let { value -> - val unboxedValue: T = NULL.unbox(value) - val timeoutMillis = timeoutMillisSelector(unboxedValue) - require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } - // set timeout when lastValue != null - onTimeout(timeoutMillis) { - lastValue = null // Consume the value - downstream.emit(unboxedValue) - } - } } } } @@ -155,7 +210,7 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F * }.sample(100) * ``` * produces `1, 3, 5, 7, 9`. - * + * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @FlowPreview diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index 6d95b043dd..cd30ec54b8 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -217,7 +217,7 @@ class DebounceTest : TestBase() { } expect(2) - val result = flow.debounce(1000.milliseconds).toList() + val result = flow.debounceWithDuration(1000.milliseconds).toList() assertEquals(listOf("A", "D", "E"), result) finish(5) } @@ -228,28 +228,28 @@ class DebounceTest : TestBase() { expect(1) val flow = flow { expect(3) - emit("A") - delay(1500) - emit("B") - delay(500) - emit("C") - delay(1) - emit("D") - delay(800) - emit("E") + emit(1) + delay(90) + emit(2) + delay(90) + emit(3) + delay(1010) + emit(4) + delay(1010) + emit(5) expect(4) } expect(2) val result = flow.debounce { - if (it == "C") { + if (it == 1) { 0 } else { 1000 } }.toList() - assertEquals(listOf("A", "C", "E"), result) + assertEquals(listOf(1, 3, 4, 5), result) finish(5) } @@ -257,17 +257,18 @@ class DebounceTest : TestBase() { fun testZeroDebounceTime() = withVirtualTime { expect(1) val flow = flow { - expect(2) + expect(3) emit("A") emit("B") emit("C") - expect(3) + expect(4) } + expect(2) val result = flow.debounce(0).toList() assertEquals(listOf("A", "B", "C"), result) - finish(4) + finish(5) } @ExperimentalTime @@ -275,16 +276,47 @@ class DebounceTest : TestBase() { fun testZeroDebounceTimeSelector() = withVirtualTime { expect(1) val flow = flow { - expect(2) + expect(3) emit("A") - delay(1) emit("B") - expect(3) + expect(4) } + expect(2) val result = flow.debounce { 0 }.toList() assertEquals(listOf("A", "B"), result) - finish(4) + finish(5) + } + + @ExperimentalTime + @Test + fun testDebounceDurationSelectorBasic() = withVirtualTime { + expect(1) + val flow = flow { + expect(3) + emit("A") + delay(1500.milliseconds) + emit("B") + delay(500.milliseconds) + emit("C") + delay(250.milliseconds) + emit("D") + delay(2000.milliseconds) + emit("E") + expect(4) + } + + expect(2) + val result = flow.debounceWithDuration { + if (it == "C") { + 0.milliseconds + } else { + 1000.milliseconds + } + }.toList() + + assertEquals(listOf("A", "C", "D", "E"), result) + finish(5) } } From 7c6fb412c2d85b322d50e045389319dff3d98a37 Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Sat, 17 Oct 2020 21:57:33 -0700 Subject: [PATCH 7/8] Revert "Merge branch 'master' of https://github.com/Kotlin/kotlinx.coroutines into debounce-selector" This reverts commit 78a20ba6c5fb0ceaa6cd66da9a186f3e353dfe71, reversing changes made to 8e8feb7bd63aed3bff1a56bab3d55aae678420a0. --- CHANGES.md | 40 - CONTRIBUTING.md | 113 --- README.md | 80 +- RELEASE.md | 12 +- benchmarks/build.gradle.kts | 12 +- .../benchmarks/tailcall/SimpleChannel.kt | 8 +- build.gradle | 45 +- buildSrc/build.gradle.kts | 30 +- buildSrc/settings.gradle.kts | 17 - buildSrc/src/main/kotlin/CacheRedirector.kt | 152 ---- buildSrc/src/main/kotlin/Dokka.kt | 26 - buildSrc/src/main/kotlin/Idea.kt | 1 - buildSrc/src/main/kotlin/MavenCentral.kt | 7 +- buildSrc/src/main/kotlin/Platform.kt | 1 - buildSrc/src/main/kotlin/Properties.kt | 11 - buildSrc/src/main/kotlin/RunR8.kt | 52 -- buildSrc/src/main/kotlin/UnpackAar.kt | 32 - coroutines-guide.md | 3 - docs/cancellation-and-timeouts.md | 109 --- docs/coroutine-context-and-dispatchers.md | 34 +- docs/coroutines-guide.md | 2 +- docs/exception-handling.md | 2 +- docs/flow.md | 16 +- docs/images/coroutine-idea-debugging-1.png | Bin 248706 -> 0 bytes docs/knit.properties | 12 + docs/knit.test.template | 1 - docs/shared-mutable-state-and-concurrency.md | 6 +- gradle.properties | 25 +- gradle/compile-common.gradle | 4 + gradle/compile-js-multiplatform.gradle | 4 + gradle/compile-js.gradle | 28 +- gradle/compile-jvm-multiplatform.gradle | 8 + gradle/compile-jvm.gradle | 7 +- gradle/compile-native-multiplatform.gradle | 40 +- gradle/targets.gradle | 28 + gradle/test-mocha-js.gradle | 4 +- .../kotlinx-coroutines-guava/build.gradle | 16 + .../kotlinx-coroutines-guava/build.gradle.kts | 13 - .../build.gradle | 34 + .../kotlinx-coroutines-slf4j/build.gradle | 17 + .../kotlinx-coroutines-slf4j/build.gradle.kts | 14 - js/example-frontend-js/README.md | 4 +- js/example-frontend-js/build.gradle | 69 +- js/example-frontend-js/npm/package.json | 22 + js/example-frontend-js/npm/webpack.config.js | 53 ++ js/example-frontend-js/src/ExampleMain.kt | 3 - .../example-frontend-js/src/main/web/main.js | 5 +- .../webpack.config.d/custom-config.js | 18 - knit.properties | 16 - .../api/kotlinx-coroutines-core.api | 114 +-- kotlinx-coroutines-core/build.gradle | 120 +-- kotlinx-coroutines-core/common/src/Await.kt | 15 +- .../common/src/Builders.common.kt | 5 +- .../common/src/CancellableContinuation.kt | 193 ++--- .../common/src/CancellableContinuationImpl.kt | 300 +++---- .../common/src/CompletableDeferred.kt | 2 +- .../common/src/CompletableJob.kt | 2 +- ...tionState.kt => CompletedExceptionally.kt} | 18 +- .../common/src/CoroutineDispatcher.kt | 1 - .../common/src/CoroutineScope.kt | 4 +- .../common/src/Deferred.kt | 2 - kotlinx-coroutines-core/common/src/Delay.kt | 59 +- kotlinx-coroutines-core/common/src/Timeout.kt | 38 +- kotlinx-coroutines-core/common/src/Yield.kt | 3 - .../common/src/channels/AbstractChannel.kt | 163 +--- .../src/channels/ArrayBroadcastChannel.kt | 6 +- .../common/src/channels/ArrayChannel.kt | 160 ++-- .../common/src/channels/Broadcast.kt | 7 +- .../common/src/channels/BroadcastChannel.kt | 7 +- .../common/src/channels/BufferOverflow.kt | 36 - .../common/src/channels/Channel.kt | 205 ++--- .../common/src/channels/Channels.common.kt | 9 +- .../src/channels/ConflatedBroadcastChannel.kt | 8 +- .../common/src/channels/ConflatedChannel.kt | 27 +- .../common/src/channels/LinkedListChannel.kt | 2 +- .../common/src/channels/Produce.kt | 28 +- .../common/src/channels/RendezvousChannel.kt | 4 +- .../common/src/flow/Builders.kt | 61 +- .../common/src/flow/Channels.kt | 33 +- .../common/src/flow/Flow.kt | 22 +- .../common/src/flow/Migration.kt | 59 +- .../common/src/flow/SharedFlow.kt | 659 --------------- .../common/src/flow/SharingStarted.kt | 216 ----- .../common/src/flow/StateFlow.kt | 263 +++--- .../src/flow/internal/AbstractSharedFlow.kt | 101 --- .../common/src/flow/internal/ChannelFlow.kt | 122 +-- .../common/src/flow/internal/Merge.kt | 30 +- .../common/src/flow/operators/Context.kt | 91 +- .../common/src/flow/operators/Delay.kt | 97 +-- .../common/src/flow/operators/Distinct.kt | 52 +- .../common/src/flow/operators/Emitters.kt | 11 +- .../common/src/flow/operators/Lint.kt | 90 +- .../common/src/flow/operators/Share.kt | 412 --------- .../common/src/flow/operators/Transform.kt | 2 +- .../common/src/flow/terminal/Reduce.kt | 37 +- .../common/src/internal/Atomic.kt | 7 +- .../src/internal/DispatchedContinuation.kt | 57 +- .../common/src/internal/DispatchedTask.kt | 99 +-- .../src/internal/LockFreeLinkedList.common.kt | 1 - .../src/internal/OnUndeliveredElement.kt | 43 - .../common/src/intrinsics/Cancellable.kt | 8 +- .../common/src/selects/Select.kt | 12 +- .../common/src/sync/Mutex.kt | 37 +- .../common/src/sync/Semaphore.kt | 25 +- .../test/AtomicCancellationCommonTest.kt | 13 +- .../common/test/AwaitCancellationTest.kt | 27 - .../CancellableContinuationHandlersTest.kt | 72 +- .../test/CancellableContinuationTest.kt | 22 - .../common/test/CancellableResumeTest.kt | 156 +--- .../common/test/DispatchedContinuationTest.kt | 78 -- .../test/channels/BasicOperationsTest.kt | 20 +- .../common/test/channels/BroadcastTest.kt | 4 +- .../channels/ChannelBufferOverflowTest.kt | 40 - .../test/channels/ChannelFactoryTest.kt | 17 +- .../ChannelUndeliveredElementFailureTest.kt | 143 ---- .../channels/ChannelUndeliveredElementTest.kt | 104 --- .../ConflatedChannelArrayModelTest.kt | 11 - .../test/channels/ConflatedChannelTest.kt | 19 +- .../common/test/channels/TestChannelKind.kt | 14 +- .../test/flow/{sharing => }/StateFlowTest.kt | 85 +- .../common/test/flow/VirtualTime.kt | 18 +- .../flow/operators/BufferConflationTest.kt | 146 ---- .../common/test/flow/operators/BufferTest.kt | 33 +- .../common/test/flow/operators/CatchTest.kt | 7 +- .../common/test/flow/operators/CombineTest.kt | 18 +- .../operators/DistinctUntilChangedTest.kt | 29 - .../common/test/flow/operators/FlowOnTest.kt | 16 - .../test/flow/operators/OnCompletionTest.kt | 4 +- .../common/test/flow/operators/ZipTest.kt | 25 +- .../test/flow/sharing/ShareInBufferTest.kt | 98 --- .../flow/sharing/ShareInConflationTest.kt | 162 ---- .../test/flow/sharing/ShareInFusionTest.kt | 56 -- .../common/test/flow/sharing/ShareInTest.kt | 215 ----- .../flow/sharing/SharedFlowScenarioTest.kt | 331 -------- .../test/flow/sharing/SharedFlowTest.kt | 798 ------------------ .../test/flow/sharing/SharingStartedTest.kt | 183 ---- .../SharingStartedWhileSubscribedTest.kt | 42 - .../common/test/flow/sharing/StateInTest.kt | 78 -- .../common/test/flow/terminal/FirstTest.kt | 6 - .../common/test/flow/terminal/SingleTest.kt | 27 +- .../common/test/selects/SelectLoopTest.kt | 23 +- kotlinx-coroutines-core/js/src/Dispatchers.kt | 21 +- .../js/src/JSDispatcher.kt | 4 +- kotlinx-coroutines-core/js/src/Promise.kt | 2 - .../js/src/internal/LinkedList.kt | 2 - .../js/test/ImmediateDispatcherTest.kt | 32 - .../jvm/resources/DebugProbesKt.bin | Bin 1728 -> 1730 bytes .../META-INF/proguard/coroutines.pro | 6 - kotlinx-coroutines-core/jvm/src/CommonPool.kt | 2 - .../jvm/src/DebugStrings.kt | 1 - .../jvm/src/DefaultExecutor.kt | 3 +- .../jvm/src/Dispatchers.kt | 6 +- kotlinx-coroutines-core/jvm/src/Executors.kt | 38 +- .../jvm/src/ThreadPoolDispatcher.kt | 10 - .../jvm/src/internal/LockFreeLinkedList.kt | 19 +- .../jvm/src/internal/MainDispatchers.kt | 9 +- .../jvm/src/scheduling/Dispatcher.kt | 8 +- .../jvm/src/test_/TestCoroutineContext.kt | 2 +- .../jvm/test/AtomicCancellationTest.kt | 27 +- .../jvm/test/ExecutorsTest.kt | 6 +- .../test/FailingCoroutinesMachineryTest.kt | 39 +- .../jvm/test/JobStructuredJoinStressTest.kt | 41 +- .../jvm/test/RejectedExecutionTest.kt | 168 ---- .../ReusableCancellableContinuationTest.kt | 52 +- kotlinx-coroutines-core/jvm/test/TestBase.kt | 4 +- .../BroadcastChannelMultiReceiveStressTest.kt | 13 +- .../channels/ChannelAtomicCancelStressTest.kt | 156 ++++ ...annelCancelUndeliveredElementStressTest.kt | 102 --- .../channels/ChannelSendReceiveStressTest.kt | 2 +- .../ChannelUndeliveredElementStressTest.kt | 255 ------ .../test/channels/InvokeOnCloseStressTest.kt | 2 +- .../test/channels/SimpleSendReceiveJvmTest.kt | 2 +- .../jvm/test/examples/example-delay-01.kt | 24 - .../jvm/test/examples/example-delay-02.kt | 19 - .../examples/example-delay-duration-01.kt | 26 - .../examples/example-delay-duration-02.kt | 21 - .../jvm/test/examples/test/FlowDelayTest.kt | 39 - .../jvm/test/flow/SharingStressTest.kt | 193 ----- .../test/flow/StateFlowCancellabilityTest.kt | 56 -- .../jvm/test/guide/example-cancel-08.kt | 31 - .../jvm/test/guide/example-cancel-09.kt | 36 - .../jvm/test/guide/test/BasicsGuideTest.kt | 1 - .../test/guide/test/CancellationGuideTest.kt | 8 - .../jvm/test/guide/test/ChannelsGuideTest.kt | 1 - .../jvm/test/guide/test/ComposingGuideTest.kt | 1 - .../test/guide/test/DispatcherGuideTest.kt | 1 - .../test/guide/test/ExceptionsGuideTest.kt | 1 - .../jvm/test/guide/test/FlowGuideTest.kt | 1 - .../jvm/test/guide/test/SelectGuideTest.kt | 1 - .../test/guide/test/SharedStateGuideTest.kt | 1 - .../jvm/test/{knit => guide/test}/TestUtil.kt | 4 +- .../test/internal/ConcurrentWeakMapTest.kt | 12 +- .../jvm/test/sync/MutexStressTest.kt | 64 -- .../jvm/test/sync/SemaphoreStressTest.kt | 8 +- kotlinx-coroutines-core/knit.properties | 10 - .../native/src/CoroutineContext.kt | 4 +- .../native/src/Dispatchers.kt | 8 + .../native/src/EventLoop.kt | 3 +- .../native/src/WorkerMain.native.kt | 8 - .../native/src/internal/LinkedList.kt | 2 - .../native/test/WorkerTest.kt | 4 +- .../nativeDarwin/src/WorkerMain.kt | 13 - .../nativeDarwin/test/Launcher.kt | 28 - .../nativeOther/test/Launcher.kt | 23 - kotlinx-coroutines-debug/README.md | 96 +-- .../test/CoroutinesDumpTest.kt | 26 +- .../test/DebugLeaksStressTest.kt | 56 ++ .../test/DebugProbesTest.kt | 27 +- kotlinx-coroutines-test/README.md | 2 +- .../api/kotlinx-coroutines-test.api | 2 +- .../src/TestCoroutineDispatcher.kt | 2 +- .../src/internal/MainTestDispatcher.kt | 4 +- .../test/TestRunBlockingOrderTest.kt | 4 +- .../api/kotlinx-coroutines-jdk9.api | 2 - reactive/kotlinx-coroutines-jdk9/build.gradle | 24 + .../kotlinx-coroutines-jdk9/build.gradle.kts | 22 - .../src/ReactiveFlow.kt | 14 +- .../kotlinx-coroutines-reactive/README.md | 4 +- .../api/kotlinx-coroutines-reactive.api | 7 +- .../build.gradle.kts | 49 +- .../kotlinx-coroutines-reactive/src/Await.kt | 48 +- .../src/Channel.kt | 2 +- .../src/ReactiveFlow.kt | 56 +- .../test/FlowAsPublisherTest.kt | 76 +- .../test/IntegrationTest.kt | 11 +- .../test/PublisherAsFlowTest.kt | 82 -- .../api/kotlinx-coroutines-reactor.api | 4 +- .../kotlinx-coroutines-reactor/build.gradle | 23 + .../build.gradle.kts | 25 - .../src/ReactorFlow.kt | 19 +- .../src/Scheduler.kt | 2 +- .../test/FlowAsFluxTest.kt | 79 +- .../test/FluxSingleTest.kt | 66 -- .../api/kotlinx-coroutines-rx2.api | 14 +- .../kotlinx-coroutines-rx2/src/RxChannel.kt | 2 +- .../kotlinx-coroutines-rx2/src/RxConvert.kt | 62 +- .../kotlinx-coroutines-rx2/src/RxScheduler.kt | 2 +- .../test/ConvertTest.kt | 7 +- .../test/FlowAsFlowableTest.kt | 89 -- .../test/FlowAsObservableTest.kt | 69 -- .../test/IntegrationTest.kt | 5 +- .../api/kotlinx-coroutines-rx3.api | 14 +- .../kotlinx-coroutines-rx3/src/RxChannel.kt | 2 +- .../kotlinx-coroutines-rx3/src/RxConvert.kt | 37 +- .../kotlinx-coroutines-rx3/src/RxScheduler.kt | 2 +- .../test/FlowAsFlowableTest.kt | 89 -- .../test/FlowAsObservableTest.kt | 69 -- site/build.gradle.kts | 6 +- ui/coroutines-guide-ui.md | 4 +- .../test/ordered/tests/TestComponent.kt | 2 +- .../animation-app/gradle.properties | 4 +- .../api/kotlinx-coroutines-android.api | 2 +- .../build.gradle.kts | 56 +- .../example-app/gradle.properties | 4 +- .../r8-from-1.6.0/coroutines.pro | 2 - .../src/HandlerDispatcher.kt | 2 +- .../api/kotlinx-coroutines-javafx.api | 2 +- ui/kotlinx-coroutines-javafx/build.gradle | 34 + ui/kotlinx-coroutines-javafx/build.gradle.kts | 50 -- .../src/JavaFxDispatcher.kt | 2 +- .../api/kotlinx-coroutines-swing.api | 2 +- .../src/SwingDispatcher.kt | 2 +- 262 files changed, 1943 insertions(+), 9795 deletions(-) delete mode 100644 CONTRIBUTING.md delete mode 100644 buildSrc/settings.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/CacheRedirector.kt delete mode 100644 buildSrc/src/main/kotlin/Dokka.kt delete mode 100644 buildSrc/src/main/kotlin/Properties.kt delete mode 100644 buildSrc/src/main/kotlin/RunR8.kt delete mode 100644 buildSrc/src/main/kotlin/UnpackAar.kt delete mode 100644 docs/images/coroutine-idea-debugging-1.png create mode 100644 gradle/targets.gradle create mode 100644 integration/kotlinx-coroutines-guava/build.gradle delete mode 100644 integration/kotlinx-coroutines-guava/build.gradle.kts create mode 100644 integration/kotlinx-coroutines-slf4j/build.gradle delete mode 100644 integration/kotlinx-coroutines-slf4j/build.gradle.kts create mode 100644 js/example-frontend-js/npm/package.json create mode 100644 js/example-frontend-js/npm/webpack.config.js rename kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt => js/example-frontend-js/src/main/web/main.js (51%) delete mode 100644 js/example-frontend-js/webpack.config.d/custom-config.js delete mode 100644 knit.properties rename kotlinx-coroutines-core/common/src/{CompletionState.kt => CompletedExceptionally.kt} (78%) delete mode 100644 kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt delete mode 100644 kotlinx-coroutines-core/common/src/flow/SharedFlow.kt delete mode 100644 kotlinx-coroutines-core/common/src/flow/SharingStarted.kt delete mode 100644 kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt delete mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Share.kt delete mode 100644 kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt delete mode 100644 kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt rename kotlinx-coroutines-core/common/test/flow/{sharing => }/StateFlowTest.kt (52%) delete mode 100644 kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt delete mode 100644 kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt rename kotlinx-coroutines-core/jvm/test/{knit => guide/test}/TestUtil.kt (98%) delete mode 100644 kotlinx-coroutines-core/knit.properties delete mode 100644 kotlinx-coroutines-core/native/src/WorkerMain.native.kt delete mode 100644 kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt delete mode 100644 kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt delete mode 100644 kotlinx-coroutines-core/nativeOther/test/Launcher.kt create mode 100644 kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle delete mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle.kts create mode 100644 reactive/kotlinx-coroutines-reactor/build.gradle delete mode 100644 reactive/kotlinx-coroutines-reactor/build.gradle.kts delete mode 100644 reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt delete mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt create mode 100644 ui/kotlinx-coroutines-javafx/build.gradle delete mode 100644 ui/kotlinx-coroutines-javafx/build.gradle.kts diff --git a/CHANGES.md b/CHANGES.md index 513c28fb33..a1e3953351 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,45 +1,5 @@ # Change log for kotlinx.coroutines -## Version 1.4.0-M1 - -### Breaking changes - -* The concept of atomic cancellation in channels is removed. All operations in channels - and corresponding `Flow` operators are cancellable in non-atomic way (#1813). -* If `CoroutineDispatcher` throws `RejectedExecutionException`, cancel current `Job` and schedule its execution to `Dispatchers.IO` (#2003). -* `CancellableContinuation.invokeOnCancellation` is invoked if the continuation was cancelled while its resume has been dispatched (#1915). -* `Flow.singleOrNull` operator is aligned with standard library and does not longer throw `IllegalStateException` on multiple values (#2289). - -### New experimental features - -* `SharedFlow` primitive for managing hot sources of events with support of various subscription mechanisms, replay logs and buffering (#2034). -* `Flow.shareIn` and `Flow.stateIn` operators to transform cold instances of flow to hot `SharedFlow` and `StateFlow` respectively (#2047). - -### Other - -* Support leak-free closeable resources transfer via `onUndeliveredElement` in channels (#1936). -* Changed ABI in reactive integrations for Java interoperability (#2182). -* Fixed ProGuard rules for `kotlinx-coroutines-core` (#2046, #2266). -* Lint settings were added to `Flow` to avoid accidental capturing of outer `CoroutineScope` for cancellation check (#2038). - -### External contributions - -* Allow nullable types in `Flow.firstOrNull` and `Flow.singleOrNull` by @ansman (#2229). -* Add `Publisher.awaitSingleOrDefault|Null|Else` extensions by @sdeleuze (#1993). -* `awaitCancellation` top-level function by @LouisCAD (#2213). -* Significant part of our Gradle build scripts were migrated to `.kts` by @turansky. - -Thank you for your contributions and participation in the Kotlin community! - -## Version 1.3.9 - -* Support of `CoroutineContext` in `Flow.asPublisher` and similar reactive builders (#2155). -* Kotlin updated to 1.4.0. -* Transition to new HMPP publication scheme for multiplatform usages: - * Artifacts `kotlinx-coroutines-core-common` and `kotlinx-coroutines-core-native` are removed. - * For multiplatform usages, it's enough to [depend directly](README.md#multiplatform) on `kotlinx-coroutines-core` in `commonMain` source-set. - * The same artifact coordinates can be used to depend on platform-specific artifact in platform-specific source-set. - ## Version 1.3.8 ### New experimental features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 93f1330943..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,113 +0,0 @@ -# Contributing Guidelines - -There are two main ways to contribute to the project — submitting issues and submitting -fixes/changes/improvements via pull requests. - -## Submitting issues - -Both bug reports and feature requests are welcome. -Submit issues [here](https://github.com/Kotlin/kotlinx.coroutines/issues). - -* Search for existing issues to avoid reporting duplicates. -* When submitting a bug report: - * Test it against the most recently released version. It might have been already fixed. - * By default, we assume that your problem reproduces in Kotlin/JVM. Please, mention if the problem is - specific to Kotlin/JS or Kotlin/Native. - * Include the code that reproduces the problem. Provide the complete reproducer code, yet minimize it as much as possible. - * However, don't put off reporting any weird or rarely appearing issues just because you cannot consistently - reproduce them. - * If the bug is in behavior, then explain what behavior you've expected and what you've got. -* When submitting a feature request: - * Explain why you need the feature — what's your use-case, what's your domain. - * Explaining the problem you face is more important than suggesting a solution. - Report your problem even if you don't have any proposed solution. - * If there is an alternative way to do what you need, then show the code of the alternative. - -## Submitting PRs - -We love PRs. Submit PRs [here](https://github.com/Kotlin/kotlinx.coroutines/pulls). -However, please keep in mind that maintainers will have to support the resulting code of the project, -so do familiarize yourself with the following guidelines. - -* All development (both new features and bug fixes) is performed in the `develop` branch. - * The `master` branch always contains sources of the most recently released version. - * Base PRs against the `develop` branch. - * The `develop` branch is pushed to the `master` branch during release. - * Documentation in markdown files can be updated directly in the `master` branch, - unless the documentation is in the source code, and the patch changes line numbers. -* If you fix documentation: - * After fixing/changing code examples in the [`docs`](docs) folder or updating any references in the markdown files - run the [Knit tool](#running-the-knit-tool) and commit the resulting changes as well. - It will not pass the tests otherwise. - * If you plan extensive rewrites/additions to the docs, then please [contact the maintainers](#contacting-maintainers) - to coordinate the work in advance. -* If you make any code changes: - * Follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html). - Use 4 spaces for indentation. - * [Build the project](#building) to make sure it all works and passes the tests. -* If you fix a bug: - * Write the test the reproduces the bug. - * Fixes without tests are accepted only in exceptional circumstances if it can be shown that writing the - corresponding test is too hard or otherwise impractical. - * Follow the style of writing tests that is used in this project: - name test functions as `testXxx`. Don't use backticks in test names. -* If you introduce any new public APIs: - * All new APIs must come with documentation and tests. - * All new APIs are initially released with `@ExperimentalCoroutineApi` annotation and are graduated later. - * [Update the public API dumps](#updating-the-public-api-dump) and commit the resulting changes as well. - It will not pass the tests otherwise. - * If you plan large API additions, then please start by submitting an issue with the proposed API design - to gather community feedback. - * [Contact the maintainers](#contacting-maintainers) to coordinate any big piece of work in advance. -* Comment on the existing issue if you want to work on it. Ensure that the issue not only describes a problem, - but also describes a solution that had received a positive feedback. Propose a solution if there isn't any. -* Steps for contributing new integration modules are explained [here](integration/README.md#Contributing). - -## Building - -This library is built with Gradle. - -* Run `./gradlew build` to build. It also runs all the tests. -* Run `./gradlew :test` to test the module you are looking at to speed - things up during development. -* Run `./gradlew jvmTest` to perform only fast JVM tests of the core multiplatform module. - -You can import this project into IDEA, but you have to delegate build actions -to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner) - -### Environment requirements - -* JDK >= 11 referred to by the `JAVA_HOME` environment variable. -* JDK 1.6 referred to by the `JDK_16` environment variable. - It is OK to have `JDK_16` pointing to a non 1.6 JDK (e.g. `JAVA_HOME`) for external contributions. -* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. - It is OK to have `JDK_18` to a non 1.8 JDK (e.g. `JAVA_HOME`) for external contributions. - -For external contributions you can for example add this to your shell startup scripts (e.g. `~/.zshrc`): -```shell -# This assumes JAVA_HOME is set already to a JDK >= 11 version -export JDK_16="$JAVA_HOME" -export JDK_18="$JAVA_HOME" -``` - -### Running the Knit tool - -* Use [Knit](https://github.com/Kotlin/kotlinx-knit/blob/master/README.md) for updates to documentation: - * Run `./gradlew knit` to update example files, links, tables of content. - * Commit updated documents and examples together with other changes. - -### Updating the public API dump - -* Use [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator/blob/master/README.md) for updates to public API: - * Run `./gradlew apiDump` to update API index files. - * Commit updated API indexes together with other changes. - -## Releases - -* Full release procedure checklist is [here](RELEASE.md). - -## Contacting maintainers - -* If something cannot be done, not convenient, or does not work — submit an [issue](#submitting-issues). -* "How to do something" questions — [StackOverflow](https://stackoverflow.com). -* Discussions and general inquiries — use `#coroutines` channel in [KotlinLang Slack](https://kotl.in/slack). diff --git a/README.md b/README.md index 1e72cc1b3e..9f8bae65ba 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0-M1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0-M1) -[![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) -[![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.8) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.8) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.4.0` release. +This is a companion version for Kotlin `1.3.71` release. ```kotlin suspend fun main() = coroutineScope { @@ -46,7 +44,7 @@ suspend fun main() = coroutineScope { * [DebugProbes] API to probe, keep track of, print and dump active coroutines; * [CoroutinesTimeout] test rule to automatically dump coroutines on test timeout. * [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries: - * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [kotlinx.coroutines.reactive.publish], etc), + * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [publish], etc), * Flow (JDK 9) (the same interface as for Reactive Streams), * RxJava 2.x ([rxFlowable], [rxSingle], etc), and * RxJava 3.x ([rxFlowable], [rxSingle], etc), and @@ -86,7 +84,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.4.0-M1 + 1.3.8 ``` @@ -94,7 +92,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.4.0 + 1.3.71 ``` @@ -104,7 +102,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8' } ``` @@ -112,7 +110,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.4.0' + ext.kotlin_version = '1.3.71' } ``` @@ -130,7 +128,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8") } ``` @@ -138,7 +136,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.4.0" + kotlin("jvm") version "1.3.71" } ``` @@ -148,14 +146,9 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re Core modules of `kotlinx.coroutines` are also available for [Kotlin/JS](#js) and [Kotlin/Native](#native). -In common code that should get compiled for different platforms, you can add dependency to `kotlinx-coroutines-core` right to the `commonMain` source set: -```groovy -commonMain { - dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") - } -} -``` +In common code that should get compiled for different platforms, add dependency to +[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.8/jar) +(follow the link to get the dependency declaration snippet). ### Android @@ -163,7 +156,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' ``` This gives you access to Android [Dispatchers.Main] @@ -176,21 +169,10 @@ threads are handled by Android runtime. R8 and ProGuard rules are bundled into the [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module. For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-android/README.md#optimization). -#### Avoiding including the debug infrastructure in the resulting APK - -The `kotlinx-coroutines-core` artifact contains a resource file that is not required for the coroutines to operate -normally and is only used by the debugger. To exclude it at no loss of functionality, add the following snippet to the -`android` block in your gradle file for the application subproject: -```groovy -packagingOptions { - exclude "DebugProbesKt.bin" -} -``` - ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0-M1/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.8/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -198,7 +180,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0-M1/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.8/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. @@ -212,9 +194,35 @@ enableFeaturePreview('GRADLE_METADATA') Since Kotlin/Native does not generally provide binary compatibility between versions, you should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`. -## Building and Contributing +## Building + +This library is built with Gradle. To build it, use `./gradlew build`. +You can import this project into IDEA, but you have to delegate build actions +to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner) + +### Requirements + +* JDK >= 11 referred to by the `JAVA_HOME` environment variable. +* JDK 1.6 referred to by the `JDK_16` environment variable. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. +* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is okay to have `JDK_18` pointing to `JAVA_HOME` for external contributions. + +## Contributions and releases + +All development (both new features and bug fixes) is performed in `develop` branch. +This way `master` sources always contain sources of the most recently released version. +Please send PRs with bug fixes to `develop` branch. +Fixes to documentation in markdown files are an exception to this rule. They are updated directly in `master`. + +The `develop` branch is pushed to `master` during release. -See [Contributing Guidelines](CONTRIBUTING.md). +* Full release procedure checklist is [here](RELEASE.md). +* Steps for contributing new integration modules are explained [here](integration/README.md#Contributing). +* Use [Knit](https://github.com/Kotlin/kotlinx-knit/blob/master/README.md) for updates to documentation: + * In project root directory run `./gradlew knit`. + * Commit updated documents and examples together with other changes. +* Use [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator/blob/master/README.md) for updates to public API: + * In project root directory run `./gradlew apiDump`. + * Commit updated API index together with other changes. @@ -276,7 +284,7 @@ See [Contributing Guidelines](CONTRIBUTING.md). [Publisher.collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/collect.html [Publisher.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-single.html -[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html +[publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html [rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html diff --git a/RELEASE.md b/RELEASE.md index 22cb61c42f..efb361f1e5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -64,14 +64,18 @@ To release new `` of `kotlinx-coroutines`: 5. Announce new release in [Slack](https://kotlinlang.slack.com) -6. Switch into `develop` branch:
+6. Create a ticket to update coroutines version on [try.kotlinlang.org](try.kotlinlang.org). + * Use [KT-30870](https://youtrack.jetbrains.com/issue/KT-30870) as a template + * This step should be skipped for eap versions that are not merged to `master` + +7. Switch into `develop` branch:
`git checkout develop` -7. Fetch the latest `master`:
+8. Fetch the latest `master`:
`git fetch` -8. Merge release from `master`:
+9. Merge release from `master`:
`git merge origin/master` -9. Push updates to `develop`:
+0. Push updates to `develop`:
`git push` diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 5da40f261e..7df4510bf4 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -2,8 +2,6 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("UnstableApiUsage") - import me.champeau.gradle.* import org.jetbrains.kotlin.gradle.tasks.* @@ -35,7 +33,7 @@ tasks.named("compileJmhKotlin") { * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. */ -val removeRedundantFiles by tasks.registering(Delete::class) { +val removeRedundantFiles = tasks.register("removeRedundantFiles") { delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class") delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class") @@ -73,10 +71,10 @@ extensions.configure("jmh") { } tasks.named("jmhJar") { - archiveBaseName by "benchmarks" - archiveClassifier by null - archiveVersion by null - destinationDirectory.file("$rootDir") + baseName = "benchmarks" + classifier = null + version = null + destinationDir = file("$rootDir") } dependencies { diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt index d961dab8d9..c217fcae91 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt @@ -70,12 +70,12 @@ class NonCancellableChannel : SimpleChannel() { } class CancellableChannel : SimpleChannel() { - override suspend fun suspendReceive(): Int = suspendCancellableCoroutine { + override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutine { consumer = it.intercepted() COROUTINE_SUSPENDED } - override suspend fun suspendSend(element: Int) = suspendCancellableCoroutine { + override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutine { enqueuedValue = element producer = it.intercepted() COROUTINE_SUSPENDED @@ -84,13 +84,13 @@ class CancellableChannel : SimpleChannel() { class CancellableReusableChannel : SimpleChannel() { @Suppress("INVISIBLE_MEMBER") - override suspend fun suspendReceive(): Int = suspendCancellableCoroutineReusable { + override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutineReusable { consumer = it.intercepted() COROUTINE_SUSPENDED } @Suppress("INVISIBLE_MEMBER") - override suspend fun suspendSend(element: Int) = suspendCancellableCoroutineReusable { + override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutineReusable { enqueuedValue = element producer = it.intercepted() COROUTINE_SUSPENDED diff --git a/build.gradle b/build.gradle index 79c7f3553e..a758393b6c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,20 +46,13 @@ buildscript { if (using_snapshot_version) { repositories { mavenLocal() + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } } repositories { jcenter() - maven { - url "https://kotlin.bintray.com/kotlinx" - credentials { - username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" - password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" - } - } - // Future replacement for kotlin-dev, with cache redirector - maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } + maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { @@ -83,14 +76,12 @@ buildscript { // JMH plugins classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" } - - CacheRedirector.configureBuildScript(buildscript, rootProject) } import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -// todo:KLUDGE: Hierarchical project structures are not fully supported in IDEA, enable only for a regular built -if (!Idea.active) { +// Hierarchical project structures are not fully supported in 1.3.7x MPP, enable conditionally for 1.4.x +if (VersionNumber.parse(kotlin_version) > VersionNumber.parse("1.3.79")) { ext.set("kotlin.mpp.enableGranularSourceSetsMetadata", "true") } @@ -148,6 +139,7 @@ apiValidation { // Configure repositories allprojects { + String projectName = it.name repositories { /* * google should be first in the repository list because some of the play services @@ -155,8 +147,6 @@ allprojects { */ google() jcenter() - // Future replacement for kotlin-dev, with cache redirector - maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { @@ -165,14 +155,7 @@ allprojects { } } maven { url "https://kotlin.bintray.com/kotlin-eap" } - maven { - url "https://kotlin.bintray.com/kotlinx" - credentials { - username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" - password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" - } - } - mavenLocal() + maven { url "https://kotlin.bintray.com/kotlinx" } } } @@ -254,14 +237,7 @@ configure(subprojects.findAll { it.name != coreModule && it.name != rootModule } } // Redefine source sets because we are not using 'kotlin/main/fqn' folder convention -configure(subprojects.findAll { - !sourceless.contains(it.name) && - it.name != "benchmarks" && - it.name != coreModule && - it.name != "example-frontend-js" -}) { - // Pure JS and pure MPP doesn't have this notion and are configured separately - // TODO detect it via platformOf and migrate benchmarks to the same scheme +configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "benchmarks" && it.name != coreModule }) { sourceSets { main.kotlin.srcDirs = ['src'] test.kotlin.srcDirs = ['test'] @@ -297,14 +273,7 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) { // Report Kotlin compiler version when building project println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION") -// --------------- Cache redirector --------------- - -allprojects { - CacheRedirector.configure(project) -} - // --------------- Configure sub-projects that are published --------------- - def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm", true) task deploy(dependsOn: publishTasks) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 96b17a3d99..91b8bda92b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,39 +1,11 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -import java.util.* - plugins { `kotlin-dsl` } -val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true - repositories { - if (cacheRedirectorEnabled) { - maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") - maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-eap") - maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-dev") - } else { - maven("https://plugins.gradle.org/m2") - maven("https://dl.bintray.com/kotlin/kotlin-eap") - maven("https://dl.bintray.com/kotlin/kotlin-dev") - } + gradlePluginPortal() } kotlinDslPluginOptions { experimentalWarning.set(false) } - -val props = Properties().apply { - file("../gradle.properties").inputStream().use { load(it) } -} - -fun version(target: String): String = - props.getProperty("${target}_version") - -dependencies { - implementation(kotlin("gradle-plugin", version("kotlin"))) - implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") -} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts deleted file mode 100644 index e5267ea3e2..0000000000 --- a/buildSrc/settings.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -pluginManagement { - repositories { - val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true - - if (cacheRedirectorEnabled) { - println("Redirecting repositories for buildSrc buildscript") - - maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") - } else { - maven("https://plugins.gradle.org/m2") - } - } -} diff --git a/buildSrc/src/main/kotlin/CacheRedirector.kt b/buildSrc/src/main/kotlin/CacheRedirector.kt deleted file mode 100644 index 7cf01d8e76..0000000000 --- a/buildSrc/src/main/kotlin/CacheRedirector.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -import org.gradle.api.* -import org.gradle.api.artifacts.dsl.* -import org.gradle.api.artifacts.repositories.* -import org.gradle.api.initialization.dsl.* -import java.net.* - -/** - * Enabled via environment variable, so that it can be reliably accessed from any piece of the build script, - * including buildSrc within TeamCity CI. - */ -private val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true - -/** - * The list of repositories supported by cache redirector should be synced with the list at https://cache-redirector.jetbrains.com/redirects_generated.html - * To add a repository to the list create an issue in ADM project (example issue https://youtrack.jetbrains.com/issue/IJI-149) - */ -private val mirroredUrls = listOf( - "https://cdn.azul.com/zulu/bin", - "https://clojars.org/repo", - "https://dl.bintray.com/d10xa/maven", - "https://dl.bintray.com/groovy/maven", - "https://dl.bintray.com/jetbrains/maven-patched", - "https://dl.bintray.com/jetbrains/scala-plugin-deps", - "https://dl.bintray.com/kodein-framework/Kodein-DI", - "https://dl.bintray.com/konsoletyper/teavm", - "https://dl.bintray.com/kotlin/kotlin-dev", - "https://dl.bintray.com/kotlin/kotlin-eap", - "https://dl.bintray.com/kotlin/kotlinx.html", - "https://dl.bintray.com/kotlin/kotlinx", - "https://dl.bintray.com/kotlin/ktor", - "https://dl.bintray.com/scalacenter/releases", - "https://dl.bintray.com/scalamacros/maven", - "https://dl.bintray.com/kotlin/exposed", - "https://dl.bintray.com/cy6ergn0m/maven", - "https://dl.bintray.com/kotlin/kotlin-js-wrappers", - "https://dl.google.com/android/repository", - "https://dl.google.com/dl/android/maven2", - "https://dl.google.com/dl/android/studio/ide-zips", - "https://dl.google.com/go", - "https://download.jetbrains.com", - "https://jcenter.bintray.com", - "https://jetbrains.bintray.com/dekaf", - "https://jetbrains.bintray.com/intellij-jbr", - "https://jetbrains.bintray.com/intellij-jdk", - "https://jetbrains.bintray.com/intellij-plugin-service", - "https://jetbrains.bintray.com/intellij-terraform", - "https://jetbrains.bintray.com/intellij-third-party-dependencies", - "https://jetbrains.bintray.com/jediterm", - "https://jetbrains.bintray.com/kotlin-native-dependencies", - "https://jetbrains.bintray.com/markdown", - "https://jetbrains.bintray.com/teamcity-rest-client", - "https://jetbrains.bintray.com/test-discovery", - "https://jetbrains.bintray.com/wormhole", - "https://jitpack.io", - "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev", - "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap", - "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/eap", - "https://kotlin.bintray.com/dukat", - "https://kotlin.bintray.com/kotlin-dependencies", - "https://oss.sonatype.org/content/repositories/releases", - "https://oss.sonatype.org/content/repositories/snapshots", - "https://oss.sonatype.org/content/repositories/staging", - "https://packages.confluent.io/maven/", - "https://plugins.gradle.org/m2", - "https://plugins.jetbrains.com/maven", - "https://repo1.maven.org/maven2", - "https://repo.grails.org/grails/core", - "https://repo.jenkins-ci.org/releases", - "https://repo.maven.apache.org/maven2", - "https://repo.spring.io/milestone", - "https://repo.typesafe.com/typesafe/ivy-releases", - "https://services.gradle.org", - "https://www.exasol.com/artifactory/exasol-releases", - "https://www.myget.org/F/intellij-go-snapshots/maven", - "https://www.myget.org/F/rd-model-snapshots/maven", - "https://www.myget.org/F/rd-snapshots/maven", - "https://www.python.org/ftp", - "https://www.jetbrains.com/intellij-repository/nightly", - "https://www.jetbrains.com/intellij-repository/releases", - "https://www.jetbrains.com/intellij-repository/snapshots" -) - -private val aliases = mapOf( - "https://repo.maven.apache.org/maven2" to "https://repo1.maven.org/maven2", // Maven Central - "https://kotlin.bintray.com/kotlin-dev" to "https://dl.bintray.com/kotlin/kotlin-dev", - "https://kotlin.bintray.com/kotlin-eap" to "https://dl.bintray.com/kotlin/kotlin-eap", - "https://kotlin.bintray.com/kotlinx" to "https://dl.bintray.com/kotlin/kotlinx" -) - -private fun URI.toCacheRedirectorUri() = URI("https://cache-redirector.jetbrains.com/$host/$path") - -private fun URI.maybeRedirect(): URI? { - val url = toString().trimEnd('/') - val dealiasedUrl = aliases.getOrDefault(url, url) - - return if (mirroredUrls.any { dealiasedUrl.startsWith(it) }) { - URI(dealiasedUrl).toCacheRedirectorUri() - } else { - null - } -} - -private fun URI.isCachedOrLocal() = scheme == "file" || - host == "cache-redirector.jetbrains.com" || - host == "teamcity.jetbrains.com" || - host == "buildserver.labs.intellij.net" - -private fun Project.checkRedirectUrl(url: URI, containerName: String): URI { - val redirected = url.maybeRedirect() - if (redirected == null && !url.isCachedOrLocal()) { - val msg = "Repository $url in $containerName should be cached with cache-redirector" - val details = "Using non cached repository may lead to download failures in CI builds." + - " Check buildSrc/src/main/kotlin/CacheRedirector.kt for details." - logger.warn("WARNING - $msg\n$details") - } - return if (cacheRedirectorEnabled) redirected ?: url else url -} - -private fun Project.checkRedirect(repositories: RepositoryHandler, containerName: String) { - if (cacheRedirectorEnabled) { - logger.info("Redirecting repositories for $containerName") - } - for (repository in repositories) { - when (repository) { - is MavenArtifactRepository -> repository.url = checkRedirectUrl(repository.url, containerName) - is IvyArtifactRepository -> repository.url = checkRedirectUrl(repository.url, containerName) - } - } -} - -// Used from Groovy scripts -object CacheRedirector { - /** - * Substitutes repositories in buildScript { } block. - */ - @JvmStatic - fun ScriptHandler.configureBuildScript(rootProject: Project) { - rootProject.checkRedirect(repositories, "${rootProject.displayName} buildscript") - } - - /** - * Substitutes repositories in a project. - */ - @JvmStatic - fun Project.configure() { - checkRedirect(repositories, displayName) - } -} diff --git a/buildSrc/src/main/kotlin/Dokka.kt b/buildSrc/src/main/kotlin/Dokka.kt deleted file mode 100644 index dd5f1ea48d..0000000000 --- a/buildSrc/src/main/kotlin/Dokka.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -import org.gradle.api.Project -import org.gradle.kotlin.dsl.delegateClosureOf -import org.gradle.kotlin.dsl.withType -import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink.Builder -import org.jetbrains.dokka.gradle.DokkaTask -import java.io.File -import java.net.URL - -/** - * Package-list by external URL for documentation generation. - */ -fun Project.externalDocumentationLink( - url: String, - packageList: File = projectDir.resolve("package.list") -) { - tasks.withType().configureEach { - externalDocumentationLink(delegateClosureOf { - this.url = URL(url) - packageListUrl = packageList.toPath().toUri().toURL() - }) - } -} diff --git a/buildSrc/src/main/kotlin/Idea.kt b/buildSrc/src/main/kotlin/Idea.kt index 615b8aad74..802b387b0d 100644 --- a/buildSrc/src/main/kotlin/Idea.kt +++ b/buildSrc/src/main/kotlin/Idea.kt @@ -1,5 +1,4 @@ object Idea { - @JvmStatic // for Gradle val active: Boolean get() = System.getProperty("idea.active") == "true" } diff --git a/buildSrc/src/main/kotlin/MavenCentral.kt b/buildSrc/src/main/kotlin/MavenCentral.kt index 3efaf33f1c..0d7e18cf15 100644 --- a/buildSrc/src/main/kotlin/MavenCentral.kt +++ b/buildSrc/src/main/kotlin/MavenCentral.kt @@ -5,9 +5,10 @@ @file:Suppress("UnstableApiUsage") import org.gradle.api.Project +import org.gradle.api.provider.Property import org.gradle.api.publish.maven.MavenPom -// Pom configuration +// --------------- pom configuration --------------- fun MavenPom.configureMavenCentralMetadata(project: Project) { name by project.name @@ -35,3 +36,7 @@ fun MavenPom.configureMavenCentralMetadata(project: Project) { url by "https://github.com/Kotlin/kotlinx.coroutines" } } + +private infix fun Property.by(value: T) { + set(value) +} diff --git a/buildSrc/src/main/kotlin/Platform.kt b/buildSrc/src/main/kotlin/Platform.kt index b667a138a8..4cacd9e026 100644 --- a/buildSrc/src/main/kotlin/Platform.kt +++ b/buildSrc/src/main/kotlin/Platform.kt @@ -1,6 +1,5 @@ import org.gradle.api.Project -// Use from Groovy for now fun platformOf(project: Project): String = when (project.name.substringAfterLast("-")) { "js" -> "js" diff --git a/buildSrc/src/main/kotlin/Properties.kt b/buildSrc/src/main/kotlin/Properties.kt deleted file mode 100644 index a0968ee699..0000000000 --- a/buildSrc/src/main/kotlin/Properties.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("UnstableApiUsage") - -import org.gradle.api.provider.* - -infix fun Property.by(value: T) { - set(value) -} diff --git a/buildSrc/src/main/kotlin/RunR8.kt b/buildSrc/src/main/kotlin/RunR8.kt deleted file mode 100644 index d9eba79bd4..0000000000 --- a/buildSrc/src/main/kotlin/RunR8.kt +++ /dev/null @@ -1,52 +0,0 @@ -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.JavaExec -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.bundling.Zip -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.named -import java.io.File - -/* - * Task used by our ui/android tests to test minification results - * and keep track of size of the binary. - * TODO move back to kotlinx-coroutines-android when it's migrated to the kts - */ -open class RunR8 : JavaExec() { - - @OutputDirectory - lateinit var outputDex: File - - @InputFile - lateinit var inputConfig: File - - @InputFile - val inputConfigCommon: File = File("testdata/r8-test-common.pro") - - @InputFiles - val jarFile: File = project.tasks.named("jar").get().archivePath - - init { - classpath = project.configurations["r8"] - main = "com.android.tools.r8.R8" - } - - override fun exec() { - // Resolve classpath only during execution - val arguments = mutableListOf( - "--release", - "--no-desugaring", - "--output", outputDex.absolutePath, - "--pg-conf", inputConfig.absolutePath - ) - arguments.addAll(project.configurations["runtimeClasspath"].files.map { it.absolutePath }) - arguments.add(jarFile.absolutePath) - - args = arguments - - project.delete(outputDex) - outputDex.mkdirs() - - super.exec() - } -} diff --git a/buildSrc/src/main/kotlin/UnpackAar.kt b/buildSrc/src/main/kotlin/UnpackAar.kt deleted file mode 100644 index c7d0b53d04..0000000000 --- a/buildSrc/src/main/kotlin/UnpackAar.kt +++ /dev/null @@ -1,32 +0,0 @@ -import org.gradle.api.artifacts.transform.InputArtifact -import org.gradle.api.artifacts.transform.TransformAction -import org.gradle.api.artifacts.transform.TransformOutputs -import org.gradle.api.artifacts.transform.TransformParameters -import org.gradle.api.file.FileSystemLocation -import org.gradle.api.provider.Provider -import java.io.File -import java.nio.file.Files -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - -// TODO move back to kotlinx-coroutines-play-services when it's migrated to the kts -@Suppress("UnstableApiUsage") -abstract class UnpackAar : TransformAction { - @get:InputArtifact - abstract val inputArtifact: Provider - - override fun transform(outputs: TransformOutputs) { - ZipFile(inputArtifact.get().asFile).use { zip -> - zip.entries().asSequence() - .filter { !it.isDirectory } - .filter { it.name.endsWith(".jar") } - .forEach { zip.unzip(it, outputs.file(it.name)) } - } - } -} - -private fun ZipFile.unzip(entry: ZipEntry, output: File) { - getInputStream(entry).use { - Files.copy(it, output.toPath()) - } -} diff --git a/coroutines-guide.md b/coroutines-guide.md index 09cfb93cab..4b3c09c40f 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -20,7 +20,6 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Closing resources with `finally`](docs/cancellation-and-timeouts.md#closing-resources-with-finally) * [Run non-cancellable block](docs/cancellation-and-timeouts.md#run-non-cancellable-block) * [Timeout](docs/cancellation-and-timeouts.md#timeout) - * [Asynchronous timeout and resources](docs/cancellation-and-timeouts.md#asynchronous-timeout-and-resources) * [Composing Suspending Functions](docs/composing-suspending-functions.md#composing-suspending-functions) * [Sequential by default](docs/composing-suspending-functions.md#sequential-by-default) @@ -33,8 +32,6 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Dispatchers and threads](docs/coroutine-context-and-dispatchers.md#dispatchers-and-threads) * [Unconfined vs confined dispatcher](docs/coroutine-context-and-dispatchers.md#unconfined-vs-confined-dispatcher) * [Debugging coroutines and threads](docs/coroutine-context-and-dispatchers.md#debugging-coroutines-and-threads) - * [Debugging with IDEA](docs/coroutine-context-and-dispatchers.md#debugging-with-idea) - * [Debugging using logging](docs/coroutine-context-and-dispatchers.md#debugging-using-logging) * [Jumping between threads](docs/coroutine-context-and-dispatchers.md#jumping-between-threads) * [Job in the context](docs/coroutine-context-and-dispatchers.md#job-in-the-context) * [Children of a coroutine](docs/coroutine-context-and-dispatchers.md#children-of-a-coroutine) diff --git a/docs/cancellation-and-timeouts.md b/docs/cancellation-and-timeouts.md index b296bde493..d8d5b7bad4 100644 --- a/docs/cancellation-and-timeouts.md +++ b/docs/cancellation-and-timeouts.md @@ -11,7 +11,6 @@ * [Closing resources with `finally`](#closing-resources-with-finally) * [Run non-cancellable block](#run-non-cancellable-block) * [Timeout](#timeout) - * [Asynchronous timeout and resources](#asynchronous-timeout-and-resources) @@ -356,114 +355,6 @@ Result is null -### Asynchronous timeout and resources - - - -The timeout event in [withTimeout] is asynchronous with respect to the code running in its block and may happen at any time, -even right before the return from inside of the timeout block. Keep this in mind if you open or acquire some -resource inside the block that needs closing or release outside of the block. - -For example, here we imitate a closeable resource with the `Resource` class, that simply keeps track of how many times -it was created by incrementing the `acquired` counter and decrementing this counter from its `close` function. -Let us run a lot of coroutines with the small timeout try acquire this resource from inside -of the `withTimeout` block after a bit of delay and release it from outside. - -
- -```kotlin -import kotlinx.coroutines.* - -//sampleStart -var acquired = 0 - -class Resource { - init { acquired++ } // Acquire the resource - fun close() { acquired-- } // Release the resource -} - -fun main() { - runBlocking { - repeat(100_000) { // Launch 100K coroutines - launch { - val resource = withTimeout(60) { // Timeout of 60 ms - delay(50) // Delay for 50 ms - Resource() // Acquire a resource and return it from withTimeout block - } - resource.close() // Release the resource - } - } - } - // Outside of runBlocking all coroutines have completed - println(acquired) // Print the number of resources still acquired -} -//sampleEnd -``` - -
- -> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). - - - -If you run the above code you'll see that it does not always print zero, though it may depend on the timings -of your machine you may need to tweak timeouts in this example to actually see non-zero values. - -> Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe, -> since it always happens from the same main thread. More on that will be explained in the next chapter -> on coroutine context. - -To workaround this problem you can store a reference to the resource in the variable as opposed to returning it -from the `withTimeout` block. - -
- -```kotlin -import kotlinx.coroutines.* - -var acquired = 0 - -class Resource { - init { acquired++ } // Acquire the resource - fun close() { acquired-- } // Release the resource -} - -fun main() { -//sampleStart - runBlocking { - repeat(100_000) { // Launch 100K coroutines - launch { - var resource: Resource? = null // Not acquired yet - try { - withTimeout(60) { // Timeout of 60 ms - delay(50) // Delay for 50 ms - resource = Resource() // Store a resource to the variable if acquired - } - // We can do something else with the resource here - } finally { - resource?.close() // Release the resource if it was acquired - } - } - } - } - // Outside of runBlocking all coroutines have completed - println(acquired) // Print the number of resources still acquired -//sampleEnd -} -``` - -
- -> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). - -This example always prints zero. Resources do not leak. - - - [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index 36e049db89..4909206e17 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -8,8 +8,6 @@ * [Dispatchers and threads](#dispatchers-and-threads) * [Unconfined vs confined dispatcher](#unconfined-vs-confined-dispatcher) * [Debugging coroutines and threads](#debugging-coroutines-and-threads) - * [Debugging with IDEA](#debugging-with-idea) - * [Debugging using logging](#debugging-using-logging) * [Jumping between threads](#jumping-between-threads) * [Job in the context](#job-in-the-context) * [Children of a coroutine](#children-of-a-coroutine) @@ -157,34 +155,8 @@ The unconfined dispatcher should not be used in general code. Coroutines can suspend on one thread and resume on another thread. Even with a single-threaded dispatcher it might be hard to -figure out what the coroutine was doing, where, and when if you don't have special tooling. - -#### Debugging with IDEA - -The Coroutine Debugger of the Kotlin plugin simplifies debugging coroutines in IntelliJ IDEA. - -> Debugging works for versions 1.3.8 or later of `kotlinx-coroutines-core`. - -The **Debug** tool window contains the **Coroutines** tab. In this tab, you can find information about both currently running and suspended coroutines. -The coroutines are grouped by the dispatcher they are running on. - -![Debugging coroutines](images/coroutine-idea-debugging-1.png) - -With the coroutine debugger, you can: -* Check the state of each coroutine. -* See the values of local and captured variables for both running and suspended coroutines. -* See a full coroutine creation stack, as well as a call stack inside the coroutine. The stack includes all frames with -variable values, even those that would be lost during standard debugging. -* Get a full report that contains the state of each coroutine and its stack. To obtain it, right-click inside the **Coroutines** tab, and then click **Get Coroutines Dump**. - -To start coroutine debugging, you just need to set breakpoints and run the application in debug mode. - -Learn more about coroutines debugging in the [tutorial](https://kotlinlang.org/docs/tutorials/coroutines/debug-coroutines-with-idea.html). - -#### Debugging using logging - -Another approach to debugging applications with -threads without Coroutine Debugger is to print the thread name in the log file on each log statement. This feature is universally supported +figure out what the coroutine was doing, where, and when. The common approach to debugging applications with +threads is to print the thread name in the log file on each log statement. This feature is universally supported by logging frameworks. When using coroutines, the thread name alone does not give much of a context, so `kotlinx.coroutines` includes debugging facilities to make it easier. @@ -680,7 +652,7 @@ stored in a thread-local variable. However, in this case you are fully responsib potentially concurrent modifications to the variable in this mutable box. For advanced usage, for example for integration with logging MDC, transactional contexts or any other libraries -which internally use thread-locals for passing data, see the documentation of the [ThreadContextElement] interface +which internally use thread-locals for passing data, see documentation of the [ThreadContextElement] interface that should be implemented. diff --git a/docs/coroutines-guide.md b/docs/coroutines-guide.md index 2d15a7bbff..e3f18d208e 100644 --- a/docs/coroutines-guide.md +++ b/docs/coroutines-guide.md @@ -10,7 +10,7 @@ coroutine-enabled primitives that this guide covers, including `launch`, `async` This is a guide on core features of `kotlinx.coroutines` with a series of examples, divided up into different topics. -In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on the `kotlinx-coroutines-core` module as explained +In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on `kotlinx-coroutines-core` module as explained [in the project README](../README.md#using-in-your-projects). ## Table of contents diff --git a/docs/exception-handling.md b/docs/exception-handling.md index d0b6b517a8..5618cafbdc 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -19,7 +19,7 @@ ## Exception Handling This section covers exception handling and cancellation on exceptions. -We already know that a cancelled coroutine throws [CancellationException] in suspension points and that it +We already know that cancelled coroutine throws [CancellationException] in suspension points and that it is ignored by the coroutines' machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same coroutine throw an exception. diff --git a/docs/flow.md b/docs/flow.md index 2b1dfd59a9..143f9e9300 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -50,7 +50,7 @@ ## Asynchronous Flow -A suspending function asynchronously returns a single value, but how can we return +Suspending functions asynchronously returns a single value, but how can we return multiple asynchronously computed values? This is where Kotlin Flows come in. ### Representing multiple values @@ -153,7 +153,7 @@ This code prints the numbers after waiting for a second. #### Flows Using the `List` result type, means we can only return all the values at once. To represent -the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would use the `Sequence` type for synchronously computed values: +the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would the `Sequence` type for synchronously computed values:
@@ -1463,15 +1463,13 @@ fun main() = runBlocking { A "Caught ..." message is not printed despite there being a `catch` operator: -```text + +--> #### Catching declaratively @@ -1512,14 +1510,12 @@ fun main() = runBlocking { Now we can see that a "Caught ..." message is printed and so we can catch all the exceptions without explicitly using a `try/catch` block: -```text + +--> ### Flow completion diff --git a/docs/images/coroutine-idea-debugging-1.png b/docs/images/coroutine-idea-debugging-1.png deleted file mode 100644 index 0afe9925157e44d5c03e5811e7703bb6d6642ce7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 248706 zcmeEucT|(x_AMZ>pn!#P01+$*gr-ylX(A%MB%xO&ARVdFn}UJ@N|z22AoLbWC;}oF zszB&PPz3V zYU&#NnnFZXBqzfUg~;1675e%OH4FtIlDly;;ci#*uTsSDUZs5>{^&&ewRT6*#10>? zBzG69m6FcF?ZKIZnoc*#x^WBTPWQeNLk}C*rJA}00zB@Xn?sWNc_|W7^5g!K3q5(7 z)t7|qxc}&ppU#?8_;0=eLvdcMDdWHRiqqD#S#(_Aijl1Y2=43u#T#igjQetR)MsP^ z(HvBC#~p$5QYrjr&#NKgjJ!ISk3Vo}U?74gjY*IH4rmDpDI#hYKQnUS*yG2l;OqPU zZ|48~%t)2tJbxv4Op*=zXo^fEKKvNQgv_QVBpwO?Pm&UQH#pG zOrTZ#K3A=E1_h0#i8}6b?3M;I?)01%`jln4!=tk!$t76F@G<)%GxX=`thdM9sNtfd zf%}SyY&Kg#)$FIzmS`Jx_#88clJqA*v$7%XghTJK zG^$?+$kr?{v{~pc2)hfTJMJ#&)?r1;9Ja^aZgg;rde2eSn+$y7>lLwx0S|_4{x$KRXFMBu-xtZN%YC~JsIhRM(J6TymDR*Z|;4cG@ z6p^>0>ZqJYh3v+vD-6m9OU#D@DVcJ0%PcX?S3GDu>inT9vA61Zt5@&0;&8Yn|6&P<~9A{>%zqE!9O-lyr#i)dY1h}xWUK7nz%$x|5xJ@`>v_kka zDwr~f>rNIlsvbU%uA`#;wEdMmS-Zx4Gx%P>RBMdhc&(Ssk1ua|S63Da>UbTJ)1Yry zB*!36v&UkLyyJ>}p+4RsBlZCvd`(+2D*>b@P7ZWR9xeJeUh&?U$nu3E+cgRiZyANS z#l*y1T%(USS$+z9YEYR&POh*+wB~5venH)#Wtik)#pYa3^TW4{h`9+anolUCu=Ac| z-h3b;I!23YC+C7`8ihalm*q(xX%xDeH9{j{;z-z)@v)VdfgzLzC%oCGXGfc}G|?D@ zHAX*1@S3qfo;oiMe5-OIfyS%hDVC^v~WH!-Cf@eL6w?#&Y9ed)&4D z$%#``Z*+~`zid*B(NVctA6aCRv-}>}`b4Yb&GEL|$Cpw#e)w+58S{?8YPS;QWQoz6 z${$QGv8$9)OYV2|QSO?(RspUemIOSIOCs*HCRZMMeX>Aq2D>|!hWyTn~bK=|S29k

gYl8~3?t?Ee@ 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

xFmWv3f&Y;2fKQix{;PFy}WRA_96 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

V8Ybh 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�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 diff --git a/docs/knit.properties b/docs/knit.properties index 2028ecb416..ab2508a114 100644 --- a/docs/knit.properties +++ b/docs/knit.properties @@ -4,7 +4,19 @@ knit.package=kotlinx.coroutines.guide knit.dir=../kotlinx-coroutines-core/jvm/test/guide/ +knit.pattern=example-[a-zA-Z0-9-]+-##\\.kt +knit.include=knit.code.include test.package=kotlinx.coroutines.guide.test test.dir=../kotlinx-coroutines-core/jvm/test/guide/test/ +test.template=knit.test.template +# Various test validation modes and their corresponding methods from TestUtil +test.mode.=verifyLines +test.mode.STARTS_WITH=verifyLinesStartWith +test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime +test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime +test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread +test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered +test.mode.LINES_START=verifyLinesStart +test.mode.EXCEPTION=verifyExceptions \ No newline at end of file diff --git a/docs/knit.test.template b/docs/knit.test.template index 727493c662..a912555a43 100644 --- a/docs/knit.test.template +++ b/docs/knit.test.template @@ -5,7 +5,6 @@ // This file was automatically generated from ${file.name} by Knit tool. Do not edit. package ${test.package} -import kotlinx.coroutines.knit.* import org.junit.Test class ${test.name} { diff --git a/docs/shared-mutable-state-and-concurrency.md b/docs/shared-mutable-state-and-concurrency.md index 8b83ad0b20..316d56e5bc 100644 --- a/docs/shared-mutable-state-and-concurrency.md +++ b/docs/shared-mutable-state-and-concurrency.md @@ -24,7 +24,7 @@ but others are unique. ### The problem -Let us launch a hundred coroutines all doing the same action a thousand times. +Let us launch a hundred coroutines all doing the same action thousand times. We'll also measure their completion time for further comparisons:

@@ -102,7 +102,7 @@ increment the `counter` concurrently from multiple threads without any synchroni ### Volatiles are of no help -There is a common misconception that making a variable `volatile` solves concurrency problem. Let us try it: +There is common misconception that making a variable `volatile` solves concurrency problem. Let us try it: @@ -158,7 +158,7 @@ do not provide atomicity of larger actions (increment in our case). ### Thread-safe data structures The general solution that works both for threads and for coroutines is to use a thread-safe (aka synchronized, -linearizable, or atomic) data structure that provides all the necessary synchronization for the corresponding +linearizable, or atomic) data structure that provides all the necessarily synchronization for the corresponding operations that needs to be performed on a shared state. In the case of a simple counter we can use `AtomicInteger` class which has atomic `incrementAndGet` operations: diff --git a/gradle.properties b/gradle.properties index 18b95166d6..6a1ae653f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,14 @@ # # Kotlin -version=1.4.0-M1-SNAPSHOT +version=1.3.8-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.4.0 +kotlin_version=1.3.71 # Dependencies junit_version=4.12 -atomicfu_version=0.14.4 -knit_version=0.2.0 +atomicfu_version=0.14.2 +knit_version=0.1.3 html_version=0.6.8 lincheck_version=2.7.1 dokka_version=0.9.16-rdev-2-mpp-hacks @@ -36,21 +36,19 @@ kotlin.js.compiler=both gradle_node_version=1.2.0 node_version=8.9.3 npm_version=5.7.1 -mocha_version=6.2.2 +mocha_version=4.1.0 mocha_headless_chrome_version=1.8.2 -mocha_teamcity_reporter_version=3.0.0 -source_map_support_version=0.5.16 -jsdom_version=15.2.1 -jsdom_global_version=3.0.2 +mocha_teamcity_reporter_version=2.2.2 +source_map_support_version=0.5.3 # Settings kotlin.incremental.multiplatform=true kotlin.native.ignoreDisabledTargets=true -# Site generation +# Site deneration jekyll_version=4.0 -# JS IR backend sometimes crashes with out-of-memory +# JS IR baceknd sometimes crashes with out-of-memory # TODO: Remove once KT-37187 is fixed org.gradle.jvmargs=-Xmx2g @@ -58,6 +56,7 @@ org.gradle.jvmargs=-Xmx2g # https://github.com/gradle/gradle/issues/11412 systemProp.org.gradle.internal.publish.checksums.insecure=true -# todo:KLUDGE: This is commented out, and the property is set conditionally in build.gradle, because IDEA doesn't work with it. +# This is commented out, and the property is set conditionally in build.gradle, because 1.3.71 doesn't work with it. +# Once this property is set by default in new versions or 1.3.71 is dropped, either uncomment or remove this line. #kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.mpp.enableCompatibilityMetadataVariant=true +kotlin.mpp.enableCompatibilityMetadataVariant=true \ No newline at end of file diff --git a/gradle/compile-common.gradle b/gradle/compile-common.gradle index 0dc1b5c014..bee61429df 100644 --- a/gradle/compile-common.gradle +++ b/gradle/compile-common.gradle @@ -3,6 +3,10 @@ */ kotlin.sourceSets { + commonMain.dependencies { + api "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" + } + commonTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" api "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version" diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle index b52cfc5230..93d371a21f 100644 --- a/gradle/compile-js-multiplatform.gradle +++ b/gradle/compile-js-multiplatform.gradle @@ -26,6 +26,10 @@ kotlin { } sourceSets { + jsMain.dependencies { + api "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" + } + jsTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } diff --git a/gradle/compile-js.gradle b/gradle/compile-js.gradle index 55c81fe56e..d0697cfd3a 100644 --- a/gradle/compile-js.gradle +++ b/gradle/compile-js.gradle @@ -4,29 +4,25 @@ // Platform-specific configuration to compile JS modules -apply plugin: 'org.jetbrains.kotlin.js' +apply plugin: 'kotlin2js' dependencies { - testImplementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } -kotlin { - js(LEGACY) { - moduleName = project.name - "-js" - } - - sourceSets { - main.kotlin.srcDirs = ['src'] - test.kotlin.srcDirs = ['test'] - main.resources.srcDirs = ['resources'] - test.resources.srcDirs = ['test-resources'] - } -} - -tasks.withType(compileKotlinJs.getClass()) { +tasks.withType(compileKotlin2Js.getClass()) { kotlinOptions { moduleKind = "umd" sourceMap = true metaInfo = true } } + +compileKotlin2Js { + kotlinOptions { + // drop -js suffix from outputFile + def baseName = project.name - "-js" + outputFile = new File(outputFile.parent, baseName + ".js") + } +} diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle index e72d30511e..b226c97a57 100644 --- a/gradle/compile-jvm-multiplatform.gradle +++ b/gradle/compile-jvm-multiplatform.gradle @@ -5,11 +5,19 @@ sourceCompatibility = 1.6 targetCompatibility = 1.6 +repositories { + maven { url "https://dl.bintray.com/devexperts/Maven/" } +} + kotlin { targets { fromPreset(presets.jvm, 'jvm') } sourceSets { + jvmMain.dependencies { + api 'org.jetbrains.kotlin:kotlin-stdlib' + } + jvmTest.dependencies { api "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index caa5c45f60..a8116595f5 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -4,12 +4,13 @@ // Platform-specific configuration to compile JVM modules -apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'kotlin' sourceCompatibility = 1.6 targetCompatibility = 1.6 dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" @@ -18,6 +19,10 @@ dependencies { testCompile "junit:junit:$junit_version" } +repositories { + maven { url "https://dl.bintray.com/devexperts/Maven/" } +} + compileKotlin { kotlinOptions { freeCompilerArgs += ['-Xexplicit-api=strict'] diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle index 4487446799..378e4f5f98 100644 --- a/gradle/compile-native-multiplatform.gradle +++ b/gradle/compile-native-multiplatform.gradle @@ -13,24 +13,36 @@ kotlin { } targets { - addTarget(presets.linuxX64) - addTarget(presets.iosArm64) - addTarget(presets.iosArm32) - addTarget(presets.iosX64) - addTarget(presets.macosX64) - addTarget(presets.mingwX64) - addTarget(presets.tvosArm64) - addTarget(presets.tvosX64) - addTarget(presets.watchosArm32) - addTarget(presets.watchosArm64) - addTarget(presets.watchosX86) + if (project.ext.ideaActive) { + fromPreset(project.ext.ideaPreset, 'native') + } else { + addTarget(presets.linuxX64) + addTarget(presets.iosArm64) + addTarget(presets.iosArm32) + addTarget(presets.iosX64) + addTarget(presets.macosX64) + addTarget(presets.mingwX64) + addTarget(presets.tvosArm64) + addTarget(presets.tvosX64) + addTarget(presets.watchosArm32) + addTarget(presets.watchosArm64) + addTarget(presets.watchosX86) + } } sourceSets { nativeMain { dependsOn commonMain } - nativeTest { dependsOn commonTest } + // Empty source set is required in order to have native tests task + nativeTest {} - configure(nativeMainSets) { dependsOn nativeMain } - configure(nativeTestSets) { dependsOn nativeTest } + if (!project.ext.ideaActive) { + configure(nativeMainSets) { + dependsOn nativeMain + } + + configure(nativeTestSets) { + dependsOn nativeTest + } + } } } diff --git a/gradle/targets.gradle b/gradle/targets.gradle new file mode 100644 index 0000000000..08f3d989aa --- /dev/null +++ b/gradle/targets.gradle @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +/* + * This is a hack to avoid creating unsupported native source sets when importing project into IDEA + */ +project.ext.ideaActive = System.getProperty('idea.active') == 'true' + +kotlin { + targets { + def manager = project.ext.hostManager + def linuxEnabled = manager.isEnabled(presets.linuxX64.konanTarget) + def macosEnabled = manager.isEnabled(presets.macosX64.konanTarget) + def winEnabled = manager.isEnabled(presets.mingwX64.konanTarget) + + project.ext.isLinuxHost = linuxEnabled + project.ext.isMacosHost = macosEnabled + project.ext.isWinHost = winEnabled + + if (project.ext.ideaActive) { + def ideaPreset = presets.linuxX64 + if (macosEnabled) ideaPreset = presets.macosX64 + if (winEnabled) ideaPreset = presets.mingwX64 + project.ext.ideaPreset = ideaPreset + } + } +} diff --git a/gradle/test-mocha-js.gradle b/gradle/test-mocha-js.gradle index 7de79b9939..6676dc9268 100644 --- a/gradle/test-mocha-js.gradle +++ b/gradle/test-mocha-js.gradle @@ -86,8 +86,8 @@ task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) { task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) { args = ['install', "mocha@$mocha_version", - "jsdom@$jsdom_version", - "jsdom-global@$jsdom_global_version", + 'jsdom@15.2.1', + 'jsdom-global@3.0.2', "source-map-support@$source_map_support_version", '--no-save'] if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"] diff --git a/integration/kotlinx-coroutines-guava/build.gradle b/integration/kotlinx-coroutines-guava/build.gradle new file mode 100644 index 0000000000..16bdea50fd --- /dev/null +++ b/integration/kotlinx-coroutines-guava/build.gradle @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +ext.guava_version = '28.0-jre' + +dependencies { + compile "com.google.guava:guava:$guava_version" +} + +tasks.withType(dokka.getClass()) { + externalDocumentationLink { + url = new URL("https://google.github.io/guava/releases/$guava_version/api/docs/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + } +} diff --git a/integration/kotlinx-coroutines-guava/build.gradle.kts b/integration/kotlinx-coroutines-guava/build.gradle.kts deleted file mode 100644 index 53e91add44..0000000000 --- a/integration/kotlinx-coroutines-guava/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -val guavaVersion = "28.0-jre" - -dependencies { - compile("com.google.guava:guava:$guavaVersion") -} - -externalDocumentationLink( - url = "https://google.github.io/guava/releases/$guavaVersion/api/docs/" -) diff --git a/integration/kotlinx-coroutines-play-services/build.gradle b/integration/kotlinx-coroutines-play-services/build.gradle index 29ce3d606f..eb554866ed 100644 --- a/integration/kotlinx-coroutines-play-services/build.gradle +++ b/integration/kotlinx-coroutines-play-services/build.gradle @@ -2,6 +2,12 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +import org.gradle.api.artifacts.transform.* + +import java.nio.file.Files +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + ext.tasks_version = '16.0.1' def artifactType = Attribute.of("artifactType", String) @@ -43,3 +49,31 @@ tasks.withType(dokka.getClass()) { packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } + +abstract class UnpackAar implements TransformAction { + @InputArtifact + abstract Provider getInputArtifact() + + @Override + void transform(TransformOutputs outputs) { + ZipFile zip = new ZipFile(inputArtifact.get().asFile) + try { + for (entry in zip.entries()) { + if (!entry.isDirectory() && entry.name.endsWith(".jar")) { + unzipEntryTo(zip, entry, outputs.file(entry.name)) + } + } + } finally { + zip.close() + } + } + + private static void unzipEntryTo(ZipFile zip, ZipEntry entry, File output) { + InputStream stream = zip.getInputStream(entry) + try { + Files.copy(stream, output.toPath()) + } finally { + stream.close() + } + } +} diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle b/integration/kotlinx-coroutines-slf4j/build.gradle new file mode 100644 index 0000000000..05accb75d3 --- /dev/null +++ b/integration/kotlinx-coroutines-slf4j/build.gradle @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + compile 'org.slf4j:slf4j-api:1.7.25' + testCompile 'io.github.microutils:kotlin-logging:1.5.4' + testRuntime 'ch.qos.logback:logback-classic:1.2.3' + testRuntime 'ch.qos.logback:logback-core:1.2.3' +} + +tasks.withType(dokka.getClass()) { + externalDocumentationLink { + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + url = new URL("https://www.slf4j.org/apidocs/") + } +} diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle.kts b/integration/kotlinx-coroutines-slf4j/build.gradle.kts deleted file mode 100644 index c7d0d82d62..0000000000 --- a/integration/kotlinx-coroutines-slf4j/build.gradle.kts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile("org.slf4j:slf4j-api:1.7.25") - testCompile("io.github.microutils:kotlin-logging:1.5.4") - testRuntime("ch.qos.logback:logback-classic:1.2.3") - testRuntime("ch.qos.logback:logback-core:1.2.3") -} - -externalDocumentationLink( - url = "https://www.slf4j.org/apidocs/" -) diff --git a/js/example-frontend-js/README.md b/js/example-frontend-js/README.md index ad61372dc9..4e534e427a 100644 --- a/js/example-frontend-js/README.md +++ b/js/example-frontend-js/README.md @@ -3,7 +3,7 @@ Build application with ``` -gradlew :example-frontend-js:build +gradlew :example-frontend-js:bundle ``` The resulting application can be found in `build/dist` subdirectory. @@ -11,7 +11,7 @@ The resulting application can be found in `build/dist` subdirectory. You can start application with webpack-dev-server using: ``` -gradlew :example-frontend-js:run +gradlew :example-frontend-js:start ``` Built and deployed application is available at the library documentation site diff --git a/js/example-frontend-js/build.gradle b/js/example-frontend-js/build.gradle index 7abde63964..735a70d55b 100644 --- a/js/example-frontend-js/build.gradle +++ b/js/example-frontend-js/build.gradle @@ -2,32 +2,53 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -project.kotlin { - js(LEGACY) { - binaries.executable() - browser { - distribution { - directory = new File(directory.parentFile, "dist") - } - webpackTask { - cssSupport.enabled = true - } - runTask { - cssSupport.enabled = true - } - testTask { - useKarma { - useChromeHeadless() - webpackConfig.cssSupport.enabled = true - } - } +apply plugin: 'kotlin-dce-js' +apply from: rootProject.file('gradle/node-js.gradle') + +// Workaround resolving new Gradle metadata with kotlin2js +// TODO: Remove once KT-37188 is fixed +try { + def jsCompilerType = Class.forName("org.jetbrains.kotlin.gradle.targets.js.KotlinJsCompilerAttribute") + def jsCompilerAttr = Attribute.of("org.jetbrains.kotlin.js.compiler", jsCompilerType) + project.dependencies.attributesSchema.attribute(jsCompilerAttr) + configurations { + matching { + it.name.endsWith("Classpath") + }.forEach { + it.attributes.attribute(jsCompilerAttr, jsCompilerType.legacy) } } +} catch (java.lang.ClassNotFoundException e) { + // org.jetbrains.kotlin.gradle.targets.js.JsCompilerType is missing in 1.3.x + // But 1.3.x doesn't generate Gradle metadata, so this workaround is not needed +} - sourceSets { - main.dependencies { - implementation "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" - implementation(npm("html-webpack-plugin", "3.2.0")) - } +dependencies { + compile "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" +} + +compileKotlin2Js { + kotlinOptions { + main = "call" + } +} + +task bundle(type: NpmTask, dependsOn: [npmInstall, runDceKotlinJs]) { + inputs.files(fileTree("$buildDir/kotlin-js-min/main")) + inputs.files(fileTree(file("src/main/web"))) + inputs.file("npm/webpack.config.js") + outputs.dir("$buildDir/dist") + args = ["run", "bundle"] +} + +task start(type: NpmTask, dependsOn: bundle) { + args = ["run", "start"] +} + +// we have not tests but kotlin-dce-js still tries to work with them and crashed. +// todo: Remove when KT-22028 is fixed +afterEvaluate { + if (tasks.findByName('unpackDependenciesTestKotlinJs')) { + tasks.unpackDependenciesTestKotlinJs.enabled = false } } diff --git a/js/example-frontend-js/npm/package.json b/js/example-frontend-js/npm/package.json new file mode 100644 index 0000000000..7668cefba3 --- /dev/null +++ b/js/example-frontend-js/npm/package.json @@ -0,0 +1,22 @@ +{ + "name": "example-frontend-js", + "version": "$version", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/Kotlin/kotlinx.coroutines.git" + }, + "devDependencies": { + "webpack": "4.29.1", + "webpack-cli": "3.2.3", + "webpack-dev-server": "3.1.14", + "html-webpack-plugin": "3.2.0", + "uglifyjs-webpack-plugin": "2.1.1", + "style-loader": "0.23.1", + "css-loader": "2.1.0" + }, + "scripts": { + "bundle": "webpack", + "start": "webpack-dev-server --open --no-inline" + } +} diff --git a/js/example-frontend-js/npm/webpack.config.js b/js/example-frontend-js/npm/webpack.config.js new file mode 100644 index 0000000000..a208d047b3 --- /dev/null +++ b/js/example-frontend-js/npm/webpack.config.js @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This script is copied to "build" directory and run from there + +const webpack = require("webpack"); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const path = require("path"); + +const dist = path.resolve(__dirname, "dist"); + +module.exports = { + mode: "production", + entry: { + main: "main" + }, + output: { + filename: "[name].bundle.js", + path: dist, + publicPath: "" + }, + devServer: { + contentBase: dist + }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader' + ] + } + ] + }, + resolve: { + modules: [ + path.resolve(__dirname, "kotlin-js-min/main"), + path.resolve(__dirname, "../src/main/web/") + ] + }, + devtool: 'source-map', + plugins: [ + new HtmlWebpackPlugin({ + title: 'Kotlin Coroutines JS Example' + }), + new UglifyJSPlugin({ + sourceMap: true + }) + ] +}; diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt index da6e4196a6..25a73c61c0 100644 --- a/js/example-frontend-js/src/ExampleMain.kt +++ b/js/example-frontend-js/src/ExampleMain.kt @@ -13,10 +13,7 @@ import kotlin.coroutines.* import kotlin.math.* import kotlin.random.Random -external fun require(resource: String) - fun main() { - require("style.css") println("Starting example application...") document.addEventListener("DOMContentLoaded", { Application().start() diff --git a/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt b/js/example-frontend-js/src/main/web/main.js similarity index 51% rename from kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt rename to js/example-frontend-js/src/main/web/main.js index cac0530e4e..d2440ffaef 100644 --- a/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt +++ b/js/example-frontend-js/src/main/web/main.js @@ -2,6 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines +// ------ Main bundle for example application ------ -internal actual inline fun workerMain(block: () -> Unit) = block() +require("example-frontend"); +require("style.css"); diff --git a/js/example-frontend-js/webpack.config.d/custom-config.js b/js/example-frontend-js/webpack.config.d/custom-config.js deleted file mode 100644 index 21939bece0..0000000000 --- a/js/example-frontend-js/webpack.config.d/custom-config.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -;(function (config) { - const HtmlWebpackPlugin = require('html-webpack-plugin'); - - config.output.filename = "[name].bundle.js" - - config.plugins.push( - new HtmlWebpackPlugin({ - title: 'Kotlin Coroutines JS Example' - }) - ) - - // path from /js/packages/example-frontend-js to src/main/web - config.resolve.modules.push("../../../../js/example-frontend-js/src/main/web"); -})(config); diff --git a/knit.properties b/knit.properties deleted file mode 100644 index bc177ce44c..0000000000 --- a/knit.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. -# - -knit.include=docs/knit.code.include -test.template=docs/knit.test.template - -# Various test validation modes and their corresponding methods from TestUtil -test.mode.=verifyLines -test.mode.STARTS_WITH=verifyLinesStartWith -test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime -test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime -test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread -test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered -test.mode.LINES_START=verifyLinesStart -test.mode.EXCEPTION=verifyExceptions \ No newline at end of file diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index bb1c0f36ab..4609c68e36 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -46,7 +46,6 @@ public abstract interface class kotlinx/coroutines/CancellableContinuation : kot public abstract fun resumeUndispatched (Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Object;)V public abstract fun resumeUndispatchedWithException (Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Throwable;)V public abstract fun tryResume (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; - public abstract fun tryResume (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun tryResumeWithException (Ljava/lang/Throwable;)Ljava/lang/Object; } @@ -57,8 +56,6 @@ public final class kotlinx/coroutines/CancellableContinuation$DefaultImpls { public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/jvm/internal/CoroutineStackFrame, kotlinx/coroutines/CancellableContinuation { public fun (Lkotlin/coroutines/Continuation;I)V - public final fun callCancelHandler (Lkotlinx/coroutines/CancelHandler;Ljava/lang/Throwable;)V - public final fun callOnCancellation (Lkotlin/jvm/functions/Function1;Ljava/lang/Throwable;)V public fun cancel (Ljava/lang/Throwable;)Z public fun completeResume (Ljava/lang/Object;)V public fun getCallerFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; @@ -78,12 +75,14 @@ public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/ public fun resumeWith (Ljava/lang/Object;)V public fun toString ()Ljava/lang/String; public fun tryResume (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; - public fun tryResume (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun tryResumeWithException (Ljava/lang/Throwable;)Ljava/lang/Object; } public final class kotlinx/coroutines/CancellableContinuationKt { public static final fun disposeOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Lkotlinx/coroutines/DisposableHandle;)V + public static final fun suspendAtomicCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun suspendAtomicCancellableCoroutine (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun suspendAtomicCancellableCoroutine$default (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun suspendCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -258,21 +257,24 @@ public final class kotlinx/coroutines/Deferred$DefaultImpls { public abstract interface class kotlinx/coroutines/Delay { public abstract fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public abstract fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public abstract fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } public final class kotlinx/coroutines/Delay$DefaultImpls { public static fun delay (Lkotlinx/coroutines/Delay;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; } public final class kotlinx/coroutines/DelayKt { - public static final fun awaitCancellation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun delay-p9JZ4hM (DLkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class kotlinx/coroutines/DispatchedContinuationKt { + public static final fun resumeCancellableWith (Lkotlin/coroutines/Continuation;Ljava/lang/Object;)V +} + public final class kotlinx/coroutines/Dispatchers { public static final field INSTANCE Lkotlinx/coroutines/Dispatchers; public static final fun getDefault ()Lkotlinx/coroutines/CoroutineDispatcher; @@ -578,14 +580,6 @@ public final class kotlinx/coroutines/channels/BroadcastKt { public static synthetic fun broadcast$default (Lkotlinx/coroutines/channels/ReceiveChannel;ILkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; } -public final class kotlinx/coroutines/channels/BufferOverflow : java/lang/Enum { - public static final field DROP_LATEST Lkotlinx/coroutines/channels/BufferOverflow; - public static final field DROP_OLDEST Lkotlinx/coroutines/channels/BufferOverflow; - public static final field SUSPEND Lkotlinx/coroutines/channels/BufferOverflow; - public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/channels/BufferOverflow; - public static fun values ()[Lkotlinx/coroutines/channels/BufferOverflow; -} - public abstract interface class kotlinx/coroutines/channels/Channel : kotlinx/coroutines/channels/ReceiveChannel, kotlinx/coroutines/channels/SendChannel { public static final field BUFFERED I public static final field CONFLATED I @@ -618,10 +612,8 @@ public final class kotlinx/coroutines/channels/ChannelIterator$DefaultImpls { } public final class kotlinx/coroutines/channels/ChannelKt { - public static final synthetic fun Channel (I)Lkotlinx/coroutines/channels/Channel; - public static final fun Channel (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/channels/Channel; + public static final fun Channel (I)Lkotlinx/coroutines/channels/Channel; public static synthetic fun Channel$default (IILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; - public static synthetic fun Channel$default (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; } public final class kotlinx/coroutines/channels/ChannelsKt { @@ -795,7 +787,7 @@ public abstract interface class kotlinx/coroutines/channels/ReceiveChannel { public abstract fun iterator ()Lkotlinx/coroutines/channels/ChannelIterator; public abstract fun poll ()Ljava/lang/Object; public abstract fun receive (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun receiveOrClosed-ZYPwvRU (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun receiveOrClosed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun receiveOrNull (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -876,7 +868,7 @@ public final class kotlinx/coroutines/debug/internal/DebuggerInfo : java/io/Seri public final fun getState ()Ljava/lang/String; } -public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/CancellableFlow, kotlinx/coroutines/flow/Flow { +public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { public fun ()V public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun collectSafely (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -903,15 +895,10 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun asFlow ([I)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([J)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun asSharedFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)Lkotlinx/coroutines/flow/SharedFlow; - public static final fun asStateFlow (Lkotlinx/coroutines/flow/MutableStateFlow;)Lkotlinx/coroutines/flow/StateFlow; public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static final synthetic fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; - public static final fun buffer (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; + public static final fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static final fun cache (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun callbackFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun cancellable (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun catch (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; @@ -1001,15 +988,10 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun onErrorReturn$default (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun onStart (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; - public static final fun onSubscription (Lkotlinx/coroutines/flow/SharedFlow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun produceIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; - public static final fun publish (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; - public static final fun publish (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun publishOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static final fun receiveAsFlow (Lkotlinx/coroutines/channels/ReceiveChannel;)Lkotlinx/coroutines/flow/Flow; public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun replay (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; - public static final fun replay (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final synthetic fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun retry (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; @@ -1021,15 +1003,11 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; - public static final fun shareIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;I)Lkotlinx/coroutines/flow/SharedFlow; - public static synthetic fun shareIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;IILjava/lang/Object;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun skip (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; - public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V @@ -1051,63 +1029,17 @@ public final class kotlinx/coroutines/flow/FlowKt { } public final class kotlinx/coroutines/flow/LintKt { - public static final fun cancel (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;)V - public static synthetic fun cancel$default (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V - public static final fun cancellable (Lkotlinx/coroutines/flow/SharedFlow;)Lkotlinx/coroutines/flow/Flow; public static final fun conflate (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; - public static final fun flowOn (Lkotlinx/coroutines/flow/SharedFlow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; - public static final fun getCoroutineContext (Lkotlinx/coroutines/flow/FlowCollector;)Lkotlin/coroutines/CoroutineContext; - public static final fun isActive (Lkotlinx/coroutines/flow/FlowCollector;)Z + public static final fun flowOn (Lkotlinx/coroutines/flow/StateFlow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; } -public abstract interface class kotlinx/coroutines/flow/MutableSharedFlow : kotlinx/coroutines/flow/FlowCollector, kotlinx/coroutines/flow/SharedFlow { - public abstract fun getSubscriptionCount ()Lkotlinx/coroutines/flow/StateFlow; - public abstract fun resetReplayCache ()V - public abstract fun tryEmit (Ljava/lang/Object;)Z -} - -public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/MutableSharedFlow, kotlinx/coroutines/flow/StateFlow { - public abstract fun compareAndSet (Ljava/lang/Object;Ljava/lang/Object;)Z +public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/StateFlow { public abstract fun getValue ()Ljava/lang/Object; public abstract fun setValue (Ljava/lang/Object;)V } -public abstract interface class kotlinx/coroutines/flow/SharedFlow : kotlinx/coroutines/flow/Flow { - public abstract fun getReplayCache ()Ljava/util/List; -} - -public final class kotlinx/coroutines/flow/SharedFlowKt { - public static final fun MutableSharedFlow (IILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/MutableSharedFlow; - public static synthetic fun MutableSharedFlow$default (IILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/MutableSharedFlow; -} - -public final class kotlinx/coroutines/flow/SharingCommand : java/lang/Enum { - public static final field START Lkotlinx/coroutines/flow/SharingCommand; - public static final field STOP Lkotlinx/coroutines/flow/SharingCommand; - public static final field STOP_AND_RESET_REPLAY_CACHE Lkotlinx/coroutines/flow/SharingCommand; - public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/flow/SharingCommand; - public static fun values ()[Lkotlinx/coroutines/flow/SharingCommand; -} - -public abstract interface class kotlinx/coroutines/flow/SharingStarted { - public static final field Companion Lkotlinx/coroutines/flow/SharingStarted$Companion; - public abstract fun command (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; -} - -public final class kotlinx/coroutines/flow/SharingStarted$Companion { - public final fun WhileSubscribed (JJ)Lkotlinx/coroutines/flow/SharingStarted; - public static synthetic fun WhileSubscribed$default (Lkotlinx/coroutines/flow/SharingStarted$Companion;JJILjava/lang/Object;)Lkotlinx/coroutines/flow/SharingStarted; - public final fun getEagerly ()Lkotlinx/coroutines/flow/SharingStarted; - public final fun getLazily ()Lkotlinx/coroutines/flow/SharingStarted; -} - -public final class kotlinx/coroutines/flow/SharingStartedKt { - public static final fun WhileSubscribed-9tZugJw (Lkotlinx/coroutines/flow/SharingStarted$Companion;DD)Lkotlinx/coroutines/flow/SharingStarted; - public static synthetic fun WhileSubscribed-9tZugJw$default (Lkotlinx/coroutines/flow/SharingStarted$Companion;DDILjava/lang/Object;)Lkotlinx/coroutines/flow/SharingStarted; -} - -public abstract interface class kotlinx/coroutines/flow/StateFlow : kotlinx/coroutines/flow/SharedFlow { +public abstract interface class kotlinx/coroutines/flow/StateFlow : kotlinx/coroutines/flow/Flow { public abstract fun getValue ()Ljava/lang/Object; } @@ -1118,15 +1050,13 @@ public final class kotlinx/coroutines/flow/StateFlowKt { public abstract class kotlinx/coroutines/flow/internal/ChannelFlow : kotlinx/coroutines/flow/internal/FusibleFlow { public final field capacity I public final field context Lkotlin/coroutines/CoroutineContext; - public final field onBufferOverflow Lkotlinx/coroutines/channels/BufferOverflow; - public fun (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)V - protected fun additionalToStringProps ()Ljava/lang/String; + public fun (Lkotlin/coroutines/CoroutineContext;I)V + public fun additionalToStringProps ()Ljava/lang/String; public fun broadcastImpl (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected abstract fun collectTo (Lkotlinx/coroutines/channels/ProducerScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - protected abstract fun create (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/internal/ChannelFlow; - public fun dropChannelOperators ()Lkotlinx/coroutines/flow/Flow; - public fun fuse (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; + protected abstract fun create (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/ChannelFlow; + public fun fuse (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/FusibleFlow; public fun produceImpl (Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; public fun toString ()Ljava/lang/String; } @@ -1140,11 +1070,11 @@ public final class kotlinx/coroutines/flow/internal/FlowExceptions_commonKt { } public abstract interface class kotlinx/coroutines/flow/internal/FusibleFlow : kotlinx/coroutines/flow/Flow { - public abstract fun fuse (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; + public abstract fun fuse (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/FusibleFlow; } public final class kotlinx/coroutines/flow/internal/FusibleFlow$DefaultImpls { - public static synthetic fun fuse$default (Lkotlinx/coroutines/flow/internal/FusibleFlow;Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun fuse$default (Lkotlinx/coroutines/flow/internal/FusibleFlow;Lkotlin/coroutines/CoroutineContext;IILjava/lang/Object;)Lkotlinx/coroutines/flow/internal/FusibleFlow; } public final class kotlinx/coroutines/flow/internal/SafeCollector_commonKt { diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index f98f6a529c..59dc5da894 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -2,61 +2,14 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -apply plugin: 'org.jetbrains.kotlin.multiplatform' +apply plugin: 'kotlin-multiplatform' +apply from: rootProject.file("gradle/targets.gradle") apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle") apply from: rootProject.file("gradle/compile-common.gradle") apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") apply from: rootProject.file('gradle/publish-npm-js.gradle') -/* ========================================================================== - Configure source sets structure for kotlinx-coroutines-core: - - TARGETS SOURCE SETS - ------- ---------------------------------------------- - - js -----------------------------------------------------+ - | - V - jvm -------------------------------> concurrent ---> common - ^ - ios \ | - macos | ---> nativeDarwin ---> native --+ - tvos | ^ - watchos / | - | - linux \ ---> nativeOther -------+ - mingw / - - ========================================================================== */ - -project.ext.sourceSetSuffixes = ["Main", "Test"] - -void defineSourceSet(newName, dependsOn, includedInPred) { - for (suffix in project.ext.sourceSetSuffixes) { - def newSS = kotlin.sourceSets.maybeCreate(newName + suffix) - for (dep in dependsOn) { - newSS.dependsOn(kotlin.sourceSets[dep + suffix]) - } - for (curSS in kotlin.sourceSets) { - def curName = curSS.name - if (curName.endsWith(suffix)) { - def prefix = curName.substring(0, curName.length() - suffix.length()) - if (includedInPred(prefix)) curSS.dependsOn(newSS) - } - } - } -} - -static boolean isNativeDarwin(String name) { return ["ios", "macos", "tvos", "watchos"].any { name.startsWith(it) } } -static boolean isNativeOther(String name) { return ["linux", "mingw"].any { name.startsWith(it) } } - -defineSourceSet("concurrent", ["common"]) { it in ["jvm", "native"] } -defineSourceSet("nativeDarwin", ["native"]) { isNativeDarwin(it) } -defineSourceSet("nativeOther", ["native"]) { isNativeOther(it) } - -/* ========================================================================== */ - /* * All platform plugins and configuration magic happens here instead of build.gradle * because JMV-only projects depend on core, thus core should always be initialized before configuration. @@ -65,7 +18,7 @@ kotlin { configure(sourceSets) { def srcDir = name.endsWith('Main') ? 'src' : 'test' def platform = name[0..-5] - kotlin.srcDirs = ["$platform/$srcDir"] + kotlin.srcDir "$platform/$srcDir" if (name == "jvmMain") { resources.srcDirs = ["$platform/resources"] } else if (name == "jvmTest") { @@ -78,18 +31,12 @@ kotlin { } configure(targets) { - // Configure additional binaries and test runs -- one for each OS - if (["macos", "linux", "mingw"].any { name.startsWith(it) }) { - binaries { - // Test for memory leaks using a special entry point that does not exit but returns from main - binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"] - // Configure a separate test where code runs in background - test("background", [org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG]) { - freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"] - } - } - testRuns { - background { setExecutionSourceFrom(binaries.backgroundDebugTest) } + def targetName = it.name + compilations.all { compilation -> + def compileTask = tasks.getByName(compilation.compileKotlinTaskName) + // binary compatibility support + if (targetName.contains("jvm") && compilation.compilationName == "main") { + compileTask.kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"] } } } @@ -107,52 +54,23 @@ compileKotlinMetadata { } } -// :KLUDGE: Idea.active: This is needed to workaround resolve problems after importing this project to IDEA -def configureNativeSourceSetPreset(name, preset) { - def hostMainCompilation = project.kotlin.targetFromPreset(preset).compilations.main - // Look for platform libraries in "implementation" for default source set - def implementationConfiguration = configurations[hostMainCompilation.defaultSourceSet.implementationMetadataConfigurationName] - // Now find the libraries: Finds platform libs & stdlib, but platform declarations are still not resolved due to IDE bugs - def hostNativePlatformLibs = files( - provider { - implementationConfiguration.findAll { - it.path.endsWith(".klib") || it.absolutePath.contains("klib${File.separator}platform") || it.absolutePath.contains("stdlib") - } - } - ) - // Add all those dependencies - for (suffix in sourceSetSuffixes) { - configure(kotlin.sourceSets[name + suffix]) { - dependencies.add(implementationMetadataConfigurationName, hostNativePlatformLibs) - } - } -} - -// :KLUDGE: Idea.active: Configure platform libraries for native source sets when working in IDEA -if (Idea.active) { - def manager = project.ext.hostManager - def linuxPreset = kotlin.presets.linuxX64 - def macosPreset = kotlin.presets.macosX64 - // linux should be always available (cross-compilation capable) -- use it as default - assert manager.isEnabled(linuxPreset.konanTarget) - // use macOS libs for nativeDarwin if available - def macosAvailable = manager.isEnabled(macosPreset.konanTarget) - // configure source sets - configureNativeSourceSetPreset("native", linuxPreset) - configureNativeSourceSetPreset("nativeOther", linuxPreset) - configureNativeSourceSetPreset("nativeDarwin", macosAvailable ? macosPreset : linuxPreset) -} - kotlin.sourceSets { jvmMain.dependencies { compileOnly "com.google.android:annotations:4.1.1.4" } jvmTest.dependencies { - api "org.jetbrains.kotlinx:lincheck:$lincheck_version" + // This is a workaround for https://youtrack.jetbrains.com/issue/KT-39037 + def excludingCurrentProject = { dependency -> + def result = project.dependencies.create(dependency) + result.exclude(module: project.name) + return result + } + + api(excludingCurrentProject("org.jetbrains.kotlinx:lincheck:$lincheck_version")) api "org.jetbrains.kotlinx:kotlinx-knit-test:$knit_version" api "com.esotericsoftware:kryo:4.0.0" - implementation project(":android-unit-tests") + implementation(excludingCurrentProject(project(":android-unit-tests"))) } } @@ -179,7 +97,7 @@ jvmTest { enableAssertions = true systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' // 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true" - if (!Idea.active && rootProject.properties['stress'] == null) { + if (!project.ext.ideaActive && rootProject.properties['stress'] == null) { exclude '**/*StressTest.*' } systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt index 7189349024..dd1e1771f2 100644 --- a/kotlinx-coroutines-core/common/src/Await.kt +++ b/kotlinx-coroutines-core/common/src/Await.kt @@ -5,7 +5,6 @@ package kotlinx.coroutines import kotlinx.atomicfu.* -import kotlinx.coroutines.channels.* import kotlin.coroutines.* /** @@ -19,8 +18,6 @@ import kotlin.coroutines.* * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun awaitAll(vararg deferreds: Deferred): List = if (deferreds.isEmpty()) emptyList() else AwaitAll(deferreds).await() @@ -36,8 +33,6 @@ public suspend fun awaitAll(vararg deferreds: Deferred): List = * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Collection>.awaitAll(): List = if (isEmpty()) emptyList() else AwaitAll(toTypedArray()).await() @@ -46,11 +41,8 @@ public suspend fun Collection>.awaitAll(): List = * Suspends current coroutine until all given jobs are complete. * This method is semantically equivalent to joining all given jobs one by one with `jobs.forEach { it.join() }`. * - * This suspending function is cancellable. - * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, + * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() } @@ -58,11 +50,8 @@ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() } * Suspends current coroutine until all given jobs are complete. * This method is semantically equivalent to joining all given jobs one by one with `forEach { it.join() }`. * - * This suspending function is cancellable. - * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, + * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Collection.joinAll(): Unit = forEach { it.join() } diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index c0924a0238..64bff500dc 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -129,9 +129,8 @@ private class LazyDeferredCoroutine( * This function uses dispatcher from the new context, shifting execution of the [block] into the * different thread if a new dispatcher is specified, and back to the original dispatcher * when it completes. Note that the result of `withContext` invocation is - * dispatched into the original context in a cancellable way with a **prompt cancellation guarantee**, - * which means that if the original [coroutineContext], in which `withContext` was invoked, - * is cancelled by the time its dispatcher starts to execute the code, + * dispatched into the original context in a cancellable way, which means that if the original [coroutineContext], + * in which `withContext` was invoked, is cancelled by the time its dispatcher starts to execute the code, * it discards the result of `withContext` and throws [CancellationException]. */ public suspend fun withContext( diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 7d9315afbf..0d3fe847dc 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -15,8 +15,6 @@ import kotlin.coroutines.intrinsics.* * When the [cancel] function is explicitly invoked, this continuation immediately resumes with a [CancellationException] or * the specified cancel cause. * - * An instance of `CancellableContinuation` is created by the [suspendCancellableCoroutine] function. - * * Cancellable continuation has three states (as subset of [Job] states): * * | **State** | [isActive] | [isCompleted] | [isCancelled] | @@ -26,12 +24,14 @@ import kotlin.coroutines.intrinsics.* * | _Canceled_ (final _completed_ state)| `false` | `true` | `true` | * * Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while - * invocation of [Continuation.resume] or [Continuation.resumeWithException] transitions it from _active_ to _resumed_ state. + * invocation of [resume] or [resumeWithException] transitions it from _active_ to _resumed_ state. * * A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted]. * - * Invocation of [Continuation.resume] or [Continuation.resumeWithException] in _resumed_ state produces an [IllegalStateException], - * but is ignored in _cancelled_ state. + * Invocation of [resume] or [resumeWithException] in _resumed_ state produces an [IllegalStateException]. + * Invocation of [resume] in _cancelled_ state is ignored (it is a trivial race between resume from the continuation owner and + * outer job's cancellation, and the cancellation wins). + * Invocation of [resumeWithException] in _cancelled_ state triggers exception handling of the passed exception. * * ``` * +-----------+ resume +---------+ @@ -43,6 +43,7 @@ import kotlin.coroutines.intrinsics.* * +-----------+ * | Cancelled | * +-----------+ + * * ``` */ public interface CancellableContinuation : Continuation { @@ -77,14 +78,6 @@ public interface CancellableContinuation : Continuation { @InternalCoroutinesApi public fun tryResume(value: T, idempotent: Any? = null): Any? - /** - * Same as [tryResume] but with [onCancellation] handler that called if and only if the value is not - * delivered to the caller because of the dispatch in the process, so that atomicity delivery - * guaranteed can be provided by having a cancellation fallback. - */ - @InternalCoroutinesApi - public fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? - /** * Tries to resume this continuation with the specified [exception] and returns a non-null object token if successful, * or `null` otherwise (it was already resumed or cancelled). When a non-null object is returned, @@ -119,8 +112,8 @@ public interface CancellableContinuation : Continuation { public fun cancel(cause: Throwable? = null): Boolean /** - * Registers a [handler] to be **synchronously** invoked on [cancellation][cancel] (regular or exceptional) of this continuation. - * When the continuation is already cancelled, the handler is immediately invoked + * Registers a [handler] to be **synchronously** invoked on cancellation (regular or exceptional) of this continuation. + * When the continuation is already cancelled, the handler will be immediately invoked * with the cancellation exception. Otherwise, the handler will be invoked as soon as this * continuation is cancelled. * @@ -129,15 +122,7 @@ public interface CancellableContinuation : Continuation { * processed as an uncaught exception in the context of the current coroutine * (see [CoroutineExceptionHandler]). * - * At most one [handler] can be installed on a continuation. Attempt to call `invokeOnCancellation` second - * time produces [IllegalStateException]. - * - * This handler is also called when this continuation [resumes][Continuation.resume] normally (with a value) and then - * is cancelled while waiting to be dispatched. More generally speaking, this handler is called whenever - * the caller of [suspendCancellableCoroutine] is getting a [CancellationException]. - * - * A typical example for `invokeOnCancellation` usage is given in - * the documentation for the [suspendCancellableCoroutine] function. + * At most one [handler] can be installed on a continuation. * * **Note**: Implementation of `CompletionHandler` must be fast, non-blocking, and thread-safe. * This `handler` can be invoked concurrently with the surrounding code. @@ -180,7 +165,7 @@ public interface CancellableContinuation : Continuation { * (see [CoroutineExceptionHandler]). * * This function shall be used when resuming with a resource that must be closed by - * code that called the corresponding suspending function, for example: + * code that called the corresponding suspending function, e.g.: * * ``` * continuation.resume(resource) { @@ -188,119 +173,17 @@ public interface CancellableContinuation : Continuation { * } * ``` * - * A more complete example and further details are given in - * the documentation for the [suspendCancellableCoroutine] function. - * * **Note**: The [onCancellation] handler must be fast, non-blocking, and thread-safe. * It can be invoked concurrently with the surrounding code. * There is no guarantee on the execution context of its invocation. */ @ExperimentalCoroutinesApi // since 1.2.0 - public fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) + public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) } /** * Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to - * the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is - * cancelled or completed while it is suspended. - * - * A typical use of this function is to suspend a coroutine while waiting for a result - * from a single-shot callback API and to return the result to the caller. - * For multi-shot callback APIs see [callbackFlow][kotlinx.coroutines.flow.callbackFlow]. - * - * ``` - * suspend fun awaitCallback(): T = suspendCancellableCoroutine { continuation -> - * val callback = object : Callback { // Implementation of some callback interface - * override fun onCompleted(value: T) { - * // Resume coroutine with a value provided by the callback - * continuation.resume(value) - * } - * override fun onApiError(cause: Throwable) { - * // Resume coroutine with an exception provided by the callback - * continuation.resumeWithException(cause) - * } - * } - * // Register callback with an API - * api.register(callback) - * // Remove callback on cancellation - * continuation.invokeOnCancellation { api.unregister(callback) } - * // At this point the coroutine is suspended by suspendCancellableCoroutine until callback fires - * } - * ``` - * - * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because - * > `invokeOnCancellation` block can be called at any time due to asynchronous nature of cancellation, even - * > concurrently with the call of the callback. - * - * ### Prompt cancellation guarantee - * - * This function provides **prompt cancellation guarantee**. - * If the [Job] of the current coroutine was cancelled while this function was suspended it will not resume - * successfully. - * - * The cancellation of the coroutine's job is generally asynchronous with respect to the suspended coroutine. - * The suspended coroutine is resumed with the call it to its [Continuation.resumeWith] member function or to - * [resume][Continuation.resume] extension function. - * However, when coroutine is resumed, it does not immediately start executing, but is passed to its - * [CoroutineDispatcher] to schedule its execution when dispatcher's resources become available for execution. - * The job's cancellation can happen both before, after, and concurrently with the call to `resume`. In any - * case, prompt cancellation guarantees that the the coroutine will not resume its code successfully. - * - * If the coroutine was resumed with an exception (for example, using [Continuation.resumeWithException] extension - * function) and cancelled, then the resulting exception of the `suspendCancellableCoroutine` function is determined - * by whichever action (exceptional resume or cancellation) that happened first. - * - * ### Returning resources from a suspended coroutine - * - * As a result of a prompt cancellation guarantee, when a closeable resource - * (like open file or a handle to another native resource) is returned from a suspended coroutine as a value - * it can be lost when the coroutine is cancelled. In order to ensure that the resource can be properly closed - * in this case, the [CancellableContinuation] interface provides two functions. - * - * * [invokeOnCancellation][CancellableContinuation.invokeOnCancellation] installs a handler that is called - * whenever a suspend coroutine is being cancelled. In addition to the example at the beginning, it can be - * used to ensure that a resource that was opened before the call to - * `suspendCancellableCoroutine` or in its body is closed in case of cancellation. - * - * ``` - * suspendCancellableCoroutine { continuation -> - * val resource = openResource() // Opens some resource - * continuation.invokeOnCancellation { - * resource.close() // Ensures the resource is closed on cancellation - * } - * // ... - * } - * ``` - * - * * [resume(value) { ... }][CancellableContinuation.resume] method on a [CancellableContinuation] takes - * an optional `onCancellation` block. It can be used when resuming with a resource that must be closed by - * the code that called the corresponding suspending function. - * - * ``` - * suspendCancellableCoroutine { continuation -> - * val callback = object : Callback { // Implementation of some callback interface - * // A callback provides a reference to some closeable resource - * override fun onCompleted(resource: T) { - * // Resume coroutine with a value provided by the callback and ensure the resource is closed in case - * // when the coroutine is cancelled before the caller gets a reference to the resource. - * continuation.resume(resource) { - * resource.close() // Close the resource on cancellation - * } - * } - * // ... - * } - * ``` - * - * ### Implementation details and custom continuation interceptors - * - * The prompt cancellation guarantee is the result of a coordinated implementation inside `suspendCancellableCoroutine` - * function and the [CoroutineDispatcher] class. The coroutine dispatcher checks for the status of the [Job] immediately - * before continuing its normal execution and aborts this normal execution, calling all the corresponding - * cancellation handlers, if the job was cancelled. - * - * If a custom implementation of [ContinuationInterceptor] is used in a coroutine's context that does not extend - * [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor - * can resume execution of a previously suspended coroutine even if its job was already cancelled. + * the [block]. This function throws a [CancellationException] if the coroutine is cancelled or completed while suspended. */ public suspend inline fun suspendCancellableCoroutine( crossinline block: (CancellableContinuation) -> Unit @@ -318,10 +201,29 @@ public suspend inline fun suspendCancellableCoroutine( } /** - * Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of - * [CancellableContinuationImpl] is reused. + * Suspends the coroutine like [suspendCancellableCoroutine], but with *atomic cancellation*. + * + * When the suspended function throws a [CancellationException], it means that the continuation was not resumed. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when the continuation + * was already resumed and was posted for execution to the thread's queue. + * + * @suppress **This an internal API and should not be used from general code.** */ -internal suspend inline fun suspendCancellableCoroutineReusable( +@InternalCoroutinesApi +public suspend inline fun suspendAtomicCancellableCoroutine( + crossinline block: (CancellableContinuation) -> Unit +): T = + suspendCoroutineUninterceptedOrReturn { uCont -> + val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_ATOMIC_DEFAULT) + block(cancellable) + cancellable.getResult() + } + +/** + * Suspends coroutine similar to [suspendAtomicCancellableCoroutine], but an instance of [CancellableContinuationImpl] is reused if possible. + */ +internal suspend inline fun suspendAtomicCancellableCoroutineReusable( crossinline block: (CancellableContinuation) -> Unit ): T = suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = getOrCreateCancellableContinuation(uCont.intercepted()) @@ -332,12 +234,12 @@ internal suspend inline fun suspendCancellableCoroutineReusable( internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl { // If used outside of our dispatcher if (delegate !is DispatchedContinuation) { - return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE) + return CancellableContinuationImpl(delegate, resumeMode = MODE_ATOMIC_DEFAULT) } /* * Attempt to claim reusable instance. * - * suspendCancellableCoroutineReusable { // <- claimed + * suspendAtomicCancellableCoroutineReusable { // <- claimed * // Any asynchronous cancellation is "postponed" while this block * // is being executed * } // postponed cancellation is checked here. @@ -348,13 +250,26 @@ internal fun getOrCreateCancellableContinuation(delegate: Continuation): * thus leaking CC instance for indefinite time. * 2) Continuation was cancelled. Then we should prevent any further reuse and bail out. */ - return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetStateReusable() } - ?: return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE) + return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetState() } + ?: return CancellableContinuationImpl(delegate, MODE_ATOMIC_DEFAULT) } /** - * Removes the specified [node] on cancellation. This function assumes that this node is already - * removed on successful resume and does not try to remove it if the continuation is cancelled during dispatch. + * @suppress **Deprecated** + */ +@Deprecated( + message = "holdCancellability parameter is deprecated and is no longer used", + replaceWith = ReplaceWith("suspendAtomicCancellableCoroutine(block)") +) +@InternalCoroutinesApi +public suspend inline fun suspendAtomicCancellableCoroutine( + holdCancellability: Boolean = false, + crossinline block: (CancellableContinuation) -> Unit +): T = + suspendAtomicCancellableCoroutine(block) + +/** + * Removes the specified [node] on cancellation. */ internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) = invokeOnCancellation(handler = RemoveOnCancel(node).asHandler) @@ -375,7 +290,7 @@ public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHa // --------------- implementation details --------------- -private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : BeforeResumeCancelHandler() { +private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : CancelHandler() { override fun invoke(cause: Throwable?) { node.remove() } override fun toString() = "RemoveOnCancel[$node]" } diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index cdb1b78882..e25ebd3a37 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -27,10 +27,6 @@ internal open class CancellableContinuationImpl( final override val delegate: Continuation, resumeMode: Int ) : DispatchedTask(resumeMode), CancellableContinuation, CoroutineStackFrame { - init { - assert { resumeMode != MODE_UNINITIALIZED } // invalid mode for CancellableContinuationImpl - } - public override val context: CoroutineContext = delegate.context /* @@ -92,17 +88,15 @@ internal open class CancellableContinuationImpl( private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this) /** - * Resets cancellability state in order to [suspendCancellableCoroutineReusable] to work. - * Invariant: used only by [suspendCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. + * Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work. + * Invariant: used only by [suspendAtomicCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. */ - @JvmName("resetStateReusable") // Prettier stack traces - internal fun resetStateReusable(): Boolean { - assert { resumeMode == MODE_CANCELLABLE_REUSABLE } // invalid mode for CancellableContinuationImpl + @JvmName("resetState") // Prettier stack traces + internal fun resetState(): Boolean { assert { parentHandle !== NonDisposableHandle } val state = _state.value assert { state !is NotCompleted } - if (state is CompletedContinuation && state.idempotentResume != null) { - // Cannot reuse continuation that was resumed with idempotent marker + if (state is CompletedIdempotentResult) { detachChild() return false } @@ -120,6 +114,7 @@ internal open class CancellableContinuationImpl( if (checkCompleted()) return if (parentHandle !== null) return // fast path 2 -- was already initialized val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent + parent.start() // make sure the parent is started val handle = parent.invokeOnCompletion( onCancelling = true, handler = ChildContinuation(parent, this).asHandler @@ -135,7 +130,7 @@ internal open class CancellableContinuationImpl( private fun checkCompleted(): Boolean { val completed = isCompleted - if (!resumeMode.isReusableMode) return completed // Do not check postponed cancellation for non-reusable continuations + if (resumeMode != MODE_ATOMIC_DEFAULT) return completed // Do not check postponed cancellation for non-reusable continuations val dispatched = delegate as? DispatchedContinuation<*> ?: return completed val cause = dispatched.checkPostponedCancellation(this) ?: return completed if (!completed) { @@ -152,26 +147,10 @@ internal open class CancellableContinuationImpl( override fun takeState(): Any? = state - // Note: takeState does not clear the state so we don't use takenState - // and we use the actual current state where in CAS-loop - override fun cancelCompletedResult(takenState: Any?, cause: Throwable): Unit = _state.loop { state -> - when (state) { - is NotCompleted -> error("Not completed") - is CompletedExceptionally -> return // already completed exception or cancelled, nothing to do - is CompletedContinuation -> { - check(!state.cancelled) { "Must be called at most once" } - val update = state.copy(cancelCause = cause) - if (_state.compareAndSet(state, update)) { - state.invokeHandlers(this, cause) - return // done - } - } - else -> { - // completed normally without marker class, promote to CompletedContinuation in case - // if invokeOnCancellation if called later - if (_state.compareAndSet(state, CompletedContinuation(state, cancelCause = cause))) { - return // done - } + override fun cancelResult(state: Any?, cause: Throwable) { + if (state is CompletedWithCancellation) { + invokeHandlerSafely { + state.onCancellation(cause) } } } @@ -180,7 +159,7 @@ internal open class CancellableContinuationImpl( * Attempt to postpone cancellation for reusable cancellable continuation */ private fun cancelLater(cause: Throwable): Boolean { - if (!resumeMode.isReusableMode) return false + if (resumeMode != MODE_ATOMIC_DEFAULT) return false val dispatched = (delegate as? DispatchedContinuation<*>) ?: return false return dispatched.postponeCancellation(cause) } @@ -192,10 +171,10 @@ internal open class CancellableContinuationImpl( val update = CancelledContinuation(this, cause, handled = state is CancelHandler) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure // Invoke cancel handler if it was present - (state as? CancelHandler)?.let { callCancelHandler(it, cause) } + if (state is CancelHandler) invokeHandlerSafely { state.invoke(cause) } // Complete state update detachChildIfNonResuable() - dispatchResume(resumeMode) // no need for additional cancellation checks + dispatchResume(mode = MODE_ATOMIC_DEFAULT) return true } } @@ -207,36 +186,14 @@ internal open class CancellableContinuationImpl( detachChildIfNonResuable() } - private inline fun callCancelHandlerSafely(block: () -> Unit) { - try { - block() - } catch (ex: Throwable) { - // Handler should never fail, if it does -- it is an unhandled exception - handleCoroutineException( - context, - CompletionHandlerException("Exception in invokeOnCancellation handler for $this", ex) - ) - } - } - - private fun callCancelHandler(handler: CompletionHandler, cause: Throwable?) = - /* - * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, - * because we play type tricks on Kotlin/JS and handler is not necessarily a function there - */ - callCancelHandlerSafely { handler.invokeIt(cause) } - - fun callCancelHandler(handler: CancelHandler, cause: Throwable?) = - callCancelHandlerSafely { handler.invoke(cause) } - - fun callOnCancellation(onCancellation: (cause: Throwable) -> Unit, cause: Throwable) { + private inline fun invokeHandlerSafely(block: () -> Unit) { try { - onCancellation.invoke(cause) + block() } catch (ex: Throwable) { // Handler should never fail, if it does -- it is an unhandled exception handleCoroutineException( context, - CompletionHandlerException("Exception in resume onCancellation handler for $this", ex) + CompletionHandlerException("Exception in cancellation handler for $this", ex) ) } } @@ -275,75 +232,64 @@ internal open class CancellableContinuationImpl( val state = this.state if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this) // if the parent job was already cancelled, then throw the corresponding cancellation exception - // otherwise, there is a race if suspendCancellableCoroutine { cont -> ... } does cont.resume(...) + // otherwise, there is a race is suspendCancellableCoroutine { cont -> ... } does cont.resume(...) // before the block returns. This getResult would return a result as opposed to cancellation // exception that should have happened if the continuation is dispatched for execution later. - if (resumeMode.isCancellableMode) { + if (resumeMode == MODE_CANCELLABLE) { val job = context[Job] if (job != null && !job.isActive) { val cause = job.getCancellationException() - cancelCompletedResult(state, cause) + cancelResult(state, cause) throw recoverStackTrace(cause, this) } } return getSuccessfulResult(state) } - override fun resumeWith(result: Result) = + override fun resumeWith(result: Result) { resumeImpl(result.toState(this), resumeMode) + } - override fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) = - resumeImpl(value, resumeMode, onCancellation) + override fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) { + val cancelled = resumeImpl(CompletedWithCancellation(value, onCancellation), resumeMode) + if (cancelled != null) { + // too late to resume (was cancelled) -- call handler + invokeHandlerSafely { + onCancellation(cancelled.cause) + } + } + } public override fun invokeOnCancellation(handler: CompletionHandler) { - val cancelHandler = makeCancelHandler(handler) + var handleCache: CancelHandler? = null _state.loop { state -> when (state) { is Active -> { - if (_state.compareAndSet(state, cancelHandler)) return // quit on cas success + val node = handleCache ?: makeHandler(handler).also { handleCache = it } + if (_state.compareAndSet(state, node)) return // quit on cas success } is CancelHandler -> multipleHandlersError(handler, state) - is CompletedExceptionally -> { + is CancelledContinuation -> { /* - * Continuation was already cancelled or completed exceptionally. + * Continuation was already cancelled, invoke directly. * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed, - * so we check to make sure handler was installed just once. + * so we check to make sure that handler was installed just once. */ if (!state.makeHandled()) multipleHandlersError(handler, state) /* - * Call the handler only if it was cancelled (not called when completed exceptionally). * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, * because we play type tricks on Kotlin/JS and handler is not necessarily a function there */ - if (state is CancelledContinuation) { - callCancelHandler(handler, (state as? CompletedExceptionally)?.cause) - } + invokeHandlerSafely { handler.invokeIt((state as? CompletedExceptionally)?.cause) } return } - is CompletedContinuation -> { - /* - * Continuation was already completed, and might already have cancel handler. - */ - if (state.cancelHandler != null) multipleHandlersError(handler, state) - // BeforeResumeCancelHandler does not need to be called on a completed continuation - if (cancelHandler is BeforeResumeCancelHandler) return - if (state.cancelled) { - // Was already cancelled while being dispatched -- invoke the handler directly - callCancelHandler(handler, state.cancelCause) - return - } - val update = state.copy(cancelHandler = cancelHandler) - if (_state.compareAndSet(state, update)) return // quit on cas success - } else -> { /* - * Continuation was already completed normally, but might get cancelled while being dispatched. - * Change its state to CompletedContinuation, unless we have BeforeResumeCancelHandler which - * does not need to be called in this case. + * Continuation was already completed, do nothing. + * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed, + * but we have no way to check that it was installed just once in this case. */ - if (cancelHandler is BeforeResumeCancelHandler) return - val update = CompletedContinuation(state, cancelHandler = cancelHandler) - if (_state.compareAndSet(state, update)) return // quit on cas success + return } } } @@ -353,7 +299,7 @@ internal open class CancellableContinuationImpl( error("It's prohibited to register multiple handlers, tried to register $handler, already has $state") } - private fun makeCancelHandler(handler: CompletionHandler): CancelHandler = + private fun makeHandler(handler: CompletionHandler): CancelHandler = if (handler is CancelHandler) handler else InvokeOnCancel(handler) private fun dispatchResume(mode: Int) { @@ -362,39 +308,15 @@ internal open class CancellableContinuationImpl( dispatch(mode) } - private fun resumedState( - state: NotCompleted, - proposedUpdate: Any?, - resumeMode: Int, - onCancellation: ((cause: Throwable) -> Unit)?, - idempotent: Any? - ): Any? = when { - proposedUpdate is CompletedExceptionally -> { - assert { idempotent == null } // there are no idempotent exceptional resumes - assert { onCancellation == null } // only successful results can be cancelled - proposedUpdate - } - !resumeMode.isCancellableMode && idempotent == null -> proposedUpdate // cannot be cancelled in process, all is fine - onCancellation != null || (state is CancelHandler && state !is BeforeResumeCancelHandler) || idempotent != null -> - // mark as CompletedContinuation if special cases are present: - // Cancellation handlers that shall be called after resume or idempotent resume - CompletedContinuation(proposedUpdate, state as? CancelHandler, onCancellation, idempotent) - else -> proposedUpdate // simple case -- use the value directly - } - - private fun resumeImpl( - proposedUpdate: Any?, - resumeMode: Int, - onCancellation: ((cause: Throwable) -> Unit)? = null - ) { + // returns null when successfully dispatched resumed, CancelledContinuation if too late (was already cancelled) + private fun resumeImpl(proposedUpdate: Any?, resumeMode: Int): CancelledContinuation? { _state.loop { state -> when (state) { is NotCompleted -> { - val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent = null) - if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure + if (!_state.compareAndSet(state, proposedUpdate)) return@loop // retry on cas failure detachChildIfNonResuable() - dispatchResume(resumeMode) // dispatch resume, but it might get cancelled in process - return // done + dispatchResume(resumeMode) + return null } is CancelledContinuation -> { /* @@ -402,48 +324,14 @@ internal open class CancellableContinuationImpl( * because cancellation is asynchronous and may race with resume. * Racy exceptions will be lost, too. */ - if (state.makeResumed()) { // check if trying to resume one (otherwise error) - // call onCancellation - onCancellation?.let { callOnCancellation(it, state.cause) } - return // done - } - } - } - alreadyResumedError(proposedUpdate) // otherwise, an error (second resume attempt) - } - } - - /** - * Similar to [tryResume], but does not actually completes resume (needs [completeResume] call). - * Returns [RESUME_TOKEN] when resumed, `null` when it was already resumed or cancelled. - */ - private fun tryResumeImpl( - proposedUpdate: Any?, - idempotent: Any?, - onCancellation: ((cause: Throwable) -> Unit)? - ): Symbol? { - _state.loop { state -> - when (state) { - is NotCompleted -> { - val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent) - if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - detachChildIfNonResuable() - return RESUME_TOKEN - } - is CompletedContinuation -> { - return if (idempotent != null && state.idempotentResume === idempotent) { - assert { state.result == proposedUpdate } // "Non-idempotent resume" - RESUME_TOKEN // resumed with the same token -- ok - } else { - null // resumed with a different token or non-idempotent -- too late - } + if (state.makeResumed()) return state // tried to resume just once, but was cancelled } - else -> return null // cannot resume -- not active anymore } + alreadyResumedError(proposedUpdate) // otherwise -- an error (second resume attempt) } } - private fun alreadyResumedError(proposedUpdate: Any?): Nothing { + private fun alreadyResumedError(proposedUpdate: Any?) { error("Already resumed, but proposed with update $proposedUpdate") } @@ -455,7 +343,7 @@ internal open class CancellableContinuationImpl( /** * Detaches from the parent. - * Invariant: used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` + * Invariant: used used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` */ internal fun detachChild() { val handle = parentHandle @@ -464,14 +352,42 @@ internal open class CancellableContinuationImpl( } // Note: Always returns RESUME_TOKEN | null - override fun tryResume(value: T, idempotent: Any?): Any? = - tryResumeImpl(value, idempotent, onCancellation = null) - - override fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? = - tryResumeImpl(value, idempotent, onCancellation) + override fun tryResume(value: T, idempotent: Any?): Any? { + _state.loop { state -> + when (state) { + is NotCompleted -> { + val update: Any? = if (idempotent == null) value else + CompletedIdempotentResult(idempotent, value) + if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure + detachChildIfNonResuable() + return RESUME_TOKEN + } + is CompletedIdempotentResult -> { + return if (state.idempotentResume === idempotent) { + assert { state.result === value } // "Non-idempotent resume" + RESUME_TOKEN + } else { + null + } + } + else -> return null // cannot resume -- not active anymore + } + } + } - override fun tryResumeWithException(exception: Throwable): Any? = - tryResumeImpl(CompletedExceptionally(exception), idempotent = null, onCancellation = null) + override fun tryResumeWithException(exception: Throwable): Any? { + _state.loop { state -> + when (state) { + is NotCompleted -> { + val update = CompletedExceptionally(exception) + if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure + detachChildIfNonResuable() + return RESUME_TOKEN + } + else -> return null // cannot resume -- not active anymore + } + } + } // note: token is always RESUME_TOKEN override fun completeResume(token: Any) { @@ -492,15 +408,11 @@ internal open class CancellableContinuationImpl( @Suppress("UNCHECKED_CAST") override fun getSuccessfulResult(state: Any?): T = when (state) { - is CompletedContinuation -> state.result as T + is CompletedIdempotentResult -> state.result as T + is CompletedWithCancellation -> state.result as T else -> state as T } - // The exceptional state in CancellableContinuationImpl is stored directly and it is not recovered yet. - // The stacktrace recovery is invoked here. - override fun getExceptionalResult(state: Any?): Throwable? = - super.getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } - // For nicer debugging public override fun toString(): String = "${nameString()}(${delegate.toDebugString()}){$state}@$hexAddress" @@ -517,20 +429,8 @@ private object Active : NotCompleted { override fun toString(): String = "Active" } -/** - * Base class for all [CancellableContinuation.invokeOnCancellation] handlers to avoid an extra instance - * on JVM, yet support JS where you cannot extend from a functional type. - */ internal abstract class CancelHandler : CancelHandlerBase(), NotCompleted -/** - * Base class for all [CancellableContinuation.invokeOnCancellation] handlers that don't need to be invoked - * if continuation is cancelled after resumption, during dispatch, because the corresponding resources - * were already released before calling `resume`. This cancel handler is called only before `resume`. - * It avoids allocation of [CompletedContinuation] instance during resume on JVM. - */ -internal abstract class BeforeResumeCancelHandler : CancelHandler() - // Wrapper for lambdas, for the performance sake CancelHandler can be subclassed directly private class InvokeOnCancel( // Clashes with InvokeOnCancellation private val handler: CompletionHandler @@ -541,18 +441,16 @@ private class InvokeOnCancel( // Clashes with InvokeOnCancellation override fun toString() = "InvokeOnCancel[${handler.classSimpleName}@$hexAddress]" } -// Completed with additional metadata -private data class CompletedContinuation( - @JvmField val result: Any?, - @JvmField val cancelHandler: CancelHandler? = null, // installed via invokeOnCancellation - @JvmField val onCancellation: ((cause: Throwable) -> Unit)? = null, // installed via resume block - @JvmField val idempotentResume: Any? = null, - @JvmField val cancelCause: Throwable? = null +private class CompletedIdempotentResult( + @JvmField val idempotentResume: Any?, + @JvmField val result: Any? ) { - val cancelled: Boolean get() = cancelCause != null + override fun toString(): String = "CompletedIdempotentResult[$result]" +} - fun invokeHandlers(cont: CancellableContinuationImpl<*>, cause: Throwable) { - cancelHandler?.let { cont.callCancelHandler(it, cause) } - onCancellation?.let { cont.callOnCancellation(it, cause) } - } +private class CompletedWithCancellation( + @JvmField val result: Any?, + @JvmField val onCancellation: (cause: Throwable) -> Unit +) { + override fun toString(): String = "CompletedWithCancellation[$result]" } diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index 0605817afa..d24f1837cd 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.selects.* * All functions on this interface are **thread-safe** and can * be safely invoked from concurrent coroutines without external synchronization. * - * **The `CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**, + * **`CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**, * as new methods might be added to this interface in the future, but is stable for use. */ public interface CompletableDeferred : Deferred { diff --git a/kotlinx-coroutines-core/common/src/CompletableJob.kt b/kotlinx-coroutines-core/common/src/CompletableJob.kt index 74a92e36e5..8e6b1ab02f 100644 --- a/kotlinx-coroutines-core/common/src/CompletableJob.kt +++ b/kotlinx-coroutines-core/common/src/CompletableJob.kt @@ -11,7 +11,7 @@ package kotlinx.coroutines * All functions on this interface are **thread-safe** and can * be safely invoked from concurrent coroutines without external synchronization. * - * **The `CompletableJob` interface is not stable for inheritance in 3rd party libraries**, + * **`CompletableJob` interface is not stable for inheritance in 3rd party libraries**, * as new methods might be added to this interface in the future, but is stable for use. */ public interface CompletableJob : Job { diff --git a/kotlinx-coroutines-core/common/src/CompletionState.kt b/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt similarity index 78% rename from kotlinx-coroutines-core/common/src/CompletionState.kt rename to kotlinx-coroutines-core/common/src/CompletedExceptionally.kt index f09aa3ccd9..b426785bd7 100644 --- a/kotlinx-coroutines-core/common/src/CompletionState.kt +++ b/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt @@ -9,17 +9,10 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -internal fun Result.toState( - onCancellation: ((cause: Throwable) -> Unit)? = null -): Any? = fold( - onSuccess = { if (onCancellation != null) CompletedWithCancellation(it, onCancellation) else it }, - onFailure = { CompletedExceptionally(it) } -) +internal fun Result.toState(): Any? = fold({ it }, { CompletedExceptionally(it) }) -internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold( - onSuccess = { it }, - onFailure = { CompletedExceptionally(recoverStackTrace(it, caller)) } -) +internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold({ it }, + { CompletedExceptionally(recoverStackTrace(it, caller)) }) @Suppress("RESULT_CLASS_IN_RETURN_TYPE", "UNCHECKED_CAST") internal fun recoverResult(state: Any?, uCont: Continuation): Result = @@ -28,11 +21,6 @@ internal fun recoverResult(state: Any?, uCont: Continuation): Result = else Result.success(state as T) -internal data class CompletedWithCancellation( - @JvmField val result: Any?, - @JvmField val onCancellation: (cause: Throwable) -> Unit -) - /** * Class for an internal state of a job that was cancelled (completed exceptionally). * diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index ab1e814b8a..1b6e7eb00f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines -import kotlinx.coroutines.internal.* import kotlin.coroutines.* /** diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 0dde6c9352..7dbd6a6d7b 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -226,10 +226,10 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni /** * Ensures that current scope is [active][CoroutineScope.isActive]. + * Throws [IllegalStateException] if the context does not have a job in it. * * If the job is no longer active, throws [CancellationException]. * If the job was cancelled, thrown exception contains the original cancellation cause. - * This function does not do anything if there is no [Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext]. * * This method is a drop-in replacement for the following code, but with more precise exception: * ``` @@ -237,8 +237,6 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni * throw CancellationException() * } * ``` - * - * @see CoroutineContext.ensureActive */ public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive() diff --git a/kotlinx-coroutines-core/common/src/Deferred.kt b/kotlinx-coroutines-core/common/src/Deferred.kt index ff996756a3..72f3fde141 100644 --- a/kotlinx-coroutines-core/common/src/Deferred.kt +++ b/kotlinx-coroutines-core/common/src/Deferred.kt @@ -43,8 +43,6 @@ public interface Deferred : Job { * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * This function can be used in [select] invocation with [onAwait] clause. * Use [isCompleted] to check for completion of this deferred value without waiting. diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index f7948443fa..ab80912269 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -21,12 +21,9 @@ import kotlin.time.* public interface Delay { /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. - * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun delay(time: Long) { if (time <= 0) return // don't delay @@ -57,57 +54,15 @@ public interface Delay { * * This implementation uses a built-in single-threaded scheduled executor service. */ - public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = - DefaultDelay.invokeOnTimeout(timeMillis, block, context) + public fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + DefaultDelay.invokeOnTimeout(timeMillis, block) } -/** - * Suspends until cancellation, in which case it will throw a [CancellationException]. - * - * This function returns [Nothing], so it can be used in any coroutine, - * regardless of the required return type. - * - * Usage example in callback adapting code: - * - * ```kotlin - * fun currentTemperature(): Flow = callbackFlow { - * val callback = SensorCallback { degreesCelsius: Double -> - * trySend(Temperature.celsius(degreesCelsius)) - * } - * try { - * registerSensorCallback(callback) - * awaitCancellation() // Suspends to keep getting updates until cancellation. - * } finally { - * unregisterSensorCallback(callback) - * } - * } - * ``` - * - * Usage example in (non declarative) UI code: - * - * ```kotlin - * suspend fun showStuffUntilCancelled(content: Stuff): Nothing { - * someSubView.text = content.title - * anotherSubView.text = content.description - * someView.visibleInScope { - * awaitCancellation() // Suspends so the view stays visible. - * } - * } - * ``` - */ -@ExperimentalCoroutinesApi -public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} - /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. - * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. - * - * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. * * Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * @@ -117,23 +72,15 @@ public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} public suspend fun delay(timeMillis: Long) { if (timeMillis <= 0) return // don't delay return suspendCancellableCoroutine sc@ { cont: CancellableContinuation -> - // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule. - if (timeMillis < Long.MAX_VALUE) { - cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) - } + cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) } } /** * Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time. - * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. - * - * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. * * Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 4bfff118e8..c8e4455c92 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -24,14 +24,7 @@ import kotlin.time.* * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, - * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some - * resource inside the [block] that needs closing or release outside of the block. - * See the - * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] - * section of the coroutines guide for details. - * - * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -55,14 +48,7 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, - * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some - * resource inside the [block] that needs closing or release outside of the block. - * See the - * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] - * section of the coroutines guide for details. - * - * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { @@ -82,14 +68,7 @@ public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineSc * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, - * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some - * resource inside the [block] that needs closing or release outside of the block. - * See the - * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] - * section of the coroutines guide for details. - * - * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -122,14 +101,7 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, - * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some - * resource inside the [block] that needs closing or release outside of the block. - * See the - * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] - * section of the coroutines guide for details. - * - * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? = @@ -142,7 +114,7 @@ private fun setupTimeout( // schedule cancellation of this coroutine on time val cont = coroutine.uCont val context = cont.context - coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context)) + coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine)) // restart the block using a new coroutine with a new job, // however, start it undispatched, because we already are in the proper context return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block) diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index 0d8bd3bc2f..e0af04ddb7 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines -import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -14,8 +13,6 @@ import kotlin.coroutines.intrinsics.* * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed when this suspending function is invoked or while * this function is waiting for dispatch, it resumes with a [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend. * diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 53ecf06a2c..28c7ceabe1 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -16,9 +16,7 @@ import kotlin.native.concurrent.* /** * Abstract send channel. It is a base class for all send channel implementations. */ -internal abstract class AbstractSendChannel( - @JvmField protected val onUndeliveredElement: OnUndeliveredElement? -) : SendChannel { +internal abstract class AbstractSendChannel : SendChannel { /** @suppress **This is unstable API and it is subject to change.** */ protected val queue = LockFreeLinkedListHead() @@ -153,34 +151,24 @@ internal abstract class AbstractSendChannel( // We should check for closed token on offer as well, otherwise offer won't be linearizable // in the face of concurrent close() // See https://github.com/Kotlin/kotlinx.coroutines/issues/359 - throw recoverStackTrace(helpCloseAndGetSendException(element, closedForSend ?: return false)) - } - result is Closed<*> -> { - throw recoverStackTrace(helpCloseAndGetSendException(element, result)) + throw recoverStackTrace(helpCloseAndGetSendException(closedForSend ?: return false)) } + result is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(result)) else -> error("offerInternal returned $result") } } - private fun helpCloseAndGetSendException(element: E, closed: Closed<*>): Throwable { + private fun helpCloseAndGetSendException(closed: Closed<*>): Throwable { // To ensure linearizablity we must ALWAYS help close the channel when we observe that it was closed // See https://github.com/Kotlin/kotlinx.coroutines/issues/1419 helpClose(closed) - // Element was not delivered -> cals onUndeliveredElement - onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let { - // If it crashes, add send exception as suppressed for better diagnostics - it.addSuppressed(closed.sendException) - throw it - } return closed.sendException } - private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont -> + private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutineReusable sc@ { cont -> loop@ while (true) { if (isFullImpl) { - val send = if (onUndeliveredElement == null) - SendElement(element, cont) else - SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement) + val send = SendElement(element, cont) val enqueueResult = enqueueSend(send) when { enqueueResult == null -> { // enqueued successfully @@ -188,7 +176,7 @@ internal abstract class AbstractSendChannel( return@sc } enqueueResult is Closed<*> -> { - cont.helpCloseAndResumeWithSendException(element, enqueueResult) + cont.helpCloseAndResumeWithSendException(enqueueResult) return@sc } enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead @@ -205,7 +193,7 @@ internal abstract class AbstractSendChannel( } offerResult === OFFER_FAILED -> continue@loop offerResult is Closed<*> -> { - cont.helpCloseAndResumeWithSendException(element, offerResult) + cont.helpCloseAndResumeWithSendException(offerResult) return@sc } else -> error("offerInternal returned $offerResult") @@ -213,15 +201,9 @@ internal abstract class AbstractSendChannel( } } - private fun Continuation<*>.helpCloseAndResumeWithSendException(element: E, closed: Closed<*>) { + private fun Continuation<*>.helpCloseAndResumeWithSendException(closed: Closed<*>) { helpClose(closed) - val sendException = closed.sendException - onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let { - it.addSuppressed(sendException) - resumeWithException(it) - return - } - resumeWithException(sendException) + resumeWithException(closed.sendException) } /** @@ -393,7 +375,7 @@ internal abstract class AbstractSendChannel( select.disposeOnSelect(node) return } - enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, enqueueResult)) + enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(enqueueResult)) enqueueResult === ENQUEUE_FAILED -> {} // try to offer enqueueResult is Receive<*> -> {} // try to offer else -> error("enqueueSend returned $enqueueResult ") @@ -409,7 +391,7 @@ internal abstract class AbstractSendChannel( block.startCoroutineUnintercepted(receiver = this, completion = select.completion) return } - offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, offerResult)) + offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(offerResult)) else -> error("offerSelectInternal returned $offerResult") } } @@ -449,7 +431,7 @@ internal abstract class AbstractSendChannel( // ------ private ------ private class SendSelect( - override val pollResult: E, // E | Closed - the result pollInternal returns when it rendezvous with this node + override val pollResult: Any?, @JvmField val channel: AbstractSendChannel, @JvmField val select: SelectInstance, @JvmField val block: suspend (SendChannel) -> R @@ -458,13 +440,11 @@ internal abstract class AbstractSendChannel( select.trySelectOther(otherOp) as Symbol? // must return symbol override fun completeResumeSend() { - block.startCoroutineCancellable(receiver = channel, completion = select.completion) + block.startCoroutine(receiver = channel, completion = select.completion) } override fun dispose() { // invoked on select completion - if (!remove()) return - // if the node was successfully removed (meaning it was added but was not received) then element not delivered - undeliveredElement() + remove() } override fun resumeSendClosed(closed: Closed<*>) { @@ -472,10 +452,6 @@ internal abstract class AbstractSendChannel( select.resumeSelectWithException(closed.sendException) } - override fun undeliveredElement() { - channel.onUndeliveredElement?.callUndeliveredElement(pollResult, select.completion.context) - } - override fun toString(): String = "SendSelect@$hexAddress($pollResult)[$channel, $select]" } @@ -493,9 +469,7 @@ internal abstract class AbstractSendChannel( /** * Abstract send/receive channel. It is a base class for all channel implementations. */ -internal abstract class AbstractChannel( - onUndeliveredElement: OnUndeliveredElement? -) : AbstractSendChannel(onUndeliveredElement), Channel { +internal abstract class AbstractChannel : AbstractSendChannel(), Channel { // ------ extension points for buffered channels ------ /** @@ -527,8 +501,6 @@ internal abstract class AbstractChannel( send.completeResumeSend() return send.pollResult } - // too late, already cancelled, but we removed it from the queue and need to notify on undelivered element - send.undeliveredElement() } } @@ -575,10 +547,8 @@ internal abstract class AbstractChannel( } @Suppress("UNCHECKED_CAST") - private suspend fun receiveSuspend(receiveMode: Int): R = suspendCancellableCoroutineReusable sc@ { cont -> - val receive = if (onUndeliveredElement == null) - ReceiveElement(cont as CancellableContinuation, receiveMode) else - ReceiveElementWithUndeliveredHandler(cont as CancellableContinuation, receiveMode, onUndeliveredElement) + private suspend fun receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + val receive = ReceiveElement(cont as CancellableContinuation, receiveMode) while (true) { if (enqueueReceive(receive)) { removeReceiveOnCancel(cont, receive) @@ -591,7 +561,7 @@ internal abstract class AbstractChannel( return@sc } if (result !== POLL_FAILED) { - cont.resume(receive.resumeValue(result as E), receive.resumeOnCancellationFun(result as E)) + cont.resume(receive.resumeValue(result as E)) return@sc } } @@ -706,11 +676,6 @@ internal abstract class AbstractChannel( assert { token === RESUME_TOKEN } return null } - - override fun onRemoved(affected: LockFreeLinkedListNode) { - // Called when we removed it from the queue but were too late to resume, so we have undelivered element - (affected as Send).undeliveredElement() - } } final override val onReceive: SelectClause1 @@ -820,7 +785,7 @@ internal abstract class AbstractChannel( private fun removeReceiveOnCancel(cont: CancellableContinuation<*>, receive: Receive<*>) = cont.invokeOnCancellation(handler = RemoveReceiveOnCancel(receive).asHandler) - private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : BeforeResumeCancelHandler() { + private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : CancelHandler() { override fun invoke(cause: Throwable?) { if (receive.remove()) onReceiveDequeued() @@ -828,7 +793,7 @@ internal abstract class AbstractChannel( override fun toString(): String = "RemoveReceiveOnCancel[$receive]" } - private class Itr(@JvmField val channel: AbstractChannel) : ChannelIterator { + private class Itr(val channel: AbstractChannel) : ChannelIterator { var result: Any? = POLL_FAILED // E | POLL_FAILED | Closed override suspend fun hasNext(): Boolean { @@ -849,7 +814,7 @@ internal abstract class AbstractChannel( return true } - private suspend fun hasNextSuspend(): Boolean = suspendCancellableCoroutineReusable sc@ { cont -> + private suspend fun hasNextSuspend(): Boolean = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val receive = ReceiveHasNext(this, cont) while (true) { if (channel.enqueueReceive(receive)) { @@ -867,8 +832,7 @@ internal abstract class AbstractChannel( return@sc } if (result !== POLL_FAILED) { - @Suppress("UNCHECKED_CAST") - cont.resume(true, channel.onUndeliveredElement?.bindCancellationFun(result as E, cont.context)) + cont.resume(true) return@sc } } @@ -887,7 +851,7 @@ internal abstract class AbstractChannel( } } - private open class ReceiveElement( + private class ReceiveElement( @JvmField val cont: CancellableContinuation, @JvmField val receiveMode: Int ) : Receive() { @@ -896,8 +860,9 @@ internal abstract class AbstractChannel( else -> value } + @Suppress("IMPLICIT_CAST_TO_ANY") override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(resumeValue(value), otherOp?.desc, resumeOnCancellationFun(value)) ?: return null + val token = cont.tryResume(resumeValue(value), otherOp?.desc) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -916,22 +881,12 @@ internal abstract class AbstractChannel( override fun toString(): String = "ReceiveElement@$hexAddress[receiveMode=$receiveMode]" } - private class ReceiveElementWithUndeliveredHandler( - cont: CancellableContinuation, - receiveMode: Int, - @JvmField val onUndeliveredElement: OnUndeliveredElement - ) : ReceiveElement(cont, receiveMode) { - override fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = - onUndeliveredElement.bindCancellationFun(value, cont.context) - } - - private open class ReceiveHasNext( + private class ReceiveHasNext( @JvmField val iterator: Itr, @JvmField val cont: CancellableContinuation ) : Receive() { override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(true, otherOp?.desc, resumeOnCancellationFun(value)) - ?: return null + val token = cont.tryResume(true, otherOp?.desc) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -951,17 +906,13 @@ internal abstract class AbstractChannel( val token = if (closed.closeCause == null) { cont.tryResume(false) } else { - cont.tryResumeWithException(closed.receiveException) + cont.tryResumeWithException(recoverStackTrace(closed.receiveException, cont)) } if (token != null) { iterator.result = closed cont.completeResume(token) } } - - override fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = - iterator.channel.onUndeliveredElement?.bindCancellationFun(value, cont.context) - override fun toString(): String = "ReceiveHasNext@$hexAddress" } @@ -976,20 +927,16 @@ internal abstract class AbstractChannel( @Suppress("UNCHECKED_CAST") override fun completeResumeReceive(value: E) { - block.startCoroutineCancellable( - if (receiveMode == RECEIVE_RESULT) ValueOrClosed.value(value) else value, - select.completion, - resumeOnCancellationFun(value) - ) + block.startCoroutine(if (receiveMode == RECEIVE_RESULT) ValueOrClosed.value(value) else value, select.completion) } override fun resumeReceiveClosed(closed: Closed<*>) { if (!select.trySelect()) return when (receiveMode) { RECEIVE_THROWS_ON_CLOSE -> select.resumeSelectWithException(closed.receiveException) - RECEIVE_RESULT -> block.startCoroutineCancellable(ValueOrClosed.closed(closed.closeCause), select.completion) + RECEIVE_RESULT -> block.startCoroutine(ValueOrClosed.closed(closed.closeCause), select.completion) RECEIVE_NULL_ON_CLOSE -> if (closed.closeCause == null) { - block.startCoroutineCancellable(null, select.completion) + block.startCoroutine(null, select.completion) } else { select.resumeSelectWithException(closed.receiveException) } @@ -1001,9 +948,6 @@ internal abstract class AbstractChannel( channel.onReceiveDequeued() // notify cancellation of receive } - override fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = - channel.onUndeliveredElement?.bindCancellationFun(value, select.completion.context) - override fun toString(): String = "ReceiveSelect@$hexAddress[$select,receiveMode=$receiveMode]" } } @@ -1015,27 +959,23 @@ internal const val RECEIVE_RESULT = 2 @JvmField @SharedImmutable -internal val EMPTY = Symbol("EMPTY") // marker for Conflated & Buffered channels - -@JvmField -@SharedImmutable -internal val OFFER_SUCCESS = Symbol("OFFER_SUCCESS") +internal val OFFER_SUCCESS: Any = Symbol("OFFER_SUCCESS") @JvmField @SharedImmutable -internal val OFFER_FAILED = Symbol("OFFER_FAILED") +internal val OFFER_FAILED: Any = Symbol("OFFER_FAILED") @JvmField @SharedImmutable -internal val POLL_FAILED = Symbol("POLL_FAILED") +internal val POLL_FAILED: Any = Symbol("POLL_FAILED") @JvmField @SharedImmutable -internal val ENQUEUE_FAILED = Symbol("ENQUEUE_FAILED") +internal val ENQUEUE_FAILED: Any = Symbol("ENQUEUE_FAILED") @JvmField @SharedImmutable -internal val HANDLER_INVOKED = Symbol("ON_CLOSE_HANDLER_INVOKED") +internal val HANDLER_INVOKED: Any = Symbol("ON_CLOSE_HANDLER_INVOKED") internal typealias Handler = (Throwable?) -> Unit @@ -1043,7 +983,7 @@ internal typealias Handler = (Throwable?) -> Unit * Represents sending waiter in the queue. */ internal abstract class Send : LockFreeLinkedListNode() { - abstract val pollResult: Any? // E | Closed - the result pollInternal returns when it rendezvous with this node + abstract val pollResult: Any? // E | Closed // Returns: null - failure, // RETRY_ATOMIC for retry (only when otherOp != null), // RESUME_TOKEN on success (call completeResumeSend) @@ -1051,7 +991,6 @@ internal abstract class Send : LockFreeLinkedListNode() { abstract fun tryResumeSend(otherOp: PrepareOp?): Symbol? abstract fun completeResumeSend() abstract fun resumeSendClosed(closed: Closed<*>) - open fun undeliveredElement() {} } /** @@ -1070,8 +1009,9 @@ internal interface ReceiveOrClosed { /** * Represents sender for a specific element. */ -internal open class SendElement( - override val pollResult: E, +@Suppress("UNCHECKED_CAST") +internal class SendElement( + override val pollResult: Any?, @JvmField val cont: CancellableContinuation ) : Send() { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { @@ -1081,27 +1021,9 @@ internal open class SendElement( otherOp?.finishPrepare() // finish preparations return RESUME_TOKEN } - override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) - override fun toString(): String = "$classSimpleName@$hexAddress($pollResult)" -} - -internal class SendElementWithUndeliveredHandler( - pollResult: E, - cont: CancellableContinuation, - @JvmField val onUndeliveredElement: OnUndeliveredElement -) : SendElement(pollResult, cont) { - override fun remove(): Boolean { - if (!super.remove()) return false - // if the node was successfully removed (meaning it was added but was not received) then we have undelivered element - undeliveredElement() - return true - } - - override fun undeliveredElement() { - onUndeliveredElement.callUndeliveredElement(pollResult, cont.context) - } + override fun toString(): String = "SendElement@$hexAddress($pollResult)" } /** @@ -1126,7 +1048,6 @@ internal class Closed( internal abstract class Receive : LockFreeLinkedListNode(), ReceiveOrClosed { override val offerResult get() = OFFER_SUCCESS abstract fun resumeReceiveClosed(closed: Closed<*>) - open fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = null } @Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index 91b5473c41..155652fd6f 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -28,7 +28,7 @@ internal class ArrayBroadcastChannel( * Buffer capacity. */ val capacity: Int -) : AbstractSendChannel(null), BroadcastChannel { +) : AbstractSendChannel(), BroadcastChannel { init { require(capacity >= 1) { "ArrayBroadcastChannel capacity must be at least 1, but $capacity was specified" } } @@ -180,8 +180,6 @@ internal class ArrayBroadcastChannel( this.tail = tail + 1 return@withLock // go out of lock to wakeup this sender } - // Too late, already cancelled, but we removed it from the queue and need to release resources. - // However, ArrayBroadcastChannel does not support onUndeliveredElement, so nothing to do } } } @@ -207,7 +205,7 @@ internal class ArrayBroadcastChannel( private class Subscriber( private val broadcastChannel: ArrayBroadcastChannel - ) : AbstractChannel(null), ReceiveChannel { + ) : AbstractChannel(), ReceiveChannel { private val subLock = ReentrantLock() private val _subHead = atomic(0L) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index 80cb8aa011..e26579eff7 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -23,31 +23,25 @@ internal open class ArrayChannel( /** * Buffer capacity. */ - private val capacity: Int, - private val onBufferOverflow: BufferOverflow, - onUndeliveredElement: OnUndeliveredElement? -) : AbstractChannel(onUndeliveredElement) { + val capacity: Int +) : AbstractChannel() { init { - // This check is actually used by the Channel(...) constructor function which checks only for known - // capacities and calls ArrayChannel constructor for everything else. require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } } private val lock = ReentrantLock() - /* * Guarded by lock. * Allocate minimum of capacity and 16 to avoid excess memory pressure for large channels when it's not necessary. */ - private var buffer: Array = arrayOfNulls(min(capacity, 8)).apply { fill(EMPTY) } - + private var buffer: Array = arrayOfNulls(min(capacity, 8)) private var head: Int = 0 private val size = atomic(0) // Invariant: size <= capacity protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = size.value == 0 protected final override val isBufferAlwaysFull: Boolean get() = false - protected final override val isBufferFull: Boolean get() = size.value == capacity && onBufferOverflow == BufferOverflow.SUSPEND + protected final override val isBufferFull: Boolean get() = size.value == capacity override val isFull: Boolean get() = lock.withLock { isFullImpl } override val isEmpty: Boolean get() = lock.withLock { isEmptyImpl } @@ -59,26 +53,31 @@ internal open class ArrayChannel( lock.withLock { val size = this.size.value closedForSend?.let { return it } - // update size before checking queue (!!!) - updateBufferSize(size)?.let { return it } - // check for receivers that were waiting on empty queue - if (size == 0) { - loop@ while (true) { - receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued - if (receive is Closed) { - this.size.value = size // restore size - return receive!! - } - val token = receive!!.tryResumeReceive(element, null) - if (token != null) { - assert { token === RESUME_TOKEN } - this.size.value = size // restore size - return@withLock + if (size < capacity) { + // tentatively put element to buffer + this.size.value = size + 1 // update size before checking queue (!!!) + // check for receivers that were waiting on empty queue + if (size == 0) { + loop@ while (true) { + receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued + if (receive is Closed) { + this.size.value = size // restore size + return receive!! + } + val token = receive!!.tryResumeReceive(element, null) + if (token != null) { + assert { token === RESUME_TOKEN } + this.size.value = size // restore size + return@withLock + } } } + ensureCapacity(size) + buffer[(head + size) % buffer.size] = element // actually queue element + return OFFER_SUCCESS } - enqueueElement(size, element) - return OFFER_SUCCESS + // size == capacity: full + return OFFER_FAILED } // breaks here if offer meets receiver receive!!.completeResumeReceive(element) @@ -91,36 +90,41 @@ internal open class ArrayChannel( lock.withLock { val size = this.size.value closedForSend?.let { return it } - // update size before checking queue (!!!) - updateBufferSize(size)?.let { return it } - // check for receivers that were waiting on empty queue - if (size == 0) { - loop@ while (true) { - val offerOp = describeTryOffer(element) - val failure = select.performAtomicTrySelect(offerOp) - when { - failure == null -> { // offered successfully - this.size.value = size // restore size - receive = offerOp.result - return@withLock - } - failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer - failure === RETRY_ATOMIC -> {} // retry - failure === ALREADY_SELECTED || failure is Closed<*> -> { - this.size.value = size // restore size - return failure + if (size < capacity) { + // tentatively put element to buffer + this.size.value = size + 1 // update size before checking queue (!!!) + // check for receivers that were waiting on empty queue + if (size == 0) { + loop@ while (true) { + val offerOp = describeTryOffer(element) + val failure = select.performAtomicTrySelect(offerOp) + when { + failure == null -> { // offered successfully + this.size.value = size // restore size + receive = offerOp.result + return@withLock + } + failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer + failure === RETRY_ATOMIC -> {} // retry + failure === ALREADY_SELECTED || failure is Closed<*> -> { + this.size.value = size // restore size + return failure + } + else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") } - else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") } } + // let's try to select sending this element to buffer + if (!select.trySelect()) { // :todo: move trySelect completion outside of lock + this.size.value = size // restore size + return ALREADY_SELECTED + } + ensureCapacity(size) + buffer[(head + size) % buffer.size] = element // actually queue element + return OFFER_SUCCESS } - // let's try to select sending this element to buffer - if (!select.trySelect()) { // :todo: move trySelect completion outside of lock - this.size.value = size // restore size - return ALREADY_SELECTED - } - enqueueElement(size, element) - return OFFER_SUCCESS + // size == capacity: full + return OFFER_FAILED } // breaks here if offer meets receiver receive!!.completeResumeReceive(element) @@ -131,35 +135,6 @@ internal open class ArrayChannel( super.enqueueSend(send) } - // Guarded by lock - // Result is `OFFER_SUCCESS | OFFER_FAILED | null` - private fun updateBufferSize(currentSize: Int): Symbol? { - if (currentSize < capacity) { - size.value = currentSize + 1 // tentatively put it into the buffer - return null // proceed - } - // buffer is full - return when (onBufferOverflow) { - BufferOverflow.SUSPEND -> OFFER_FAILED - BufferOverflow.DROP_LATEST -> OFFER_SUCCESS - BufferOverflow.DROP_OLDEST -> null // proceed, will drop oldest in enqueueElement - } - } - - // Guarded by lock - private fun enqueueElement(currentSize: Int, element: E) { - if (currentSize < capacity) { - ensureCapacity(currentSize) - buffer[(head + currentSize) % buffer.size] = element // actually queue element - } else { - // buffer is full - assert { onBufferOverflow == BufferOverflow.DROP_OLDEST } // the only way we can get here - buffer[head % buffer.size] = null // drop oldest element - buffer[(head + currentSize) % buffer.size] = element // actually queue element - head = (head + 1) % buffer.size - } - } - // Guarded by lock private fun ensureCapacity(currentSize: Int) { if (currentSize >= buffer.size) { @@ -168,7 +143,6 @@ internal open class ArrayChannel( for (i in 0 until currentSize) { newBuffer[i] = buffer[(head + i) % buffer.size] } - newBuffer.fill(EMPTY, currentSize, newSize) buffer = newBuffer head = 0 } @@ -198,8 +172,6 @@ internal open class ArrayChannel( replacement = send!!.pollResult break@loop } - // too late, already cancelled, but we removed it from the queue and need to notify on undelivered element - send!!.undeliveredElement() } } if (replacement !== POLL_FAILED && replacement !is Closed<*>) { @@ -282,23 +254,17 @@ internal open class ArrayChannel( // Note: this function is invoked when channel is already closed override fun onCancelIdempotent(wasClosed: Boolean) { // clear buffer first, but do not wait for it in helpers - val onUndeliveredElement = onUndeliveredElement - var undeliveredElementException: UndeliveredElementException? = null // first cancel exception, others suppressed - lock.withLock { - repeat(size.value) { - val value = buffer[head] - if (onUndeliveredElement != null && value !== EMPTY) { - @Suppress("UNCHECKED_CAST") - undeliveredElementException = onUndeliveredElement.callUndeliveredElementCatchingException(value as E, undeliveredElementException) + if (wasClosed) { + lock.withLock { + repeat(size.value) { + buffer[head] = 0 + head = (head + 1) % buffer.size } - buffer[head] = EMPTY - head = (head + 1) % buffer.size + size.value = 0 } - size.value = 0 } // then clean all queued senders super.onCancelIdempotent(wasClosed) - undeliveredElementException?.let { throw it } // throw cancel exception at the end if there was one } // ------ debug ------ diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index 790580e0a3..863d1387fc 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* +import kotlin.native.concurrent.* /** * Broadcasts all elements of the channel. @@ -33,10 +34,8 @@ import kotlin.coroutines.intrinsics.* * * This function has an inappropriate result type of [BroadcastChannel] which provides * [send][BroadcastChannel.send] and [close][BroadcastChannel.close] operations that interfere with - * the broadcasting coroutine in hard-to-specify ways. - * - * **Note: This API is obsolete.** It will be deprecated and replaced with the - * [Flow.shareIn][kotlinx.coroutines.flow.shareIn] operator when it becomes stable. + * the broadcasting coroutine in hard-to-specify ways. It will be replaced with + * sharing operators on [Flow][kotlinx.coroutines.flow.Flow] in the future. * * @param start coroutine start option. The default value is [CoroutineStart.LAZY]. */ diff --git a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt index d356566f17..312480f943 100644 --- a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt @@ -7,9 +7,9 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.channels.Channel.Factory.CHANNEL_DEFAULT_CAPACITY -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED /** @@ -20,10 +20,9 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED * See `BroadcastChannel()` factory function for the description of available * broadcast channel implementations. * - * **Note: This API is obsolete.** It will be deprecated and replaced by [SharedFlow][kotlinx.coroutines.flow.SharedFlow] - * when it becomes stable. + * **Note: This is an experimental api.** It may be changed in the future updates. */ -@ExperimentalCoroutinesApi // not @ObsoleteCoroutinesApi to reduce burden for people who are still using it +@ExperimentalCoroutinesApi public interface BroadcastChannel : SendChannel { /** * Subscribes to this [BroadcastChannel] and returns a channel to receive elements from it. diff --git a/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt deleted file mode 100644 index 99994ea81b..0000000000 --- a/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.coroutines.* - -/** - * A strategy for buffer overflow handling in [channels][Channel] and [flows][kotlinx.coroutines.flow.Flow] that - * controls what is going to be sacrificed on buffer overflow: - * - * * [SUSPEND] — the upstream that is [sending][SendChannel.send] or - * is [emitting][kotlinx.coroutines.flow.FlowCollector.emit] a value is **suspended** while the buffer is full. - * * [DROP_OLDEST] — drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. - * * [DROP_LATEST] — drop **the latest** value that is being added to the buffer right now on buffer overflow - * (so that buffer contents stay the same), do not suspend. - */ -@ExperimentalCoroutinesApi -public enum class BufferOverflow { - /** - * Suspend on buffer overflow. - */ - SUSPEND, - - /** - * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. - */ - DROP_OLDEST, - - /** - * Drop **the latest** value that is being added to the buffer right now on buffer overflow - * (so that buffer contents stay the same), do not suspend. - */ - DROP_LATEST -} diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index 72c08e1acd..c4b4a9b25e 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -44,17 +44,19 @@ public interface SendChannel { * Sends the specified [element] to this channel, suspending the caller while the buffer of this channel is full * or if it does not exist, or throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). * - * [Closing][close] a channel _after_ this function has suspended does not cause this suspended [send] invocation + * Note that closing a channel _after_ this function has suspended does not cause this suspended [send] invocation * to abort, because closing a channel is conceptually like sending a special "close token" over this channel. * All elements sent over the channel are delivered in first-in first-out order. The sent element * will be delivered to receivers before the close token. * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. The `send` call can send the element to the channel, - * but then throw [CancellationException], thus an exception should not be treated as a failure to deliver the element. - * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. + * + * *Cancellation of suspended `send` is atomic*: when this function + * throws a [CancellationException], it means that the [element] was not sent to this channel. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this `send` operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -79,11 +81,6 @@ public interface SendChannel { * in situations when `send` suspends. * * Throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). - * - * When `offer` call returns `false` it guarantees that the element was not delivered to the consumer and it - * it does not call `onUndeliveredElement` that was installed for this channel. If the channel was closed, - * then it calls `onUndeliveredElement` before throwing an exception. - * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. */ public fun offer(element: E): Boolean @@ -173,10 +170,12 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. The `receive` call can retrieve the element from the channel, - * but then throw [CancellationException], thus failing to deliver the element. - * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. + * + * *Cancellation of suspended `receive` is atomic*: when this function + * throws a [CancellationException], it means that the element was not retrieved from this channel. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this `receive` operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -201,10 +200,12 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. The `receiveOrNull` call can retrieve the element from the channel, - * but then throw [CancellationException], thus failing to deliver the element. - * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. + * + * *Cancellation of suspended `receive` is atomic*: when this function + * throws a [CancellationException], it means that the element was not retrieved from this channel. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this `receive` operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -249,10 +250,12 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. The `receiveOrClosed` call can retrieve the element from the channel, - * but then throw [CancellationException], thus failing to deliver the element. - * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. + * + * *Cancellation of suspended `receive` is atomic*: when this function + * throws a [CancellationException], it means that the element was not retrieved from this channel. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this receive operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -329,7 +332,7 @@ public interface ReceiveChannel { * @suppress *This is an internal API, do not use*: Inline classes ABI is not stable yet and * [KT-27524](https://youtrack.jetbrains.com/issue/KT-27524) needs to be fixed. */ -@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS", "EXPERIMENTAL_FEATURE_WARNING") +@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") @InternalCoroutinesApi // until https://youtrack.jetbrains.com/issue/KT-27524 is fixed public inline class ValueOrClosed internal constructor(private val holder: Any?) { @@ -436,10 +439,12 @@ public interface ChannelIterator { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. The `hasNext` call can retrieve the element from the channel, - * but then throw [CancellationException], thus failing to deliver the element. - * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. + * + * *Cancellation of suspended `receive` is atomic*: when this function + * throws a [CancellationException], it means that the element was not retrieved from this channel. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this receive operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -481,108 +486,28 @@ public interface ChannelIterator { * Conceptually, a channel is similar to Java's [BlockingQueue][java.util.concurrent.BlockingQueue], * but it has suspending operations instead of blocking ones and can be [closed][SendChannel.close]. * - * ### Creating channels - * * The `Channel(capacity)` factory function is used to create channels of different kinds depending on * the value of the `capacity` integer: * - * * When `capacity` is 0 — it creates a _rendezvous_ channel. + * * When `capacity` is 0 — it creates a `RendezvousChannel`. * This channel does not have any buffer at all. An element is transferred from the sender * to the receiver only when [send] and [receive] invocations meet in time (rendezvous), so [send] suspends * until another coroutine invokes [receive], and [receive] suspends until another coroutine invokes [send]. * - * * When `capacity` is [Channel.UNLIMITED] — it creates a channel with effectively unlimited buffer. + * * When `capacity` is [Channel.UNLIMITED] — it creates a `LinkedListChannel`. * This channel has a linked-list buffer of unlimited capacity (limited only by available memory). * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * - * * When `capacity` is [Channel.CONFLATED] — it creates a _conflated_ channel + * * When `capacity` is [Channel.CONFLATED] — it creates a `ConflatedChannel`. * This channel buffers at most one element and conflates all subsequent `send` and `offer` invocations, * so that the receiver always gets the last element sent. - * Back-to-send sent elements are conflated — only the last sent element is received, + * Back-to-send sent elements are _conflated_ — only the last sent element is received, * while previously sent elements **are lost**. * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * * * When `capacity` is positive but less than [UNLIMITED] — it creates an array-based channel with the specified capacity. * This channel has an array buffer of a fixed `capacity`. * [Sending][send] suspends only when the buffer is full, and [receiving][receive] suspends only when the buffer is empty. - * - * Buffered channels can be configured with an additional [`onBufferOverflow`][BufferOverflow] parameter. It controls the behaviour - * of the channel's [send][Channel.send] function on buffer overflow: - * - * * [SUSPEND][BufferOverflow.SUSPEND] — the default, suspend `send` on buffer overflow until there is - * free space in the buffer. - * * [DROP_OLDEST][BufferOverflow.DROP_OLDEST] — do not suspend the `send`, add the latest value to the buffer, - * drop the oldest one from the buffer. - * A channel with `capacity = 1` and `onBufferOverflow = DROP_OLDEST` is a _conflated_ channel. - * * [DROP_LATEST][BufferOverflow.DROP_LATEST] — do not suspend the `send`, drop the value that is being sent, - * keep the buffer contents intact. - * - * A non-default `onBufferOverflow` implicitly creates a channel with at least one buffered element and - * is ignored for a channel with unlimited buffer. It cannot be specified for `capacity = CONFLATED`, which - * is a shortcut by itself. - * - * ### Prompt cancellation guarantee - * - * All suspending functions with channels provide **prompt cancellation guarantee**. - * If the job was cancelled while send or receive function was suspended, it will not resume successfully, - * but throws a [CancellationException]. - * With a single-threaded [dispatcher][CoroutineDispatcher] like [Dispatchers.Main] this gives a - * guarantee that if a piece code running in this thread cancels a [Job], then a coroutine running this job cannot - * resume successfully and continue to run, ensuring a prompt response to its cancellation. - * - * > **Prompt cancellation guarantee** for channel operations was added since `kotlinx.coroutines` version `1.4.0` - * > and had replaced a channel-specific atomic-cancellation that was not consistent with other suspending functions. - * > The low-level mechanics of prompt cancellation are explained in [suspendCancellableCoroutine] function. - * - * ### Undelivered elements - * - * As a result of a prompt cancellation guarantee, when a closeable resource - * (like open file or a handle to another native resource) is transferred via channel from one coroutine to another - * it can fail to be delivered and will be lost if either send or receive operations are cancelled in transit. - * - * A `Channel()` constructor function has an `onUndeliveredElement` optional parameter. - * When `onUndeliveredElement` parameter is set, the corresponding function is called once for each element - * that was sent to the channel with the call to the [send][SendChannel.send] function but failed to be delivered, - * which can happen in the following cases: - * - * * When [send][SendChannel.send] operation throws an exception because it was cancelled before it had a chance to actually - * send the element or because the channel was [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel]. - * * When [offer][SendChannel.offer] operation throws an exception when - * the channel was [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel]. - * * When [receive][ReceiveChannel.receive], [receiveOrNull][ReceiveChannel.receiveOrNull], or [hasNext][ChannelIterator.hasNext] - * operation throws an exception when it had retrieved the element from the - * channel but was cancelled before the code following the receive call resumed. - * * The channel was [cancelled][ReceiveChannel.cancel], in which case `onUndeliveredElement` is called on every - * remaining element in the channel's buffer. - * - * Note, that `onUndeliveredElement` function is called synchronously in an arbitrary context. It should be fast, non-blocking, - * and should not throw exceptions. Any exception thrown by `onUndeliveredElement` is wrapped into an internal runtime - * exception which is either rethrown from the caller method or handed off to the exception handler in the current context - * (see [CoroutineExceptionHandler]) when one is available. - * - * A typical usage for `onDeliveredElement` is to close a resource that is being transferred via the channel. The - * following code pattern guarantees that opened resources are closed even if producer, consumer, and/or channel - * are cancelled. Resources are never lost. - * - * ``` - * // Create the channel with onUndeliveredElement block that closes a resource - * val channel = Channel(capacity) { resource -> resource.close() } - * - * // Producer code - * val resourceToSend = openResource() - * channel.send(resourceToSend) - * - * // Consumer code - * val resourceReceived = channel.receive() - * try { - * // work with received resource - * } finally { - * resourceReceived.close() - * } - * ``` - * - * > Note, that if you do any kind of work in between `openResource()` and `channel.send(...)`, then you should - * > ensure that resource gets closed in case this additional code fails. */ public interface Channel : SendChannel, ReceiveChannel { /** @@ -590,26 +515,25 @@ public interface Channel : SendChannel, ReceiveChannel { */ public companion object Factory { /** - * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function. + * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function */ public const val UNLIMITED: Int = Int.MAX_VALUE /** - * Requests a rendezvous channel in the `Channel(...)` factory function — a channel that does not have a buffer. + * Requests a rendezvous channel in the `Channel(...)` factory function — a `RendezvousChannel` gets created. */ public const val RENDEZVOUS: Int = 0 /** - * Requests a conflated channel in the `Channel(...)` factory function. This is a shortcut to creating - * a channel with [`onBufferOverflow = DROP_OLDEST`][BufferOverflow.DROP_OLDEST]. + * Requests a conflated channel in the `Channel(...)` factory function — a `ConflatedChannel` gets created. */ public const val CONFLATED: Int = -1 /** - * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function. - * The default capacity for a channel that [suspends][BufferOverflow.SUSPEND] on overflow - * is 64 and can be overridden by setting [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. - * For non-suspending channels, a buffer of capacity 1 is used. + * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function — + * an `ArrayChannel` gets created with the default capacity. + * The default capacity is 64 and can be overridden by setting + * [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. */ public const val BUFFERED: Int = -2 @@ -633,48 +557,17 @@ public interface Channel : SendChannel, ReceiveChannel { * See [Channel] interface documentation for details. * * @param capacity either a positive channel capacity or one of the constants defined in [Channel.Factory]. - * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to - * a [suspending][BufferOverflow.SUSPEND] attempt to [send][Channel.send] a value, - * supported only when `capacity >= 0` or `capacity == Channel.BUFFERED`, - * implicitly creates a channel with at least one buffered element). - * @param onUndeliveredElement an optional function that is called when element was sent but was not delivered to the consumer. - * See "Undelivered elements" section in [Channel] documentation. * @throws IllegalArgumentException when [capacity] < -2 */ -public fun Channel( - capacity: Int = RENDEZVOUS, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, - onUndeliveredElement: ((E) -> Unit)? = null -): Channel = +public fun Channel(capacity: Int = RENDEZVOUS): Channel = when (capacity) { - RENDEZVOUS -> { - if (onBufferOverflow == BufferOverflow.SUSPEND) - RendezvousChannel(onUndeliveredElement) // an efficient implementation of rendezvous channel - else - ArrayChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel - } - CONFLATED -> { - require(onBufferOverflow == BufferOverflow.SUSPEND) { - "CONFLATED capacity cannot be used with non-default onBufferOverflow" - } - ConflatedChannel(onUndeliveredElement) - } - UNLIMITED -> LinkedListChannel(onUndeliveredElement) // ignores onBufferOverflow: it has buffer, but it never overflows - BUFFERED -> ArrayChannel( // uses default capacity with SUSPEND - if (onBufferOverflow == BufferOverflow.SUSPEND) CHANNEL_DEFAULT_CAPACITY else 1, - onBufferOverflow, onUndeliveredElement - ) - else -> { - if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST) - ConflatedChannel(onUndeliveredElement) // conflated implementation is more efficient but appears to work in the same way - else - ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement) - } + RENDEZVOUS -> RendezvousChannel() + UNLIMITED -> LinkedListChannel() + CONFLATED -> ConflatedChannel() + BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) + else -> ArrayChannel(capacity) } -@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") -public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity) - /** * Indicates an attempt to [send][SendChannel.send] to a [isClosedForSend][SendChannel.isClosedForSend] channel * that was closed without a cause. A _failed_ channel rethrows the original [close][SendChannel.close] cause diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index d19028bf63..8c61928aa4 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -40,9 +40,12 @@ public inline fun BroadcastChannel.consume(block: ReceiveChannel.() * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. If the `receiveOrNull` call threw [CancellationException] there is no way - * to tell if some element was already received from the channel or not. See [Channel] documentation for details. + * + * *Cancellation of suspended receive is atomic* -- when this function + * throws [CancellationException] it means that the element was not retrieved from this channel. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this receive operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note, that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 5986dae3d4..2b9375ddec 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlin.jvm.* +import kotlin.native.concurrent.* /** * Broadcasts the most recently sent element (aka [value]) to all [openSubscription] subscribers. @@ -26,10 +27,9 @@ import kotlin.jvm.* * [opening][openSubscription] and [closing][ReceiveChannel.cancel] subscription takes O(N) time, where N is the * number of subscribers. * - * **Note: This API is obsolete.** It will be deprecated and replaced by [StateFlow][kotlinx.coroutines.flow.StateFlow] - * when it becomes stable. + * **Note: This API is experimental.** It may be changed in the future updates. */ -@ExperimentalCoroutinesApi // not @ObsoleteCoroutinesApi to reduce burden for people who are still using it +@ExperimentalCoroutinesApi public class ConflatedBroadcastChannel() : BroadcastChannel { /** * Creates an instance of this class that already holds a value. @@ -282,7 +282,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { private class Subscriber( private val broadcastChannel: ConflatedBroadcastChannel - ) : ConflatedChannel(null), ReceiveChannel { + ) : ConflatedChannel(), ReceiveChannel { override fun onCancelIdempotent(wasClosed: Boolean) { if (wasClosed) { diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 75e421c6e7..4734766914 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* +import kotlin.native.concurrent.* /** * Channel that buffers at most one element and conflates all subsequent `send` and `offer` invocations, @@ -17,7 +18,7 @@ import kotlinx.coroutines.selects.* * * This channel is created by `Channel(Channel.CONFLATED)` factory function invocation. */ -internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { +internal open class ConflatedChannel : AbstractChannel() { protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = value === EMPTY protected final override val isBufferAlwaysFull: Boolean get() = false @@ -29,6 +30,10 @@ internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredEleme private var value: Any? = EMPTY + private companion object { + private val EMPTY = Symbol("EMPTY") + } + // result is `OFFER_SUCCESS | Closed` protected override fun offerInternal(element: E): Any { var receive: ReceiveOrClosed? = null @@ -49,7 +54,7 @@ internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredEleme } } } - updateValueLocked(element)?.let { throw it } + value = element return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -82,7 +87,7 @@ internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredEleme if (!select.trySelect()) { return ALREADY_SELECTED } - updateValueLocked(element)?.let { throw it } + value = element return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -115,20 +120,12 @@ internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredEleme } protected override fun onCancelIdempotent(wasClosed: Boolean) { - var undeliveredElementException: UndeliveredElementException? = null // resource cancel exception - lock.withLock { - undeliveredElementException = updateValueLocked(EMPTY) + if (wasClosed) { + lock.withLock { + value = EMPTY + } } super.onCancelIdempotent(wasClosed) - undeliveredElementException?.let { throw it } // throw exception at the end if there was one - } - - private fun updateValueLocked(element: Any?): UndeliveredElementException? { - val old = value - val undeliveredElementException = if (old === EMPTY) null else - onUndeliveredElement?.callUndeliveredElementCatchingException(old as E) - value = element - return undeliveredElementException } override fun enqueueReceiveInternal(receive: Receive): Boolean = lock.withLock { diff --git a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt index 2f46421344..e66bbb2279 100644 --- a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.selects.* * * @suppress **This an internal API and should not be used from general code.** */ -internal open class LinkedListChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { +internal open class LinkedListChannel : AbstractChannel() { protected final override val isBufferAlwaysEmpty: Boolean get() = true protected final override val isBufferEmpty: Boolean get() = true protected final override val isBufferAlwaysFull: Boolean get() = false diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 10a15e2a93..1b1581a99e 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -27,11 +27,7 @@ public interface ProducerScope : CoroutineScope, SendChannel { /** * Suspends the current coroutine until the channel is either [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel] - * and invokes the given [block] before resuming the coroutine. - * - * This suspending function is cancellable. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * and invokes the given [block] before resuming the coroutine. This suspending function is cancellable. * * Note that when the producer channel is cancelled, this function resumes with a cancellation exception. * Therefore, in case of cancellation, no code after the call to this function will be executed. @@ -95,8 +91,13 @@ public fun CoroutineScope.produce( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, @BuilderInference block: suspend ProducerScope.() -> Unit -): ReceiveChannel = - produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block) +): ReceiveChannel { + val channel = Channel(capacity) + val newContext = newCoroutineContext(context) + val coroutine = ProducerCoroutine(newContext, channel) + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) + return coroutine +} /** * **This is an internal API and should not be used from general code.** @@ -117,19 +118,8 @@ public fun CoroutineScope.produce( start: CoroutineStart = CoroutineStart.DEFAULT, onCompletion: CompletionHandler? = null, @BuilderInference block: suspend ProducerScope.() -> Unit -): ReceiveChannel = - produce(context, capacity, BufferOverflow.SUSPEND, start, onCompletion, block) - -// Internal version of produce that is maximally flexible, but is not exposed through public API (too many params) -internal fun CoroutineScope.produce( - context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = 0, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, - start: CoroutineStart = CoroutineStart.DEFAULT, - onCompletion: CompletionHandler? = null, - @BuilderInference block: suspend ProducerScope.() -> Unit ): ReceiveChannel { - val channel = Channel(capacity, onBufferOverflow) + val channel = Channel(capacity) val newContext = newCoroutineContext(context) val coroutine = ProducerCoroutine(newContext, channel) if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion) diff --git a/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt b/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt index 857a97938f..700f50908c 100644 --- a/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt @@ -4,8 +4,6 @@ package kotlinx.coroutines.channels -import kotlinx.coroutines.internal.* - /** * Rendezvous channel. This channel does not have any buffer at all. An element is transferred from sender * to receiver only when [send] and [receive] invocations meet in time (rendezvous), so [send] suspends @@ -15,7 +13,7 @@ import kotlinx.coroutines.internal.* * * This implementation is fully lock-free. **/ -internal open class RendezvousChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { +internal open class RendezvousChannel : AbstractChannel() { protected final override val isBufferAlwaysEmpty: Boolean get() = true protected final override val isBufferEmpty: Boolean get() = true protected final override val isBufferAlwaysFull: Boolean get() = true diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 7e47e6947a..8fd9ae76a4 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -16,8 +16,7 @@ import kotlin.jvm.* import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** - * Creates a _cold_ flow from the given suspendable [block]. - * The flow being _cold_ means that the [block] is called every time a terminal operator is applied to the resulting flow. + * Creates a flow from the given suspendable [block]. * * Example of usage: * @@ -63,7 +62,7 @@ private class SafeFlow(private val block: suspend FlowCollector.() -> Unit } /** - * Creates a _cold_ flow that produces a single value from the given functional type. + * Creates a flow that produces a single value from the given functional type. */ @FlowPreview public fun (() -> T).asFlow(): Flow = flow { @@ -71,10 +70,8 @@ public fun (() -> T).asFlow(): Flow = flow { } /** - * Creates a _cold_ flow that produces a single value from the given functional type. - * + * Creates a flow that produces a single value from the given functional type. * Example of usage: - * * ``` * suspend fun remoteCall(): R = ... * fun remoteCallFlow(): Flow = ::remoteCall.asFlow() @@ -86,7 +83,7 @@ public fun (suspend () -> T).asFlow(): Flow = flow { } /** - * Creates a _cold_ flow that produces values from the given iterable. + * Creates a flow that produces values from the given iterable. */ public fun Iterable.asFlow(): Flow = flow { forEach { value -> @@ -95,7 +92,7 @@ public fun Iterable.asFlow(): Flow = flow { } /** - * Creates a _cold_ flow that produces values from the given iterator. + * Creates a flow that produces values from the given iterator. */ public fun Iterator.asFlow(): Flow = flow { forEach { value -> @@ -104,7 +101,7 @@ public fun Iterator.asFlow(): Flow = flow { } /** - * Creates a _cold_ flow that produces values from the given sequence. + * Creates a flow that produces values from the given sequence. */ public fun Sequence.asFlow(): Flow = flow { forEach { value -> @@ -116,7 +113,6 @@ public fun Sequence.asFlow(): Flow = flow { * Creates a flow that produces values from the specified `vararg`-arguments. * * Example of usage: - * * ``` * flowOf(1, 2, 3) * ``` @@ -128,7 +124,7 @@ public fun flowOf(vararg elements: T): Flow = flow { } /** - * Creates a flow that produces the given [value]. + * Creates flow that produces the given [value]. */ public fun flowOf(value: T): Flow = flow { /* @@ -148,9 +144,7 @@ private object EmptyFlow : Flow { } /** - * Creates a _cold_ flow that produces values from the given array. - * The flow being _cold_ means that the array components are read every time a terminal operator is applied - * to the resulting flow. + * Creates a flow that produces values from the given array. */ public fun Array.asFlow(): Flow = flow { forEach { value -> @@ -159,9 +153,7 @@ public fun Array.asFlow(): Flow = flow { } /** - * Creates a _cold_ flow that produces values from the array. - * The flow being _cold_ means that the array components are read every time a terminal operator is applied - * to the resulting flow. + * Creates a flow that produces values from the array. */ public fun IntArray.asFlow(): Flow = flow { forEach { value -> @@ -170,9 +162,7 @@ public fun IntArray.asFlow(): Flow = flow { } /** - * Creates a _cold_ flow that produces values from the given array. - * The flow being _cold_ means that the array components are read every time a terminal operator is applied - * to the resulting flow. + * Creates a flow that produces values from the array. */ public fun LongArray.asFlow(): Flow = flow { forEach { value -> @@ -218,7 +208,7 @@ public fun flowViaChannel( } /** - * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] + * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be * produced by code that is running in a different context or concurrently. * The resulting flow is _cold_, which means that [block] is called every time a terminal operator @@ -266,7 +256,7 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() ChannelFlowBuilder(block) /** - * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] + * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be * produced by code that is running in a different context or concurrently. * @@ -293,12 +283,11 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * Adjacent applications of [callbackFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are * always fused so that only one properly configured channel is used for execution. * - * Example of usage that converts a multi-short callback API to a flow. - * For single-shot callbacks use [suspendCancellableCoroutine]. + * Example of usage: * * ``` * fun flowFrom(api: CallbackBasedApi): Flow = callbackFlow { - * val callback = object : Callback { // Implementation of some callback interface + * val callback = object : Callback { // implementation of some callback interface * override fun onNextValue(value: T) { * // To avoid blocking you can configure channel capacity using * // either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill @@ -322,10 +311,6 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * awaitClose { api.unregister(callback) } * } * ``` - * - * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because - * > `awaitClose` block can be called at any time due to asynchronous nature of cancellation, even - * > concurrently with the call of the callback. */ @ExperimentalCoroutinesApi public fun callbackFlow(@BuilderInference block: suspend ProducerScope.() -> Unit): Flow = CallbackFlowBuilder(block) @@ -334,11 +319,10 @@ public fun callbackFlow(@BuilderInference block: suspend ProducerScope.() private open class ChannelFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = BUFFERED, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlow(context, capacity, onBufferOverflow) { - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - ChannelFlowBuilder(block, context, capacity, onBufferOverflow) + capacity: Int = BUFFERED +) : ChannelFlow(context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowBuilder(block, context, capacity) override suspend fun collectTo(scope: ProducerScope) = block(scope) @@ -350,9 +334,8 @@ private open class ChannelFlowBuilder( private class CallbackFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = BUFFERED, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlowBuilder(block, context, capacity, onBufferOverflow) { + capacity: Int = BUFFERED +) : ChannelFlowBuilder(block, context, capacity) { override suspend fun collectTo(scope: ProducerScope) { super.collectTo(scope) @@ -372,6 +355,6 @@ private class CallbackFlowBuilder( } } - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - CallbackFlowBuilder(block, context, capacity, onBufferOverflow) + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + CallbackFlowBuilder(block, context, capacity) } diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 762cdcad1b..2d3ef95aa1 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -20,9 +20,6 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * the channel afterwards. If you need to iterate over the channel without consuming it, * a regular `for` loop should be used instead. * - * Note, that emitting values from a channel into a flow is not atomic. A value that was received from the - * channel many not reach the flow collector if it was cancelled and will be lost. - * * This function provides a more efficient shorthand for `channel.consumeEach { value -> emit(value) }`. * See [consumeEach][ReceiveChannel.consumeEach]. */ @@ -119,9 +116,8 @@ private class ChannelAsFlow( private val channel: ReceiveChannel, private val consume: Boolean, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlow(context, capacity, onBufferOverflow) { + capacity: Int = Channel.OPTIONAL_CHANNEL +) : ChannelFlow(context, capacity) { private val consumed = atomic(false) private fun markConsumed() { @@ -130,11 +126,8 @@ private class ChannelAsFlow( } } - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - ChannelAsFlow(channel, consume, context, capacity, onBufferOverflow) - - override fun dropChannelOperators(): Flow? = - ChannelAsFlow(channel, consume) + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelAsFlow(channel, consume, context, capacity) override suspend fun collectTo(scope: ProducerScope) = SendingCollector(scope).emitAllImpl(channel, consume) // use efficient channel receiving code from emitAll @@ -161,7 +154,7 @@ private class ChannelAsFlow( } } - override fun additionalToStringProps(): String = "channel=$channel" + override fun additionalToStringProps(): String = "channel=$channel, " } /** @@ -188,22 +181,8 @@ public fun BroadcastChannel.asFlow(): Flow = flow { * Use [buffer] operator on the flow before calling `broadcastIn` to specify a value other than * default and to control what happens when data is produced faster than it is consumed, * that is to control backpressure behavior. - * - * ### Deprecated - * - * **This API is deprecated.** The [BroadcastChannel] provides a complex channel-like API for hot flows. - * [SharedFlow] is a easier-to-use and more flow-centric API for the same purposes, so using - * [shareIn] operator is preferred. It is not a direct replacement, so please - * study [shareIn] documentation to see what kind of shared flow fits your use-case. As a rule of thumb: - * - * * Replace `broadcastIn(scope)` and `broadcastIn(scope, CoroutineStart.LAZY)` with `shareIn(scope, 0, SharingStarted.Lazily)`. - * * Replace `broadcastIn(scope, CoroutineStart.DEFAULT)` with `shareIn(scope, 0, SharingStarted.Eagerly)`. */ -@Deprecated( - message = "Use shareIn operator and the resulting SharedFlow as a replacement for BroadcastChannel", - replaceWith = ReplaceWith("shareIn(scope, 0, SharingStarted.Lazily)"), - level = DeprecationLevel.WARNING -) +@FlowPreview public fun Flow.broadcastIn( scope: CoroutineScope, start: CoroutineStart = CoroutineStart.LAZY diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 19a5b43f31..b7e2518694 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -9,7 +9,8 @@ import kotlinx.coroutines.flow.internal.* import kotlin.coroutines.* /** - * An asynchronous data stream that sequentially emits values and completes normally or with an exception. + * A cold asynchronous data stream that sequentially emits values + * and completes normally or with an exception. * * _Intermediate operators_ on the flow such as [map], [filter], [take], [zip], etc are functions that are * applied to the _upstream_ flow or flows and return a _downstream_ flow where further operators can be applied to. @@ -38,12 +39,11 @@ import kotlin.coroutines.* * with an exception for a few operations specifically designed to introduce concurrency into flow * execution such as [buffer] and [flatMapMerge]. See their documentation for details. * - * The `Flow` interface does not carry information whether a flow is a _cold_ stream that can be collected repeatedly and - * triggers execution of the same code every time it is collected, or if it is a _hot_ stream that emits different - * values from the same running source on each collection. Usually flows represent _cold_ streams, but - * there is a [SharedFlow] subtype that represents _hot_ streams. In addition to that, any flow can be turned - * into a _hot_ one by the [stateIn] and [shareIn] operators, or by converting the flow into a hot channel - * via the [produceIn] operator. + * The `Flow` interface does not carry information whether a flow truly is a cold stream that can be collected repeatedly and + * triggers execution of the same code every time it is collected, or if it is a hot stream that emits different + * values from the same running source on each collection. However, conventionally flows represent cold streams. + * Transitions between hot and cold streams are supported via channels and the corresponding API: + * [channelFlow], [produceIn], [broadcastIn]. * * ### Flow builders * @@ -55,8 +55,6 @@ import kotlin.coroutines.* * sequential calls to [emit][FlowCollector.emit] function. * * [channelFlow { ... }][channelFlow] builder function to construct arbitrary flows from * potentially concurrent calls to the [send][kotlinx.coroutines.channels.SendChannel.send] function. - * * [MutableStateFlow] and [MutableSharedFlow] define the corresponding constructor functions to create - * a _hot_ flow that can be directly updated. * * ### Flow constraints * @@ -161,9 +159,9 @@ import kotlin.coroutines.* * * ### Not stable for inheritance * - * **The `Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **`Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use the `flow { ... }` builder function to create an implementation. + * Use `flow { ... }` builder function to create an implementation. */ public interface Flow { /** @@ -203,7 +201,7 @@ public interface Flow { * ``` */ @FlowPreview -public abstract class AbstractFlow : Flow, CancellableFlow { +public abstract class AbstractFlow : Flow { @InternalCoroutinesApi public final override suspend fun collect(collector: FlowCollector) { diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 59873eba5f..bb2f584474 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -9,6 +9,8 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.flow.internal.unsafeFlow import kotlin.coroutines.* import kotlin.jvm.* @@ -97,7 +99,7 @@ public fun Flow.publishOn(context: CoroutineContext): Flow = noImpl() * Opposed to subscribeOn, it it **possible** to use multiple `flowOn` operators in the one flow * @suppress */ -@Deprecated(message = "Use 'flowOn' instead", level = DeprecationLevel.ERROR) +@Deprecated(message = "Use flowOn instead", level = DeprecationLevel.ERROR) public fun Flow.subscribeOn(context: CoroutineContext): Flow = noImpl() /** @@ -149,7 +151,7 @@ public fun Flow.onErrorResumeNext(fallback: Flow): Flow = noImpl() * @suppress */ @Deprecated( - message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", + message = "Use launchIn with onEach, onCompletion and catch operators instead", level = DeprecationLevel.ERROR ) public fun Flow.subscribe(): Unit = noImpl() @@ -159,7 +161,7 @@ public fun Flow.subscribe(): Unit = noImpl() * @suppress */ @Deprecated( - message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", + message = "Use launchIn with onEach, onCompletion and catch operators instead", level = DeprecationLevel.ERROR )public fun Flow.subscribe(onEach: suspend (T) -> Unit): Unit = noImpl() @@ -168,7 +170,7 @@ public fun Flow.subscribe(): Unit = noImpl() * @suppress */ @Deprecated( - message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", + message = "Use launchIn with onEach, onCompletion and catch operators instead", level = DeprecationLevel.ERROR )public fun Flow.subscribe(onEach: suspend (T) -> Unit, onError: suspend (Throwable) -> Unit): Unit = noImpl() @@ -179,7 +181,7 @@ public fun Flow.subscribe(): Unit = noImpl() */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Flow analogue is 'flatMapConcat'", + message = "Flow analogue is named flatMapConcat", replaceWith = ReplaceWith("flatMapConcat(mapper)") ) public fun Flow.flatMap(mapper: suspend (T) -> Flow): Flow = noImpl() @@ -436,50 +438,3 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F ) @ExperimentalCoroutinesApi public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = runningReduce(operation) - -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Flow analogue of 'publish()' is 'shareIn'. \n" + - "publish().connect() is the default strategy (no extra call is needed), \n" + - "publish().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + - "publish().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", - replaceWith = ReplaceWith("this.shareIn(scope, 0)") -) -public fun Flow.publish(): Flow = noImpl() - -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Flow analogue of 'publish(bufferSize)' is 'buffer' followed by 'shareIn'. \n" + - "publish().connect() is the default strategy (no extra call is needed), \n" + - "publish().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + - "publish().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", - replaceWith = ReplaceWith("this.buffer(bufferSize).shareIn(scope, 0)") -) -public fun Flow.publish(bufferSize: Int): Flow = noImpl() - -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Flow analogue of 'replay()' is 'shareIn' with unlimited replay. \n" + - "replay().connect() is the default strategy (no extra call is needed), \n" + - "replay().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + - "replay().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", - replaceWith = ReplaceWith("this.shareIn(scope, Int.MAX_VALUE)") -) -public fun Flow.replay(): Flow = noImpl() - -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Flow analogue of 'replay(bufferSize)' is 'shareIn' with the specified replay parameter. \n" + - "replay().connect() is the default strategy (no extra call is needed), \n" + - "replay().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + - "replay().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", - replaceWith = ReplaceWith("this.shareIn(scope, bufferSize)") -) -public fun Flow.replay(bufferSize: Int): Flow = noImpl() - -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Flow analogue of 'cache()' is 'shareIn' with unlimited replay and 'started = SharingStared.Lazily' argument'", - replaceWith = ReplaceWith("this.shareIn(scope, Int.MAX_VALUE, started = SharingStared.Lazily)") -) -public fun Flow.cache(): Flow = noImpl() \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt deleted file mode 100644 index 88dc775842..0000000000 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ /dev/null @@ -1,659 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.internal.* -import kotlinx.coroutines.internal.* -import kotlin.coroutines.* -import kotlin.jvm.* -import kotlin.native.concurrent.* - -/** - * A _hot_ [Flow] that shares emitted values among all its collectors in a broadcast fashion, so that all collectors - * get all emitted values. A shared flow is called _hot_ because its active instance exists independently of the - * presence of collectors. This is opposed to a regular [Flow], such as defined by the [`flow { ... }`][flow] function, - * which is _cold_ and is started separately for each collector. - * - * **Shared flow never completes**. A call to [Flow.collect] on a shared flow never completes normally, and - * neither does a coroutine started by the [Flow.launchIn] function. An active collector of a shared flow is called a _subscriber_. - * - * A subscriber of a shared flow can be cancelled. This usually happens when the scope in which the coroutine is running - * is cancelled. A subscriber to a shared flow is always [cancellable][Flow.cancellable], and checks for - * cancellation before each emission. Note that most terminal operators like [Flow.toList] would also not complete, - * when applied to a shared flow, but flow-truncating operators like [Flow.take] and [Flow.takeWhile] can be used on a - * shared flow to turn it into a completing one. - * - * A [mutable shared flow][MutableSharedFlow] is created using the [MutableSharedFlow(...)] constructor function. - * Its state can be updated by [emitting][MutableSharedFlow.emit] values to it and performing other operations. - * See the [MutableSharedFlow] documentation for details. - * - * [SharedFlow] is useful for broadcasting events that happen inside an application to subscribers that can come and go. - * For example, the following class encapsulates an event bus that distributes events to all subscribers - * in a _rendezvous_ manner, suspending until all subscribers process each event: - * - * ``` - * class EventBus { - * private val _events = MutableSharedFlow() // private mutable shared flow - * val events = _events.asSharedFlow() // publicly exposed as read-only shared flow - * - * suspend fun produceEvent(event: Event) { - * _events.emit(event) // suspends until all subscribers receive it - * } - * } - * ``` - * - * As an alternative to the above usage with the `MutableSharedFlow(...)` constructor function, - * any _cold_ [Flow] can be converted to a shared flow using the [shareIn] operator. - * - * There is a specialized implementation of shared flow for the case where the most recent state value needs - * to be shared. See [StateFlow] for details. - * - * ### Replay cache and buffer - * - * A shared flow keeps a specific number of the most recent values in its _replay cache_. Every new subscriber first - * gets the values from the replay cache and then gets new emitted values. The maximum size of the replay cache is - * specified when the shared flow is created by the `replay` parameter. A snapshot of the current replay cache - * is available via the [replayCache] property and it can be reset with the [MutableSharedFlow.resetReplayCache] function. - * - * A replay cache also provides buffer for emissions to the shared flow, allowing slow subscribers to - * get values from the buffer without suspending emitters. The buffer space determines how much slow subscribers - * can lag from the fast ones. When creating a shared flow, additional buffer capacity beyond replay can be reserved - * using the `extraBufferCapacity` parameter. - * - * A shared flow with a buffer can be configured to avoid suspension of emitters on buffer overflow using - * the `onBufferOverflow` parameter, which is equal to one of the entries of the [BufferOverflow] enum. When a strategy other - * than [SUSPENDED][BufferOverflow.SUSPEND] is configured, emissions to the shared flow never suspend. - * - * ### SharedFlow vs BroadcastChannel - * - * Conceptually shared flow is similar to [BroadcastChannel][BroadcastChannel] - * and is designed to completely replace `BroadcastChannel` in the future. - * It has the following important differences: - * - * * `SharedFlow` is simpler, because it does not have to implement all the [Channel] APIs, which allows - * for faster and simpler implementation. - * * `SharedFlow` supports configurable replay and buffer overflow strategy. - * * `SharedFlow` has a clear separation into a read-only `SharedFlow` interface and a [MutableSharedFlow]. - * * `SharedFlow` cannot be closed like `BroadcastChannel` and can never represent a failure. - * All errors and completion signals should be explicitly _materialized_ if needed. - * - * To migrate [BroadcastChannel] usage to [SharedFlow], start by replacing usages of the `BroadcastChannel(capacity)` - * constructor with `MutableSharedFlow(0, extraBufferCapacity=capacity)` (broadcast channel does not replay - * values to new subscribers). Replace [send][BroadcastChannel.send] and [offer][BroadcastChannel.offer] calls - * with [emit][MutableStateFlow.emit] and [tryEmit][MutableStateFlow.tryEmit], and convert subscribers' code to flow operators. - * - * ### Concurrency - * - * All methods of shared flow are **thread-safe** and can be safely invoked from concurrent coroutines without - * external synchronization. - * - * ### Operator fusion - * - * Application of [flowOn][Flow.flowOn], [buffer] with [RENDEZVOUS][Channel.RENDEZVOUS] capacity, - * or [cancellable] operators to a shared flow has no effect. - * - * ### Implementation notes - * - * Shared flow implementation uses a lock to ensure thread-safety, but suspending collector and emitter coroutines are - * resumed outside of this lock to avoid dead-locks when using unconfined coroutines. Adding new subscribers - * has `O(1)` amortized cost, but emitting has `O(N)` cost, where `N` is the number of subscribers. - * - * ### Not stable for inheritance - * - * **The `SharedFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods - * might be added to this interface in the future, but is stable for use. - * Use the `MutableSharedFlow(replay, ...)` constructor function to create an implementation. - */ -@ExperimentalCoroutinesApi -public interface SharedFlow : Flow { - /** - * A snapshot of the replay cache. - */ - public val replayCache: List -} - -/** - * A mutable [SharedFlow] that provides functions to [emit] values to the flow. - * An instance of `MutableSharedFlow` with the given configuration parameters can be created using `MutableSharedFlow(...)` - * constructor function. - * - * See the [SharedFlow] documentation for details on shared flows. - * - * `MutableSharedFlow` is a [SharedFlow] that also provides the abilities to [emit] a value, - * to [tryEmit] without suspension if possible, to track the [subscriptionCount], - * and to [resetReplayCache]. - * - * ### Concurrency - * - * All methods of shared flow are **thread-safe** and can be safely invoked from concurrent coroutines without - * external synchronization. - * - * ### Not stable for inheritance - * - * **The `MutableSharedFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods - * might be added to this interface in the future, but is stable for use. - * Use the `MutableSharedFlow(...)` constructor function to create an implementation. - */ -@ExperimentalCoroutinesApi -public interface MutableSharedFlow : SharedFlow, FlowCollector { - /** - * Tries to emit a [value] to this shared flow without suspending. It returns `true` if the value was - * emitted successfully. When this function returns `false`, it means that the call to a plain [emit] - * function will suspend until there is a buffer space available. - * - * A shared flow configured with a [BufferOverflow] strategy other than [SUSPEND][BufferOverflow.SUSPEND] - * (either [DROP_OLDEST][BufferOverflow.DROP_OLDEST] or [DROP_LATEST][BufferOverflow.DROP_LATEST]) never - * suspends on [emit], and thus `tryEmit` to such a shared flow always returns `true`. - */ - public fun tryEmit(value: T): Boolean - - /** - * The number of subscribers (active collectors) to this shared flow. - * - * The integer in the resulting [StateFlow] is not negative and starts with zero for a freshly created - * shared flow. - * - * This state can be used to react to changes in the number of subscriptions to this shared flow. - * For example, if you need to call `onActive` when the first subscriber appears and `onInactive` - * when the last one disappears, you can set it up like this: - * - * ``` - * sharedFlow.subscriptionCount - * .map { count -> count > 0 } // map count into active/inactive flag - * .distinctUntilChanged() // only react to true<->false changes - * .onEach { isActive -> // configure an action - * if (isActive) onActive() else onInactive() - * } - * .launchIn(scope) // launch it - * ``` - */ - public val subscriptionCount: StateFlow - - /** - * Resets the [replayCache] of this shared flow to an empty state. - * New subscribers will be receiving only the values that were emitted after this call, - * while old subscribers will still be receiving previously buffered values. - * To reset a shared flow to an initial value, emit the value after this call. - * - * On a [MutableStateFlow], which always contains a single value, this function is not - * supported, and throws an [UnsupportedOperationException]. To reset a [MutableStateFlow] - * to an initial value, just update its [value][MutableStateFlow.value]. - * - * **Note: This is an experimental api.** This function may be removed or renamed in the future. - */ - @ExperimentalCoroutinesApi - public fun resetReplayCache() -} - -/** - * Creates a [MutableSharedFlow] with the given configuration parameters. - * - * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. - * - * @param replay the number of values replayed to new subscribers (cannot be negative, defaults to zero). - * @param extraBufferCapacity the number of values buffered in addition to `replay`. - * [emit][MutableSharedFlow.emit] does not suspend while there is a buffer space remaining (optional, cannot be negative, defaults to zero). - * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to - * [suspending][BufferOverflow.SUSPEND] attempts to [emit][MutableSharedFlow.emit] a value, - * supported only when `replay > 0` or `extraBufferCapacity > 0`). - */ -@Suppress("FunctionName", "UNCHECKED_CAST") -@ExperimentalCoroutinesApi -public fun MutableSharedFlow( - replay: Int = 0, - extraBufferCapacity: Int = 0, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -): MutableSharedFlow { - require(replay >= 0) { "replay cannot be negative, but was $replay" } - require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" } - require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) { - "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow" - } - val bufferCapacity0 = replay + extraBufferCapacity - val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow - return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow) -} - -// ------------------------------------ Implementation ------------------------------------ - -private class SharedFlowSlot : AbstractSharedFlowSlot>() { - @JvmField - var index = -1L // current "to-be-emitted" index, -1 means the slot is free now - - @JvmField - var cont: Continuation? = null // collector waiting for new value - - override fun allocateLocked(flow: SharedFlowImpl<*>): Boolean { - if (index >= 0) return false // not free - index = flow.updateNewCollectorIndexLocked() - return true - } - - override fun freeLocked(flow: SharedFlowImpl<*>): Array?> { - assert { index >= 0 } - val oldIndex = index - index = -1L - cont = null // cleanup continuation reference - return flow.updateCollectorIndexLocked(oldIndex) - } -} - -private class SharedFlowImpl( - private val replay: Int, - private val bufferCapacity: Int, - private val onBufferOverflow: BufferOverflow -) : AbstractSharedFlow(), MutableSharedFlow, CancellableFlow, FusibleFlow { - /* - Logical structure of the buffer - - buffered values - /-----------------------\ - replayCache queued emitters - /----------\/----------------------\ - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - | | 1 | 2 | 3 | 4 | 5 | 6 | E | E | E | E | E | E | | | | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - ^ ^ ^ ^ - | | | | - head | head + bufferSize head + totalSize - | | | - index of the slowest | index of the fastest - possible collector | possible collector - | | - | replayIndex == new collector's index - \---------------------- / - range of possible minCollectorIndex - - head == minOf(minCollectorIndex, replayIndex) // by definition - totalSize == bufferSize + queueSize // by definition - - INVARIANTS: - minCollectorIndex = activeSlots.minOf { it.index } ?: (head + bufferSize) - replayIndex <= head + bufferSize - */ - - // Stored state - private var buffer: Array? = null // allocated when needed, allocated size always power of two - private var replayIndex = 0L // minimal index from which new collector gets values - private var minCollectorIndex = 0L // minimal index of active collectors, equal to replayIndex if there are none - private var bufferSize = 0 // number of buffered values - private var queueSize = 0 // number of queued emitters - - // Computed state - private val head: Long get() = minOf(minCollectorIndex, replayIndex) - private val replaySize: Int get() = (head + bufferSize - replayIndex).toInt() - private val totalSize: Int get() = bufferSize + queueSize - private val bufferEndIndex: Long get() = head + bufferSize - private val queueEndIndex: Long get() = head + bufferSize + queueSize - - override val replayCache: List - get() = synchronized(this) { - val replaySize = this.replaySize - if (replaySize == 0) return emptyList() - val result = ArrayList(replaySize) - val buffer = buffer!! // must be allocated, because replaySize > 0 - @Suppress("UNCHECKED_CAST") - for (i in 0 until replaySize) result += buffer.getBufferAt(replayIndex + i) as T - result - } - - @Suppress("UNCHECKED_CAST") - override suspend fun collect(collector: FlowCollector) { - val slot = allocateSlot() - try { - if (collector is SubscribedFlowCollector) collector.onSubscription() - val collectorJob = currentCoroutineContext()[Job] - while (true) { - var newValue: Any? - while (true) { - newValue = tryTakeValue(slot) // attempt no-suspend fast path first - if (newValue !== NO_VALUE) break - awaitValue(slot) // await signal that the new value is available - } - collectorJob?.ensureActive() - collector.emit(newValue as T) - } - } finally { - freeSlot(slot) - } - } - - override fun tryEmit(value: T): Boolean { - var resumes: Array?> = EMPTY_RESUMES - val emitted = synchronized(this) { - if (tryEmitLocked(value)) { - resumes = findSlotsToResumeLocked() - true - } else { - false - } - } - for (cont in resumes) cont?.resume(Unit) - return emitted - } - - override suspend fun emit(value: T) { - if (tryEmit(value)) return // fast-path - emitSuspend(value) - } - - @Suppress("UNCHECKED_CAST") - private fun tryEmitLocked(value: T): Boolean { - // Fast path without collectors -> no buffering - if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true - // With collectors we'll have to buffer - // cannot emit now if buffer is full & blocked by slow collectors - if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) { - when (onBufferOverflow) { - BufferOverflow.SUSPEND -> return false // will suspend - BufferOverflow.DROP_LATEST -> return true // just drop incoming - BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead - } - } - enqueueLocked(value) - bufferSize++ // value was added to buffer - // drop oldest from the buffer if it became more than bufferCapacity - if (bufferSize > bufferCapacity) dropOldestLocked() - // keep replaySize not larger that needed - if (replaySize > replay) { // increment replayIndex by one - updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex) - } - return true - } - - private fun tryEmitNoCollectorsLocked(value: T): Boolean { - assert { nCollectors == 0 } - if (replay == 0) return true // no need to replay, just forget it now - enqueueLocked(value) // enqueue to replayCache - bufferSize++ // value was added to buffer - // drop oldest from the buffer if it became more than replay - if (bufferSize > replay) dropOldestLocked() - minCollectorIndex = head + bufferSize // a default value (max allowed) - return true - } - - private fun dropOldestLocked() { - buffer!!.setBufferAt(head, null) - bufferSize-- - val newHead = head + 1 - if (replayIndex < newHead) replayIndex = newHead - if (minCollectorIndex < newHead) correctCollectorIndexesOnDropOldest(newHead) - assert { head == newHead } // since head = minOf(minCollectorIndex, replayIndex) it should have updated - } - - private fun correctCollectorIndexesOnDropOldest(newHead: Long) { - forEachSlotLocked { slot -> - @Suppress("ConvertTwoComparisonsToRangeCheck") // Bug in JS backend - if (slot.index >= 0 && slot.index < newHead) { - slot.index = newHead // force move it up (this collector was too slow and missed the value at its index) - } - } - minCollectorIndex = newHead - } - - // enqueues item to buffer array, caller shall increment either bufferSize or queueSize - private fun enqueueLocked(item: Any?) { - val curSize = totalSize - val buffer = when (val curBuffer = buffer) { - null -> growBuffer(null, 0, 2) - else -> if (curSize >= curBuffer.size) growBuffer(curBuffer, curSize,curBuffer.size * 2) else curBuffer - } - buffer.setBufferAt(head + curSize, item) - } - - private fun growBuffer(curBuffer: Array?, curSize: Int, newSize: Int): Array { - check(newSize > 0) { "Buffer size overflow" } - val newBuffer = arrayOfNulls(newSize).also { buffer = it } - if (curBuffer == null) return newBuffer - val head = head - for (i in 0 until curSize) { - newBuffer.setBufferAt(head + i, curBuffer.getBufferAt(head + i)) - } - return newBuffer - } - - private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine sc@{ cont -> - var resumes: Array?> = EMPTY_RESUMES - val emitter = synchronized(this) lock@{ - // recheck buffer under lock again (make sure it is really full) - if (tryEmitLocked(value)) { - cont.resume(Unit) - resumes = findSlotsToResumeLocked() - return@lock null - } - // add suspended emitter to the buffer - Emitter(this, head + totalSize, value, cont).also { - enqueueLocked(it) - queueSize++ // added to queue of waiting emitters - // synchronous shared flow might rendezvous with waiting emitter - if (bufferCapacity == 0) resumes = findSlotsToResumeLocked() - } - } - // outside of the lock: register dispose on cancellation - emitter?.let { cont.disposeOnCancellation(it) } - // outside of the lock: resume slots if needed - for (cont in resumes) cont?.resume(Unit) - } - - private fun cancelEmitter(emitter: Emitter) = synchronized(this) { - if (emitter.index < head) return // already skipped past this index - val buffer = buffer!! - if (buffer.getBufferAt(emitter.index) !== emitter) return // already resumed - buffer.setBufferAt(emitter.index, NO_VALUE) - cleanupTailLocked() - } - - internal fun updateNewCollectorIndexLocked(): Long { - val index = replayIndex - if (index < minCollectorIndex) minCollectorIndex = index - return index - } - - // Is called when a collector disappears or changes index, returns a list of continuations to resume after lock - internal fun updateCollectorIndexLocked(oldIndex: Long): Array?> { - assert { oldIndex >= minCollectorIndex } - if (oldIndex > minCollectorIndex) return EMPTY_RESUMES // nothing changes, it was not min - // start computing new minimal index of active collectors - val head = head - var newMinCollectorIndex = head + bufferSize - // take into account a special case of sync shared flow that can go past 1st queued emitter - if (bufferCapacity == 0 && queueSize > 0) newMinCollectorIndex++ - forEachSlotLocked { slot -> - @Suppress("ConvertTwoComparisonsToRangeCheck") // Bug in JS backend - if (slot.index >= 0 && slot.index < newMinCollectorIndex) newMinCollectorIndex = slot.index - } - assert { newMinCollectorIndex >= minCollectorIndex } // can only grow - if (newMinCollectorIndex <= minCollectorIndex) return EMPTY_RESUMES // nothing changes - // Compute new buffer size if we drop items we no longer need and no emitter is resumed: - // We must keep all the items from newMinIndex to the end of buffer - var newBufferEndIndex = bufferEndIndex // var to grow when waiters are resumed - val maxResumeCount = if (nCollectors > 0) { - // If we have collectors we can resume up to maxResumeCount waiting emitters - // a) queueSize -> that's how many waiting emitters we have - // b) bufferCapacity - newBufferSize0 -> that's how many we can afford to resume to add w/o exceeding bufferCapacity - val newBufferSize0 = (newBufferEndIndex - newMinCollectorIndex).toInt() - minOf(queueSize, bufferCapacity - newBufferSize0) - } else { - // If we don't have collectors anymore we must resume all waiting emitters - queueSize // that's how many waiting emitters we have (at most) - } - var resumes: Array?> = EMPTY_RESUMES - val newQueueEndIndex = newBufferEndIndex + queueSize - if (maxResumeCount > 0) { // collect emitters to resume if we have them - resumes = arrayOfNulls(maxResumeCount) - var resumeCount = 0 - val buffer = buffer!! - for (curEmitterIndex in newBufferEndIndex until newQueueEndIndex) { - val emitter = buffer.getBufferAt(curEmitterIndex) - if (emitter !== NO_VALUE) { - emitter as Emitter // must have Emitter class - resumes[resumeCount++] = emitter.cont - buffer.setBufferAt(curEmitterIndex, NO_VALUE) // make as canceled if we moved ahead - buffer.setBufferAt(newBufferEndIndex, emitter.value) - newBufferEndIndex++ - if (resumeCount >= maxResumeCount) break // enough resumed, done - } - } - } - // Compute new buffer size -> how many values we now actually have after resume - val newBufferSize1 = (newBufferEndIndex - head).toInt() - // Compute new replay size -> limit to replay the number of items we need, take into account that it can only grow - var newReplayIndex = maxOf(replayIndex, newBufferEndIndex - minOf(replay, newBufferSize1)) - // adjustment for synchronous case with cancelled emitter (NO_VALUE) - if (bufferCapacity == 0 && newReplayIndex < newQueueEndIndex && buffer!!.getBufferAt(newReplayIndex) == NO_VALUE) { - newBufferEndIndex++ - newReplayIndex++ - } - // Update buffer state - updateBufferLocked(newReplayIndex, newMinCollectorIndex, newBufferEndIndex, newQueueEndIndex) - // just in case we've moved all buffered emitters and have NO_VALUE's at the tail now - cleanupTailLocked() - return resumes - } - - private fun updateBufferLocked( - newReplayIndex: Long, - newMinCollectorIndex: Long, - newBufferEndIndex: Long, - newQueueEndIndex: Long - ) { - // Compute new head value - val newHead = minOf(newMinCollectorIndex, newReplayIndex) - assert { newHead >= head } - // cleanup items we don't have to buffer anymore (because head is about to move) - for (index in head until newHead) buffer!!.setBufferAt(index, null) - // update all state variables to newly computed values - replayIndex = newReplayIndex - minCollectorIndex = newMinCollectorIndex - bufferSize = (newBufferEndIndex - newHead).toInt() - queueSize = (newQueueEndIndex - newBufferEndIndex).toInt() - // check our key invariants (just in case) - assert { bufferSize >= 0 } - assert { queueSize >= 0 } - assert { replayIndex <= this.head + bufferSize } - } - - // Removes all the NO_VALUE items from the end of the queue and reduces its size - private fun cleanupTailLocked() { - // If we have synchronous case, then keep one emitter queued - if (bufferCapacity == 0 && queueSize <= 1) return // return, don't clear it - val buffer = buffer!! - while (queueSize > 0 && buffer.getBufferAt(head + totalSize - 1) === NO_VALUE) { - queueSize-- - buffer.setBufferAt(head + totalSize, null) - } - } - - // returns NO_VALUE if cannot take value without suspension - private fun tryTakeValue(slot: SharedFlowSlot): Any? { - var resumes: Array?> = EMPTY_RESUMES - val value = synchronized(this) { - val index = tryPeekLocked(slot) - if (index < 0) { - NO_VALUE - } else { - val oldIndex = slot.index - val newValue = getPeekedValueLockedAt(index) - slot.index = index + 1 // points to the next index after peeked one - resumes = updateCollectorIndexLocked(oldIndex) - newValue - } - } - for (resume in resumes) resume?.resume(Unit) - return value - } - - // returns -1 if cannot peek value without suspension - private fun tryPeekLocked(slot: SharedFlowSlot): Long { - // return buffered value if possible - val index = slot.index - if (index < bufferEndIndex) return index - if (bufferCapacity > 0) return -1L // if there's a buffer, never try to rendezvous with emitters - // Synchronous shared flow (bufferCapacity == 0) tries to rendezvous - if (index > head) return -1L // ... but only with the first emitter (never look forward) - if (queueSize == 0) return -1L // nothing there to rendezvous with - return index // rendezvous with the first emitter - } - - private fun getPeekedValueLockedAt(index: Long): Any? = - when (val item = buffer!!.getBufferAt(index)) { - is Emitter -> item.value - else -> item - } - - private suspend fun awaitValue(slot: SharedFlowSlot): Unit = suspendCancellableCoroutine { cont -> - synchronized(this) lock@{ - val index = tryPeekLocked(slot) // recheck under this lock - if (index < 0) { - slot.cont = cont // Ok -- suspending - } else { - cont.resume(Unit) // has value, no need to suspend - return@lock - } - slot.cont = cont // suspend, waiting - } - } - - private fun findSlotsToResumeLocked(): Array?> { - var resumes: Array?> = EMPTY_RESUMES - var resumeCount = 0 - forEachSlotLocked loop@{ slot -> - val cont = slot.cont ?: return@loop // only waiting slots - if (tryPeekLocked(slot) < 0) return@loop // only slots that can peek a value - if (resumeCount >= resumes.size) resumes = resumes.copyOf(maxOf(2, 2 * resumes.size)) - resumes[resumeCount++] = cont - slot.cont = null // not waiting anymore - } - return resumes - } - - override fun createSlot() = SharedFlowSlot() - override fun createSlotArray(size: Int): Array = arrayOfNulls(size) - - override fun resetReplayCache() = synchronized(this) { - // Update buffer state - updateBufferLocked( - newReplayIndex = bufferEndIndex, - newMinCollectorIndex = minCollectorIndex, - newBufferEndIndex = bufferEndIndex, - newQueueEndIndex = queueEndIndex - ) - } - - override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = - fuseSharedFlow(context, capacity, onBufferOverflow) - - private class Emitter( - @JvmField val flow: SharedFlowImpl<*>, - @JvmField var index: Long, - @JvmField val value: Any?, - @JvmField val cont: Continuation - ) : DisposableHandle { - override fun dispose() = flow.cancelEmitter(this) - } -} - -@SharedImmutable -@JvmField -internal val NO_VALUE = Symbol("NO_VALUE") - -private fun Array.getBufferAt(index: Long) = get(index.toInt() and (size - 1)) -private fun Array.setBufferAt(index: Long, item: Any?) = set(index.toInt() and (size - 1), item) - -internal fun SharedFlow.fuseSharedFlow( - context: CoroutineContext, - capacity: Int, - onBufferOverflow: BufferOverflow -): Flow { - // context is irrelevant for shared flow and making additional rendezvous is meaningless - // however, additional non-trivial buffering after shared flow could make sense for very slow subscribers - if ((capacity == Channel.RENDEZVOUS || capacity == Channel.OPTIONAL_CHANNEL) && onBufferOverflow == BufferOverflow.SUSPEND) { - return this - } - // Apply channel flow operator as usual - return ChannelFlowOperatorImpl(this, context, capacity, onBufferOverflow) -} diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt deleted file mode 100644 index 935efdae2b..0000000000 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.internal.* -import kotlin.time.* - -/** - * A command emitted by [SharingStarted] implementations to control the sharing coroutine in - * the [shareIn] and [stateIn] operators. - */ -@ExperimentalCoroutinesApi -public enum class SharingCommand { - /** - * Starts sharing, launching collection of the upstream flow. - * - * Emitting this command again does not do anything. Emit [STOP] and then [START] to restart an - * upstream flow. - */ - START, - - /** - * Stops sharing, cancelling collection of the upstream flow. - */ - STOP, - - /** - * Stops sharing, cancelling collection of the upstream flow, and resets the [SharedFlow.replayCache] - * to its initial state. - * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; - * the [stateIn] operator resets the value to its original `initialValue`. - */ - STOP_AND_RESET_REPLAY_CACHE -} - -/** - * A strategy for starting and stopping the sharing coroutine in [shareIn] and [stateIn] operators. - * - * This interface provides a set of built-in strategies: [Eagerly], [Lazily], [WhileSubscribed], and - * supports custom strategies by implementing this interface's [command] function. - * - * For example, it is possible to define a custom strategy that starts the upstream only when the number - * of subscribers exceeds the given `threshold` and make it an extension on [SharingStarted.Companion] so - * that it looks like a built-in strategy on the use-site: - * - * ``` - * fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = - * object : SharingStarted { - * override fun command(subscriptionCount: StateFlow): Flow = - * subscriptionCount - * .map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } - * } - * ``` - * - * ### Commands - * - * The `SharingStarted` strategy works by emitting [commands][SharingCommand] that control upstream flow from its - * [`command`][command] flow implementation function. Back-to-back emissions of the same command have no effect. - * Only emission of a different command has effect: - * - * * [START][SharingCommand.START] — the upstream flow is stared. - * * [STOP][SharingCommand.STOP] — the upstream flow is stopped. - * * [STOP_AND_RESET_REPLAY_CACHE][SharingCommand.STOP_AND_RESET_REPLAY_CACHE] — - * the upstream flow is stopped and the [SharedFlow.replayCache] is reset to its initial state. - * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; - * the [stateIn] operator resets the value to its original `initialValue`. - * - * Initially, the upstream flow is stopped and is in the initial state, so the emission of additional - * [STOP][SharingCommand.STOP] and [STOP_AND_RESET_REPLAY_CACHE][SharingCommand.STOP_AND_RESET_REPLAY_CACHE] commands will - * have no effect. - * - * The completion of the `command` flow normally has no effect (the upstream flow keeps running if it was running). - * The failure of the `command` flow cancels the sharing coroutine and the upstream flow. - */ -@ExperimentalCoroutinesApi -public interface SharingStarted { - public companion object { - /** - * Sharing is started immediately and never stops. - */ - @ExperimentalCoroutinesApi - public val Eagerly: SharingStarted = StartedEagerly() - - /** - * Sharing is started when the first subscriber appears and never stops. - */ - @ExperimentalCoroutinesApi - public val Lazily: SharingStarted = StartedLazily() - - /** - * Sharing is started when the first subscriber appears, immediately stops when the last - * subscriber disappears (by default), keeping the replay cache forever (by default). - * - * It has the following optional parameters: - * - * * [stopTimeoutMillis] — configures a delay (in milliseconds) between the disappearance of the last - * subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately). - * * [replayExpirationMillis] — configures a delay (in milliseconds) between the stopping of - * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator - * and resets the cached value to the original `initialValue` for the [stateIn] operator). - * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer). - * Use zero value to expire the cache immediately. - * - * This function throws [IllegalArgumentException] when either [stopTimeoutMillis] or [replayExpirationMillis] - * are negative. - */ - @Suppress("FunctionName") - @ExperimentalCoroutinesApi - public fun WhileSubscribed( - stopTimeoutMillis: Long = 0, - replayExpirationMillis: Long = Long.MAX_VALUE - ): SharingStarted = - StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis) - } - - /** - * Transforms the [subscriptionCount][MutableSharedFlow.subscriptionCount] state of the shared flow into the - * flow of [commands][SharingCommand] that control the sharing coroutine. See the [SharingStarted] interface - * documentation for details. - */ - public fun command(subscriptionCount: StateFlow): Flow -} - -/** - * Sharing is started when the first subscriber appears, immediately stops when the last - * subscriber disappears (by default), keeping the replay cache forever (by default). - * - * It has the following optional parameters: - * - * * [stopTimeout] — configures a delay between the disappearance of the last - * subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately). - * * [replayExpiration] — configures a delay between the stopping of - * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator - * and resets the cached value to the original `initialValue` for the [stateIn] operator). - * It defaults to [Duration.INFINITE] (keep replay cache forever, never reset buffer). - * Use [Duration.ZERO] value to expire the cache immediately. - * - * This function throws [IllegalArgumentException] when either [stopTimeout] or [replayExpiration] - * are negative. - */ -@Suppress("FunctionName") -@ExperimentalTime -@ExperimentalCoroutinesApi -public fun SharingStarted.Companion.WhileSubscribed( - stopTimeout: Duration = Duration.ZERO, - replayExpiration: Duration = Duration.INFINITE -): SharingStarted = - StartedWhileSubscribed(stopTimeout.toLongMilliseconds(), replayExpiration.toLongMilliseconds()) - -// -------------------------------- implementation -------------------------------- - -private class StartedEagerly : SharingStarted { - override fun command(subscriptionCount: StateFlow): Flow = - flowOf(SharingCommand.START) - override fun toString(): String = "SharingStarted.Eagerly" -} - -private class StartedLazily : SharingStarted { - override fun command(subscriptionCount: StateFlow): Flow = flow { - var started = false - subscriptionCount.collect { count -> - if (count > 0 && !started) { - started = true - emit(SharingCommand.START) - } - } - } - - override fun toString(): String = "SharingStarted.Lazily" -} - -private class StartedWhileSubscribed( - private val stopTimeout: Long, - private val replayExpiration: Long -) : SharingStarted { - init { - require(stopTimeout >= 0) { "stopTimeout($stopTimeout ms) cannot be negative" } - require(replayExpiration >= 0) { "replayExpiration($replayExpiration ms) cannot be negative" } - } - - override fun command(subscriptionCount: StateFlow): Flow = subscriptionCount - .transformLatest { count -> - if (count > 0) { - emit(SharingCommand.START) - } else { - delay(stopTimeout) - if (replayExpiration > 0) { - emit(SharingCommand.STOP) - delay(replayExpiration) - } - emit(SharingCommand.STOP_AND_RESET_REPLAY_CACHE) - } - } - .dropWhile { it != SharingCommand.START } // don't emit any STOP/RESET_BUFFER to start with, only START - .distinctUntilChanged() // just in case somebody forgets it, don't leak our multiple sending of START - - @OptIn(ExperimentalStdlibApi::class) - override fun toString(): String { - val params = buildList(2) { - if (stopTimeout > 0) add("stopTimeout=${stopTimeout}ms") - if (replayExpiration < Long.MAX_VALUE) add("replayExpiration=${replayExpiration}ms") - } - return "SharingStarted.WhileSubscribed(${params.joinToString()})" - } - - // equals & hashcode to facilitate testing, not documented in public contract - override fun equals(other: Any?): Boolean = - other is StartedWhileSubscribed && - stopTimeout == other.stopTimeout && - replayExpiration == other.replayExpiration - - override fun hashCode(): Int = stopTimeout.hashCode() * 31 + replayExpiration.hashCode() -} diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 8587606633..b2bbb6d3ae 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -13,12 +13,9 @@ import kotlin.coroutines.* import kotlin.native.concurrent.* /** - * A [SharedFlow] that represents a read-only state with a single updatable data [value] that emits updates - * to the value to its collectors. A state flow is a _hot_ flow because its active instance exists independently - * of the presence of collectors. Its current value can be retrieved via the [value] property. - * - * **State flow never completes**. A call to [Flow.collect] on a state flow never completes normally, and - * neither does a coroutine started by the [Flow.launchIn] function. An active collector of a state flow is called a _subscriber_. + * A [Flow] that represents a read-only state with a single updatable data [value] that emits updates + * to the value to its collectors. The current value can be retrieved via [value] property. + * The flow of future updates to the value can be observed by collecting values from this flow. * * A [mutable state flow][MutableStateFlow] is created using `MutableStateFlow(value)` constructor function with * the initial value. The value of mutable state flow can be updated by setting its [value] property. @@ -34,7 +31,7 @@ import kotlin.native.concurrent.* * ``` * class CounterModel { * private val _counter = MutableStateFlow(0) // private mutable state flow - * val counter = _counter.asStateFlow() // publicly exposed as read-only state flow + * val counter: StateFlow get() = _counter // publicly exposed as read-only state flow * * fun inc() { * _counter.value++ @@ -50,9 +47,6 @@ import kotlin.native.concurrent.* * val sumFlow: Flow = aModel.counter.combine(bModel.counter) { a, b -> a + b } * ``` * - * As an alternative to the above usage with the `MutableStateFlow(...)` constructor function, - * any _cold_ [Flow] can be converted to a state flow using the [stateIn] operator. - * * ### Strong equality-based conflation * * Values in state flow are conflated using [Any.equals] comparison in a similar way to @@ -61,35 +55,12 @@ import kotlin.native.concurrent.* * when new value is equal to the previously emitted one. State flow behavior with classes that violate * the contract for [Any.equals] is unspecified. * - * ### State flow is a shared flow - * - * State flow is a special-purpose, high-performance, and efficient implementation of [SharedFlow] for the narrow, - * but widely used case of sharing a state. See the [SharedFlow] documentation for the basic rules, - * constraints, and operators that are applicable to all shared flows. - * - * State flow always has an initial value, replays one most recent value to new subscribers, does not buffer any - * more values, but keeps the last emitted one, and does not support [resetReplayCache][MutableSharedFlow.resetReplayCache]. - * A state flow behaves identically to a shared flow when it is created - * with the following parameters and the [distinctUntilChanged] operator is applied to it: - * - * ``` - * // MutableStateFlow(initialValue) is a shared flow with the following parameters: - * val shared = MutableSharedFlow( - * replay = 1, - * onBufferOverflow = BufferOverflow.DROP_OLDEST - * ) - * shared.tryEmit(initialValue) // emit the initial value - * val state = shared.distinctUntilChanged() // get StateFlow-like behavior - * ``` - * - * Use [SharedFlow] when you need a [StateFlow] with tweaks in its behavior such as extra buffering, replaying more - * values, or omitting the initial value. - * * ### StateFlow vs ConflatedBroadcastChannel * - * Conceptually, state flow is similar to [ConflatedBroadcastChannel] + * Conceptually state flow is similar to + * [ConflatedBroadcastChannel][kotlinx.coroutines.channels.ConflatedBroadcastChannel] * and is designed to completely replace `ConflatedBroadcastChannel` in the future. - * It has the following important differences: + * It has the following important difference: * * * `StateFlow` is simpler, because it does not have to implement all the [Channel] APIs, which allows * for faster, garbage-free implementation, unlike `ConflatedBroadcastChannel` implementation that @@ -99,44 +70,38 @@ import kotlin.native.concurrent.* * * `StateFlow` has a clear separation into a read-only `StateFlow` interface and a [MutableStateFlow]. * * `StateFlow` conflation is based on equality like [distinctUntilChanged] operator, * unlike conflation in `ConflatedBroadcastChannel` that is based on reference identity. - * * `StateFlow` cannot be closed like `ConflatedBroadcastChannel` and can never represent a failure. - * All errors and completion signals should be explicitly _materialized_ if needed. + * * `StateFlow` cannot be currently closed like `ConflatedBroadcastChannel` and can never represent a failure. + * This feature might be added in the future if enough compelling use-cases are found. * * `StateFlow` is designed to better cover typical use-cases of keeping track of state changes in time, taking * more pragmatic design choices for the sake of convenience. * - * To migrate [ConflatedBroadcastChannel] usage to [StateFlow], start by replacing usages of the `ConflatedBroadcastChannel()` - * constructor with `MutableStateFlow(initialValue)`, using `null` as an initial value if you don't have one. - * Replace [send][ConflatedBroadcastChannel.send] and [offer][ConflatedBroadcastChannel.offer] calls - * with updates to the state flow's [MutableStateFlow.value], and convert subscribers' code to flow operators. - * You can use the [filterNotNull] operator to mimic behavior of a `ConflatedBroadcastChannel` without initial value. - * * ### Concurrency * - * All methods of state flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * All methods of data flow are **thread-safe** and can be safely invoked from concurrent coroutines without * external synchronization. * * ### Operator fusion * * Application of [flowOn][Flow.flowOn], [conflate][Flow.conflate], * [buffer] with [CONFLATED][Channel.CONFLATED] or [RENDEZVOUS][Channel.RENDEZVOUS] capacity, - * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. + * or a [distinctUntilChanged][Flow.distinctUntilChanged] operator has no effect on the state flow. * * ### Implementation notes * * State flow implementation is optimized for memory consumption and allocation-freedom. It uses a lock to ensure * thread-safety, but suspending collector coroutines are resumed outside of this lock to avoid dead-locks when - * using unconfined coroutines. Adding new subscribers has `O(1)` amortized cost, but updating a [value] has `O(N)` - * cost, where `N` is the number of active subscribers. + * using unconfined coroutines. Adding new collectors has `O(1)` amortized cost, but updating a [value] has `O(N)` + * cost, where `N` is the number of active collectors. * * ### Not stable for inheritance * - * **`The StateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **`StateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use the `MutableStateFlow(value)` constructor function to create an implementation. + * Use `MutableStateFlow()` constructor function to create an implementation. */ @ExperimentalCoroutinesApi -public interface StateFlow : SharedFlow { +public interface StateFlow : Flow { /** * The current value of this state flow. */ @@ -145,35 +110,23 @@ public interface StateFlow : SharedFlow { /** * A mutable [StateFlow] that provides a setter for [value]. - * An instance of `MutableStateFlow` with the given initial `value` can be created using - * `MutableStateFlow(value)` constructor function. * - * See the [StateFlow] documentation for details on state flows. + * See [StateFlow] documentation for details. * * ### Not stable for inheritance * - * **The `MutableStateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **`MutableStateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use the `MutableStateFlow()` constructor function to create an implementation. + * Use `MutableStateFlow()` constructor function to create an implementation. */ @ExperimentalCoroutinesApi -public interface MutableStateFlow : StateFlow, MutableSharedFlow { +public interface MutableStateFlow : StateFlow { /** * The current value of this state flow. * * Setting a value that is [equal][Any.equals] to the previous one does nothing. */ public override var value: T - - /** - * Atomically compares the current [value] with [expect] and sets it to [update] if it is equal to [expect]. - * The result is `true` if the [value] was set to [update] and `false` otherwise. - * - * This function use a regular comparison using [Any.equals]. If both [expect] and [update] are equal to the - * current [value], this function returns `true`, but it does not actually change the reference that is - * stored in the [value]. - */ - public fun compareAndSet(expect: T, update: T): Boolean } /** @@ -191,12 +144,14 @@ private val NONE = Symbol("NONE") @SharedImmutable private val PENDING = Symbol("PENDING") +private const val INITIAL_SIZE = 2 // optimized for just a few collectors + // StateFlow slots are allocated for its collectors -private class StateFlowSlot : AbstractSharedFlowSlot>() { +private class StateFlowSlot { /** * Each slot can have one of the following states: * - * * `null` -- it is not used right now. Can [allocateLocked] to new collector. + * * `null` -- it is not used right now. Can [allocate] to new collector. * * `NONE` -- used by a collector, but neither suspended nor has pending value. * * `PENDING` -- pending to process new value. * * `CancellableContinuationImpl` -- suspended waiting for new value. @@ -206,16 +161,15 @@ private class StateFlowSlot : AbstractSharedFlowSlot>() { */ private val _state = atomic(null) - override fun allocateLocked(flow: StateFlowImpl<*>): Boolean { + fun allocate(): Boolean { // No need for atomic check & update here, since allocated happens under StateFlow lock if (_state.value != null) return false // not free _state.value = NONE // allocated return true } - override fun freeLocked(flow: StateFlowImpl<*>): Array?> { + fun free() { _state.value = null // free now - return EMPTY_RESUMES // nothing more to do } @Suppress("UNCHECKED_CAST") @@ -253,97 +207,72 @@ private class StateFlowSlot : AbstractSharedFlowSlot>() { } } -private class StateFlowImpl( - initialState: Any // T | NULL -) : AbstractSharedFlow(), MutableStateFlow, CancellableFlow, FusibleFlow { - private val _state = atomic(initialState) // T | NULL +private class StateFlowImpl(initialValue: Any) : SynchronizedObject(), MutableStateFlow, FusibleFlow { + private val _state = atomic(initialValue) // T | NULL private var sequence = 0 // serializes updates, value update is in process when sequence is odd + private var slots = arrayOfNulls(INITIAL_SIZE) + private var nSlots = 0 // number of allocated (!free) slots + private var nextIndex = 0 // oracle for the next free slot index @Suppress("UNCHECKED_CAST") public override var value: T get() = NULL.unbox(_state.value) - set(value) { updateState(null, value ?: NULL) } - - override fun compareAndSet(expect: T, update: T): Boolean = - updateState(expect ?: NULL, update ?: NULL) - - private fun updateState(expectedState: Any?, newState: Any): Boolean { - var curSequence = 0 - var curSlots: Array? = this.slots // benign race, we will not use it - synchronized(this) { - val oldState = _state.value - if (expectedState != null && oldState != expectedState) return false // CAS support - if (oldState == newState) return true // Don't do anything if value is not changing, but CAS -> true - _state.value = newState - curSequence = sequence - if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update) - curSequence++ // make it odd - sequence = curSequence - } else { - // update is already in process, notify it, and return - sequence = curSequence + 2 // change sequence to notify, keep it odd - return true // updated - } - curSlots = slots // read current reference to collectors under lock - } - /* - Fire value updates outside of the lock to avoid deadlocks with unconfined coroutines. - Loop until we're done firing all the changes. This is a sort of simple flat combining that - ensures sequential firing of concurrent updates and avoids the storm of collector resumes - when updates happen concurrently from many threads. - */ - while (true) { - // Benign race on element read from array - curSlots?.forEach { - it?.makePending() - } - // check if the value was updated again while we were updating the old one + set(value) { + var curSequence = 0 + var curSlots: Array = this.slots // benign race, we will not use it + val newState = value ?: NULL synchronized(this) { - if (sequence == curSequence) { // nothing changed, we are done - sequence = curSequence + 1 // make sequence even again - return true // done, updated - } - // reread everything for the next loop under the lock + val oldState = _state.value + if (oldState == newState) return // Don't do anything if value is not changing + _state.value = newState curSequence = sequence - curSlots = slots + if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update) + curSequence++ // make it odd + sequence = curSequence + } else { + // update is already in process, notify it, and return + sequence = curSequence + 2 // change sequence to notify, keep it odd + return + } + curSlots = slots // read current reference to collectors under lock + } + /* + Fire value updates outside of the lock to avoid deadlocks with unconfined coroutines + Loop until we're done firing all the changes. This is sort of simple flat combining that + ensures sequential firing of concurrent updates and avoids the storm of collector resumes + when updates happen concurrently from many threads. + */ + while (true) { + // Benign race on element read from array + for (col in curSlots) { + col?.makePending() + } + // check if the value was updated again while we were updating the old one + synchronized(this) { + if (sequence == curSequence) { // nothing changed, we are done + sequence = curSequence + 1 // make sequence even again + return // done + } + // reread everything for the next loop under the lock + curSequence = sequence + curSlots = slots + } } } - } - - override val replayCache: List - get() = listOf(value) - - override fun tryEmit(value: T): Boolean { - this.value = value - return true - } - - override suspend fun emit(value: T) { - this.value = value - } - - @Suppress("UNCHECKED_CAST") - override fun resetReplayCache() { - throw UnsupportedOperationException("MutableStateFlow.resetReplayCache is not supported") - } override suspend fun collect(collector: FlowCollector) { val slot = allocateSlot() + var prevState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet) try { - if (collector is SubscribedFlowCollector) collector.onSubscription() - val collectorJob = currentCoroutineContext()[Job] - var oldState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet) // The loop is arranged so that it starts delivering current value without waiting first while (true) { // Here the coroutine could have waited for a while to be dispatched, // so we use the most recent state here to ensure the best possible conflation of stale values val newState = _state.value - // always check for cancellation - collectorJob?.ensureActive() // Conflate value emissions using equality - if (oldState == null || oldState != newState) { + if (prevState == null || newState != prevState) { collector.emit(NULL.unbox(newState)) - oldState = newState + prevState = newState } // Note: if awaitPending is cancelled, then it bails out of this loop and calls freeSlot if (!slot.takePending()) { // try fast-path without suspending first @@ -355,29 +284,33 @@ private class StateFlowImpl( } } - override fun createSlot() = StateFlowSlot() - override fun createSlotArray(size: Int): Array = arrayOfNulls(size) - - override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = - fuseStateFlow(context, capacity, onBufferOverflow) -} + private fun allocateSlot(): StateFlowSlot = synchronized(this) { + val size = slots.size + if (nSlots >= size) slots = slots.copyOf(2 * size) + var index = nextIndex + var slot: StateFlowSlot + while (true) { + slot = slots[index] ?: StateFlowSlot().also { slots[index] = it } + index++ + if (index >= slots.size) index = 0 + if (slot.allocate()) break // break when found and allocated free slot + } + nextIndex = index + nSlots++ + slot + } -internal fun MutableStateFlow.increment(delta: Int) { - while (true) { // CAS loop - val current = value - if (compareAndSet(current, current + delta)) return + private fun freeSlot(slot: StateFlowSlot): Unit = synchronized(this) { + slot.free() + nSlots-- } -} -internal fun StateFlow.fuseStateFlow( - context: CoroutineContext, - capacity: Int, - onBufferOverflow: BufferOverflow -): Flow { - // state flow is always conflated so additional conflation does not have any effect - assert { capacity != Channel.CONFLATED } // should be desugared by callers - if ((capacity in 0..1 || capacity == Channel.BUFFERED) && onBufferOverflow == BufferOverflow.DROP_OLDEST) { - return this + override fun fuse(context: CoroutineContext, capacity: Int): FusibleFlow { + // context is irrelevant for state flow and it is always conflated + // so it should not do anything unless buffering is requested + return when (capacity) { + Channel.CONFLATED, Channel.RENDEZVOUS -> this + else -> ChannelFlowOperatorImpl(this, context, capacity) + } } - return fuseSharedFlow(context, capacity, onBufferOverflow) -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt deleted file mode 100644 index ccb5343084..0000000000 --- a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow.internal - -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.internal.* -import kotlin.coroutines.* -import kotlin.jvm.* -import kotlin.native.concurrent.* - -@JvmField -@SharedImmutable -internal val EMPTY_RESUMES = arrayOfNulls?>(0) - -internal abstract class AbstractSharedFlowSlot { - abstract fun allocateLocked(flow: F): Boolean - abstract fun freeLocked(flow: F): Array?> // returns continuations to resume after lock -} - -internal abstract class AbstractSharedFlow> : SynchronizedObject() { - @Suppress("UNCHECKED_CAST") - protected var slots: Array? = null // allocated when needed - private set - protected var nCollectors = 0 // number of allocated (!free) slots - private set - private var nextIndex = 0 // oracle for the next free slot index - private var _subscriptionCount: MutableStateFlow? = null // init on first need - - val subscriptionCount: StateFlow - get() = synchronized(this) { - // allocate under lock in sync with nCollectors variable - _subscriptionCount ?: MutableStateFlow(nCollectors).also { - _subscriptionCount = it - } - } - - protected abstract fun createSlot(): S - - protected abstract fun createSlotArray(size: Int): Array - - @Suppress("UNCHECKED_CAST") - protected fun allocateSlot(): S { - // Actually create slot under lock - var subscriptionCount: MutableStateFlow? = null - val slot = synchronized(this) { - val slots = when (val curSlots = slots) { - null -> createSlotArray(2).also { slots = it } - else -> if (nCollectors >= curSlots.size) { - curSlots.copyOf(2 * curSlots.size).also { slots = it } - } else { - curSlots - } - } - var index = nextIndex - var slot: S - while (true) { - slot = slots[index] ?: createSlot().also { slots[index] = it } - index++ - if (index >= slots.size) index = 0 - if ((slot as AbstractSharedFlowSlot).allocateLocked(this)) break // break when found and allocated free slot - } - nextIndex = index - nCollectors++ - subscriptionCount = _subscriptionCount // retrieve under lock if initialized - slot - } - // increments subscription count - subscriptionCount?.increment(1) - return slot - } - - @Suppress("UNCHECKED_CAST") - protected fun freeSlot(slot: S) { - // Release slot under lock - var subscriptionCount: MutableStateFlow? = null - val resumes = synchronized(this) { - nCollectors-- - subscriptionCount = _subscriptionCount // retrieve under lock if initialized - // Reset next index oracle if we have no more active collectors for more predictable behavior next time - if (nCollectors == 0) nextIndex = 0 - (slot as AbstractSharedFlowSlot).freeLocked(this) - } - /* - Resume suspended coroutines. - This can happens when the subscriber that was freed was a slow one and was holding up buffer. - When this subscriber was freed, previously queued emitted can now wake up and are resumed here. - */ - for (cont in resumes) cont?.resume(Unit) - // decrement subscription count - subscriptionCount?.increment(-1) - } - - protected inline fun forEachSlotLocked(block: (S) -> Unit) { - if (nCollectors == 0) return - slots?.forEach { slot -> - if (slot != null) block(slot) - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt index e53ef35c45..994d38074e 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -16,7 +16,7 @@ internal fun Flow.asChannelFlow(): ChannelFlow = this as? ChannelFlow ?: ChannelFlowOperatorImpl(this) /** - * Operators that can fuse with **downstream** [buffer] and [flowOn] operators implement this interface. + * Operators that can fuse with [buffer] and [flowOn] operators implement this interface. * * @suppress **This an internal API and should not be used from general code.** */ @@ -24,18 +24,16 @@ internal fun Flow.asChannelFlow(): ChannelFlow = public interface FusibleFlow : Flow { /** * This function is called by [flowOn] (with context) and [buffer] (with capacity) operators - * that are applied to this flow. Should not be used with [capacity] of [Channel.CONFLATED] - * (it shall be desugared to `capacity = 0, onBufferOverflow = DROP_OLDEST`). + * that are applied to this flow. */ public fun fuse( context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND - ): Flow + capacity: Int = Channel.OPTIONAL_CHANNEL + ): FusibleFlow } /** - * Operators that use channels as their "output" extend this `ChannelFlow` and are always fused with each other. + * Operators that use channels extend this `ChannelFlow` and are always fused with each other. * This class servers as a skeleton implementation of [FusibleFlow] and provides other cross-cutting * methods like ability to [produceIn] and [broadcastIn] the corresponding flow, thus making it * possible to directly use the backing channel if it exists (hence the `ChannelFlow` name). @@ -47,13 +45,8 @@ public abstract class ChannelFlow( // upstream context @JvmField public val context: CoroutineContext, // buffer capacity between upstream and downstream context - @JvmField public val capacity: Int, - // buffer overflow strategy - @JvmField public val onBufferOverflow: BufferOverflow + @JvmField public val capacity: Int ) : FusibleFlow { - init { - assert { capacity != Channel.CONFLATED } // CONFLATED must be desugared to 0, DROP_OLDEST by callers - } // shared code to create a suspend lambda from collectTo function in one place internal val collectToFun: suspend (ProducerScope) -> Unit @@ -62,62 +55,35 @@ public abstract class ChannelFlow( private val produceCapacity: Int get() = if (capacity == Channel.OPTIONAL_CHANNEL) Channel.BUFFERED else capacity - /** - * When this [ChannelFlow] implementation can work without a channel (supports [Channel.OPTIONAL_CHANNEL]), - * then it should return a non-null value from this function, so that a caller can use it without the effect of - * additional [flowOn] and [buffer] operators, by incorporating its - * [context], [capacity], and [onBufferOverflow] into its own implementation. - */ - public open fun dropChannelOperators(): Flow? = null - - public override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): Flow { - assert { capacity != Channel.CONFLATED } // CONFLATED must be desugared to (0, DROP_OLDEST) by callers + public override fun fuse(context: CoroutineContext, capacity: Int): FusibleFlow { // note: previous upstream context (specified before) takes precedence val newContext = context + this.context - val newCapacity: Int - val newOverflow: BufferOverflow - if (onBufferOverflow != BufferOverflow.SUSPEND) { - // this additional buffer never suspends => overwrite preceding buffering configuration - newCapacity = capacity - newOverflow = onBufferOverflow - } else { - // combine capacities, keep previous overflow strategy - newCapacity = when { - this.capacity == Channel.OPTIONAL_CHANNEL -> capacity - capacity == Channel.OPTIONAL_CHANNEL -> this.capacity - this.capacity == Channel.BUFFERED -> capacity - capacity == Channel.BUFFERED -> this.capacity - else -> { - // sanity checks - assert { this.capacity >= 0 } - assert { capacity >= 0 } - // combine capacities clamping to UNLIMITED on overflow - val sum = this.capacity + capacity - if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow - } + val newCapacity = when { + this.capacity == Channel.OPTIONAL_CHANNEL -> capacity + capacity == Channel.OPTIONAL_CHANNEL -> this.capacity + this.capacity == Channel.BUFFERED -> capacity + capacity == Channel.BUFFERED -> this.capacity + this.capacity == Channel.CONFLATED -> Channel.CONFLATED + capacity == Channel.CONFLATED -> Channel.CONFLATED + else -> { + // sanity checks + assert { this.capacity >= 0 } + assert { capacity >= 0 } + // combine capacities clamping to UNLIMITED on overflow + val sum = this.capacity + capacity + if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow } - newOverflow = this.onBufferOverflow } - if (newContext == this.context && newCapacity == this.capacity && newOverflow == this.onBufferOverflow) - return this - return create(newContext, newCapacity, newOverflow) + if (newContext == this.context && newCapacity == this.capacity) return this + return create(newContext, newCapacity) } - protected abstract fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow + protected abstract fun create(context: CoroutineContext, capacity: Int): ChannelFlow protected abstract suspend fun collectTo(scope: ProducerScope) - // broadcastImpl is used in broadcastIn operator which is obsolete and replaced by SharedFlow. - // BroadcastChannel does not support onBufferOverflow beyond simple conflation - public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel { - val broadcastCapacity = when (onBufferOverflow) { - BufferOverflow.SUSPEND -> produceCapacity - BufferOverflow.DROP_OLDEST -> Channel.CONFLATED - BufferOverflow.DROP_LATEST -> - throw IllegalArgumentException("Broadcast channel does not support BufferOverflow.DROP_LATEST") - } - return scope.broadcast(context, broadcastCapacity, start, block = collectToFun) - } + public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel = + scope.broadcast(context, produceCapacity, start, block = collectToFun) /** * Here we use ATOMIC start for a reason (#1825). @@ -128,33 +94,26 @@ public abstract class ChannelFlow( * Thus `onCompletion` and `finally` blocks won't be executed and it may lead to a different kinds of memory leaks. */ public open fun produceImpl(scope: CoroutineScope): ReceiveChannel = - scope.produce(context, produceCapacity, onBufferOverflow, start = CoroutineStart.ATOMIC, block = collectToFun) + scope.produce(context, produceCapacity, start = CoroutineStart.ATOMIC, block = collectToFun) override suspend fun collect(collector: FlowCollector): Unit = coroutineScope { collector.emitAll(produceImpl(this)) } - protected open fun additionalToStringProps(): String? = null + public open fun additionalToStringProps(): String = "" // debug toString - override fun toString(): String { - val props = ArrayList(4) - additionalToStringProps()?.let { props.add(it) } - if (context !== EmptyCoroutineContext) props.add("context=$context") - if (capacity != Channel.OPTIONAL_CHANNEL) props.add("capacity=$capacity") - if (onBufferOverflow != BufferOverflow.SUSPEND) props.add("onBufferOverflow=$onBufferOverflow") - return "$classSimpleName[${props.joinToString(", ")}]" - } + override fun toString(): String = + "$classSimpleName[${additionalToStringProps()}context=$context, capacity=$capacity]" } // ChannelFlow implementation that operates on another flow before it internal abstract class ChannelFlowOperator( - @JvmField protected val flow: Flow, + @JvmField val flow: Flow, context: CoroutineContext, - capacity: Int, - onBufferOverflow: BufferOverflow -) : ChannelFlow(context, capacity, onBufferOverflow) { + capacity: Int +) : ChannelFlow(context, capacity) { protected abstract suspend fun flowCollect(collector: FlowCollector) // Changes collecting context upstream to the specified newContext, while collecting in the original context @@ -189,19 +148,14 @@ internal abstract class ChannelFlowOperator( override fun toString(): String = "$flow -> ${super.toString()}" } -/** - * Simple channel flow operator: [flowOn], [buffer], or their fused combination. - */ +// Simple channel flow operator: flowOn, buffer, or their fused combination internal class ChannelFlowOperatorImpl( flow: Flow, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlowOperator(flow, context, capacity, onBufferOverflow) { - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - ChannelFlowOperatorImpl(flow, context, capacity, onBufferOverflow) - - override fun dropChannelOperators(): Flow? = flow + capacity: Int = Channel.OPTIONAL_CHANNEL +) : ChannelFlowOperator(flow, context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowOperatorImpl(flow, context, capacity) override suspend fun flowCollect(collector: FlowCollector) = flow.collect(collector) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt index 530bcc1e5a..798f38b8bd 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt @@ -14,11 +14,10 @@ internal class ChannelFlowTransformLatest( private val transform: suspend FlowCollector.(value: T) -> Unit, flow: Flow, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlowOperator(flow, context, capacity, onBufferOverflow) { - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - ChannelFlowTransformLatest(transform, flow, context, capacity, onBufferOverflow) + capacity: Int = Channel.BUFFERED +) : ChannelFlowOperator(flow, context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowTransformLatest(transform, flow, context, capacity) override suspend fun flowCollect(collector: FlowCollector) { assert { collector is SendingCollector } // So cancellation behaviour is not leaking into the downstream @@ -42,11 +41,10 @@ internal class ChannelFlowMerge( private val flow: Flow>, private val concurrency: Int, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlow(context, capacity, onBufferOverflow) { - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - ChannelFlowMerge(flow, concurrency, context, capacity, onBufferOverflow) + capacity: Int = Channel.BUFFERED +) : ChannelFlow(context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelFlowMerge(flow, concurrency, context, capacity) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { return scope.flowProduce(context, capacity, block = collectToFun) @@ -74,17 +72,17 @@ internal class ChannelFlowMerge( } } - override fun additionalToStringProps(): String = "concurrency=$concurrency" + override fun additionalToStringProps(): String = + "concurrency=$concurrency, " } internal class ChannelLimitedFlowMerge( private val flows: Iterable>, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlow(context, capacity, onBufferOverflow) { - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - ChannelLimitedFlowMerge(flows, context, capacity, onBufferOverflow) + capacity: Int = Channel.BUFFERED +) : ChannelFlow(context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + ChannelLimitedFlowMerge(flows, context, capacity) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { return scope.flowProduce(context, capacity, block = collectToFun) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index a6d6b76dae..010d781c02 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -60,23 +60,13 @@ import kotlin.jvm.* * Q : -->---------- [2A] -- [2B] -- [2C] -->-- // collect * ``` * - * When the operator's code takes some time to execute, this decreases the total execution time of the flow. + * When operator's code takes time to execute this decreases the total execution time of the flow. * A [channel][Channel] is used between the coroutines to send elements emitted by the coroutine `P` to * the coroutine `Q`. If the code before `buffer` operator (in the coroutine `P`) is faster than the code after * `buffer` operator (in the coroutine `Q`), then this channel will become full at some point and will suspend * the producer coroutine `P` until the consumer coroutine `Q` catches up. * The [capacity] parameter defines the size of this buffer. * - * ### Buffer overflow - * - * By default, the emitter is suspended when the buffer overflows, to let collector catch up. This strategy can be - * overridden with an optional [onBufferOverflow] parameter so that the emitter is never suspended. In this - * case, on buffer overflow either the oldest value in the buffer is dropped with the [DROP_OLDEST][BufferOverflow.DROP_OLDEST] - * strategy and the latest emitted value is added to the buffer, - * or the latest value that is being emitted is dropped with the [DROP_LATEST][BufferOverflow.DROP_LATEST] strategy, - * keeping the buffer intact. - * To implement either of the custom strategies, a buffer of at least one element is used. - * * ### Operator fusion * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are @@ -86,12 +76,9 @@ import kotlin.jvm.* * which effectively requests a buffer of any size. Multiple requests with a specified buffer * size produce a buffer with the sum of the requested buffer sizes. * - * A `buffer` call with a non-default value of the [onBufferOverflow] parameter overrides all immediately preceding - * buffering operators, because it never suspends its upstream, and thus no upstream buffer would ever be used. - * * ### Conceptual implementation * - * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its basic + * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its * implementation is equivalent to the following code that can be written using [produce] * coroutine builder to produce a channel and [consumeEach][ReceiveChannel.consumeEach] extension to consume it: * @@ -109,43 +96,24 @@ import kotlin.jvm.* * * ### Conflation * - * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is a shortcut to - * `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`, and is available via - * a separate [conflate] operator. See its documentation for details. + * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is provided as a shortcut via + * [conflate] operator. See its documentation for details. * * @param capacity type/capacity of the buffer between coroutines. Allowed values are the same as in `Channel(...)` - * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], - * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating - * an explicitly requested size. - * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to - * [SUSPEND][BufferOverflow.SUSPEND], supported only when `capacity >= 0` or `capacity == Channel.BUFFERED`, - * implicitly creates a channel with at least one buffered element). + * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], + * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating + * an explicitly requested size. */ -@Suppress("NAME_SHADOWING") -public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Flow { +public fun Flow.buffer(capacity: Int = BUFFERED): Flow { require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) { "Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity" } - require(capacity != CONFLATED || onBufferOverflow == BufferOverflow.SUSPEND) { - "CONFLATED capacity cannot be used with non-default onBufferOverflow" - } - // desugar CONFLATED capacity to (0, DROP_OLDEST) - var capacity = capacity - var onBufferOverflow = onBufferOverflow - if (capacity == CONFLATED) { - capacity = 0 - onBufferOverflow = BufferOverflow.DROP_OLDEST - } - // create a flow return when (this) { - is FusibleFlow -> fuse(capacity = capacity, onBufferOverflow = onBufferOverflow) - else -> ChannelFlowOperatorImpl(this, capacity = capacity, onBufferOverflow = onBufferOverflow) + is FusibleFlow -> fuse(capacity = capacity) + else -> ChannelFlowOperatorImpl(this, capacity = capacity) } } -@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") -public fun Flow.buffer(capacity: Int = BUFFERED): Flow = buffer(capacity) - /** * Conflates flow emissions via conflated channel and runs collector in a separate coroutine. * The effect of this is that emitter is never suspended due to a slow collector, but collector @@ -170,9 +138,7 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow = buffer(capaci * assertEquals(listOf(1, 10, 20, 30), result) * ``` * - * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED], - * with is, in turn, a shortcut to a buffer that only keeps the latest element as - * created by `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`. + * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED]. * * ### Operator fusion * @@ -206,17 +172,13 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * * For more explanation of context preservation please refer to [Flow] documentation. * - * This operator retains a _sequential_ nature of flow if changing the context does not call for changing + * This operators retains a _sequential_ nature of flow if changing the context does not call for changing * the [dispatcher][CoroutineDispatcher]. Otherwise, if changing dispatcher is required, it collects * flow emissions in one coroutine that is run using a specified [context] and emits them from another coroutines * with the original collector's context using a channel with a [default][Channel.BUFFERED] buffer size * between two coroutines similarly to [buffer] operator, unless [buffer] operator is explicitly called * before or after `flowOn`, which requests buffering behavior and specifies channel size. * - * Note, that flows operating across different dispatchers might lose some in-flight elements when cancelled. - * In particular, this operator ensures that downstream flow does not resume on cancellation even if the element - * was already emitted by the upstream flow. - * * ### Operator fusion * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are @@ -232,8 +194,8 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * .flowOn(Dispatchers.Default) * ``` * - * Note that an instance of [SharedFlow] does not have an execution context by itself, - * so applying `flowOn` to a `SharedFlow` has not effect. See the [SharedFlow] documentation on Operator Fusion. + * Note that an instance of [StateFlow] does not have an execution context by itself, + * so applying `flowOn` to a `StateFlow` has not effect. See [StateFlow] documentation on Operator Fusion. * * @throws [IllegalArgumentException] if provided context contains [Job] instance. */ @@ -249,30 +211,17 @@ public fun Flow.flowOn(context: CoroutineContext): Flow { /** * Returns a flow which checks cancellation status on each emission and throws * the corresponding cancellation cause if flow collector was cancelled. - * Note that [flow] builder and all implementations of [SharedFlow] are [cancellable] by default. + * Note that [flow] builder is [cancellable] by default. * * This operator provides a shortcut for `.onEach { currentCoroutineContext().ensureActive() }`. * See [ensureActive][CoroutineContext.ensureActive] for details. */ -public fun Flow.cancellable(): Flow = - when (this) { - is CancellableFlow<*> -> this // Fast-path, already cancellable - else -> CancellableFlowImpl(this) - } - -/** - * Internal marker for flows that are [cancellable]. - */ -internal interface CancellableFlow : Flow - -/** - * Named implementation class for a flow that is defined by the [cancellable] function. - */ -private class CancellableFlowImpl(private val flow: Flow) : CancellableFlow { - override suspend fun collect(collector: FlowCollector) { - flow.collect { +public fun Flow.cancellable(): Flow { + if (this is AbstractFlow<*>) return this // Fast-path, already cancellable + return unsafeFlow { + collect { currentCoroutineContext().ensureActive() - collector.emit(it) + emit(it) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 0c40691640..d5d4278a2e 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -11,33 +11,17 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.internal.* import kotlinx.coroutines.selects.* +import kotlin.coroutines.* import kotlin.jvm.* import kotlin.time.* -/* Scaffolding for Knit code examples - - -*/ - /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout][timeoutMillis]. * The latest value is always emitted. * * Example: - * - * ```kotlin + * ``` * flow { * emit(1) * delay(90) @@ -50,14 +34,7 @@ fun main() = runBlocking { * emit(5) * }.debounce(1000) * ``` - * - * - * produces the following emissions - * - * ```text - * 3, 4, 5 - * ``` - * + * produces `3, 4, 5`. * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. @@ -77,8 +54,7 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * A variation of [debounce] that allows specifying the timeout value dynamically. * * Example: - * - * ```kotlin + * ``` * flow { * emit(1) * delay(90) @@ -97,14 +73,7 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * } * } * ``` - * - * - * produces the following emissions - * - * ```text - * 1, 3, 4, 5 - * ``` - * + * produces `1, 3, 4, 5`. * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. @@ -123,8 +92,7 @@ public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = * The latest value is always emitted. * * Example: - * - * ```kotlin + * ``` * flow { * emit(1) * delay(90.milliseconds) @@ -137,14 +105,7 @@ public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = * emit(5) * }.debounce(1000.milliseconds) * ``` - * - * - * produces the following emissions - * - * ```text - * 3, 4, 5 - * ``` - * + * produces `3, 4, 5`. * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] milliseconds. @@ -161,8 +122,7 @@ public fun Flow.debounceWithDuration(timeout: Duration): Flow = deboun * A variation of [debounceWithDuration] that allows specifying the timeout value dynamically. * * Example: - * - * ```kotlin + * ``` * flow { * emit(1) * delay(90.milliseconds) @@ -181,14 +141,7 @@ public fun Flow.debounceWithDuration(timeout: Duration): Flow = deboun * } * } * ``` - * - * - * produces the following emissions - * - * ```text - * 1, 3, 4, 5 - * ``` - * + * produces `1, 3, 4, 5`. * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] milliseconds. @@ -248,24 +201,16 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis]. * * Example: - * - * ```kotlin + * ``` * flow { * repeat(10) { * emit(it) - * delay(110) + * delay(50) * } - * }.sample(200) + * }.sample(100) * ``` - * - * - * produces the following emissions + * produces `1, 3, 5, 7, 9`. * - * ```text - * 1, 3, 5, 7, 9 - * ``` - * - * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @FlowPreview @@ -319,23 +264,15 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period]. * * Example: - * - * ```kotlin + * ``` * flow { * repeat(10) { * emit(it) - * delay(110.milliseconds) + * delay(50.milliseconds) * } - * }.sample(200.milliseconds) - * ``` - * - * - * produces the following emissions - * - * ```text - * 1, 3, 5, 7, 9 + * }.sample(100.milliseconds) * ``` - * + * produces `1, 3, 5, 7, 9`. * * Note that the latest element is not emitted if it does not fit into the sampling window. */ diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index 1a34af776f..35048484d2 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -7,10 +7,9 @@ package kotlinx.coroutines.flow -import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* -import kotlin.native.concurrent.* +import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** * Returns flow where all subsequent repetitions of the same value are filtered out. @@ -18,69 +17,44 @@ import kotlin.native.concurrent.* * Note that any instance of [StateFlow] already behaves as if `distinctUtilChanged` operator is * applied to it, so applying `distinctUntilChanged` to a `StateFlow` has no effect. * See [StateFlow] documentation on Operator Fusion. - * Also, repeated application of `distinctUntilChanged` operator on any flow has no effect. */ public fun Flow.distinctUntilChanged(): Flow = when (this) { - is StateFlow<*> -> this // state flows are always distinct - else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent) + is StateFlow<*> -> this + else -> distinctUntilChangedBy { it } } /** * Returns flow where all subsequent repetitions of the same value are filtered out, when compared * with each other via the provided [areEquivalent] function. - * - * Note that repeated application of `distinctUntilChanged` operator with the same parameter has no effect. */ -@Suppress("UNCHECKED_CAST") public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> Boolean): Flow = - distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = areEquivalent as (Any?, Any?) -> Boolean) + distinctUntilChangedBy(keySelector = { it }, areEquivalent = areEquivalent) /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * key is extracted with [keySelector] function. - * - * Note that repeated application of `distinctUntilChanged` operator with the same parameter has no effect. */ public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = - distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent) - -@SharedImmutable -private val defaultKeySelector: (Any?) -> Any? = { it } - -@SharedImmutable -private val defaultAreEquivalent: (Any?, Any?) -> Boolean = { old, new -> old == new } + distinctUntilChangedBy(keySelector = keySelector, areEquivalent = { old, new -> old == new }) /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * keys are extracted with [keySelector] function and compared with each other via the * provided [areEquivalent] function. - * - * NOTE: It is non-inline to share a single implementing class. */ -private fun Flow.distinctUntilChangedBy( - keySelector: (T) -> Any?, - areEquivalent: (old: Any?, new: Any?) -> Boolean -): Flow = when { - this is DistinctFlowImpl<*> && this.keySelector === keySelector && this.areEquivalent === areEquivalent -> this // same - else -> DistinctFlowImpl(this, keySelector, areEquivalent) -} - -private class DistinctFlowImpl( - private val upstream: Flow, - @JvmField val keySelector: (T) -> Any?, - @JvmField val areEquivalent: (old: Any?, new: Any?) -> Boolean -): Flow { - @InternalCoroutinesApi - override suspend fun collect(collector: FlowCollector) { +private inline fun Flow.distinctUntilChangedBy( + crossinline keySelector: (T) -> K, + crossinline areEquivalent: (old: K, new: K) -> Boolean +): Flow = + flow { var previousKey: Any? = NULL - upstream.collect { value -> + collect { value -> val key = keySelector(value) @Suppress("UNCHECKED_CAST") - if (previousKey === NULL || !areEquivalent(previousKey, key)) { + if (previousKey === NULL || !areEquivalent(previousKey as K, key)) { previousKey = key - collector.emit(value) + emit(value) } } } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 3ffe5fe943..fb37da3a83 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -55,12 +55,7 @@ internal inline fun Flow.unsafeTransform( } /** - * Returns a flow that invokes the given [action] **before** this flow starts to be collected. - * - * The [action] is called before the upstream flow is started, so if it is used with a [SharedFlow] - * there is **no guarantee** that emissions from the upstream flow that happen inside or immediately - * after this `onStart` action will be collected - * (see [onSubscription] for an alternative operator on shared flows). + * Invokes the given [action] when this flow starts to be collected. * * The receiver of the [action] is [FlowCollector], so `onStart` can emit additional elements. * For example: @@ -85,7 +80,7 @@ public fun Flow.onStart( } /** - * Returns a flow that invokes the given [action] **after** the flow is completed or cancelled, passing + * Invokes the given [action] when the given flow is completed or cancelled, passing * the cancellation exception or failure as cause parameter of [action]. * * Conceptually, `onCompletion` is similar to wrapping the flow collection into a `finally` block, @@ -131,7 +126,7 @@ public fun Flow.onStart( * ``` * * The receiver of the [action] is [FlowCollector] and this operator can be used to emit additional - * elements at the end **if it completed successfully**. For example: + * elements at the end if it completed successfully. For example: * * ``` * flowOf("a", "b", "c") diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index 7a70fbf7f2..5500034e9f 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -2,81 +2,71 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("unused") - package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.coroutines.* /** - * Applying [cancellable][Flow.cancellable] to a [SharedFlow] has no effect. - * See the [SharedFlow] documentation on Operator Fusion. - */ -@Deprecated( - level = DeprecationLevel.ERROR, - message = "Applying 'cancellable' to a SharedFlow has no effect. See the SharedFlow documentation on Operator Fusion.", - replaceWith = ReplaceWith("this") -) -public fun SharedFlow.cancellable(): Flow = noImpl() - -/** - * Applying [flowOn][Flow.flowOn] to [SharedFlow] has no effect. - * See the [SharedFlow] documentation on Operator Fusion. + * Returns this. + * Applying [flowOn][Flow.flowOn] operator to [StateFlow] has no effect. + * See [StateFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying 'flowOn' to SharedFlow has no effect. See the SharedFlow documentation on Operator Fusion.", + message = "Applying flowOn operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) -public fun SharedFlow.flowOn(context: CoroutineContext): Flow = noImpl() +public fun StateFlow.flowOn(context: CoroutineContext): Flow = noImpl() /** - * Applying [conflate][Flow.conflate] to [StateFlow] has no effect. - * See the [StateFlow] documentation on Operator Fusion. + * Returns this. + * Applying [conflate][Flow.conflate] operator to [StateFlow] has no effect. + * See [StateFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying 'conflate' to StateFlow has no effect. See the StateFlow documentation on Operator Fusion.", + message = "Applying conflate operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) public fun StateFlow.conflate(): Flow = noImpl() /** - * Applying [distinctUntilChanged][Flow.distinctUntilChanged] to [StateFlow] has no effect. - * See the [StateFlow] documentation on Operator Fusion. + * Returns this. + * Applying [distinctUntilChanged][Flow.distinctUntilChanged] operator to [StateFlow] has no effect. + * See [StateFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying 'distinctUntilChanged' to StateFlow has no effect. See the StateFlow documentation on Operator Fusion.", + message = "Applying distinctUntilChanged operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) public fun StateFlow.distinctUntilChanged(): Flow = noImpl() -@Deprecated( - message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." + - "Use currentCoroutineContext().isActive or cancellable() operator instead " + - "or specify the receiver of isActive explicitly. " + - "Additionally, flow {} builder emissions are cancellable by default.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("currentCoroutineContext().isActive") -) -public val FlowCollector<*>.isActive: Boolean - get() = noImpl() - -@Deprecated( - message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." + - "Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("currentCoroutineContext().cancel(cause)") -) -public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl() - -@Deprecated( - message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." + - "Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("currentCoroutineContext()") -) -public val FlowCollector<*>.coroutineContext: CoroutineContext - get() = noImpl() \ No newline at end of file +//@Deprecated( +// message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." + +// "Use currentCoroutineContext().isActive or cancellable() operator instead " + +// "or specify the receiver of isActive explicitly. " + +// "Additionally, flow {} builder emissions are cancellable by default.", +// level = DeprecationLevel.WARNING, // ERROR in 1.4 +// replaceWith = ReplaceWith("currentCoroutineContext().isActive") +//) +//public val FlowCollector<*>.isActive: Boolean +// get() = noImpl() +// +//@Deprecated( +// message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." + +// "Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly", +// level = DeprecationLevel.WARNING, // ERROR in 1.4 +// replaceWith = ReplaceWith("currentCoroutineContext().cancel(cause)") +//) +//public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl() +// +//@Deprecated( +// message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." + +// "Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly", +// level = DeprecationLevel.WARNING, // ERROR in 1.4 +// replaceWith = ReplaceWith("currentCoroutineContext()") +//) +//public val FlowCollector<*>.coroutineContext: CoroutineContext +// get() = noImpl() \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt deleted file mode 100644 index 4dd89ee4bf..0000000000 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:JvmMultifileClass -@file:JvmName("FlowKt") - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.internal.* -import kotlin.coroutines.* -import kotlin.jvm.* - -// -------------------------------- shareIn -------------------------------- - -/** - * Converts a _cold_ [Flow] into a _hot_ [SharedFlow] that is started in the given coroutine [scope], - * sharing emissions from a single running instance of the upstream flow with multiple downstream subscribers, - * and replaying a specified number of [replay] values to new subscribers. See the [SharedFlow] documentation - * for the general concepts of shared flows. - * - * The starting of the sharing coroutine is controlled by the [started] parameter. The following options - * are supported. - * - * * [Eagerly][SharingStarted.Eagerly] — the upstream flow is started even before the first subscriber appears. Note - * that in this case all values emitted by the upstream beyond the most recent values as specified by - * [replay] parameter **will be immediately discarded**. - * * [Lazily][SharingStarted.Lazily] — starts the upstream flow after the first subscriber appears, which guarantees - * that this first subscriber gets all the emitted values, while subsequent subscribers are only guaranteed to - * get the most recent [replay] values. The upstream flow continues to be active even when all subscribers - * disappear, but only the most recent [replay] values are cached without subscribers. - * * [WhileSubscribed()][SharingStarted.WhileSubscribed] — starts the upstream flow when the first subscriber - * appears, immediately stops when the last subscriber disappears, keeping the replay cache forever. - * It has additional optional configuration parameters as explained in its documentation. - * * A custom strategy can be supplied by implementing the [SharingStarted] interface. - * - * The `shareIn` operator is useful in situations when there is a _cold_ flow that is expensive to create and/or - * to maintain, but there are multiple subscribers that need to collect its values. For example, consider a - * flow of messages coming from a backend over the expensive network connection, taking a lot of - * time to establish. Conceptually, it might be implemented like this: - * - * ``` - * val backendMessages: Flow = flow { - * connectToBackend() // takes a lot of time - * try { - * while (true) { - * emit(receiveMessageFromBackend()) - * } - * } finally { - * disconnectFromBackend() - * } - * } - * ``` - * - * If this flow is directly used in the application, then every time it is collected a fresh connection is - * established, and it will take a while before messages start flowing. However, we can share a single connection - * and establish it eagerly like this: - * - * ``` - * val messages: SharedFlow = backendMessages.shareIn(scope, SharingStarted.Eagerly) - * ``` - * - * Now a single connection is shared between all collectors from `messages`, and there is a chance that the connection - * is already established by the time it is needed. - * - * ### Upstream completion and error handling - * - * **Normal completion of the upstream flow has no effect on subscribers**, and the sharing coroutine continues to run. If a - * a strategy like [SharingStarted.WhileSubscribed] is used, then the upstream can get restarted again. If a special - * action on upstream completion is needed, then an [onCompletion] operator can be used before the - * `shareIn` operator to emit a special value in this case, like this: - * - * ``` - * backendMessages - * .onCompletion { cause -> if (cause == null) emit(UpstreamHasCompletedMessage) } - * .shareIn(scope, SharingStarted.Eagerly) - * ``` - * - * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, - * and will be handled by the [scope] in which the sharing coroutine is launched. Custom exception handling - * can be configured by using the [catch] or [retry] operators before the `shareIn` operator. - * For example, to retry connection on any `IOException` with 1 second delay between attempts, use: - * - * ``` - * val messages = backendMessages - * .retry { e -> - * val shallRetry = e is IOException // other exception are bugs - handle them - * if (shallRetry) delay(1000) - * shallRetry - * } - * .shareIn(scope, SharingStarted.Eagerly) - * ``` - * - * ### Initial value - * - * When a special initial value is needed to signal to subscribers that the upstream is still loading the data, - * use the [onStart] operator on the upstream flow. For example: - * - * ``` - * backendMessages - * .onStart { emit(UpstreamIsStartingMessage) } - * .shareIn(scope, SharingStarted.Eagerly, 1) // replay one most recent message - * ``` - * - * ### Buffering and conflation - * - * The `shareIn` operator runs the upstream flow in a separate coroutine, and buffers emissions from upstream as explained - * in the [buffer] operator's description, using a buffer of [replay] size or the default (whichever is larger). - * This default buffering can be overridden with an explicit buffer configuration by preceding the `shareIn` call - * with [buffer] or [conflate], for example: - * - * * `buffer(0).shareIn(scope, started, 0)` — overrides the default buffer size and creates a [SharedFlow] without a buffer. - * Effectively, it configures sequential processing between the upstream emitter and subscribers, - * as the emitter is suspended until all subscribers process the value. Note, that the value is still immediately - * discarded when there are no subscribers. - * * `buffer(b).shareIn(scope, started, r)` — creates a [SharedFlow] with `replay = r` and `extraBufferCapacity = b`. - * * `conflate().shareIn(scope, started, r)` — creates a [SharedFlow] with `replay = r`, `onBufferOverflow = DROP_OLDEST`, - * and `extraBufferCapacity = 1` when `replay == 0` to support this strategy. - * - * ### Operator fusion - * - * Application of [flowOn][Flow.flowOn], [buffer] with [RENDEZVOUS][Channel.RENDEZVOUS] capacity, - * or [cancellable] operators to the resulting shared flow has no effect. - * - * ### Exceptions - * - * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. - * - * @param scope the coroutine scope in which sharing is started. - * @param started the strategy that controls when sharing is started and stopped. - * @param replay the number of values replayed to new subscribers (cannot be negative, defaults to zero). - */ -@ExperimentalCoroutinesApi -public fun Flow.shareIn( - scope: CoroutineScope, - started: SharingStarted, - replay: Int = 0 -): SharedFlow { - val config = configureSharing(replay) - val shared = MutableSharedFlow( - replay = replay, - extraBufferCapacity = config.extraBufferCapacity, - onBufferOverflow = config.onBufferOverflow - ) - @Suppress("UNCHECKED_CAST") - scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T) - return shared.asSharedFlow() -} - -private class SharingConfig( - @JvmField val upstream: Flow, - @JvmField val extraBufferCapacity: Int, - @JvmField val onBufferOverflow: BufferOverflow, - @JvmField val context: CoroutineContext -) - -// Decomposes upstream flow to fuse with it when possible -private fun Flow.configureSharing(replay: Int): SharingConfig { - assert { replay >= 0 } - val defaultExtraCapacity = replay.coerceAtLeast(Channel.CHANNEL_DEFAULT_CAPACITY) - replay - // Combine with preceding buffer/flowOn and channel-using operators - if (this is ChannelFlow) { - // Check if this ChannelFlow can operate without a channel - val upstream = dropChannelOperators() - if (upstream != null) { // Yes, it can => eliminate the intermediate channel - return SharingConfig( - upstream = upstream, - extraBufferCapacity = when (capacity) { - Channel.OPTIONAL_CHANNEL, Channel.BUFFERED, 0 -> // handle special capacities - when { - onBufferOverflow == BufferOverflow.SUSPEND -> // buffer was configured with suspension - if (capacity == 0) 0 else defaultExtraCapacity // keep explicitly configured 0 or use default - replay == 0 -> 1 // no suspension => need at least buffer of one - else -> 0 // replay > 0 => no need for extra buffer beyond replay because we don't suspend - } - else -> capacity // otherwise just use the specified capacity as extra capacity - }, - onBufferOverflow = onBufferOverflow, - context = context - ) - } - } - // Add sharing operator on top with a default buffer - return SharingConfig( - upstream = this, - extraBufferCapacity = defaultExtraCapacity, - onBufferOverflow = BufferOverflow.SUSPEND, - context = EmptyCoroutineContext - ) -} - -// Launches sharing coroutine -private fun CoroutineScope.launchSharing( - context: CoroutineContext, - upstream: Flow, - shared: MutableSharedFlow, - started: SharingStarted, - initialValue: T -) { - launch(context) { // the single coroutine to rule the sharing - // Optimize common built-in started strategies - when { - started === SharingStarted.Eagerly -> { - // collect immediately & forever - upstream.collect(shared) - } - started === SharingStarted.Lazily -> { - // start collecting on the first subscriber - wait for it first - shared.subscriptionCount.first { it > 0 } - upstream.collect(shared) - } - else -> { - // other & custom strategies - started.command(shared.subscriptionCount) - .distinctUntilChanged() // only changes in command have effect - .collectLatest { // cancels block on new emission - when (it) { - SharingCommand.START -> upstream.collect(shared) // can be cancelled - SharingCommand.STOP -> { /* just cancel and do nothing else */ } - SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> { - if (initialValue === NO_VALUE) { - shared.resetReplayCache() // regular shared flow -> reset cache - } else { - shared.tryEmit(initialValue) // state flow -> reset to initial value - } - } - } - } - } - } - } -} - -// -------------------------------- stateIn -------------------------------- - -/** - * Converts a _cold_ [Flow] into a _hot_ [StateFlow] that is started in the given coroutine [scope], - * sharing the most recently emitted value from a single running instance of the upstream flow with multiple - * downstream subscribers. See the [StateFlow] documentation for the general concepts of state flows. - * - * The starting of the sharing coroutine is controlled by the [started] parameter, as explained in the - * documentation for [shareIn] operator. - * - * The `stateIn` operator is useful in situations when there is a _cold_ flow that provides updates to the - * value of some state and is expensive to create and/or to maintain, but there are multiple subscribers - * that need to collect the most recent state value. For example, consider a - * flow of state updates coming from a backend over the expensive network connection, taking a lot of - * time to establish. Conceptually it might be implemented like this: - * - * ``` - * val backendState: Flow = flow { - * connectToBackend() // takes a lot of time - * try { - * while (true) { - * emit(receiveStateUpdateFromBackend()) - * } - * } finally { - * disconnectFromBackend() - * } - * } - * ``` - * - * If this flow is directly used in the application, then every time it is collected a fresh connection is - * established, and it will take a while before state updates start flowing. However, we can share a single connection - * and establish it eagerly like this: - * - * ``` - * val state: StateFlow = backendMessages.stateIn(scope, SharingStarted.Eagerly, State.LOADING) - * ``` - * - * Now, a single connection is shared between all collectors from `state`, and there is a chance that the connection - * is already established by the time it is needed. - * - * ### Upstream completion and error handling - * - * **Normal completion of the upstream flow has no effect on subscribers**, and the sharing coroutine continues to run. If a - * a strategy like [SharingStarted.WhileSubscribed] is used, then the upstream can get restarted again. If a special - * action on upstream completion is needed, then an [onCompletion] operator can be used before - * the `stateIn` operator to emit a special value in this case. See the [shareIn] operator's documentation for an example. - * - * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, - * and will be handled by the [scope] in which the sharing coroutine is launched. Custom exception handling - * can be configured by using the [catch] or [retry] operators before the `stateIn` operator, similarly to - * the [shareIn] operator. - * - * ### Operator fusion - * - * Application of [flowOn][Flow.flowOn], [conflate][Flow.conflate], - * [buffer] with [CONFLATED][Channel.CONFLATED] or [RENDEZVOUS][Channel.RENDEZVOUS] capacity, - * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. - * - * @param scope the coroutine scope in which sharing is started. - * @param started the strategy that controls when sharing is started and stopped. - * @param initialValue the initial value of the state flow. - * This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy - * with the `replayExpirationMillis` parameter. - */ -@ExperimentalCoroutinesApi -public fun Flow.stateIn( - scope: CoroutineScope, - started: SharingStarted, - initialValue: T -): StateFlow { - val config = configureSharing(1) - val state = MutableStateFlow(initialValue) - scope.launchSharing(config.context, config.upstream, state, started, initialValue) - return state.asStateFlow() -} - -/** - * Starts the upstream flow in a given [scope], suspends until the first value is emitted, and returns a _hot_ - * [StateFlow] of future emissions, sharing the most recently emitted value from this running instance of the upstream flow - * with multiple downstream subscribers. See the [StateFlow] documentation for the general concepts of state flows. - * - * @param scope the coroutine scope in which sharing is started. - */ -@ExperimentalCoroutinesApi -public suspend fun Flow.stateIn(scope: CoroutineScope): StateFlow { - val config = configureSharing(1) - val result = CompletableDeferred>() - scope.launchSharingDeferred(config.context, config.upstream, result) - return result.await() -} - -private fun CoroutineScope.launchSharingDeferred( - context: CoroutineContext, - upstream: Flow, - result: CompletableDeferred> -) { - launch(context) { - var state: MutableStateFlow? = null - upstream.collect { value -> - state?.let { it.value = value } ?: run { - state = MutableStateFlow(value).also { - result.complete(it.asStateFlow()) - } - } - } - } -} - -// -------------------------------- asSharedFlow/asStateFlow -------------------------------- - -/** - * Represents this mutable shared flow as a read-only shared flow. - */ -@ExperimentalCoroutinesApi -public fun MutableSharedFlow.asSharedFlow(): SharedFlow = - ReadonlySharedFlow(this) - -/** - * Represents this mutable state flow as a read-only state flow. - */ -@ExperimentalCoroutinesApi -public fun MutableStateFlow.asStateFlow(): StateFlow = - ReadonlyStateFlow(this) - -private class ReadonlySharedFlow( - flow: SharedFlow -) : SharedFlow by flow, CancellableFlow, FusibleFlow { - override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = - fuseSharedFlow(context, capacity, onBufferOverflow) -} - -private class ReadonlyStateFlow( - flow: StateFlow -) : StateFlow by flow, CancellableFlow, FusibleFlow { - override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = - fuseStateFlow(context, capacity, onBufferOverflow) -} - -// -------------------------------- onSubscription -------------------------------- - -/** - * Returns a flow that invokes the given [action] **after** this shared flow starts to be collected - * (after the subscription is registered). - * - * The [action] is called before any value is emitted from the upstream - * flow to this subscription but after the subscription is established. It is guaranteed that all emissions to - * the upstream flow that happen inside or immediately after this `onSubscription` action will be - * collected by this subscription. - * - * The receiver of the [action] is [FlowCollector], so `onSubscription` can emit additional elements. - */ -@ExperimentalCoroutinesApi -public fun SharedFlow.onSubscription(action: suspend FlowCollector.() -> Unit): SharedFlow = - SubscribedSharedFlow(this, action) - -private class SubscribedSharedFlow( - private val sharedFlow: SharedFlow, - private val action: suspend FlowCollector.() -> Unit -) : SharedFlow by sharedFlow { - override suspend fun collect(collector: FlowCollector) = - sharedFlow.collect(SubscribedFlowCollector(collector, action)) -} - -internal class SubscribedFlowCollector( - private val collector: FlowCollector, - private val action: suspend FlowCollector.() -> Unit -) : FlowCollector by collector { - suspend fun onSubscription() { - val safeCollector = SafeCollector(collector, currentCoroutineContext()) - try { - safeCollector.action() - } finally { - safeCollector.releaseIntercepted() - } - if (collector is SubscribedFlowCollector) collector.onSubscription() - } -} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index e3552d2893..520311ee5d 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -67,7 +67,7 @@ public fun Flow.withIndex(): Flow> = flow { } /** - * Returns a flow that invokes the given [action] **before** each value of the upstream flow is emitted downstream. + * Returns a flow which performs the given [action] on each value of the original flow. */ public fun Flow.onEach(action: suspend (T) -> Unit): Flow = transform { value -> action(value) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index 83f5498e4d..d36e1bbf7b 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -9,7 +9,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.flow.internal.* -import kotlinx.coroutines.internal.Symbol import kotlin.jvm.* /** @@ -48,39 +47,33 @@ public suspend inline fun Flow.fold( } /** - * The terminal operator that awaits for one and only one value to be emitted. + * The terminal operator, that awaits for one and only one value to be published. * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow * that contains more than one element. */ public suspend fun Flow.single(): T { var result: Any? = NULL collect { value -> - require(result === NULL) { "Flow has more than one element" } + if (result !== NULL) error("Expected only one element") result = value } - if (result === NULL) throw NoSuchElementException("Flow is empty") + if (result === NULL) throw NoSuchElementException("Expected at least one element") + @Suppress("UNCHECKED_CAST") return result as T } /** - * The terminal operator that awaits for one and only one value to be emitted. - * Returns the single value or `null`, if the flow was empty or emitted more than one value. + * The terminal operator, that awaits for one and only one value to be published. + * Throws [IllegalStateException] for flow that contains more than one element. */ -public suspend fun Flow.singleOrNull(): T? { - var result: Any? = NULL - collectWhile { - // No values yet, update result - if (result === NULL) { - result = it - true - } else { - // Second value, reset result and bail out - result = NULL - false - } +public suspend fun Flow.singleOrNull(): T? { + var result: T? = null + collect { value -> + if (result != null) error("Expected only one element") + result = value } - return if (result === NULL) null else result as T + return result } /** @@ -119,7 +112,7 @@ public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { * The terminal operator that returns the first element emitted by the flow and then cancels flow's collection. * Returns `null` if the flow was empty. */ -public suspend fun Flow.firstOrNull(): T? { +public suspend fun Flow.firstOrNull(): T? { var result: T? = null collectWhile { result = it @@ -129,10 +122,10 @@ public suspend fun Flow.firstOrNull(): T? { } /** - * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. + * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. * Returns `null` if the flow did not contain an element matching the [predicate]. */ -public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { +public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { var result: T? = null collectWhile { if (predicate(it)) { diff --git a/kotlinx-coroutines-core/common/src/internal/Atomic.kt b/kotlinx-coroutines-core/common/src/internal/Atomic.kt index a27d5491d1..94f6ab9cf2 100644 --- a/kotlinx-coroutines-core/common/src/internal/Atomic.kt +++ b/kotlinx-coroutines-core/common/src/internal/Atomic.kt @@ -39,8 +39,7 @@ public abstract class OpDescriptor { } @SharedImmutable -@JvmField -internal val NO_DECISION: Any = Symbol("NO_DECISION") +private val NO_DECISION: Any = Symbol("NO_DECISION") /** * Descriptor for multi-word atomic operation. @@ -53,13 +52,9 @@ internal val NO_DECISION: Any = Symbol("NO_DECISION") * * @suppress **This is unstable API and it is subject to change.** */ -@InternalCoroutinesApi public abstract class AtomicOp : OpDescriptor() { private val _consensus = atomic(NO_DECISION) - // Returns NO_DECISION when there is not decision yet - val consensus: Any? get() = _consensus.value - val isDecided: Boolean get() = _consensus.value !== NO_DECISION /** diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index b7b2954f6a..cf31fcf07d 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -2,10 +2,10 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines.internal +package kotlinx.coroutines import kotlinx.atomicfu.* -import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* import kotlin.native.concurrent.* @@ -19,7 +19,7 @@ internal val REUSABLE_CLAIMED = Symbol("REUSABLE_CLAIMED") internal class DispatchedContinuation( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation -) : DispatchedTask(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation by continuation { +) : DispatchedTask(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation by continuation { @JvmField @Suppress("PropertyName") internal var _state: Any? = UNDEFINED @@ -37,19 +37,19 @@ internal class DispatchedContinuation( * 3) [REUSABLE_CLAIMED]. CC is currently being reused and its owner executes `suspend` block: * ``` * // state == null | CC - * suspendCancellableCoroutineReusable { cont -> + * suspendAtomicCancellableCoroutineReusable { cont -> * // state == REUSABLE_CLAIMED * block(cont) * } * // state == CC * ``` - * 4) [Throwable] continuation was cancelled with this cause while being in [suspendCancellableCoroutineReusable], + * 4) [Throwable] continuation was cancelled with this cause while being in [suspendAtomicCancellableCoroutineReusable], * [CancellableContinuationImpl.getResult] will check for cancellation later. * * [REUSABLE_CLAIMED] state is required to prevent the lost resume in the channel. * AbstractChannel.receive method relies on the fact that the following pattern * ``` - * suspendCancellableCoroutineReusable { cont -> + * suspendAtomicCancellableCoroutineReusable { cont -> * val result = pollFastPath() * if (result != null) cont.resume(result) * } @@ -67,12 +67,12 @@ internal class DispatchedContinuation( /* * Reusability control: * `null` -> no reusability at all, false - * If current state is not CCI, then we are within `suspendCancellableCoroutineReusable`, true + * If current state is not CCI, then we are within `suspendAtomicCancellableCoroutineReusable`, true * Else, if result is CCI === requester. * Identity check my fail for the following pattern: * ``` * loop: - * suspendCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle + * suspendAtomicCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle * suspendCancellableCoroutine { } // **Not reusable**, handle should be disposed after {}, otherwise * it will leak because it won't be freed by `releaseInterceptedContinuation` * ``` @@ -83,7 +83,7 @@ internal class DispatchedContinuation( } /** - * Claims the continuation for [suspendCancellableCoroutineReusable] block, + * Claims the continuation for [suspendAtomicCancellableCoroutineReusable] block, * so all cancellations will be postponed. */ @Suppress("UNCHECKED_CAST") @@ -119,7 +119,7 @@ internal class DispatchedContinuation( * If continuation was cancelled, it becomes non-reusable. * * ``` - * suspendCancellableCoroutineReusable { // <- claimed + * suspendAtomicCancellableCoroutineReusable { // <- claimed * // Any asynchronous cancellation is "postponed" while this block * // is being executed * } // postponed cancellation is checked here in `getResult` @@ -180,10 +180,10 @@ internal class DispatchedContinuation( val state = result.toState() if (dispatcher.isDispatchNeeded(context)) { _state = state - resumeMode = MODE_ATOMIC + resumeMode = MODE_ATOMIC_DEFAULT dispatcher.dispatch(context, this) } else { - executeUnconfined(state, MODE_ATOMIC) { + executeUnconfined(state, MODE_ATOMIC_DEFAULT) { withCoroutineContext(this.context, countOrElement) { continuation.resumeWith(result) } @@ -194,42 +194,29 @@ internal class DispatchedContinuation( // We inline it to save an entry on the stack in cases where it shows (unconfined dispatcher) // It is used only in Continuation.resumeCancellableWith @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancellableWith( - result: Result, - noinline onCancellation: ((cause: Throwable) -> Unit)? - ) { - val state = result.toState(onCancellation) + inline fun resumeCancellableWith(result: Result) { + val state = result.toState() if (dispatcher.isDispatchNeeded(context)) { _state = state resumeMode = MODE_CANCELLABLE dispatcher.dispatch(context, this) } else { executeUnconfined(state, MODE_CANCELLABLE) { - if (!resumeCancelled(state)) { + if (!resumeCancelled()) { resumeUndispatchedWith(result) } } } } - // takeState had already cleared the state so we cancel takenState here - override fun cancelCompletedResult(takenState: Any?, cause: Throwable) { - // It is Ok to call onCancellation here without try/catch around it, since this function only faces - // a "bound" cancellation handler that performs the safe call to the user-specified code. - if (takenState is CompletedWithCancellation) { - takenState.onCancellation(cause) - } - } - @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancelled(state: Any?): Boolean { + inline fun resumeCancelled(): Boolean { val job = context[Job] if (job != null && !job.isActive) { - val cause = job.getCancellationException() - cancelCompletedResult(state, cause) - resumeWithException(cause) + resumeWithException(job.getCancellationException()) return true } + return false } @@ -258,11 +245,8 @@ internal class DispatchedContinuation( * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public fun Continuation.resumeCancellableWith( - result: Result, - onCancellation: ((cause: Throwable) -> Unit)? = null -): Unit = when (this) { - is DispatchedContinuation -> resumeCancellableWith(result, onCancellation) +public fun Continuation.resumeCancellableWith(result: Result): Unit = when (this) { + is DispatchedContinuation -> resumeCancellableWith(result) else -> resumeWith(result) } @@ -281,7 +265,6 @@ private inline fun DispatchedContinuation<*>.executeUnconfined( contState: Any?, mode: Int, doYield: Boolean = false, block: () -> Unit ): Boolean { - assert { mode != MODE_UNINITIALIZED } // invalid execution mode val eventLoop = ThreadLocalEventLoop.eventLoop // If we are yielding and unconfined queue is empty, we can bail out as part of fast path if (doYield && eventLoop.isUnconfinedQueueEmpty) return false diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 1f4942a358..32258ba101 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -8,44 +8,12 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -/** - * Non-cancellable dispatch mode. - * - * **DO NOT CHANGE THE CONSTANT VALUE**. It might be inlined into legacy user code that was calling - * inline `suspendAtomicCancellableCoroutine` function and did not support reuse. - */ -internal const val MODE_ATOMIC = 0 - -/** - * Cancellable dispatch mode. It is used by user-facing [suspendCancellableCoroutine]. - * Note, that implementation of cancellability checks mode via [Int.isCancellableMode] extension. - * - * **DO NOT CHANGE THE CONSTANT VALUE**. It is being into the user code from [suspendCancellableCoroutine]. - */ -@PublishedApi -internal const val MODE_CANCELLABLE = 1 - -/** - * Cancellable dispatch mode for [suspendCancellableCoroutineReusable]. - * Note, that implementation of cancellability checks mode via [Int.isCancellableMode] extension; - * implementation of reuse checks mode via [Int.isReusableMode] extension. - */ -internal const val MODE_CANCELLABLE_REUSABLE = 2 - -/** - * Undispatched mode for [CancellableContinuation.resumeUndispatched]. - * It is used when the thread is right, but it needs to be marked with the current coroutine. - */ -internal const val MODE_UNDISPATCHED = 4 +@PublishedApi internal const val MODE_ATOMIC_DEFAULT = 0 // schedule non-cancellable dispatch for suspendCoroutine +@PublishedApi internal const val MODE_CANCELLABLE = 1 // schedule cancellable dispatch for suspendCancellableCoroutine +@PublishedApi internal const val MODE_UNDISPATCHED = 2 // when the thread is right, but need to mark it with current coroutine -/** - * Initial mode for [DispatchedContinuation] implementation, should never be used for dispatch, because it is always - * overwritten when continuation is resumed with the actual resume mode. - */ -internal const val MODE_UNINITIALIZED = -1 - -internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE || this == MODE_CANCELLABLE_REUSABLE -internal val Int.isReusableMode get() = this == MODE_CANCELLABLE_REUSABLE +internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE +internal val Int.isDispatchedMode get() = this == MODE_ATOMIC_DEFAULT || this == MODE_CANCELLABLE internal abstract class DispatchedTask( @JvmField public var resumeMode: Int @@ -54,32 +22,16 @@ internal abstract class DispatchedTask( internal abstract fun takeState(): Any? - /** - * Called when this task was cancelled while it was being dispatched. - */ - internal open fun cancelCompletedResult(takenState: Any?, cause: Throwable) {} + internal open fun cancelResult(state: Any?, cause: Throwable) {} - /** - * There are two implementations of `DispatchedTask`: - * * [DispatchedContinuation] keeps only simple values as successfully results. - * * [CancellableContinuationImpl] keeps additional data with values and overrides this method to unwrap it. - */ @Suppress("UNCHECKED_CAST") internal open fun getSuccessfulResult(state: Any?): T = state as T - /** - * There are two implementations of `DispatchedTask`: - * * [DispatchedContinuation] is just an intermediate storage that stores the exception that has its stack-trace - * properly recovered and is ready to pass to the [delegate] continuation directly. - * * [CancellableContinuationImpl] stores raw cause of the failure in its state; when it needs to be dispatched - * its stack-trace has to be recovered, so it overrides this method for that purpose. - */ - internal open fun getExceptionalResult(state: Any?): Throwable? = + internal fun getExceptionalResult(state: Any?): Throwable? = (state as? CompletedExceptionally)?.cause public final override fun run() { - assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching val taskContext = this.taskContext var fatalException: Throwable? = null try { @@ -89,22 +41,19 @@ internal abstract class DispatchedTask( val state = takeState() // NOTE: Must take state in any case, even if cancelled withCoroutineContext(context, delegate.countOrElement) { val exception = getExceptionalResult(state) + val job = if (resumeMode.isCancellableMode) context[Job] else null /* * Check whether continuation was originally resumed with an exception. * If so, it dominates cancellation, otherwise the original exception * will be silently lost. */ - val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null - if (job != null && !job.isActive) { + if (exception == null && job != null && !job.isActive) { val cause = job.getCancellationException() - cancelCompletedResult(state, cause) + cancelResult(state, cause) continuation.resumeWithStackTrace(cause) } else { - if (exception != null) { - continuation.resumeWithException(exception) - } else { - continuation.resume(getSuccessfulResult(state)) - } + if (exception != null) continuation.resumeWithException(exception) + else continuation.resume(getSuccessfulResult(state)) } } } catch (e: Throwable) { @@ -148,10 +97,8 @@ internal abstract class DispatchedTask( } internal fun DispatchedTask.dispatch(mode: Int) { - assert { mode != MODE_UNINITIALIZED } // invalid mode value for this method val delegate = this.delegate - val undispatched = mode == MODE_UNDISPATCHED - if (!undispatched && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { + if (mode.isDispatchedMode && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { // dispatch directly using this instance's Runnable implementation val dispatcher = delegate.dispatcher val context = delegate.context @@ -161,21 +108,21 @@ internal fun DispatchedTask.dispatch(mode: Int) { resumeUnconfined() } } else { - // delegate is coming from 3rd-party interceptor implementation (and does not support cancellation) - // or undispatched mode was requested - resume(delegate, undispatched) + resume(delegate, mode) } } @Suppress("UNCHECKED_CAST") -internal fun DispatchedTask.resume(delegate: Continuation, undispatched: Boolean) { - // This resume is never cancellable. The result is always delivered to delegate continuation. +internal fun DispatchedTask.resume(delegate: Continuation, useMode: Int) { + // slow-path - use delegate val state = takeState() - val exception = getExceptionalResult(state) + val exception = getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } val result = if (exception != null) Result.failure(exception) else Result.success(getSuccessfulResult(state)) - when { - undispatched -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) - else -> delegate.resumeWith(result) + when (useMode) { + MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) + MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) + MODE_UNDISPATCHED -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) + else -> error("Invalid mode $useMode") } } @@ -187,7 +134,7 @@ private fun DispatchedTask<*>.resumeUnconfined() { } else { // Was not active -- run event loop until all unconfined tasks are executed runUnconfinedEventLoop(eventLoop) { - resume(delegate, undispatched = true) + resume(delegate, MODE_UNDISPATCHED) } } } diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index 8508e39239..f1663c3ddc 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -73,7 +73,6 @@ public expect abstract class AbstractAtomicDesc : AtomicDesc { protected open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure public open fun onPrepare(prepareOp: PrepareOp): Any? // non-null on failure - public open fun onRemoved(affected: LockFreeLinkedListNode) // non-null on failure protected abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } diff --git a/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt b/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt deleted file mode 100644 index 1744359e93..0000000000 --- a/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlinx.coroutines.* -import kotlin.coroutines.* - -internal typealias OnUndeliveredElement = (E) -> Unit - -internal fun OnUndeliveredElement.callUndeliveredElementCatchingException( - element: E, - undeliveredElementException: UndeliveredElementException? = null -): UndeliveredElementException? { - try { - invoke(element) - } catch (ex: Throwable) { - // undeliveredElementException.cause !== ex is an optimization in case the same exception is thrown - // over and over again by on OnUndeliveredElement - if (undeliveredElementException != null && undeliveredElementException.cause !== ex) { - undeliveredElementException.addSuppressedThrowable(ex) - } else { - return UndeliveredElementException("Exception in undelivered element handler for $element", ex) - } - } - return undeliveredElementException -} - -internal fun OnUndeliveredElement.callUndeliveredElement(element: E, context: CoroutineContext) { - callUndeliveredElementCatchingException(element, null)?.let { ex -> - handleCoroutineException(context, ex) - } -} - -internal fun OnUndeliveredElement.bindCancellationFun(element: E, context: CoroutineContext): (Throwable) -> Unit = - { _: Throwable -> callUndeliveredElement(element, context) } - -/** - * Internal exception that is thrown when [OnUndeliveredElement] handler in - * a [kotlinx.coroutines.channels.Channel] throws an exception. - */ -internal class UndeliveredElementException(message: String, cause: Throwable) : RuntimeException(message, cause) diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt index f814b152b2..1b1c389dc4 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt @@ -5,7 +5,6 @@ package kotlinx.coroutines.intrinsics import kotlinx.coroutines.* -import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -22,12 +21,9 @@ public fun (suspend () -> T).startCoroutineCancellable(completion: Continuat * Use this function to start coroutine in a cancellable way, so that it can be cancelled * while waiting to be dispatched. */ -internal fun (suspend (R) -> T).startCoroutineCancellable( - receiver: R, completion: Continuation, - onCancellation: ((cause: Throwable) -> Unit)? = null -) = +internal fun (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation) = runSafely(completion) { - createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation) + createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit)) } /** diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 99c54f8417..e744a0c724 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -189,8 +189,14 @@ public interface SelectInstance { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * + * Atomicity of cancellation depends on the clause: [onSend][SendChannel.onSend], [onReceive][ReceiveChannel.onReceive], + * [onReceiveOrNull][ReceiveChannel.onReceiveOrNull], and [onLock][Mutex.onLock] clauses are + * *atomically cancellable*. When select throws [CancellationException] it means that those clauses had not performed + * their respective operations. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this select operation + * was already resumed on atomically cancellable clause and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -649,7 +655,7 @@ internal class SelectBuilderImpl( if (trySelect()) block.startCoroutineCancellable(completion) // shall be cancellable while waits for dispatch } - disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action, context)) + disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action)) } private class DisposeNode( diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 73aaab5fbf..61e046c77a 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -45,10 +45,12 @@ public interface Mutex { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. - * This function releases the lock if it was already acquired by this function before the [CancellationException] - * was thrown. + * + * *Cancellation of suspended lock invocation is atomic* -- when this function + * throws [CancellationException] it means that the mutex was not locked. + * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may + * continue to execute even after it was cancelled from the same thread in the case when this lock operation + * was already resumed and the continuation was posted for execution to the thread's queue. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -122,6 +124,8 @@ public suspend inline fun Mutex.withLock(owner: Any? = null, action: () -> T @SharedImmutable private val LOCK_FAIL = Symbol("LOCK_FAIL") @SharedImmutable +private val ENQUEUE_FAIL = Symbol("ENQUEUE_FAIL") +@SharedImmutable private val UNLOCK_FAIL = Symbol("UNLOCK_FAIL") @SharedImmutable private val SELECT_SUCCESS = Symbol("SELECT_SUCCESS") @@ -190,7 +194,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { return lockSuspend(owner) } - private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable sc@ { cont -> + private suspend fun lockSuspend(owner: Any?) = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val waiter = LockCont(owner, cont) _state.loop { state -> when (state) { @@ -250,7 +254,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { } is LockedQueue -> { check(state.owner !== owner) { "Already locked by $owner" } - val node = LockSelect(owner, select, block) + val node = LockSelect(owner, this, select, block) if (state.addLastIf(node) { _state.value === state }) { // successfully enqueued select.disposeOnSelect(node) @@ -348,7 +352,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { override fun toString(): String = "LockedQueue[$owner]" } - private abstract inner class LockWaiter( + private abstract class LockWaiter( @JvmField val owner: Any? ) : LockFreeLinkedListNode(), DisposableHandle { final override fun dispose() { remove() } @@ -356,32 +360,27 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { abstract fun completeResumeLockWaiter(token: Any) } - private inner class LockCont( + private class LockCont( owner: Any?, @JvmField val cont: CancellableContinuation ) : LockWaiter(owner) { - override fun tryResumeLockWaiter() = cont.tryResume(Unit, idempotent = null) { - // if this continuation gets cancelled during dispatch to the caller, then release the lock - unlock(owner) - } + override fun tryResumeLockWaiter() = cont.tryResume(Unit) override fun completeResumeLockWaiter(token: Any) = cont.completeResume(token) - override fun toString(): String = "LockCont[$owner, $cont] for ${this@MutexImpl}" + override fun toString(): String = "LockCont[$owner, $cont]" } - private inner class LockSelect( + private class LockSelect( owner: Any?, + @JvmField val mutex: Mutex, @JvmField val select: SelectInstance, @JvmField val block: suspend (Mutex) -> R ) : LockWaiter(owner) { override fun tryResumeLockWaiter(): Any? = if (select.trySelect()) SELECT_SUCCESS else null override fun completeResumeLockWaiter(token: Any) { assert { token === SELECT_SUCCESS } - block.startCoroutineCancellable(receiver = this@MutexImpl, completion = select.completion) { - // if this continuation gets cancelled during dispatch to the caller, then release the lock - unlock(owner) - } + block.startCoroutine(receiver = mutex, completion = select.completion) } - override fun toString(): String = "LockSelect[$owner, $select] for ${this@MutexImpl}" + override fun toString(): String = "LockSelect[$owner, $mutex, $select]" } // atomic unlock operation that checks that waiters queue is empty diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index 84b7f4f8a2..27c976ce3f 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -33,10 +33,9 @@ public interface Semaphore { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. - * This function releases the semaphore if it was already acquired by this function before the [CancellationException] - * was thrown. + * + * *Cancellation of suspended semaphore acquisition is atomic* -- when this function + * throws [CancellationException] it means that the semaphore was not acquired. * * Note, that this function does not check for cancellation when it does not suspend. * Use [CoroutineScope.isActive] or [CoroutineScope.ensureActive] to periodically @@ -149,8 +148,6 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se private val _availablePermits = atomic(permits - acquiredPermits) override val availablePermits: Int get() = max(_availablePermits.value, 0) - private val onCancellationRelease = { _: Throwable -> release() } - override fun tryAcquire(): Boolean { _availablePermits.loop { p -> if (p <= 0) return false @@ -167,7 +164,7 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se acquireSlowPath() } - private suspend fun acquireSlowPath() = suspendCancellableCoroutineReusable sc@ { cont -> + private suspend fun acquireSlowPath() = suspendAtomicCancellableCoroutineReusable sc@ { cont -> while (true) { if (addAcquireToQueue(cont)) return@sc val p = _availablePermits.getAndDecrement() @@ -206,8 +203,6 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se // On CAS failure -- the cell must be either PERMIT or BROKEN // If the cell already has PERMIT from tryResumeNextFromQueue, try to grab it if (segment.cas(i, PERMIT, TAKEN)) { // took permit thus eliminating acquire/release pair - // The following resume must always succeed, since continuation was not published yet and we don't have - // to pass onCancellationRelease handle, since the coroutine did not suspend yet and cannot be cancelled cont.resume(Unit) return true } @@ -237,15 +232,15 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se return !segment.cas(i, PERMIT, BROKEN) } cellState === CANCELLED -> return false // the acquire was already cancelled - else -> return (cellState as CancellableContinuation).tryResumeAcquire() + else -> return (cellState as CancellableContinuation).tryResume() } } +} - private fun CancellableContinuation.tryResumeAcquire(): Boolean { - val token = tryResume(Unit, null, onCancellationRelease) ?: return false - completeResume(token) - return true - } +private fun CancellableContinuation.tryResume(): Boolean { + val token = tryResume(Unit) ?: return false + completeResume(token) + return true } private class CancelSemaphoreAcquisitionHandler( diff --git a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt index c763faf225..a9f58dd6ee 100644 --- a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt +++ b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt @@ -87,23 +87,23 @@ class AtomicCancellationCommonTest : TestBase() { } @Test - fun testLockCancellable() = runTest { + fun testLockAtomicCancel() = runTest { expect(1) val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) mutex.lock() // suspends - expectUnreached() // should NOT execute because of cancellation + expect(4) // should execute despite cancellation } expect(3) mutex.unlock() // unlock mutex first job.cancel() // cancel the job next yield() // now yield - finish(4) + finish(5) } @Test - fun testSelectLockCancellable() = runTest { + fun testSelectLockAtomicCancel() = runTest { expect(1) val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -114,12 +114,13 @@ class AtomicCancellationCommonTest : TestBase() { "OK" } } - expectUnreached() // should NOT execute because of cancellation + assertEquals("OK", result) + expect(5) // should execute despite cancellation } expect(3) mutex.unlock() // unlock mutex first job.cancel() // cancel the job next yield() // now yield - finish(4) + finish(6) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt b/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt deleted file mode 100644 index 2fe0c914e6..0000000000 --- a/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import kotlin.test.* - -class AwaitCancellationTest : TestBase() { - - @Test - fun testCancellation() = runTest(expected = { it is CancellationException }) { - expect(1) - coroutineScope { - val deferred: Deferred = async { - expect(2) - awaitCancellation() - } - yield() - expect(3) - require(deferred.isActive) - deferred.cancel() - finish(4) - deferred.await() - } - } -} diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt index 3c11182e00..00f719e632 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt @@ -23,23 +23,10 @@ class CancellableContinuationHandlersTest : TestBase() { fun testDoubleSubscriptionAfterCompletion() = runTest { suspendCancellableCoroutine { c -> c.resume(Unit) - // First invokeOnCancellation is Ok + // Nothing happened + c.invokeOnCancellation { expectUnreached() } + // Cannot validate after completion c.invokeOnCancellation { expectUnreached() } - // Second invokeOnCancellation is not allowed - assertFailsWith { c.invokeOnCancellation { expectUnreached() } } - } - } - - @Test - fun testDoubleSubscriptionAfterCompletionWithException() = runTest { - assertFailsWith { - suspendCancellableCoroutine { c -> - c.resumeWithException(TestException()) - // First invokeOnCancellation is Ok - c.invokeOnCancellation { expectUnreached() } - // Second invokeOnCancellation is not allowed - assertFailsWith { c.invokeOnCancellation { expectUnreached() } } - } } } @@ -59,59 +46,6 @@ class CancellableContinuationHandlersTest : TestBase() { } } - @Test - fun testSecondSubscriptionAfterCancellation() = runTest { - try { - suspendCancellableCoroutine { c -> - // Set IOC first - c.invokeOnCancellation { - assertNull(it) - expect(2) - } - expect(1) - // then cancel (it gets called) - c.cancel() - // then try to install another one - assertFailsWith { c.invokeOnCancellation { expectUnreached() } } - } - } catch (e: CancellationException) { - finish(3) - } - } - - @Test - fun testSecondSubscriptionAfterResumeCancelAndDispatch() = runTest { - var cont: CancellableContinuation? = null - val job = launch(start = CoroutineStart.UNDISPATCHED) { - // will be cancelled during dispatch - assertFailsWith { - suspendCancellableCoroutine { c -> - cont = c - // Set IOC first -- not called (completed) - c.invokeOnCancellation { - assertTrue(it is CancellationException) - expect(4) - } - expect(1) - } - } - expect(5) - } - expect(2) - // then resume it - cont!!.resume(Unit) // schedule cancelled continuation for dispatch - // then cancel the job during dispatch - job.cancel() - expect(3) - yield() // finish dispatching (will call IOC handler here!) - expect(6) - // then try to install another one after we've done dispatching it - assertFailsWith { - cont!!.invokeOnCancellation { expectUnreached() } - } - finish(7) - } - @Test fun testDoubleSubscriptionAfterCancellationWithCause() = runTest { try { diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt index f9f610ceb5..38fc9ff281 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt @@ -116,26 +116,4 @@ class CancellableContinuationTest : TestBase() { continuation!!.resume(Unit) // Should not fail finish(4) } - - @Test - fun testCompleteJobWhileSuspended() = runTest { - expect(1) - val completableJob = Job() - val coroutineBlock = suspend { - assertFailsWith { - suspendCancellableCoroutine { cont -> - expect(2) - assertSame(completableJob, cont.context[Job]) - completableJob.complete() - } - expectUnreached() - } - expect(3) - } - coroutineBlock.startCoroutine(Continuation(completableJob) { - assertEquals(Unit, it.getOrNull()) - expect(4) - }) - finish(5) - } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt index fbfa082555..39176a9a94 100644 --- a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 @@ -44,33 +44,6 @@ class CancellableResumeTest : TestBase() { expectUnreached() } - @Test - fun testResumeImmediateAfterCancelWithHandlerFailure() = runTest( - expected = { it is TestException }, - unhandled = listOf( - { it is CompletionHandlerException && it.cause is TestException2 }, - { it is CompletionHandlerException && it.cause is TestException3 } - ) - ) { - expect(1) - suspendCancellableCoroutine { cont -> - expect(2) - cont.invokeOnCancellation { - expect(3) - throw TestException2("FAIL") // invokeOnCancellation handler fails with exception - } - cont.cancel(TestException("FAIL")) - expect(4) - cont.resume("OK") { cause -> - expect(5) - assertTrue(cause is TestException) - throw TestException3("FAIL") // onCancellation block fails with exception - } - finish(6) - } - expectUnreached() - } - @Test fun testResumeImmediateAfterIndirectCancel() = runTest( expected = { it is CancellationException } @@ -90,33 +63,6 @@ class CancellableResumeTest : TestBase() { expectUnreached() } - @Test - fun testResumeImmediateAfterIndirectCancelWithHandlerFailure() = runTest( - expected = { it is CancellationException }, - unhandled = listOf( - { it is CompletionHandlerException && it.cause is TestException2 }, - { it is CompletionHandlerException && it.cause is TestException3 } - ) - ) { - expect(1) - val ctx = coroutineContext - suspendCancellableCoroutine { cont -> - expect(2) - cont.invokeOnCancellation { - expect(3) - throw TestException2("FAIL") // invokeOnCancellation handler fails with exception - } - ctx.cancel() - expect(4) - cont.resume("OK") { cause -> - expect(5) - throw TestException3("FAIL") // onCancellation block fails with exception - } - finish(6) - } - expectUnreached() - } - @Test fun testResumeLaterNormally() = runTest { expect(1) @@ -163,42 +109,6 @@ class CancellableResumeTest : TestBase() { expect(8) } - @Test - fun testResumeLaterAfterCancelWithHandlerFailure() = runTest( - unhandled = listOf( - { it is CompletionHandlerException && it.cause is TestException2 }, - { it is CompletionHandlerException && it.cause is TestException3 } - ) - ) { - expect(1) - lateinit var cc: CancellableContinuation - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - try { - suspendCancellableCoroutine { cont -> - expect(3) - cont.invokeOnCancellation { - expect(5) - throw TestException2("FAIL") // invokeOnCancellation handler fails with exception - } - cc = cont - } - expectUnreached() - } catch (e: CancellationException) { - finish(9) - } - } - expect(4) - job.cancel(TestCancellationException()) - expect(6) - cc.resume("OK") { cause -> - expect(7) - assertTrue(cause is TestCancellationException) - throw TestException3("FAIL") // onCancellation block fails with exception - } - expect(8) - } - @Test fun testResumeCancelWhileDispatched() = runTest { expect(1) @@ -208,86 +118,36 @@ class CancellableResumeTest : TestBase() { try { suspendCancellableCoroutine { cont -> expect(3) - // resumed first, dispatched, then cancelled, but still got invokeOnCancellation call - cont.invokeOnCancellation { cause -> - // Note: invokeOnCancellation is called before cc.resume(value) { ... } handler - expect(7) - assertTrue(cause is TestCancellationException) - } + // resumed first, then cancelled, so no invokeOnCancellation call + cont.invokeOnCancellation { expectUnreached() } cc = cont } expectUnreached() } catch (e: CancellationException) { - expect(9) + expect(8) } } expect(4) cc.resume("OK") { cause -> - // Note: this handler is called after invokeOnCancellation handler - expect(8) + expect(7) assertTrue(cause is TestCancellationException) } expect(5) job.cancel(TestCancellationException()) // cancel while execution is dispatched expect(6) yield() // to coroutine -- throws cancellation exception - finish(10) + finish(9) } - @Test - fun testResumeCancelWhileDispatchedWithHandlerFailure() = runTest( - unhandled = listOf( - { it is CompletionHandlerException && it.cause is TestException2 }, - { it is CompletionHandlerException && it.cause is TestException3 } - ) - ) { - expect(1) - lateinit var cc: CancellableContinuation - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - try { - suspendCancellableCoroutine { cont -> - expect(3) - // resumed first, dispatched, then cancelled, but still got invokeOnCancellation call - cont.invokeOnCancellation { cause -> - // Note: invokeOnCancellation is called before cc.resume(value) { ... } handler - expect(7) - assertTrue(cause is TestCancellationException) - throw TestException2("FAIL") // invokeOnCancellation handler fails with exception - } - cc = cont - } - expectUnreached() - } catch (e: CancellationException) { - expect(9) - } - } - expect(4) - cc.resume("OK") { cause -> - // Note: this handler is called after invokeOnCancellation handler - expect(8) - assertTrue(cause is TestCancellationException) - throw TestException3("FAIL") // onCancellation block fails with exception - } - expect(5) - job.cancel(TestCancellationException()) // cancel while execution is dispatched - expect(6) - yield() // to coroutine -- throws cancellation exception - finish(10) - } @Test fun testResumeUnconfined() = runTest { val outerScope = this withContext(Dispatchers.Unconfined) { val result = suspendCancellableCoroutine { - outerScope.launch { - it.resume("OK") { - expectUnreached() - } - } + outerScope.launch { it.resume("OK", {}) } } assertEquals("OK", result) } } -} +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt b/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt deleted file mode 100644 index b69eb22e17..0000000000 --- a/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import kotlin.coroutines.* -import kotlin.test.* - -/** - * When using [suspendCoroutine] from the standard library the continuation must be dispatched atomically, - * without checking for cancellation at any point in time. - */ -class DispatchedContinuationTest : TestBase() { - private lateinit var cont: Continuation - - @Test - fun testCancelThenResume() = runTest { - expect(1) - launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - coroutineContext[Job]!!.cancel() - // a regular suspendCoroutine will still suspend despite the fact that coroutine was cancelled - val value = suspendCoroutine { - expect(3) - cont = it - } - expect(6) - assertEquals("OK", value) - } - expect(4) - cont.resume("OK") - expect(5) - yield() // to the launched job - finish(7) - } - - @Test - fun testCancelThenResumeUnconfined() = runTest { - expect(1) - launch(Dispatchers.Unconfined) { - expect(2) - coroutineContext[Job]!!.cancel() - // a regular suspendCoroutine will still suspend despite the fact that coroutine was cancelled - val value = suspendCoroutine { - expect(3) - cont = it - } - expect(5) - assertEquals("OK", value) - } - expect(4) - cont.resume("OK") // immediately resumes -- because unconfined - finish(6) - } - - @Test - fun testResumeThenCancel() = runTest { - expect(1) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - val value = suspendCoroutine { - expect(3) - cont = it - } - expect(7) - assertEquals("OK", value) - } - expect(4) - cont.resume("OK") - expect(5) - // now cancel the job, which the coroutine is waiting to be dispatched - job.cancel() - expect(6) - yield() // to the launched job - finish(8) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt index 91d941b32c..a6ddd81185 100644 --- a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt @@ -42,7 +42,7 @@ class BasicOperationsTest : TestBase() { @Test fun testInvokeOnClose() = TestChannelKind.values().forEach { kind -> reset() - val channel = kind.create() + val channel = kind.create() channel.invokeOnClose { if (it is AssertionError) { expect(3) @@ -59,7 +59,7 @@ class BasicOperationsTest : TestBase() { fun testInvokeOnClosed() = TestChannelKind.values().forEach { kind -> reset() expect(1) - val channel = kind.create() + val channel = kind.create() channel.close() channel.invokeOnClose { expect(2) } assertFailsWith { channel.invokeOnClose { expect(3) } } @@ -69,7 +69,7 @@ class BasicOperationsTest : TestBase() { @Test fun testMultipleInvokeOnClose() = TestChannelKind.values().forEach { kind -> reset() - val channel = kind.create() + val channel = kind.create() channel.invokeOnClose { expect(3) } expect(1) assertFailsWith { channel.invokeOnClose { expect(4) } } @@ -81,7 +81,7 @@ class BasicOperationsTest : TestBase() { @Test fun testIterator() = runTest { TestChannelKind.values().forEach { kind -> - val channel = kind.create() + val channel = kind.create() val iterator = channel.iterator() assertFailsWith { iterator.next() } channel.close() @@ -91,7 +91,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testReceiveOrNull(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async(NonCancellable) { channel.receive() } @@ -108,7 +108,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testReceiveOrNullException(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async(NonCancellable) { channel.receive() } @@ -132,7 +132,7 @@ class BasicOperationsTest : TestBase() { @Suppress("ReplaceAssertBooleanWithAssertEquality") private suspend fun testReceiveOrClosed(kind: TestChannelKind) = coroutineScope { reset() - val channel = kind.create() + val channel = kind.create() launch { expect(2) channel.send(1) @@ -159,7 +159,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testOffer(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async { channel.send(42) } yield() channel.close() @@ -184,7 +184,7 @@ class BasicOperationsTest : TestBase() { private suspend fun testSendAfterClose(kind: TestChannelKind) { assertFailsWith { coroutineScope { - val channel = kind.create() + val channel = kind.create() channel.close() launch { @@ -195,7 +195,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testSendReceive(kind: TestChannelKind, iterations: Int) = coroutineScope { - val channel = kind.create() + val channel = kind.create() launch { repeat(iterations) { channel.send(it) } channel.close() diff --git a/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt b/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt index ab1a85d697..bb3142e54c 100644 --- a/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt @@ -63,7 +63,7 @@ class BroadcastTest : TestBase() { val a = produce { expect(3) send("MSG") - expectUnreached() // is not executed, because send is cancelled + expect(5) } expect(2) yield() // to produce @@ -72,7 +72,7 @@ class BroadcastTest : TestBase() { expect(4) yield() // to abort produce assertTrue(a.isClosedForReceive) // the source channel was consumed - finish(5) + finish(6) } @Test diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt deleted file mode 100644 index 41f60479f2..0000000000 --- a/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.coroutines.* -import kotlin.test.* - -class ChannelBufferOverflowTest : TestBase() { - @Test - fun testDropLatest() = runTest { - val c = Channel(2, BufferOverflow.DROP_LATEST) - assertTrue(c.offer(1)) - assertTrue(c.offer(2)) - assertTrue(c.offer(3)) // overflows, dropped - c.send(4) // overflows dropped - assertEquals(1, c.receive()) - assertTrue(c.offer(5)) - assertTrue(c.offer(6)) // overflows, dropped - assertEquals(2, c.receive()) - assertEquals(5, c.receive()) - assertEquals(null, c.poll()) - } - - @Test - fun testDropOldest() = runTest { - val c = Channel(2, BufferOverflow.DROP_OLDEST) - assertTrue(c.offer(1)) - assertTrue(c.offer(2)) - assertTrue(c.offer(3)) // overflows, keeps 2, 3 - c.send(4) // overflows, keeps 3, 4 - assertEquals(3, c.receive()) - assertTrue(c.offer(5)) - assertTrue(c.offer(6)) // overflows, keeps 5, 6 - assertEquals(5, c.receive()) - assertEquals(6, c.receive()) - assertEquals(null, c.poll()) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt index 413c91f5a7..72ba315450 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -9,6 +9,7 @@ import kotlin.test.* class ChannelFactoryTest : TestBase() { + @Test fun testRendezvousChannel() { assertTrue(Channel() is RendezvousChannel) @@ -18,31 +19,21 @@ class ChannelFactoryTest : TestBase() { @Test fun testLinkedListChannel() { assertTrue(Channel(Channel.UNLIMITED) is LinkedListChannel) - assertTrue(Channel(Channel.UNLIMITED, BufferOverflow.DROP_OLDEST) is LinkedListChannel) - assertTrue(Channel(Channel.UNLIMITED, BufferOverflow.DROP_LATEST) is LinkedListChannel) } @Test fun testConflatedChannel() { assertTrue(Channel(Channel.CONFLATED) is ConflatedChannel) - assertTrue(Channel(1, BufferOverflow.DROP_OLDEST) is ConflatedChannel) } @Test fun testArrayChannel() { assertTrue(Channel(1) is ArrayChannel) - assertTrue(Channel(1, BufferOverflow.DROP_LATEST) is ArrayChannel) assertTrue(Channel(10) is ArrayChannel) } @Test - fun testInvalidCapacityNotSupported() { - assertFailsWith { Channel(-3) } - } - - @Test - fun testUnsupportedBufferOverflow() { - assertFailsWith { Channel(Channel.CONFLATED, BufferOverflow.DROP_OLDEST) } - assertFailsWith { Channel(Channel.CONFLATED, BufferOverflow.DROP_LATEST) } + fun testInvalidCapacityNotSupported() = runTest({ it is IllegalArgumentException }) { + Channel(-3) } } diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt deleted file mode 100644 index d2ef3d2691..0000000000 --- a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.coroutines.* -import kotlinx.coroutines.internal.* -import kotlinx.coroutines.selects.* -import kotlin.test.* - -/** - * Tests for failures inside `onUndeliveredElement` handler in [Channel]. - */ -class ChannelUndeliveredElementFailureTest : TestBase() { - private val item = "LOST" - private val onCancelFail: (String) -> Unit = { throw TestException(it) } - private val shouldBeUnhandled: List<(Throwable) -> Boolean> = listOf({ it.isElementCancelException() }) - - private fun Throwable.isElementCancelException() = - this is UndeliveredElementException && cause is TestException && cause!!.message == item - - @Test - fun testSendCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - channel.send(item) - expectUnreached() - } - job.cancel() - } - - @Test - fun testSendSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - select { - channel.onSend(item) { - expectUnreached() - } - } - } - job.cancel() - } - - @Test - fun testReceiveCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - channel.receive() - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testReceiveSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - select { - channel.onReceive { - expectUnreached() - } - } - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testReceiveOrNullCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - channel.receiveOrNull() - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testReceiveOrNullSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - select { - channel.onReceiveOrNull { - expectUnreached() - } - } - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testReceiveOrClosedCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - channel.receiveOrClosed() - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testReceiveOrClosedSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - select { - channel.onReceiveOrClosed { - expectUnreached() - } - } - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testHasNextCancelledFail() = runTest(unhandled = shouldBeUnhandled) { - val channel = Channel(onUndeliveredElement = onCancelFail) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - channel.iterator().hasNext() - expectUnreached() // will be cancelled before it dispatches - } - channel.send(item) - job.cancel() - } - - @Test - fun testChannelCancelledFail() = runTest(expected = { it.isElementCancelException()}) { - val channel = Channel(1, onUndeliveredElement = onCancelFail) - channel.send(item) - channel.cancel() - expectUnreached() - } - -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt deleted file mode 100644 index 0391e00033..0000000000 --- a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package kotlinx.coroutines.channels - -import kotlinx.atomicfu.* -import kotlinx.coroutines.* -import kotlin.test.* - -class ChannelUndeliveredElementTest : TestBase() { - @Test - fun testSendSuccessfully() = runAllKindsTest { kind -> - val channel = kind.create { it.cancel() } - val res = Resource("OK") - launch { - channel.send(res) - } - val ok = channel.receive() - assertEquals("OK", ok.value) - assertFalse(res.isCancelled) // was not cancelled - channel.close() - assertFalse(res.isCancelled) // still was not cancelled - } - - @Test - fun testRendezvousSendCancelled() = runTest { - val channel = Channel { it.cancel() } - val res = Resource("OK") - val sender = launch(start = CoroutineStart.UNDISPATCHED) { - assertFailsWith { - channel.send(res) // suspends & get cancelled - } - } - sender.cancelAndJoin() - assertTrue(res.isCancelled) - } - - @Test - fun testBufferedSendCancelled() = runTest { - val channel = Channel(1) { it.cancel() } - val resA = Resource("A") - val resB = Resource("B") - val sender = launch(start = CoroutineStart.UNDISPATCHED) { - channel.send(resA) // goes to buffer - assertFailsWith { - channel.send(resB) // suspends & get cancelled - } - } - sender.cancelAndJoin() - assertFalse(resA.isCancelled) // it is in buffer, not cancelled - assertTrue(resB.isCancelled) // send was cancelled - channel.cancel() // now cancel the channel - assertTrue(resA.isCancelled) // now cancelled in buffer - } - - @Test - fun testConflatedResourceCancelled() = runTest { - val channel = Channel(Channel.CONFLATED) { it.cancel() } - val resA = Resource("A") - val resB = Resource("B") - channel.send(resA) - assertFalse(resA.isCancelled) - assertFalse(resB.isCancelled) - channel.send(resB) - assertTrue(resA.isCancelled) // it was conflated (lost) and thus cancelled - assertFalse(resB.isCancelled) - channel.close() - assertFalse(resB.isCancelled) // not cancelled yet, can be still read by receiver - channel.cancel() - assertTrue(resB.isCancelled) // now it is cancelled - } - - @Test - fun testSendToClosedChannel() = runAllKindsTest { kind -> - val channel = kind.create { it.cancel() } - channel.close() // immediately close channel - val res = Resource("OK") - assertFailsWith { - channel.send(res) // send fails to closed channel, resource was not delivered - } - assertTrue(res.isCancelled) - } - - private fun runAllKindsTest(test: suspend CoroutineScope.(TestChannelKind) -> Unit) { - for (kind in TestChannelKind.values()) { - if (kind.viaBroadcast) continue // does not support onUndeliveredElement - try { - runTest { - test(kind) - } - } catch(e: Throwable) { - error("$kind: $e", e) - } - } - } - - private class Resource(val value: String) { - private val _cancelled = atomic(false) - - val isCancelled: Boolean - get() = _cancelled.value - - fun cancel() { - check(!_cancelled.getAndSet(true)) { "Already cancelled" } - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt deleted file mode 100644 index e80309be89..0000000000 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -// Test that ArrayChannel(1, DROP_OLDEST) works just like ConflatedChannel() -class ConflatedChannelArrayModelTest : ConflatedChannelTest() { - override fun createConflatedChannel(): Channel = - ArrayChannel(1, BufferOverflow.DROP_OLDEST, null) -} diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 18f2843868..4deb3858f0 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -7,13 +7,10 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlin.test.* -open class ConflatedChannelTest : TestBase() { - protected open fun createConflatedChannel() = - Channel(Channel.CONFLATED) - +class ConflatedChannelTest : TestBase() { @Test fun testBasicConflationOfferPoll() { - val q = createConflatedChannel() + val q = Channel(Channel.CONFLATED) assertNull(q.poll()) assertTrue(q.offer(1)) assertTrue(q.offer(2)) @@ -24,7 +21,7 @@ open class ConflatedChannelTest : TestBase() { @Test fun testConflatedSend() = runTest { - val q = createConflatedChannel() + val q = ConflatedChannel() q.send(1) q.send(2) // shall conflated previously sent assertEquals(2, q.receiveOrNull()) @@ -32,7 +29,7 @@ open class ConflatedChannelTest : TestBase() { @Test fun testConflatedClose() = runTest { - val q = createConflatedChannel() + val q = Channel(Channel.CONFLATED) q.send(1) q.close() // shall become closed but do not conflate last sent item yet assertTrue(q.isClosedForSend) @@ -46,7 +43,7 @@ open class ConflatedChannelTest : TestBase() { @Test fun testConflationSendReceive() = runTest { - val q = createConflatedChannel() + val q = Channel(Channel.CONFLATED) expect(1) launch { // receiver coroutine expect(4) @@ -74,7 +71,7 @@ open class ConflatedChannelTest : TestBase() { @Test fun testConsumeAll() = runTest { - val q = createConflatedChannel() + val q = Channel(Channel.CONFLATED) expect(1) for (i in 1..10) { q.send(i) // stores as last @@ -88,7 +85,7 @@ open class ConflatedChannelTest : TestBase() { @Test fun testCancelWithCause() = runTest({ it is TestCancellationException }) { - val channel = createConflatedChannel() + val channel = Channel(Channel.CONFLATED) channel.cancel(TestCancellationException()) channel.receiveOrNull() } diff --git a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt index 993be78e17..69d8fd03e3 100644 --- a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt +++ b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt @@ -7,10 +7,9 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.selects.* -enum class TestChannelKind( - val capacity: Int, - private val description: String, - val viaBroadcast: Boolean = false +enum class TestChannelKind(val capacity: Int, + private val description: String, + private val viaBroadcast: Boolean = false ) { RENDEZVOUS(0, "RendezvousChannel"), ARRAY_1(1, "ArrayChannel(1)"), @@ -23,11 +22,8 @@ enum class TestChannelKind( CONFLATED_BROADCAST(Channel.CONFLATED, "ConflatedBroadcastChannel", viaBroadcast = true) ; - fun create(onUndeliveredElement: ((T) -> Unit)? = null): Channel = when { - viaBroadcast && onUndeliveredElement != null -> error("Broadcast channels to do not support onUndeliveredElement") - viaBroadcast -> ChannelViaBroadcast(BroadcastChannel(capacity)) - else -> Channel(capacity, onUndeliveredElement = onUndeliveredElement) - } + fun create(): Channel = if (viaBroadcast) ChannelViaBroadcast(BroadcastChannel(capacity)) + else Channel(capacity) val isConflated get() = capacity == Channel.CONFLATED override fun toString(): String = description diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt similarity index 52% rename from kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt rename to kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt index 0a2c0458c4..a6be97eb97 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt @@ -5,7 +5,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* import kotlin.test.* class StateFlowTest : TestBase() { @@ -57,7 +56,7 @@ class StateFlowTest : TestBase() { expect(2) assertFailsWith { state.collect { value -> - when (value.i) { + when(value.i) { 0 -> expect(3) // initial value 2 -> expect(5) 4 -> expect(7) @@ -104,7 +103,6 @@ class StateFlowTest : TestBase() { class CounterModel { // private data flow private val _counter = MutableStateFlow(0) - // publicly exposed as a flow val counter: StateFlow get() = _counter @@ -112,85 +110,4 @@ class StateFlowTest : TestBase() { _counter.value++ } } - - @Test - public fun testOnSubscriptionWithException() = runTest { - expect(1) - val state = MutableStateFlow("A") - state - .onSubscription { - emit("collector->A") - state.value = "A" - } - .onSubscription { - emit("collector->B") - state.value = "B" - throw TestException() - } - .onStart { - emit("collector->C") - state.value = "C" - } - .onStart { - emit("collector->D") - state.value = "D" - } - .onEach { - when (it) { - "collector->D" -> expect(2) - "collector->C" -> expect(3) - "collector->A" -> expect(4) - "collector->B" -> expect(5) - else -> expectUnreached() - } - } - .catch { e -> - assertTrue(e is TestException) - expect(6) - } - .launchIn(this) - .join() - assertEquals(0, state.subscriptionCount.value) - finish(7) - } - - @Test - fun testOperatorFusion() { - val state = MutableStateFlow(String) - assertSame(state, (state as Flow<*>).cancellable()) - assertSame(state, (state as Flow<*>).distinctUntilChanged()) - assertSame(state, (state as Flow<*>).flowOn(Dispatchers.Default)) - assertSame(state, (state as Flow<*>).conflate()) - assertSame(state, state.buffer(Channel.CONFLATED)) - assertSame(state, state.buffer(Channel.RENDEZVOUS)) - } - - @Test - fun testResetUnsupported() { - val state = MutableStateFlow(42) - assertFailsWith { state.resetReplayCache() } - assertEquals(42, state.value) - assertEquals(listOf(42), state.replayCache) - } - - @Test - fun testReferenceUpdatesAndCAS() { - val d0 = Data(0) - val d0_1 = Data(0) - val d1 = Data(1) - val d1_1 = Data(1) - val d1_2 = Data(1) - val state = MutableStateFlow(d0) - assertSame(d0, state.value) - state.value = d0_1 // equal, nothing changes - assertSame(d0, state.value) - state.value = d1 // updates - assertSame(d1, state.value) - assertFalse(state.compareAndSet(d0, d0)) // wrong value - assertSame(d1, state.value) - assertTrue(state.compareAndSet(d1_1, d1_2)) // "updates", but ref stays - assertSame(d1, state.value) - assertTrue(state.compareAndSet(d1_1, d0)) // updates, reference changes - assertSame(d0, state.value) - } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt index ddb1d88ae2..9b257d933e 100644 --- a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt +++ b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -7,12 +7,11 @@ package kotlinx.coroutines import kotlin.coroutines.* import kotlin.jvm.* -internal class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineDispatcher(), Delay { +private class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineDispatcher(), Delay { + private val originalDispatcher = enclosingScope.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher private val heap = ArrayList() // TODO use MPP heap/ordered set implementation (commonize ThreadSafeHeap) - - var currentTime = 0L - private set + private var currentTime = 0L init { /* @@ -51,20 +50,17 @@ internal class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : Coroutine @ExperimentalCoroutinesApi override fun isDispatchNeeded(context: CoroutineContext): Boolean = originalDispatcher.isDispatchNeeded(context) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - val task = TimedTask(block, deadline(timeMillis)) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + val task = TimedTask(block, currentTime + timeMillis) heap += task return task } override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - val task = TimedTask(Runnable { with(continuation) { resumeUndispatched(Unit) } }, deadline(timeMillis)) + val task = TimedTask(Runnable { with(continuation) { resumeUndispatched(Unit) } }, currentTime + timeMillis) heap += task continuation.invokeOnCancellation { task.dispose() } } - - private fun deadline(timeMillis: Long) = - if (timeMillis == Long.MAX_VALUE) Long.MAX_VALUE else currentTime + timeMillis } /** diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt deleted file mode 100644 index 7b66977226..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.test.* - -/** - * A _behavioral_ test for conflation options that can be configured by the [buffer] operator to test that it is - * implemented properly and that adjacent [buffer] calls are fused properly. -*/ -class BufferConflationTest : TestBase() { - private val n = 100 // number of elements to emit for test - - private fun checkConflate( - capacity: Int, - onBufferOverflow: BufferOverflow = BufferOverflow.DROP_OLDEST, - op: suspend Flow.() -> Flow - ) = runTest { - expect(1) - // emit all and conflate, then collect first & last - val expectedList = when (onBufferOverflow) { - BufferOverflow.DROP_OLDEST -> listOf(0) + (n - capacity until n).toList() // first item & capacity last ones - BufferOverflow.DROP_LATEST -> (0..capacity).toList() // first & capacity following ones - else -> error("cannot happen") - } - flow { - repeat(n) { i -> - expect(i + 2) - emit(i) - } - } - .op() - .collect { i -> - val j = expectedList.indexOf(i) - expect(n + 2 + j) - } - finish(n + 2 + expectedList.size) - } - - @Test - fun testConflate() = - checkConflate(1) { - conflate() - } - - @Test - fun testBufferConflated() = - checkConflate(1) { - buffer(Channel.CONFLATED) - } - - @Test - fun testBufferDropOldest() = - checkConflate(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) - } - - @Test - fun testBuffer0DropOldest() = - checkConflate(1) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) - } - - @Test - fun testBuffer1DropOldest() = - checkConflate(1) { - buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - } - - @Test - fun testBuffer10DropOldest() = - checkConflate(10) { - buffer(10, onBufferOverflow = BufferOverflow.DROP_OLDEST) - } - - @Test - fun testConflateOverridesBuffer() = - checkConflate(1) { - buffer(42).conflate() - } - - @Test // conflate().conflate() should work like a single conflate - fun testDoubleConflate() = - checkConflate(1) { - conflate().conflate() - } - - @Test - fun testConflateBuffer10Combine() = - checkConflate(10) { - conflate().buffer(10) - } - - @Test - fun testBufferDropLatest() = - checkConflate(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) - } - - @Test - fun testBuffer0DropLatest() = - checkConflate(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST) - } - - @Test - fun testBuffer1DropLatest() = - checkConflate(1, BufferOverflow.DROP_LATEST) { - buffer(1, onBufferOverflow = BufferOverflow.DROP_LATEST) - } - - @Test // overrides previous buffer - fun testBufferDropLatestOverrideBuffer() = - checkConflate(1, BufferOverflow.DROP_LATEST) { - buffer(42).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) - } - - @Test // overrides previous conflate - fun testBufferDropLatestOverrideConflate() = - checkConflate(1, BufferOverflow.DROP_LATEST) { - conflate().buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) - } - - @Test - fun testBufferDropLatestBuffer7Combine() = - checkConflate(7, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).buffer(7) - } - - @Test - fun testConflateOverrideBufferDropLatest() = - checkConflate(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).conflate() - } - - @Test - fun testBuffer3DropOldestOverrideBuffer8DropLatest() = - checkConflate(3, BufferOverflow.DROP_OLDEST) { - buffer(8, onBufferOverflow = BufferOverflow.DROP_LATEST) - .buffer(3, BufferOverflow.DROP_OLDEST) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt index 6352aacf41..b68e115637 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.flow @@ -9,21 +9,13 @@ import kotlinx.coroutines.channels.* import kotlin.math.* import kotlin.test.* -/** - * A _behavioral_ test for buffering that is introduced by the [buffer] operator to test that it is - * implemented properly and that adjacent [buffer] calls are fused properly. - */ class BufferTest : TestBase() { - private val n = 200 // number of elements to emit for test + private val n = 50 // number of elements to emit for test private val defaultBufferSize = 64 // expected default buffer size (per docs) // Use capacity == -1 to check case of "no buffer" private fun checkBuffer(capacity: Int, op: suspend Flow.() -> Flow) = runTest { expect(1) - /* - Channels perform full rendezvous. Sender does not suspend when there is a suspended receiver and vice-versa. - Thus, perceived batch size is +2 from capacity. - */ val batchSize = capacity + 2 flow { repeat(n) { i -> @@ -171,6 +163,27 @@ class BufferTest : TestBase() { .flowOn(wrapperDispatcher()).buffer(5) } + @Test + fun testConflate() = runTest { + expect(1) + // emit all and conflate / then collect first & last + flow { + repeat(n) { i -> + expect(i + 2) + emit(i) + } + } + .buffer(Channel.CONFLATED) + .collect { i -> + when (i) { + 0 -> expect(n + 2) // first value + n - 1 -> expect(n + 3) // last value + else -> error("Unexpected $i") + } + } + finish(n + 4) + } + @Test fun testCancellation() = runTest { val result = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt index eedfac2ea3..802ba1ef2f 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt @@ -134,14 +134,15 @@ class CatchTest : TestBase() { // flowOn with a different dispatcher introduces asynchrony so that all exceptions in the // upstream flows are handled before they go downstream .onEach { value -> - expectUnreached() // already cancelled + expect(8) + assertEquals("OK", value) } .catch { e -> - expect(8) + expect(9) assertTrue(e is TestException) assertSame(d0, kotlin.coroutines.coroutineContext[ContinuationInterceptor] as CoroutineContext) } .collect() - finish(9) + finish(10) } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt index 2893321998..a619355b68 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.flow @@ -238,22 +238,6 @@ abstract class CombineTestBase : TestBase() { assertFailsWith(flow) finish(7) } - - @Test - fun testCancelledCombine() = runTest( - expected = { it is CancellationException } - ) { - coroutineScope { - val flow = flow { - emit(Unit) // emit - } - cancel() // cancel the scope - flow.combineLatest(flow) { u, _ -> u }.collect { - // should not be reached, because cancelled before it runs - expectUnreached() - } - } - } } class CombineTest : CombineTestBase() { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt index 68e7f66b9d..fc03d367c6 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt @@ -94,33 +94,4 @@ class DistinctUntilChangedTest : TestBase() { val flow = flowOf(null, 1, null, null).distinctUntilChanged() assertEquals(listOf(null, 1, null), flow.toList()) } - - @Test - fun testRepeatedDistinctFusionDefault() = testRepeatedDistinctFusion { - distinctUntilChanged() - } - - // A separate variable is needed for K/N that does not optimize non-captured lambdas (yet) - private val areEquivalentTestFun: (old: Int, new: Int) -> Boolean = { old, new -> old == new } - - @Test - fun testRepeatedDistinctFusionAreEquivalent() = testRepeatedDistinctFusion { - distinctUntilChanged(areEquivalentTestFun) - } - - // A separate variable is needed for K/N that does not optimize non-captured lambdas (yet) - private val keySelectorTestFun: (Int) -> Int = { it % 2 } - - @Test - fun testRepeatedDistinctFusionByKey() = testRepeatedDistinctFusion { - distinctUntilChangedBy(keySelectorTestFun) - } - - private fun testRepeatedDistinctFusion(op: Flow.() -> Flow) = runTest { - val flow = (1..10).asFlow() - val d1 = flow.op() - assertNotSame(flow, d1) - val d2 = d1.op() - assertSame(d1, d2) - } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt index 68653281cc..f8350ff584 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -341,20 +341,4 @@ class FlowOnTest : TestBase() { assertEquals(expected, value) } } - - @Test - fun testCancelledFlowOn() = runTest { - assertFailsWith { - coroutineScope { - val scope = this - flow { - emit(Unit) // emit to buffer - scope.cancel() // now cancel outer scope - }.flowOn(wrapperDispatcher()).collect { - // should not be reached, because cancelled before it runs - expectUnreached() - } - } - } - } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt index f55e8beeb2..7f0c548ca6 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt @@ -231,7 +231,7 @@ class OnCompletionTest : TestBase() { @Test fun testSingle() = runTest { - assertFailsWith { + assertFailsWith { flowOf(239).onCompletion { assertNull(it) expect(1) @@ -240,7 +240,7 @@ class OnCompletionTest : TestBase() { expectUnreached() } catch (e: Throwable) { // Second emit -- failure - assertTrue { e is IllegalArgumentException } + assertTrue { e is IllegalStateException } throw e } }.single() diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index 5f2b5a74cd..b28320c391 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -67,12 +67,14 @@ class ZipTest : TestBase() { val f1 = flow { emit("1") emit("2") - expectUnreached() // the above emit will get cancelled because f2 ends + hang { + expect(1) + } } val f2 = flowOf("a", "b") assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) - finish(1) + finish(2) } @Test @@ -90,6 +92,25 @@ class ZipTest : TestBase() { finish(2) } + @Test + fun testCancelWhenFlowIsDone2() = runTest { + val f1 = flow { + emit("1") + emit("2") + try { + emit("3") + expectUnreached() + } finally { + expect(1) + } + + } + + val f2 = flowOf("a", "b") + assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) + finish(2) + } + @Test fun testContextIsIsolatedReversed() = runTest { val f1 = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt deleted file mode 100644 index 9c6aed211a..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlin.math.* -import kotlin.test.* - -/** - * Similar to [BufferTest], but tests [shareIn] buffering and its fusion with [buffer] operators. - */ -class ShareInBufferTest : TestBase() { - private val n = 200 // number of elements to emit for test - private val defaultBufferSize = 64 // expected default buffer size (per docs) - - // Use capacity == -1 to check case of "no buffer" - private fun checkBuffer(capacity: Int, op: suspend Flow.(CoroutineScope) -> Flow) = runTest { - expect(1) - /* - Shared flows do not perform full rendezvous. On buffer overflow emitter always suspends until all - subscribers get the value and then resumes. Thus, perceived batch size is +1 from buffer capacity. - */ - val batchSize = capacity + 1 - val upstream = flow { - repeat(n) { i -> - val batchNo = i / batchSize - val batchIdx = i % batchSize - expect(batchNo * batchSize * 2 + batchIdx + 2) - emit(i) - } - emit(-1) // done - } - coroutineScope { - upstream - .op(this) - .takeWhile { i -> i >= 0 } // until done - .collect { i -> - val batchNo = i / batchSize - val batchIdx = i % batchSize - // last batch might have smaller size - val k = min((batchNo + 1) * batchSize, n) - batchNo * batchSize - expect(batchNo * batchSize * 2 + k + batchIdx + 2) - } - coroutineContext.cancelChildren() // cancels sharing - } - finish(2 * n + 2) - } - - @Test - fun testReplay0DefaultBuffer() = - checkBuffer(defaultBufferSize) { - shareIn(it, SharingStarted.Eagerly) - } - - @Test - fun testReplay1DefaultBuffer() = - checkBuffer(defaultBufferSize) { - shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test // buffer is padded to default size as needed - fun testReplay10DefaultBuffer() = - checkBuffer(maxOf(10, defaultBufferSize)) { - shareIn(it, SharingStarted.Eagerly, 10) - } - - @Test // buffer is padded to default size as needed - fun testReplay100DefaultBuffer() = - checkBuffer( maxOf(100, defaultBufferSize)) { - shareIn(it, SharingStarted.Eagerly, 100) - } - - @Test - fun testDefaultBufferKeepsDefault() = - checkBuffer(defaultBufferSize) { - buffer().shareIn(it, SharingStarted.Eagerly) - } - - @Test - fun testOverrideDefaultBuffer0() = - checkBuffer(0) { - buffer(0).shareIn(it, SharingStarted.Eagerly) - } - - @Test - fun testOverrideDefaultBuffer10() = - checkBuffer(10) { - buffer(10).shareIn(it, SharingStarted.Eagerly) - } - - @Test // buffer and replay sizes add up - fun testBufferReplaySum() = - checkBuffer(41) { - buffer(10).buffer(20).shareIn(it, SharingStarted.Eagerly, 11) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt deleted file mode 100644 index 0528e97e7d..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.test.* - -/** - * Similar to [ShareInBufferTest] and [BufferConflationTest], - * but tests [shareIn] and its fusion with [conflate] operator. - */ -class ShareInConflationTest : TestBase() { - private val n = 100 - - private fun checkConflation( - bufferCapacity: Int, - onBufferOverflow: BufferOverflow = BufferOverflow.DROP_OLDEST, - op: suspend Flow.(CoroutineScope) -> Flow - ) = runTest { - expect(1) - // emit all and conflate, then should collect bufferCapacity latest ones - val done = Job() - flow { - repeat(n) { i -> - expect(i + 2) - emit(i) - } - done.join() // wait until done collection - emit(-1) // signal flow completion - } - .op(this) - .takeWhile { i -> i >= 0 } - .collect { i -> - val first = if (onBufferOverflow == BufferOverflow.DROP_LATEST) 0 else n - bufferCapacity - val last = first + bufferCapacity - 1 - if (i in first..last) { - expect(n + i - first + 2) - if (i == last) done.complete() // received the last one - } else { - error("Unexpected $i") - } - } - finish(n + bufferCapacity + 2) - } - - @Test - fun testConflateReplay1() = - checkConflation(1) { - conflate().shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test // still looks like conflating the last value for the first subscriber (will not replay to others though) - fun testConflateReplay0() = - checkConflation(1) { - conflate().shareIn(it, SharingStarted.Eagerly, 0) - } - - @Test - fun testConflateReplay5() = - checkConflation(5) { - conflate().shareIn(it, SharingStarted.Eagerly, 5) - } - - @Test - fun testBufferDropOldestReplay1() = - checkConflation(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test - fun testBufferDropOldestReplay0() = - checkConflation(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) - } - - @Test - fun testBufferDropOldestReplay10() = - checkConflation(10) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 10) - } - - @Test - fun testBuffer20DropOldestReplay0() = - checkConflation(20) { - buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) - } - - @Test - fun testBuffer7DropOldestReplay11() = - checkConflation(18) { - buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 11) - } - - @Test // a preceding buffer() gets overridden by conflate() - fun testBufferConflateOverride() = - checkConflation(1) { - buffer(23).conflate().shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) - fun testBufferDropOldestOverride() = - checkConflation(1) { - buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test - fun testBufferDropLatestReplay0() = - checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) - } - - @Test - fun testBufferDropLatestReplay1() = - checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test - fun testBufferDropLatestReplay10() = - checkConflation(10, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) - } - - @Test - fun testBuffer0DropLatestReplay0() = - checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) - } - - @Test - fun testBuffer0DropLatestReplay1() = - checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 1) - } - - @Test - fun testBuffer0DropLatestReplay10() = - checkConflation(10, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) - } - - @Test - fun testBuffer5DropLatestReplay0() = - checkConflation(5, BufferOverflow.DROP_LATEST) { - buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) - } - - @Test - fun testBuffer5DropLatestReplay10() = - checkConflation(15, BufferOverflow.DROP_LATEST) { - buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) - } - - @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) - fun testBufferDropLatestOverride() = - checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt deleted file mode 100644 index 371d01472e..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.test.* - -class ShareInFusionTest : TestBase() { - /** - * Test perfect fusion for operators **after** [shareIn]. - */ - @Test - fun testOperatorFusion() = runTest { - val sh = emptyFlow().shareIn(this, SharingStarted.Eagerly) - assertTrue(sh !is MutableSharedFlow<*>) // cannot be cast to mutable shared flow!!! - assertSame(sh, (sh as Flow<*>).cancellable()) - assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) - assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) - coroutineContext.cancelChildren() - } - - @Test - fun testFlowOnContextFusion() = runTest { - val flow = flow { - assertEquals("FlowCtx", currentCoroutineContext()[CoroutineName]?.name) - emit("OK") - }.flowOn(CoroutineName("FlowCtx")) - assertEquals("OK", flow.shareIn(this, SharingStarted.Eagerly, 1).first()) - coroutineContext.cancelChildren() - } - - /** - * Tests that `channelFlow { ... }.buffer(x)` works according to the [channelFlow] docs, and subsequent - * application of [shareIn] does not leak upstream. - */ - @Test - fun testChannelFlowBufferShareIn() = runTest { - expect(1) - val flow = channelFlow { - // send a batch of 10 elements using [offer] - for (i in 1..10) { - assertTrue(offer(i)) // offer must succeed, because buffer - } - send(0) // done - }.buffer(10) // request a buffer of 10 - // ^^^^^^^^^ buffer stays here - val shared = flow.shareIn(this, SharingStarted.Eagerly) - shared - .takeWhile { it > 0 } - .collect { i -> expect(i + 1) } - finish(12) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt deleted file mode 100644 index 9020f5f311..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.test.* - -class ShareInTest : TestBase() { - @Test - fun testReplay0Eager() = runTest { - expect(1) - val flow = flowOf("OK") - val shared = flow.shareIn(this, SharingStarted.Eagerly) - yield() // actually start sharing - // all subscribers miss "OK" - val jobs = List(10) { - shared.onEach { expectUnreached() }.launchIn(this) - } - yield() // ensure nothing is collected - jobs.forEach { it.cancel() } - finish(2) - } - - @Test - fun testReplay0Lazy() = testReplayZeroOrOne(0) - - @Test - fun testReplay1Lazy() = testReplayZeroOrOne(1) - - private fun testReplayZeroOrOne(replay: Int) = runTest { - expect(1) - val doneBarrier = Job() - val flow = flow { - expect(2) - emit("OK") - doneBarrier.join() - emit("DONE") - } - val sharingJob = Job() - val shared = flow.shareIn(this + sharingJob, started = SharingStarted.Lazily, replay = replay) - yield() // should not start sharing - // first subscriber gets "OK", other subscribers miss "OK" - val n = 10 - val replayOfs = replay * (n - 1) - val subscriberJobs = List(n) { index -> - val subscribedBarrier = Job() - val job = shared - .onSubscription { - subscribedBarrier.complete() - } - .onEach { value -> - when (value) { - "OK" -> { - expect(3 + index) - if (replay == 0) { // only the first subscriber collects "OK" without replay - assertEquals(0, index) - } - } - "DONE" -> { - expect(4 + index + replayOfs) - } - else -> expectUnreached() - } - } - .takeWhile { it != "DONE" } - .launchIn(this) - subscribedBarrier.join() // wait until the launched job subscribed before launching the next one - job - } - doneBarrier.complete() - subscriberJobs.joinAll() - expect(4 + n + replayOfs) - sharingJob.cancel() - finish(5 + n + replayOfs) - } - - @Test - fun testUpstreamCompleted() = - testUpstreamCompletedOrFailed(failed = false) - - @Test - fun testUpstreamFailed() = - testUpstreamCompletedOrFailed(failed = true) - - private fun testUpstreamCompletedOrFailed(failed: Boolean) = runTest { - val emitted = Job() - val terminate = Job() - val sharingJob = CompletableDeferred() - val upstream = flow { - emit("OK") - emitted.complete() - terminate.join() - if (failed) throw TestException() - } - val shared = upstream.shareIn(this + sharingJob, SharingStarted.Eagerly, 1) - assertEquals(emptyList(), shared.replayCache) - emitted.join() // should start sharing, emit & cache - assertEquals(listOf("OK"), shared.replayCache) - terminate.complete() - sharingJob.complete(Unit) - sharingJob.join() // should complete sharing - assertEquals(listOf("OK"), shared.replayCache) // cache is still there - if (failed) { - assertTrue(sharingJob.getCompletionExceptionOrNull() is TestException) - } else { - assertNull(sharingJob.getCompletionExceptionOrNull()) - } - } - - @Test - fun testWhileSubscribedBasic() = - testWhileSubscribed(1, SharingStarted.WhileSubscribed()) - - @Test - fun testWhileSubscribedCustomAtLeast1() = - testWhileSubscribed(1, SharingStarted.WhileSubscribedAtLeast(1)) - - @Test - fun testWhileSubscribedCustomAtLeast2() = - testWhileSubscribed(2, SharingStarted.WhileSubscribedAtLeast(2)) - - @OptIn(ExperimentalStdlibApi::class) - private fun testWhileSubscribed(threshold: Int, started: SharingStarted) = runTest { - expect(1) - val flowState = FlowState() - val n = 3 // max number of subscribers - val log = Channel(2 * n) - - suspend fun checkStartTransition(subscribers: Int) { - when (subscribers) { - in 0 until threshold -> assertFalse(flowState.started) - threshold -> { - flowState.awaitStart() // must eventually start the flow - for (i in 1..threshold) { - assertEquals("sub$i: OK", log.receive()) // threshold subs must receive the values - } - } - in threshold + 1..n -> assertTrue(flowState.started) - } - } - - suspend fun checkStopTransition(subscribers: Int) { - when (subscribers) { - in threshold + 1..n -> assertTrue(flowState.started) - threshold - 1 -> flowState.awaitStop() // upstream flow must be eventually stopped - in 0..threshold - 2 -> assertFalse(flowState.started) // should have stopped already - } - } - - val flow = flow { - flowState.track { - emit("OK") - delay(Long.MAX_VALUE) // await forever, will get cancelled - } - } - - val shared = flow.shareIn(this, started) - repeat(5) { // repeat scenario a few times - yield() - assertFalse(flowState.started) // flow is not running even if we yield - // start 3 subscribers - val subs = ArrayList() - for (i in 1..n) { - subs += shared - .onEach { value -> // only the first threshold subscribers get the value - when (i) { - in 1..threshold -> log.offer("sub$i: $value") - else -> expectUnreached() - } - } - .onCompletion { log.offer("sub$i: completion") } - .launchIn(this) - checkStartTransition(i) - } - // now cancel all subscribers - for (i in 1..n) { - subs.removeFirst().cancel() // cancel subscriber - assertEquals("sub$i: completion", log.receive()) // subscriber shall shutdown - checkStopTransition(n - i) - } - } - coroutineContext.cancelChildren() // cancel sharing job - finish(2) - } - - @Suppress("TestFunctionName") - private fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = - object : SharingStarted { - override fun command(subscriptionCount: StateFlow): Flow = - subscriptionCount - .map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } - } - - private class FlowState { - private val timeLimit = 10000L - private val _started = MutableStateFlow(false) - val started: Boolean get() = _started.value - fun start() = check(_started.compareAndSet(expect = false, update = true)) - fun stop() = check(_started.compareAndSet(expect = true, update = false)) - suspend fun awaitStart() = withTimeout(timeLimit) { _started.first { it } } - suspend fun awaitStop() = withTimeout(timeLimit) { _started.first { !it } } - } - - private suspend fun FlowState.track(block: suspend () -> Unit) { - start() - try { - block() - } finally { - stop() - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt deleted file mode 100644 index f716389fb7..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.coroutines.* -import kotlin.test.* - -/** - * This test suit for [SharedFlow] has a dense framework that allows to test complex - * suspend/resume scenarios while keeping the code readable. Each test here is for - * one specific [SharedFlow] configuration, testing all the various corner cases in its - * behavior. - */ -class SharedFlowScenarioTest : TestBase() { - @Test - fun testReplay1Extra2() = - testSharedFlow(MutableSharedFlow(1, 2)) { - // total buffer size == 3 - expectReplayOf() - emitRightNow(1); expectReplayOf(1) - emitRightNow(2); expectReplayOf(2) - emitRightNow(3); expectReplayOf(3) - emitRightNow(4); expectReplayOf(4) // no prob - no subscribers - val a = subscribe("a"); collect(a, 4) - emitRightNow(5); expectReplayOf(5) - emitRightNow(6); expectReplayOf(6) - emitRightNow(7); expectReplayOf(7) - // suspend/collect sequentially - val e8 = emitSuspends(8); collect(a, 5); emitResumes(e8); expectReplayOf(8) - val e9 = emitSuspends(9); collect(a, 6); emitResumes(e9); expectReplayOf(9) - // buffer full, but parallel emitters can still suspend (queue up) - val e10 = emitSuspends(10) - val e11 = emitSuspends(11) - val e12 = emitSuspends(12) - collect(a, 7); emitResumes(e10); expectReplayOf(10) // buffer 8, 9 | 10 - collect(a, 8); emitResumes(e11); expectReplayOf(11) // buffer 9, 10 | 11 - sharedFlow.resetReplayCache(); expectReplayOf() // 9, 10 11 | no replay - collect(a, 9); emitResumes(e12); expectReplayOf(12) - collect(a, 10, 11, 12); expectReplayOf(12) // buffer empty | 12 - emitRightNow(13); expectReplayOf(13) - emitRightNow(14); expectReplayOf(14) - emitRightNow(15); expectReplayOf(15) // buffer 13, 14 | 15 - val e16 = emitSuspends(16) - val e17 = emitSuspends(17) - val e18 = emitSuspends(18) - cancel(e17); expectReplayOf(15) // cancel in the middle of three emits; buffer 13, 14 | 15 - collect(a, 13); emitResumes(e16); expectReplayOf(16) // buffer 14, 15, | 16 - collect(a, 14); emitResumes(e18); expectReplayOf(18) // buffer 15, 16 | 18 - val e19 = emitSuspends(19) - val e20 = emitSuspends(20) - val e21 = emitSuspends(21) - cancel(e21); expectReplayOf(18) // cancel last emit; buffer 15, 16, 18 - collect(a, 15); emitResumes(e19); expectReplayOf(19) // buffer 16, 18 | 19 - collect(a, 16); emitResumes(e20); expectReplayOf(20) // buffer 18, 19 | 20 - collect(a, 18, 19, 20); expectReplayOf(20) // buffer empty | 20 - emitRightNow(22); expectReplayOf(22) - emitRightNow(23); expectReplayOf(23) - emitRightNow(24); expectReplayOf(24) // buffer 22, 23 | 24 - val e25 = emitSuspends(25) - val e26 = emitSuspends(26) - val e27 = emitSuspends(27) - cancel(e25); expectReplayOf(24) // cancel first emit, buffer 22, 23 | 24 - sharedFlow.resetReplayCache(); expectReplayOf() // buffer 22, 23, 24 | no replay - val b = subscribe("b") // new subscriber - collect(a, 22); emitResumes(e26); expectReplayOf(26) // buffer 23, 24 | 26 - collect(b, 26) - collect(a, 23); emitResumes(e27); expectReplayOf(27) // buffer 24, 26 | 27 - collect(a, 24, 26, 27) // buffer empty | 27 - emitRightNow(28); expectReplayOf(28) - emitRightNow(29); expectReplayOf(29) // buffer 27, 28 | 29 - collect(a, 28, 29) // but b is slow - val e30 = emitSuspends(30) - val e31 = emitSuspends(31) - val e32 = emitSuspends(32) - val e33 = emitSuspends(33) - val e34 = emitSuspends(34) - val e35 = emitSuspends(35) - val e36 = emitSuspends(36) - val e37 = emitSuspends(37) - val e38 = emitSuspends(38) - val e39 = emitSuspends(39) - cancel(e31) // cancel emitter in queue - cancel(b) // cancel slow subscriber -> 3 emitters resume - emitResumes(e30); emitResumes(e32); emitResumes(e33); expectReplayOf(33) // buffer 30, 32 | 33 - val c = subscribe("c"); collect(c, 33) // replays - cancel(e34) - collect(a, 30); emitResumes(e35); expectReplayOf(35) // buffer 32, 33 | 35 - cancel(e37) - cancel(a); emitResumes(e36); emitResumes(e38); expectReplayOf(38) // buffer 35, 36 | 38 - collect(c, 35); emitResumes(e39); expectReplayOf(39) // buffer 36, 38 | 39 - collect(c, 36, 38, 39); expectReplayOf(39) - cancel(c); expectReplayOf(39) // replay stays - } - - @Test - fun testReplay1() = - testSharedFlow(MutableSharedFlow(1)) { - emitRightNow(0); expectReplayOf(0) - emitRightNow(1); expectReplayOf(1) - emitRightNow(2); expectReplayOf(2) - sharedFlow.resetReplayCache(); expectReplayOf() - sharedFlow.resetReplayCache(); expectReplayOf() - emitRightNow(3); expectReplayOf(3) - emitRightNow(4); expectReplayOf(4) - val a = subscribe("a"); collect(a, 4) - emitRightNow(5); expectReplayOf(5); collect(a, 5) - emitRightNow(6) - sharedFlow.resetReplayCache(); expectReplayOf() - sharedFlow.resetReplayCache(); expectReplayOf() - val e7 = emitSuspends(7) - val e8 = emitSuspends(8) - val e9 = emitSuspends(9) - collect(a, 6); emitResumes(e7); expectReplayOf(7) - sharedFlow.resetReplayCache(); expectReplayOf() - sharedFlow.resetReplayCache(); expectReplayOf() // buffer 7 | -- no replay, but still buffered - val b = subscribe("b") - collect(a, 7); emitResumes(e8); expectReplayOf(8) - collect(b, 8) // buffer | 8 -- a is slow - val e10 = emitSuspends(10) - val e11 = emitSuspends(11) - val e12 = emitSuspends(12) - cancel(e9) - collect(a, 8); emitResumes(e10); expectReplayOf(10) - collect(a, 10) // now b's slow - cancel(e11) - collect(b, 10); emitResumes(e12); expectReplayOf(12) - collect(a, 12) - collect(b, 12) - sharedFlow.resetReplayCache(); expectReplayOf() - sharedFlow.resetReplayCache(); expectReplayOf() // nothing is buffered -- both collectors up to date - emitRightNow(13); expectReplayOf(13) - collect(b, 13) // a is slow - val e14 = emitSuspends(14) - val e15 = emitSuspends(15) - val e16 = emitSuspends(16) - cancel(e14) - cancel(a); emitResumes(e15); expectReplayOf(15) // cancelling slow subscriber - collect(b, 15); emitResumes(e16); expectReplayOf(16) - collect(b, 16) - } - - @Test - fun testReplay2Extra2DropOldest() = - testSharedFlow(MutableSharedFlow(2, 2, BufferOverflow.DROP_OLDEST)) { - emitRightNow(0); expectReplayOf(0) - emitRightNow(1); expectReplayOf(0, 1) - emitRightNow(2); expectReplayOf(1, 2) - emitRightNow(3); expectReplayOf(2, 3) - emitRightNow(4); expectReplayOf(3, 4) - val a = subscribe("a") - collect(a, 3) - emitRightNow(5); expectReplayOf(4, 5) - emitRightNow(6); expectReplayOf(5, 6) - emitRightNow(7); expectReplayOf(6, 7) // buffer 4, 5 | 6, 7 - emitRightNow(8); expectReplayOf(7, 8) // buffer 5, 6 | 7, 8 - emitRightNow(9); expectReplayOf(8, 9) // buffer 6, 7 | 8, 9 - collect(a, 6, 7) - val b = subscribe("b") - collect(b, 8, 9) // buffer | 8, 9 - emitRightNow(10); expectReplayOf(9, 10) // buffer 8 | 9, 10 - collect(a, 8, 9, 10) // buffer | 9, 10, note "b" had not collected 10 yet - emitRightNow(11); expectReplayOf(10, 11) // buffer | 10, 11 - emitRightNow(12); expectReplayOf(11, 12) // buffer 10 | 11, 12 - emitRightNow(13); expectReplayOf(12, 13) // buffer 10, 11 | 12, 13 - emitRightNow(14); expectReplayOf(13, 14) // buffer 11, 12 | 13, 14, "b" missed 10 - collect(b, 11, 12, 13, 14) - sharedFlow.resetReplayCache(); expectReplayOf() // buffer 11, 12, 13, 14 | - sharedFlow.resetReplayCache(); expectReplayOf() - collect(a, 11, 12, 13, 14) - emitRightNow(15); expectReplayOf(15) - collect(a, 15) - collect(b, 15) - } - - private fun testSharedFlow( - sharedFlow: MutableSharedFlow, - scenario: suspend ScenarioDsl.() -> Unit - ) = runTest { - var dsl: ScenarioDsl? = null - try { - coroutineScope { - dsl = ScenarioDsl(sharedFlow, coroutineContext) - dsl!!.scenario() - dsl!!.stop() - } - } catch (e: Throwable) { - dsl?.printLog() - throw e - } - } - - private data class TestJob(val job: Job, val name: String) { - override fun toString(): String = name - } - - private open class Action - private data class EmitResumes(val job: TestJob) : Action() - private data class Collected(val job: TestJob, val value: Any?) : Action() - private data class ResumeCollecting(val job: TestJob) : Action() - private data class Cancelled(val job: TestJob) : Action() - - @OptIn(ExperimentalStdlibApi::class) - private class ScenarioDsl( - val sharedFlow: MutableSharedFlow, - coroutineContext: CoroutineContext - ) { - private val log = ArrayList() - private val timeout = 10000L - private val scope = CoroutineScope(coroutineContext + Job()) - private val actions = HashSet() - private val actionWaiters = ArrayDeque>() - private var expectedReplay = emptyList() - - private fun checkReplay() { - assertEquals(expectedReplay, sharedFlow.replayCache) - } - - private fun wakeupWaiters() { - repeat(actionWaiters.size) { - actionWaiters.removeFirst().resume(Unit) - } - } - - private fun addAction(action: Action) { - actions.add(action) - wakeupWaiters() - } - - private suspend fun awaitAction(action: Action) { - withTimeoutOrNull(timeout) { - while (!actions.remove(action)) { - suspendCancellableCoroutine { actionWaiters.add(it) } - } - } ?: error("Timed out waiting for action: $action") - wakeupWaiters() - } - - private fun launchEmit(a: T): TestJob { - val name = "emit($a)" - val job = scope.launch(start = CoroutineStart.UNDISPATCHED) { - val job = TestJob(coroutineContext[Job]!!, name) - try { - log(name) - sharedFlow.emit(a) - log("$name resumes") - addAction(EmitResumes(job)) - } catch(e: CancellationException) { - log("$name cancelled") - addAction(Cancelled(job)) - } - } - return TestJob(job, name) - } - - fun expectReplayOf(vararg a: T) { - expectedReplay = a.toList() - checkReplay() - } - - fun emitRightNow(a: T) { - val job = launchEmit(a) - assertTrue(actions.remove(EmitResumes(job))) - } - - fun emitSuspends(a: T): TestJob { - val job = launchEmit(a) - assertFalse(EmitResumes(job) in actions) - checkReplay() - return job - } - - suspend fun emitResumes(job: TestJob) { - awaitAction(EmitResumes(job)) - } - - suspend fun cancel(job: TestJob) { - log("cancel(${job.name})") - job.job.cancel() - awaitAction(Cancelled(job)) - } - - fun subscribe(id: String): TestJob { - val name = "collect($id)" - val job = scope.launch(start = CoroutineStart.UNDISPATCHED) { - val job = TestJob(coroutineContext[Job]!!, name) - try { - awaitAction(ResumeCollecting(job)) - log("$name start") - sharedFlow.collect { value -> - log("$name -> $value") - addAction(Collected(job, value)) - awaitAction(ResumeCollecting(job)) - log("$name -> $value resumes") - } - error("$name completed") - } catch(e: CancellationException) { - log("$name cancelled") - addAction(Cancelled(job)) - } - } - return TestJob(job, name) - } - - suspend fun collect(job: TestJob, vararg a: T) { - for (value in a) { - checkReplay() // should not have changed - addAction(ResumeCollecting(job)) - awaitAction(Collected(job, value)) - } - } - - fun stop() { - log("--- stop") - scope.cancel() - } - - private fun log(text: String) { - log.add(text) - } - - fun printLog() { - println("--- The most recent log entries ---") - log.takeLast(30).forEach(::println) - println("--- That's it ---") - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt deleted file mode 100644 index 32d88f3c99..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt +++ /dev/null @@ -1,798 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.random.* -import kotlin.test.* - -/** - * This test suite contains some basic tests for [SharedFlow]. There are some scenarios here written - * using [expect] and they are not very readable. See [SharedFlowScenarioTest] for a better - * behavioral test-suit. - */ -class SharedFlowTest : TestBase() { - @Test - fun testRendezvousSharedFlowBasic() = runTest { - expect(1) - val sh = MutableSharedFlow() - assertTrue(sh.replayCache.isEmpty()) - assertEquals(0, sh.subscriptionCount.value) - sh.emit(1) // no suspend - assertTrue(sh.replayCache.isEmpty()) - assertEquals(0, sh.subscriptionCount.value) - expect(2) - // one collector - val job1 = launch(start = CoroutineStart.UNDISPATCHED) { - expect(3) - sh.collect { - when(it) { - 4 -> expect(5) - 6 -> expect(7) - 10 -> expect(11) - 13 -> expect(14) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(4) - assertEquals(1, sh.subscriptionCount.value) - sh.emit(4) - assertTrue(sh.replayCache.isEmpty()) - expect(6) - sh.emit(6) - expect(8) - // one more collector - val job2 = launch(start = CoroutineStart.UNDISPATCHED) { - expect(9) - sh.collect { - when(it) { - 10 -> expect(12) - 13 -> expect(15) - 17 -> expect(18) - null -> expect(20) - 21 -> expect(22) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(10) - assertEquals(2, sh.subscriptionCount.value) - sh.emit(10) // to both collectors now! - assertTrue(sh.replayCache.isEmpty()) - expect(13) - sh.emit(13) - expect(16) - job1.cancel() // cancel the first collector - yield() - assertEquals(1, sh.subscriptionCount.value) - expect(17) - sh.emit(17) // only to second collector - expect(19) - sh.emit(null) // emit null to the second collector - expect(21) - sh.emit(21) // non-null again - expect(23) - job2.cancel() // cancel the second collector - yield() - assertEquals(0, sh.subscriptionCount.value) - expect(24) - sh.emit(24) // does not go anywhere - assertEquals(0, sh.subscriptionCount.value) - assertTrue(sh.replayCache.isEmpty()) - finish(25) - } - - @Test - fun testRendezvousSharedFlowReset() = runTest { - expect(1) - val sh = MutableSharedFlow() - val barrier = Channel(1) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - sh.collect { - when (it) { - 3 -> { - expect(4) - barrier.receive() // hold on before collecting next one - } - 6 -> expect(10) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(3) - sh.emit(3) // rendezvous - expect(5) - assertFalse(sh.tryEmit(5)) // collector is not ready now - launch(start = CoroutineStart.UNDISPATCHED) { - expect(6) - sh.emit(6) // suspends - expect(12) - } - expect(7) - yield() // no wakeup -> all suspended - expect(8) - // now reset cache -> nothing happens, there is no cache - sh.resetReplayCache() - yield() - expect(9) - // now resume collector - barrier.send(Unit) - yield() // to collector - expect(11) - yield() // to emitter - expect(13) - assertFalse(sh.tryEmit(13)) // rendezvous does not work this way - job.cancel() - finish(14) - } - - @Test - fun testReplay1SharedFlowBasic() = runTest { - expect(1) - val sh = MutableSharedFlow(1) - assertTrue(sh.replayCache.isEmpty()) - assertEquals(0, sh.subscriptionCount.value) - sh.emit(1) // no suspend - assertEquals(listOf(1), sh.replayCache) - assertEquals(0, sh.subscriptionCount.value) - expect(2) - sh.emit(2) // no suspend - assertEquals(listOf(2), sh.replayCache) - expect(3) - // one collector - val job1 = launch(start = CoroutineStart.UNDISPATCHED) { - expect(4) - sh.collect { - when(it) { - 2 -> expect(5) // got it immediately from replay cache - 6 -> expect(8) - null -> expect(14) - 17 -> expect(18) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(6) - assertEquals(1, sh.subscriptionCount.value) - sh.emit(6) // does not suspend, but buffers - assertEquals(listOf(6), sh.replayCache) - expect(7) - yield() - expect(9) - // one more collector - val job2 = launch(start = CoroutineStart.UNDISPATCHED) { - expect(10) - sh.collect { - when(it) { - 6 -> expect(11) // from replay cache - null -> expect(15) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(12) - assertEquals(2, sh.subscriptionCount.value) - sh.emit(null) - expect(13) - assertEquals(listOf(null), sh.replayCache) - yield() - assertEquals(listOf(null), sh.replayCache) - expect(16) - job2.cancel() - yield() - assertEquals(1, sh.subscriptionCount.value) - expect(17) - sh.emit(17) - assertEquals(listOf(17), sh.replayCache) - yield() - expect(19) - job1.cancel() - yield() - assertEquals(0, sh.subscriptionCount.value) - assertEquals(listOf(17), sh.replayCache) - finish(20) - } - - @Test - fun testReplay1() = runTest { - expect(1) - val sh = MutableSharedFlow(1) - assertEquals(listOf(), sh.replayCache) - val barrier = Channel(1) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - sh.collect { - when (it) { - 3 -> { - expect(4) - barrier.receive() // collector waits - } - 5 -> expect(10) - 6 -> expect(11) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(3) - assertTrue(sh.tryEmit(3)) // buffered - assertEquals(listOf(3), sh.replayCache) - yield() // to collector - expect(5) - assertTrue(sh.tryEmit(5)) // buffered - assertEquals(listOf(5), sh.replayCache) - launch(start = CoroutineStart.UNDISPATCHED) { - expect(6) - sh.emit(6) // buffer full, suspended - expect(13) - } - expect(7) - assertEquals(listOf(5), sh.replayCache) - sh.resetReplayCache() // clear cache - assertEquals(listOf(), sh.replayCache) - expect(8) - yield() // emitter still suspended - expect(9) - assertEquals(listOf(), sh.replayCache) - assertFalse(sh.tryEmit(10)) // still no buffer space - assertEquals(listOf(), sh.replayCache) - barrier.send(Unit) // resume collector - yield() // to collector - expect(12) - yield() // to emitter, that should have resumed - expect(14) - job.cancel() - assertEquals(listOf(6), sh.replayCache) - finish(15) - } - - @Test - fun testReplay2Extra1() = runTest { - expect(1) - val sh = MutableSharedFlow( - replay = 2, - extraBufferCapacity = 1 - ) - assertEquals(listOf(), sh.replayCache) - assertTrue(sh.tryEmit(0)) - assertEquals(listOf(0), sh.replayCache) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - var cnt = 0 - sh.collect { - when (it) { - 0 -> when (cnt++) { - 0 -> expect(3) - 1 -> expect(14) - else -> expectUnreached() - } - 1 -> expect(6) - 2 -> expect(7) - 3 -> expect(8) - 4 -> expect(12) - 5 -> expect(13) - 16 -> expect(17) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(4) - assertTrue(sh.tryEmit(1)) // buffered - assertEquals(listOf(0, 1), sh.replayCache) - assertTrue(sh.tryEmit(2)) // buffered - assertEquals(listOf(1, 2), sh.replayCache) - assertTrue(sh.tryEmit(3)) // buffered (buffer size is 3) - assertEquals(listOf(2, 3), sh.replayCache) - expect(5) - yield() // to collector - expect(9) - assertEquals(listOf(2, 3), sh.replayCache) - assertTrue(sh.tryEmit(4)) // can buffer now - assertEquals(listOf(3, 4), sh.replayCache) - assertTrue(sh.tryEmit(5)) // can buffer now - assertEquals(listOf(4, 5), sh.replayCache) - assertTrue(sh.tryEmit(0)) // can buffer one more, let it be zero again - assertEquals(listOf(5, 0), sh.replayCache) - expect(10) - assertFalse(sh.tryEmit(10)) // cannot buffer anymore! - sh.resetReplayCache() // replay cache - assertEquals(listOf(), sh.replayCache) // empty - assertFalse(sh.tryEmit(0)) // still cannot buffer anymore (reset does not help) - assertEquals(listOf(), sh.replayCache) // empty - expect(11) - yield() // resume collector, will get next values - expect(15) - sh.resetReplayCache() // reset again, nothing happens - assertEquals(listOf(), sh.replayCache) // empty - yield() // collector gets nothing -- no change - expect(16) - assertTrue(sh.tryEmit(16)) - assertEquals(listOf(16), sh.replayCache) - yield() // gets it - expect(18) - job.cancel() - finish(19) - } - - @Test - fun testBufferNoReplayCancelWhileBuffering() = runTest { - val n = 123 - val sh = MutableSharedFlow(replay = 0, extraBufferCapacity = n) - repeat(3) { - val m = n / 2 // collect half, then suspend - val barrier = Channel(1) - val collectorJob = sh - .onSubscription { - barrier.send(1) - } - .onEach { value -> - if (value == m) { - barrier.send(2) - delay(Long.MAX_VALUE) - } - } - .launchIn(this) - assertEquals(1, barrier.receive()) // make sure it subscribes - launch(start = CoroutineStart.UNDISPATCHED) { - for (i in 0 until n + m) sh.emit(i) // these emits should go Ok - barrier.send(3) - sh.emit(n + 4) // this emit will suspend on buffer overflow - barrier.send(4) - } - assertEquals(2, barrier.receive()) // wait until m collected - assertEquals(3, barrier.receive()) // wait until all are emitted - collectorJob.cancel() // cancelling collector job must clear buffer and resume emitter - assertEquals(4, barrier.receive()) // verify that emitter resumes - } - } - - @Test - fun testRepeatedResetWithReplay() = runTest { - val n = 10 - val sh = MutableSharedFlow(n) - var i = 0 - repeat(3) { - // collector is slow - val collector = sh.onEach { delay(Long.MAX_VALUE) }.launchIn(this) - val emitter = launch { - repeat(3 * n) { sh.emit(i); i++ } - } - repeat(3) { yield() } // enough to run it to suspension - assertEquals((i - n until i).toList(), sh.replayCache) - sh.resetReplayCache() - assertEquals(emptyList(), sh.replayCache) - repeat(3) { yield() } // enough to run it to suspension - assertEquals(emptyList(), sh.replayCache) // still blocked - collector.cancel() - emitter.cancel() - repeat(3) { yield() } // enough to run it to suspension - } - } - - @Test - fun testSynchronousSharedFlowEmitterCancel() = runTest { - expect(1) - val sh = MutableSharedFlow() - val barrier1 = Job() - val barrier2 = Job() - val barrier3 = Job() - val collector1 = sh.onEach { - when (it) { - 1 -> expect(3) - 2 -> { - expect(6) - barrier2.complete() - } - 3 -> { - expect(9) - barrier3.complete() - } - else -> expectUnreached() - } - }.launchIn(this) - val collector2 = sh.onEach { - when (it) { - 1 -> { - expect(4) - barrier1.complete() - delay(Long.MAX_VALUE) - } - else -> expectUnreached() - } - }.launchIn(this) - repeat(2) { yield() } // launch both subscribers - val emitter = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - sh.emit(1) - barrier1.join() - expect(5) - sh.emit(2) // suspends because of slow collector2 - expectUnreached() // will be cancelled - } - barrier2.join() // wait - expect(7) - // Now cancel the emitter! - emitter.cancel() - yield() - // Cancel slow collector - collector2.cancel() - yield() - // emit to fast collector1 - expect(8) - sh.emit(3) - barrier3.join() - expect(10) - // cancel it, too - collector1.cancel() - finish(11) - } - - @Test - fun testDifferentBufferedFlowCapacities() { - for (replay in 0..10) { - for (extraBufferCapacity in 0..5) { - if (replay == 0 && extraBufferCapacity == 0) continue // test only buffered shared flows - try { - val sh = MutableSharedFlow(replay, extraBufferCapacity) - // repeat the whole test a few times to make sure it works correctly when slots are reused - repeat(3) { - testBufferedFlow(sh, replay) - } - } catch (e: Throwable) { - error("Failed for replay=$replay, extraBufferCapacity=$extraBufferCapacity", e) - } - } - } - } - - private fun testBufferedFlow(sh: MutableSharedFlow, replay: Int) = runTest { - reset() - expect(1) - val n = 100 // initially emitted to fill buffer - for (i in 1..n) assertTrue(sh.tryEmit(i)) - // initial expected replayCache - val rcStart = n - replay + 1 - val rcRange = rcStart..n - val rcSize = n - rcStart + 1 - assertEquals(rcRange.toList(), sh.replayCache) - // create collectors - val m = 10 // collectors created - var ofs = 0 - val k = 42 // emissions to collectors - val ecRange = n + 1..n + k - val jobs = List(m) { jobIndex -> - launch(start = CoroutineStart.UNDISPATCHED) { - sh.collect { i -> - when (i) { - in rcRange -> expect(2 + i - rcStart + jobIndex * rcSize) - in ecRange -> expect(2 + ofs + jobIndex) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - } - ofs = rcSize * m + 2 - expect(ofs) - // emit to all k times - for (p in ecRange) { - sh.emit(p) - expect(1 + ofs) // buffered, no suspend - yield() - ofs += 2 + m - expect(ofs) - } - assertEquals(ecRange.toList().takeLast(replay), sh.replayCache) - // cancel all collectors - jobs.forEach { it.cancel() } - yield() - // replay cache is still there - assertEquals(ecRange.toList().takeLast(replay), sh.replayCache) - finish(1 + ofs) - } - - @Test - fun testDropLatest() = testDropLatestOrOldest(BufferOverflow.DROP_LATEST) - - @Test - fun testDropOldest() = testDropLatestOrOldest(BufferOverflow.DROP_OLDEST) - - private fun testDropLatestOrOldest(bufferOverflow: BufferOverflow) = runTest { - reset() - expect(1) - val sh = MutableSharedFlow(1, onBufferOverflow = bufferOverflow) - sh.emit(1) - sh.emit(2) - // always keeps last w/o collectors - assertEquals(listOf(2), sh.replayCache) - assertEquals(0, sh.subscriptionCount.value) - // one collector - val valueAfterOverflow = when (bufferOverflow) { - BufferOverflow.DROP_OLDEST -> 5 - BufferOverflow.DROP_LATEST -> 4 - else -> error("not supported in this test: $bufferOverflow") - } - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - sh.collect { - when(it) { - 2 -> { // replayed - expect(3) - yield() // and suspends, busy waiting - } - valueAfterOverflow -> expect(7) - 8 -> expect(9) - else -> expectUnreached() - } - } - expectUnreached() // does not complete normally - } - expect(4) - assertEquals(1, sh.subscriptionCount.value) - assertEquals(listOf(2), sh.replayCache) - sh.emit(4) // buffering, collector is busy - assertEquals(listOf(4), sh.replayCache) - expect(5) - sh.emit(5) // Buffer overflow here, will not suspend - assertEquals(listOf(valueAfterOverflow), sh.replayCache) - expect(6) - yield() // to the job - expect(8) - sh.emit(8) // not busy now - assertEquals(listOf(8), sh.replayCache) // buffered - yield() // to process - expect(10) - job.cancel() // cancel the job - yield() - assertEquals(0, sh.subscriptionCount.value) - finish(11) - } - - @Test - public fun testOnSubscription() = runTest { - expect(1) - val sh = MutableSharedFlow() - fun share(s: String) { launch(start = CoroutineStart.UNDISPATCHED) { sh.emit(s) } } - sh - .onSubscription { - emit("collector->A") - share("share->A") - } - .onSubscription { - emit("collector->B") - share("share->B") - } - .onStart { - emit("collector->C") - share("share->C") // get's lost, no subscribers yet - } - .onStart { - emit("collector->D") - share("share->D") // get's lost, no subscribers yet - } - .onEach { - when (it) { - "collector->D" -> expect(2) - "collector->C" -> expect(3) - "collector->A" -> expect(4) - "collector->B" -> expect(5) - "share->A" -> expect(6) - "share->B" -> { - expect(7) - currentCoroutineContext().cancel() - } - else -> expectUnreached() - } - } - .launchIn(this) - .join() - finish(8) - } - - @Test - fun onSubscriptionThrows() = runTest { - expect(1) - val sh = MutableSharedFlow(1) - sh.tryEmit("OK") // buffer a string - assertEquals(listOf("OK"), sh.replayCache) - sh - .onSubscription { - expect(2) - throw TestException() - } - .catch { e -> - assertTrue(e is TestException) - expect(3) - } - .collect { - // onSubscription throw before replay is emitted, so no value is collected if it throws - expectUnreached() - } - assertEquals(0, sh.subscriptionCount.value) - finish(4) - } - - @Test - fun testBigReplayManySubscribers() = testManySubscribers(true) - - @Test - fun testBigBufferManySubscribers() = testManySubscribers(false) - - private fun testManySubscribers(replay: Boolean) = runTest { - val n = 100 - val rnd = Random(replay.hashCode()) - val sh = MutableSharedFlow( - replay = if (replay) n else 0, - extraBufferCapacity = if (replay) 0 else n - ) - val subs = ArrayList() - for (i in 1..n) { - sh.emit(i) - val subBarrier = Channel() - val subJob = SubJob() - subs += subJob - // will receive all starting from replay or from new emissions only - subJob.lastReceived = if (replay) 0 else i - subJob.job = sh - .onSubscription { - subBarrier.send(Unit) // signal subscribed - } - .onEach { value -> - assertEquals(subJob.lastReceived + 1, value) - subJob.lastReceived = value - } - .launchIn(this) - subBarrier.receive() // wait until subscribed - // must have also receive all from the replay buffer directly after being subscribed - assertEquals(subJob.lastReceived, i) - // 50% of time cancel one subscriber - if (i % 2 == 0) { - val victim = subs.removeAt(rnd.nextInt(subs.size)) - yield() // make sure victim processed all emissions - assertEquals(victim.lastReceived, i) - victim.job.cancel() - } - } - yield() // make sure the last emission is processed - for (subJob in subs) { - assertEquals(subJob.lastReceived, n) - subJob.job.cancel() - } - } - - private class SubJob { - lateinit var job: Job - var lastReceived = 0 - } - - @Test - fun testStateFlowModel() = runTest { - val stateFlow = MutableStateFlow(null) - val expect = modelLog(stateFlow) - val sharedFlow = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - sharedFlow.tryEmit(null) // initial value - val actual = modelLog(sharedFlow) { distinctUntilChanged() } - for (i in 0 until minOf(expect.size, actual.size)) { - if (actual[i] != expect[i]) { - for (j in maxOf(0, i - 10)..i) println("Actual log item #$j: ${actual[j]}") - assertEquals(expect[i], actual[i], "Log item #$i") - } - } - assertEquals(expect.size, actual.size) - } - - private suspend fun modelLog( - sh: MutableSharedFlow, - op: Flow.() -> Flow = { this } - ): List = coroutineScope { - val rnd = Random(1) - val result = ArrayList() - val job = launch { - sh.op().collect { value -> - result.add("Collect: $value") - repeat(rnd.nextInt(0..2)) { - result.add("Collect: yield") - yield() - } - } - } - repeat(1000) { - val value = if (rnd.nextBoolean()) null else rnd.nextData() - if (rnd.nextInt(20) == 0) { - result.add("resetReplayCache & emit: $value") - if (sh !is StateFlow<*>) sh.resetReplayCache() - assertTrue(sh.tryEmit(value)) - } else { - result.add("Emit: $value") - sh.emit(value) - } - repeat(rnd.nextInt(0..2)) { - result.add("Emit: yield") - yield() - } - } - result.add("main: cancel") - job.cancel() - result.add("main: yield") - yield() - result.add("main: join") - job.join() - result - } - - data class Data(val x: Int) - private val dataCache = (1..5).associateWith { Data(it) } - - // Note that we test proper null support here, too - private fun Random.nextData(): Data? { - val x = nextInt(0..5) - if (x == 0) return null - // randomly reuse ref or create a new instance - return if(nextBoolean()) dataCache[x] else Data(x) - } - - @Test - fun testOperatorFusion() { - val sh = MutableSharedFlow() - assertSame(sh, (sh as Flow<*>).cancellable()) - assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) - assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) - } - - @Test - fun testIllegalArgumentException() { - assertFailsWith { MutableSharedFlow(-1) } - assertFailsWith { MutableSharedFlow(0, extraBufferCapacity = -1) } - assertFailsWith { MutableSharedFlow(0, onBufferOverflow = BufferOverflow.DROP_LATEST) } - assertFailsWith { MutableSharedFlow(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) } - } - - @Test - public fun testReplayCancellability() = testCancellability(fromReplay = true) - - @Test - public fun testEmitCancellability() = testCancellability(fromReplay = false) - - private fun testCancellability(fromReplay: Boolean) = runTest { - expect(1) - val sh = MutableSharedFlow(5) - fun emitTestData() { - for (i in 1..5) assertTrue(sh.tryEmit(i)) - } - if (fromReplay) emitTestData() // fill in replay first - var subscribed = true - val job = sh - .onSubscription { subscribed = true } - .onEach { i -> - when (i) { - 1 -> expect(2) - 2 -> expect(3) - 3 -> { - expect(4) - currentCoroutineContext().cancel() - } - else -> expectUnreached() // shall check for cancellation - } - } - .launchIn(this) - yield() - assertTrue(subscribed) // yielding in enough - if (!fromReplay) emitTestData() // emit after subscription - job.join() - finish(5) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt deleted file mode 100644 index 496fb7f8ff..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlin.coroutines.* -import kotlin.test.* - -/** - * Functional tests for [SharingStarted] using [withVirtualTime] and a DSL to describe - * testing scenarios and expected behavior for different implementations. - */ -class SharingStartedTest : TestBase() { - @Test - fun testEagerly() = - testSharingStarted(SharingStarted.Eagerly, SharingCommand.START) { - subscriptions(1) - rampUpAndDown() - subscriptions(0) - delay(100) - } - - @Test - fun testLazily() = - testSharingStarted(SharingStarted.Lazily) { - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0) - } - - @Test - fun testWhileSubscribed() = - testSharingStarted(SharingStarted.WhileSubscribed()) { - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0, SharingCommand.STOP) - delay(100) - } - - @Test - fun testWhileSubscribedExpireImmediately() = - testSharingStarted(SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) { - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) - delay(100) - } - - @Test - fun testWhileSubscribedWithTimeout() = - testSharingStarted(SharingStarted.WhileSubscribed(stopTimeoutMillis = 100)) { - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0) - delay(50) // don't give it time to stop - subscriptions(1) // resubscribe again - rampUpAndDown() - subscriptions(0) - afterTime(100, SharingCommand.STOP) - delay(100) - } - - @Test - fun testWhileSubscribedExpiration() = - testSharingStarted(SharingStarted.WhileSubscribed(replayExpirationMillis = 200)) { - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0, SharingCommand.STOP) - delay(150) // don't give it time to reset cache - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0, SharingCommand.STOP) - afterTime(200, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) - } - - @Test - fun testWhileSubscribedStopAndExpiration() = - testSharingStarted(SharingStarted.WhileSubscribed(stopTimeoutMillis = 400, replayExpirationMillis = 300)) { - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0) - delay(350) // don't give it time to stop - subscriptions(1) - rampUpAndDown() - subscriptions(0) - afterTime(400, SharingCommand.STOP) - delay(250) // don't give it time to reset cache - subscriptions(1, SharingCommand.START) - rampUpAndDown() - subscriptions(0) - afterTime(400, SharingCommand.STOP) - afterTime(300, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) - delay(100) - } - - private suspend fun SharingStartedDsl.rampUpAndDown() { - for (i in 2..10) { - delay(100) - subscriptions(i) - } - delay(1000) - for (i in 9 downTo 1) { - subscriptions(i) - delay(100) - } - } - - private fun testSharingStarted( - started: SharingStarted, - initialCommand: SharingCommand? = null, - scenario: suspend SharingStartedDsl.() -> Unit - ) = withVirtualTime { - expect(1) - val dsl = SharingStartedDsl(started, initialCommand, coroutineContext) - dsl.launch() - // repeat every scenario 3 times - repeat(3) { - dsl.scenario() - delay(1000) - } - dsl.stop() - finish(2) - } - - private class SharingStartedDsl( - val started: SharingStarted, - initialCommand: SharingCommand?, - coroutineContext: CoroutineContext - ) { - val subscriptionCount = MutableStateFlow(0) - var previousCommand: SharingCommand? = null - var expectedCommand: SharingCommand? = initialCommand - var expectedTime = 0L - - val dispatcher = coroutineContext[ContinuationInterceptor] as VirtualTimeDispatcher - val scope = CoroutineScope(coroutineContext + Job()) - - suspend fun launch() { - started - .command(subscriptionCount.asStateFlow()) - .onEach { checkCommand(it) } - .launchIn(scope) - letItRun() - } - - fun checkCommand(command: SharingCommand) { - assertTrue(command != previousCommand) - previousCommand = command - assertEquals(expectedCommand, command) - assertEquals(expectedTime, dispatcher.currentTime) - } - - suspend fun subscriptions(count: Int, command: SharingCommand? = null) { - expectedTime = dispatcher.currentTime - subscriptionCount.value = count - if (command != null) { - afterTime(0, command) - } else { - letItRun() - } - } - - suspend fun afterTime(time: Long = 0, command: SharingCommand) { - expectedCommand = command - val remaining = (time - 1).coerceAtLeast(0) // previous letItRun delayed 1ms - expectedTime += remaining - delay(remaining) - letItRun() - } - - private suspend fun letItRun() { - delay(1) - assertEquals(expectedCommand, previousCommand) // make sure expected command was emitted - expectedTime++ // make one more time tick we've delayed - } - - fun stop() { - scope.cancel() - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt deleted file mode 100644 index bcf626e3e3..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlin.test.* -import kotlin.time.* - -class SharingStartedWhileSubscribedTest : TestBase() { - @Test // make sure equals works properly, or otherwise other tests don't make sense - fun testEqualsAndHashcode() { - val params = listOf(0L, 1L, 10L, 100L, 213L, Long.MAX_VALUE) - // HashMap will simultaneously test equals, hashcode and their consistency - val map = HashMap>() - for (i in params) { - for (j in params) { - map[SharingStarted.WhileSubscribed(i, j)] = i to j - } - } - for (i in params) { - for (j in params) { - assertEquals(i to j, map[SharingStarted.WhileSubscribed(i, j)]) - } - } - } - - @OptIn(ExperimentalTime::class) - @Test - fun testDurationParams() { - assertEquals(SharingStarted.WhileSubscribed(0), SharingStarted.WhileSubscribed(Duration.ZERO)) - assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(10.milliseconds)) - assertEquals(SharingStarted.WhileSubscribed(1000), SharingStarted.WhileSubscribed(1.seconds)) - assertEquals(SharingStarted.WhileSubscribed(Long.MAX_VALUE), SharingStarted.WhileSubscribed(Duration.INFINITE)) - assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 0), SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)) - assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed(replayExpiration = 3.milliseconds)) - assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), SharingStarted.WhileSubscribed(replayExpiration = 7.seconds)) - assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = Long.MAX_VALUE), SharingStarted.WhileSubscribed(replayExpiration = Duration.INFINITE)) - } -} - diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt deleted file mode 100644 index 2a613afaf7..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlin.test.* - -/** - * It is mostly covered by [ShareInTest], this just add state-specific checks. - */ -class StateInTest : TestBase() { - @Test - fun testOperatorFusion() = runTest { - val state = flowOf("OK").stateIn(this) - assertTrue(state !is MutableStateFlow<*>) // cannot be cast to mutable state flow - assertSame(state, (state as Flow<*>).cancellable()) - assertSame(state, (state as Flow<*>).distinctUntilChanged()) - assertSame(state, (state as Flow<*>).flowOn(Dispatchers.Default)) - assertSame(state, (state as Flow<*>).conflate()) - assertSame(state, state.buffer(Channel.CONFLATED)) - assertSame(state, state.buffer(Channel.RENDEZVOUS)) - assertSame(state, state.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)) - assertSame(state, state.buffer(0, onBufferOverflow = BufferOverflow.DROP_OLDEST)) - assertSame(state, state.buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST)) - coroutineContext.cancelChildren() - } - - @Test - fun testUpstreamCompletedNoInitialValue() = - testUpstreamCompletedOrFailedReset(failed = false, iv = false) - - @Test - fun testUpstreamFailedNoInitialValue() = - testUpstreamCompletedOrFailedReset(failed = true, iv = false) - - @Test - fun testUpstreamCompletedWithInitialValue() = - testUpstreamCompletedOrFailedReset(failed = false, iv = true) - - @Test - fun testUpstreamFailedWithInitialValue() = - testUpstreamCompletedOrFailedReset(failed = true, iv = true) - - private fun testUpstreamCompletedOrFailedReset(failed: Boolean, iv: Boolean) = runTest { - val emitted = Job() - val terminate = Job() - val sharingJob = CompletableDeferred() - val upstream = flow { - emit("OK") - emitted.complete() - terminate.join() - if (failed) throw TestException() - } - val scope = this + sharingJob - val shared: StateFlow - if (iv) { - shared = upstream.stateIn(scope, SharingStarted.Eagerly, null) - assertEquals(null, shared.value) - } else { - shared = upstream.stateIn(scope) - assertEquals("OK", shared.value) // waited until upstream emitted - } - emitted.join() // should start sharing, emit & cache - assertEquals("OK", shared.value) - terminate.complete() - sharingJob.complete(Unit) - sharingJob.join() // should complete sharing - assertEquals("OK", shared.value) // value is still there - if (failed) { - assertTrue(sharingJob.getCompletionExceptionOrNull() is TestException) - } else { - assertNull(sharingJob.getCompletionExceptionOrNull()) - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt index fa7fc9cb6c..edb9f00fa6 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt @@ -128,12 +128,6 @@ class FirstTest : TestBase() { assertNull(emptyFlow().firstOrNull { true }) } - @Test - fun testFirstOrNullWithNullElement() = runTest { - assertNull(flowOf(null).firstOrNull()) - assertNull(flowOf(null).firstOrNull { true }) - } - @Test fun testFirstOrNullWhenErrorCancelsUpstream() = runTest { val latch = Channel() diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt index 2c1277b1e1..4e89b93bd7 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt @@ -7,7 +7,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.test.* -class SingleTest : TestBase() { +class SingleTest : TestBase() { @Test fun testSingle() = runTest { @@ -25,8 +25,8 @@ class SingleTest : TestBase() { emit(239L) emit(240L) } - assertFailsWith { flow.single() } - assertNull(flow.singleOrNull()) + assertFailsWith { flow.single() } + assertFailsWith { flow.singleOrNull() } } @Test @@ -61,10 +61,6 @@ class SingleTest : TestBase() { assertEquals(1, flowOf(1).single()) assertNull(flowOf(null).single()) assertFailsWith { flowOf().single() } - - assertEquals(1, flowOf(1).singleOrNull()) - assertNull(flowOf(null).singleOrNull()) - assertNull(flowOf().singleOrNull()) } @Test @@ -73,22 +69,5 @@ class SingleTest : TestBase() { val flow = flowOf(instance) assertSame(instance, flow.single()) assertSame(instance, flow.singleOrNull()) - - val flow2 = flow { - emit(BadClass()) - emit(BadClass()) - } - assertFailsWith { flow2.single() } - } - - @Test - fun testSingleNoWait() = runTest { - val flow = flow { - emit(1) - emit(2) - awaitCancellation() - } - - assertNull(flow.singleOrNull()) } } diff --git a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt index e31ccfc16d..5af68f6be5 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt @@ -24,20 +24,19 @@ class SelectLoopTest : TestBase() { expect(3) throw TestException() } - try { - while (true) { - select { - channel.onReceiveOrNull { - expectUnreached() - } - job.onJoin { - expectUnreached() - } + var isDone = false + while (!isDone) { + select { + channel.onReceiveOrNull { + expect(4) + assertEquals(Unit, it) + } + job.onJoin { + expect(5) + isDone = true } } - } catch (e: CancellationException) { - // select will get cancelled because of the failure of job - finish(4) } + finish(6) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 06b938d41a..033b39c7e0 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -7,19 +7,24 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { + public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default, false) + + public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default) + public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined } -private class JsMainDispatcher( - val delegate: CoroutineDispatcher, - private val invokeImmediately: Boolean -) : MainCoroutineDispatcher() { - override val immediate: MainCoroutineDispatcher = - if (invokeImmediately) this else JsMainDispatcher(delegate, true) - override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately +private class JsMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { + + override val immediate: MainCoroutineDispatcher + get() = throw UnsupportedOperationException("Immediate dispatching is not supported on JS") + override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) + override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) + override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index e1b3dcd7a9..a0dfcba2b7 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -35,7 +35,7 @@ internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { messageQueue.enqueue(block) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) return ClearTimeout(handle) } @@ -81,7 +81,7 @@ internal class WindowDispatcher(private val window: Window) : CoroutineDispatche window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis)) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-core/js/src/Promise.kt b/kotlinx-coroutines-core/js/src/Promise.kt index ab2003236a..6c3de76426 100644 --- a/kotlinx-coroutines-core/js/src/Promise.kt +++ b/kotlinx-coroutines-core/js/src/Promise.kt @@ -62,8 +62,6 @@ public fun Promise.asDeferred(): Deferred { * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * stops waiting for the promise and immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was - * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> this@await.then( diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index b69850576e..342b11c69a 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -124,8 +124,6 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { return null } - actual open fun onRemoved(affected: Node) {} - actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode val failure = failure(affected) diff --git a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt b/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt deleted file mode 100644 index 7ca6a242b2..0000000000 --- a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import kotlin.test.* - -class ImmediateDispatcherTest : TestBase() { - - @Test - fun testImmediate() = runTest { - expect(1) - val job = launch { expect(3) } - withContext(Dispatchers.Main.immediate) { - expect(2) - } - job.join() - finish(4) - } - - @Test - fun testMain() = runTest { - expect(1) - val job = launch { expect(2) } - withContext(Dispatchers.Main) { - expect(3) - } - job.join() - finish(4) - } -} diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin index 76ee41159dbc84c6de5231dbec11bebf0d37b008..8a7160fbad39f25ba582998c532c6ff65c788d5f 100644 GIT binary patch delta 74 zcmX@Wdx&>KA)|zNaz<)$c0giLVs2_lYLRELUw(;SX->}Oc1Ck1ZUIIHrlc}v1|Zmc bl6eOczosXH7K3&)1G6Uskf}3y0-FH; } -# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when -# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used. --dontwarn java.lang.instrument.ClassFileTransformer --dontwarn sun.misc.SignalHandler --dontwarn java.lang.instrument.Instrumentation --dontwarn sun.misc.Signal diff --git a/kotlinx-coroutines-core/jvm/src/CommonPool.kt b/kotlinx-coroutines-core/jvm/src/CommonPool.kt index 2203313120..60f30cfe14 100644 --- a/kotlinx-coroutines-core/jvm/src/CommonPool.kt +++ b/kotlinx-coroutines-core/jvm/src/CommonPool.kt @@ -103,8 +103,6 @@ internal object CommonPool : ExecutorCoroutineDispatcher() { (pool ?: getOrCreatePoolSync()).execute(wrapTask(block)) } catch (e: RejectedExecutionException) { unTrackTask() - // CommonPool only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.enqueue(block) } } diff --git a/kotlinx-coroutines-core/jvm/src/DebugStrings.kt b/kotlinx-coroutines-core/jvm/src/DebugStrings.kt index 2ccfebc6d3..184fb655e3 100644 --- a/kotlinx-coroutines-core/jvm/src/DebugStrings.kt +++ b/kotlinx-coroutines-core/jvm/src/DebugStrings.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines -import kotlinx.coroutines.internal.* import kotlin.coroutines.* // internal debugging tools for string representation diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index 787cbf9c44..ed84f55e74 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -5,7 +5,6 @@ package kotlinx.coroutines import java.util.concurrent.* -import kotlin.coroutines.* internal actual val DefaultDelay: Delay = DefaultExecutor @@ -55,7 +54,7 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { * Livelock is possible only if `runBlocking` is called on internal default executed (which is used by default [delay]), * but it's not exposed as public API. */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) override fun run() { diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt index 8033fb38e5..8cd3bb1bae 100644 --- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt @@ -97,7 +97,7 @@ public actual object Dispatchers { * The [CoroutineDispatcher] that is designed for offloading blocking IO tasks to a shared pool of threads. * * Additional threads in this pool are created and are shutdown on demand. - * The number of threads used by tasks in this dispatcher is limited by the value of + * The number of threads used by this dispatcher is limited by the value of * "`kotlinx.coroutines.io.parallelism`" ([IO_PARALLELISM_PROPERTY_NAME]) system property. * It defaults to the limit of 64 threads or the number of cores (whichever is larger). * @@ -106,13 +106,9 @@ public actual object Dispatchers { * If you need a higher number of parallel threads, * you should use a custom dispatcher backed by your own thread pool. * - * ### Implementation note - * * This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using * `withContext(Dispatchers.IO) { ... }` does not lead to an actual switching to another thread — * typically execution continues in the same thread. - * As a result of thread sharing, more than 64 (default parallelism) threads can be created (but not used) - * during operations over IO dispatcher. */ @JvmStatic public val IO: CoroutineDispatcher = DefaultScheduler.IO diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index 8ffc22d8bb..a4d6b46c43 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* * Instances of [ExecutorCoroutineDispatcher] should be closed by the owner of the dispatcher. * * This class is generally used as a bridge between coroutine-based API and - * asynchronous API that requires an instance of the [Executor]. + * asynchronous API which requires instance of the [Executor]. */ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable { /** @suppress */ @@ -38,12 +38,6 @@ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closea /** * Converts an instance of [ExecutorService] to an implementation of [ExecutorCoroutineDispatcher]. - * - * If the underlying executor throws [RejectedExecutionException] on - * attempt to submit a continuation task (it happens when [closing][ExecutorCoroutineDispatcher.close] the - * resulting dispatcher, on underlying executor [shutdown][ExecutorService.shutdown], or when it uses limited queues), - * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the - * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. */ @JvmName("from") // this is for a nice Java API, see issue #255 public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher = @@ -51,12 +45,6 @@ public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher /** * Converts an instance of [Executor] to an implementation of [CoroutineDispatcher]. - * - * If the underlying executor throws [RejectedExecutionException] on - * attempt to submit a continuation task (it happens when [closing][ExecutorCoroutineDispatcher.close] the - * resulting dispatcher, on underlying executor [shutdown][ExecutorService.shutdown], or when it uses limited queues), - * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the - * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. */ @JvmName("from") // this is for a nice Java API, see issue #255 public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher = @@ -94,8 +82,7 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa executor.execute(wrapTask(block)) } catch (e: RejectedExecutionException) { unTrackTask() - cancelJobOnRejection(context, e) - Dispatchers.IO.dispatch(context, block) + DefaultExecutor.enqueue(block) } } @@ -106,7 +93,7 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { val future = if (removesFutureOnCancellation) { - scheduleBlock(ResumeUndispatchedRunnable(this, continuation), continuation.context, timeMillis) + scheduleBlock(ResumeUndispatchedRunnable(this, continuation), timeMillis, TimeUnit.MILLISECONDS) } else { null } @@ -119,31 +106,24 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa DefaultExecutor.scheduleResumeAfterDelay(timeMillis, continuation) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val future = if (removesFutureOnCancellation) { - scheduleBlock(block, context, timeMillis) + scheduleBlock(block, timeMillis, TimeUnit.MILLISECONDS) } else { null } - return when { - future != null -> DisposableFutureHandle(future) - else -> DefaultExecutor.invokeOnTimeout(timeMillis, block, context) - } + + return if (future != null ) DisposableFutureHandle(future) else DefaultExecutor.invokeOnTimeout(timeMillis, block) } - private fun scheduleBlock(block: Runnable, context: CoroutineContext, timeMillis: Long): ScheduledFuture<*>? { + private fun scheduleBlock(block: Runnable, time: Long, unit: TimeUnit): ScheduledFuture<*>? { return try { - (executor as? ScheduledExecutorService)?.schedule(block, timeMillis, TimeUnit.MILLISECONDS) + (executor as? ScheduledExecutorService)?.schedule(block, time, unit) } catch (e: RejectedExecutionException) { - cancelJobOnRejection(context, e) null } } - private fun cancelJobOnRejection(context: CoroutineContext, exception: RejectedExecutionException) { - context.cancel(CancellationException("The task was rejected", exception)) - } - override fun close() { (executor as? ExecutorService)?.shutdown() } diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt index aa18cd38d6..a0e1ffa2c7 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt @@ -14,11 +14,6 @@ import kotlin.coroutines.* * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its thread). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * - * If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and - * attempt to submit a continuation task is made, - * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the - * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. - * * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due @@ -40,11 +35,6 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its threads). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * - * If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and - * attempt to submit a continuation task is made, - * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the - * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. - * * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index 97f9978139..29f37dac28 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -416,26 +416,20 @@ public actual open class LockFreeLinkedListNode { val next = this.next val removed = next.removed() if (affected._next.compareAndSet(this, removed)) { - // The element was actually removed - desc.onRemoved(affected) // Complete removal operation here. It bails out if next node is also removed and it becomes // responsibility of the next's removes to call correctPrev which would help fix all the links. next.correctPrev(null) } return REMOVE_PREPARED } - // We need to ensure progress even if it operation result consensus was already decided - val consensus = if (decision != null) { + val isDecided = if (decision != null) { // some other logic failure, including RETRY_ATOMIC -- reach consensus on decision fail reason ASAP atomicOp.decide(decision) + true // atomicOp.isDecided will be true as a result } else { - atomicOp.consensus // consult with current decision status like in Harris DCSS - } - val update: Any = when { - consensus === NO_DECISION -> atomicOp // desc.onPrepare returned null -> start doing atomic op - consensus == null -> desc.updatedNext(affected, next) // move forward if consensus on success - else -> next // roll back if consensus if failure + atomicOp.isDecided // consult with current decision status like in Harris DCSS } + val update: Any = if (isDecided) next else atomicOp // restore if decision was already reached affected._next.compareAndSet(this, update) return null } @@ -451,10 +445,9 @@ public actual open class LockFreeLinkedListNode { protected open fun takeAffectedNode(op: OpDescriptor): Node? = affectedNode!! // null for RETRY_ATOMIC protected open fun failure(affected: Node): Any? = null // next: Node | Removed protected open fun retry(affected: Node, next: Any): Boolean = false // next: Node | Removed + protected abstract fun updatedNext(affected: Node, next: Node): Any protected abstract fun finishOnSuccess(affected: Node, next: Node) - public abstract fun updatedNext(affected: Node, next: Node): Any - public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure @@ -463,8 +456,6 @@ public actual open class LockFreeLinkedListNode { return null } - public open fun onRemoved(affected: Node) {} // called once when node was prepared & later removed - @Suppress("UNCHECKED_CAST") final override fun prepare(op: AtomicOp<*>): Any? { while (true) { // lock free loop on next diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 5b2b9ff68c..ddfcdbb142 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -87,14 +87,17 @@ private class MissingMainCoroutineDispatcher( override val immediate: MainCoroutineDispatcher get() = this - override fun isDispatchNeeded(context: CoroutineContext): Boolean = + override fun isDispatchNeeded(context: CoroutineContext): Boolean { missing() + } - override suspend fun delay(time: Long) = + override suspend fun delay(time: Long) { missing() + } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { missing() + } override fun dispatch(context: CoroutineContext, block: Runnable) = missing() diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt index 202c6e1d06..e0890eff66 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt @@ -65,8 +65,6 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block) } catch (e: RejectedExecutionException) { - // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.dispatch(context, block) } @@ -74,8 +72,6 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block, tailDispatch = true) } catch (e: RejectedExecutionException) { - // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.dispatchYield(context, block) } @@ -114,9 +110,7 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block, context, tailDispatch) } catch (e: RejectedExecutionException) { - // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved - // for testing purposes, so we don't have to worry about cancelling the affected Job here. - // TaskContext shouldn't be lost here to properly invoke before/after task + // Context shouldn't be lost here to properly invoke before/after task DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context)) } } diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt index 649c95375d..e7c8b6b671 100644 --- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt @@ -230,7 +230,7 @@ public class TestCoroutineContext(private val name: String? = null) : CoroutineC }, timeMillis) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val node = postDelayed(block, timeMillis) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt b/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt index 2612b84153..8a7dce01ee 100644 --- a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt @@ -9,24 +9,25 @@ import kotlinx.coroutines.selects.* import kotlin.test.* class AtomicCancellationTest : TestBase() { + @Test - fun testSendCancellable() = runBlocking { + fun testSendAtomicCancel() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) channel.send(42) // suspends - expectUnreached() // should NOT execute because of cancellation + expect(4) // should execute despite cancellation } expect(3) assertEquals(42, channel.receive()) // will schedule sender for further execution job.cancel() // cancel the job next yield() // now yield - finish(4) + finish(5) } @Test - fun testSelectSendCancellable() = runBlocking { + fun testSelectSendAtomicCancel() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -37,33 +38,34 @@ class AtomicCancellationTest : TestBase() { "OK" } } - expectUnreached() // should NOT execute because of cancellation + assertEquals("OK", result) + expect(5) // should execute despite cancellation } expect(3) assertEquals(42, channel.receive()) // will schedule sender for further execution job.cancel() // cancel the job next yield() // now yield - finish(4) + finish(6) } @Test - fun testReceiveCancellable() = runBlocking { + fun testReceiveAtomicCancel() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) assertEquals(42, channel.receive()) // suspends - expectUnreached() // should NOT execute because of cancellation + expect(4) // should execute despite cancellation } expect(3) channel.send(42) // will schedule receiver for further execution job.cancel() // cancel the job next yield() // now yield - finish(4) + finish(5) } @Test - fun testSelectReceiveCancellable() = runBlocking { + fun testSelectReceiveAtomicCancel() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -75,13 +77,14 @@ class AtomicCancellationTest : TestBase() { "OK" } } - expectUnreached() // should NOT execute because of cancellation + assertEquals("OK", result) + expect(5) // should execute despite cancellation } expect(3) channel.send(42) // will schedule receiver for further execution job.cancel() // cancel the job next yield() // now yield - finish(4) + finish(6) } @Test diff --git a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt index ebf08a03d0..033b9b7bc9 100644 --- a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -29,8 +29,6 @@ class ExecutorsTest : TestBase() { val context = newFixedThreadPoolContext(2, "TestPool") runBlocking(context) { checkThreadName("TestPool") - delay(10) - checkThreadName("TestPool") // should dispatch on the right thread } context.close() } @@ -40,8 +38,6 @@ class ExecutorsTest : TestBase() { val executor = Executors.newSingleThreadExecutor { r -> Thread(r, "TestExecutor") } runBlocking(executor.asCoroutineDispatcher()) { checkThreadName("TestExecutor") - delay(10) - checkThreadName("TestExecutor") // should dispatch on the right thread } executor.shutdown() } diff --git a/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt b/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt index c9f722a5b8..9bf8ffad85 100644 --- a/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -15,21 +15,8 @@ import kotlin.test.* @RunWith(Parameterized::class) class FailingCoroutinesMachineryTest( private val element: CoroutineContext.Element, - private val dispatcher: TestDispatcher + private val dispatcher: CoroutineDispatcher ) : TestBase() { - class TestDispatcher(val name: String, val block: () -> CoroutineDispatcher) { - private var _value: CoroutineDispatcher? = null - - val value: CoroutineDispatcher - get() = _value ?: block().also { _value = it } - - override fun toString(): String = name - - fun reset() { - runCatching { (_value as? ExecutorCoroutineDispatcher)?.close() } - _value = null - } - } private var caught: Throwable? = null private val latch = CountDownLatch(1) @@ -88,7 +75,7 @@ class FailingCoroutinesMachineryTest( @After fun tearDown() { - dispatcher.reset() + runCatching { (dispatcher as? ExecutorCoroutineDispatcher)?.close() } if (lazyOuterDispatcher.isInitialized()) lazyOuterDispatcher.value.close() } @@ -97,14 +84,14 @@ class FailingCoroutinesMachineryTest( @Parameterized.Parameters(name = "Element: {0}, dispatcher: {1}") fun dispatchers(): List> { val elements = listOf(FailingRestore, FailingUpdate) - val dispatchers = listOf( - TestDispatcher("Dispatchers.Unconfined") { Dispatchers.Unconfined }, - TestDispatcher("Dispatchers.Default") { Dispatchers.Default }, - TestDispatcher("Executors.newFixedThreadPool(1)") { Executors.newFixedThreadPool(1).asCoroutineDispatcher() }, - TestDispatcher("Executors.newScheduledThreadPool(1)") { Executors.newScheduledThreadPool(1).asCoroutineDispatcher() }, - TestDispatcher("ThrowingDispatcher") { ThrowingDispatcher }, - TestDispatcher("ThrowingDispatcher2") { ThrowingDispatcher2 } + val dispatchers = listOf( + Dispatchers.Unconfined, + Dispatchers.Default, + Executors.newFixedThreadPool(1).asCoroutineDispatcher(), + Executors.newScheduledThreadPool(1).asCoroutineDispatcher(), + ThrowingDispatcher, ThrowingDispatcher2 ) + return elements.flatMap { element -> dispatchers.map { dispatcher -> arrayOf(element, dispatcher) @@ -115,13 +102,13 @@ class FailingCoroutinesMachineryTest( @Test fun testElement() = runTest { - launch(NonCancellable + dispatcher.value + exceptionHandler + element) {} + launch(NonCancellable + dispatcher + exceptionHandler + element) {} checkException() } @Test fun testNestedElement() = runTest { - launch(NonCancellable + dispatcher.value + exceptionHandler) { + launch(NonCancellable + dispatcher + exceptionHandler) { launch(element) { } } checkException() @@ -130,7 +117,7 @@ class FailingCoroutinesMachineryTest( @Test fun testNestedDispatcherAndElement() = runTest { launch(lazyOuterDispatcher.value + NonCancellable + exceptionHandler) { - launch(element + dispatcher.value) { } + launch(element + dispatcher) { } } checkException() } diff --git a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt b/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt index 50d86f32be..ec3635ca36 100644 --- a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt @@ -5,7 +5,6 @@ package kotlinx.coroutines import org.junit.* -import kotlin.coroutines.* /** * Test a race between job failure and join. @@ -13,52 +12,22 @@ import kotlin.coroutines.* * See [#1123](https://github.com/Kotlin/kotlinx.coroutines/issues/1123). */ class JobStructuredJoinStressTest : TestBase() { - private val nRepeats = 10_000 * stressTestMultiplier + private val nRepeats = 1_000 * stressTestMultiplier @Test - fun testStressRegularJoin() { - stress(Job::join) - } - - @Test - fun testStressSuspendCancellable() { - stress { job -> - suspendCancellableCoroutine { cont -> - job.invokeOnCompletion { cont.resume(Unit) } - } - } - } - - @Test - fun testStressSuspendCancellableReusable() { - stress { job -> - suspendCancellableCoroutineReusable { cont -> - job.invokeOnCompletion { cont.resume(Unit) } - } - } - } - - private fun stress(join: suspend (Job) -> Unit) { - expect(1) - repeat(nRepeats) { index -> + fun testStress() { + repeat(nRepeats) { assertFailsWith { runBlocking { // launch in background val job = launch(Dispatchers.Default) { throw TestException("OK") // crash } - try { - join(job) - error("Should not complete successfully") - } catch (e: CancellationException) { - // must always crash with cancellation exception - expect(2 + index) - } catch (e: Throwable) { - error("Unexpected exception", e) + assertFailsWith { + job.join() } } } } - finish(2 + nRepeats) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt deleted file mode 100644 index a6f4dd6b18..0000000000 --- a/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.scheduling.* -import org.junit.* -import org.junit.Test -import java.util.concurrent.* -import kotlin.test.* - -class RejectedExecutionTest : TestBase() { - private val threadName = "RejectedExecutionTest" - private val executor = RejectingExecutor() - - @After - fun tearDown() { - executor.shutdown() - executor.awaitTermination(10, TimeUnit.SECONDS) - } - - @Test - fun testRejectOnLaunch() = runTest { - expect(1) - val job = launch(executor.asCoroutineDispatcher()) { - expectUnreached() - } - assertEquals(1, executor.submittedTasks) - assertTrue(job.isCancelled) - finish(2) - } - - @Test - fun testRejectOnLaunchAtomic() = runTest { - expect(1) - val job = launch(executor.asCoroutineDispatcher(), start = CoroutineStart.ATOMIC) { - expect(2) - assertEquals(true, coroutineContext[Job]?.isCancelled) - assertIoThread() // was rejected on start, but start was atomic - } - assertEquals(1, executor.submittedTasks) - job.join() - finish(3) - } - - @Test - fun testRejectOnWithContext() = runTest { - expect(1) - assertFailsWith { - withContext(executor.asCoroutineDispatcher()) { - expectUnreached() - } - } - assertEquals(1, executor.submittedTasks) - finish(2) - } - - @Test - fun testRejectOnResumeInContext() = runTest { - expect(1) - executor.acceptTasks = 1 // accept one task - assertFailsWith { - withContext(executor.asCoroutineDispatcher()) { - expect(2) - assertExecutorThread() - try { - withContext(Dispatchers.Default) { - expect(3) - assertDefaultDispatcherThread() - // We have to wait until caller executor thread had already suspended (if not running task), - // so that we resume back to it a new task is posted - executor.awaitNotRunningTask() - expect(4) - assertDefaultDispatcherThread() - } - // cancelled on resume back - } finally { - expect(5) - assertIoThread() - } - expectUnreached() - } - } - assertEquals(2, executor.submittedTasks) - finish(6) - } - - @Test - fun testRejectOnDelay() = runTest { - expect(1) - executor.acceptTasks = 1 // accept one task - assertFailsWith { - withContext(executor.asCoroutineDispatcher()) { - expect(2) - assertExecutorThread() - try { - delay(10) // cancelled - } finally { - // Since it was cancelled on attempt to delay, it still stays on the same thread - assertExecutorThread() - } - expectUnreached() - } - } - assertEquals(2, executor.submittedTasks) - finish(3) - } - - @Test - fun testRejectWithTimeout() = runTest { - expect(1) - executor.acceptTasks = 1 // accept one task - assertFailsWith { - withContext(executor.asCoroutineDispatcher()) { - expect(2) - assertExecutorThread() - withTimeout(1000) { - expect(3) // atomic entry into the block (legacy behavior, it seem to be Ok with way) - assertEquals(true, coroutineContext[Job]?.isCancelled) // but the job is already cancelled - } - expectUnreached() - } - } - assertEquals(2, executor.submittedTasks) - finish(4) - } - - private inner class RejectingExecutor : ScheduledThreadPoolExecutor(1, { r -> Thread(r, threadName) }) { - var acceptTasks = 0 - var submittedTasks = 0 - val runningTask = MutableStateFlow(false) - - override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> { - submittedTasks++ - if (submittedTasks > acceptTasks) throw RejectedExecutionException() - val wrapper = Runnable { - runningTask.value = true - try { - command.run() - } finally { - runningTask.value = false - } - } - return super.schedule(wrapper, delay, unit) - } - - suspend fun awaitNotRunningTask() = runningTask.first { !it } - } - - private fun assertExecutorThread() { - val thread = Thread.currentThread() - if (!thread.name.startsWith(threadName)) error("Not an executor thread: $thread") - } - - private fun assertDefaultDispatcherThread() { - val thread = Thread.currentThread() - if (thread !is CoroutineScheduler.Worker) error("Not a thread from Dispatchers.Default: $thread") - assertEquals(CoroutineScheduler.WorkerState.CPU_ACQUIRED, thread.state) - } - - private fun assertIoThread() { - val thread = Thread.currentThread() - if (thread !is CoroutineScheduler.Worker) error("Not a thread from Dispatchers.IO: $thread") - assertEquals(CoroutineScheduler.WorkerState.BLOCKING, thread.state) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt index 56f1e28313..892a2a62d4 100644 --- a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt @@ -11,14 +11,15 @@ import kotlin.coroutines.* import kotlin.test.* class ReusableCancellableContinuationTest : TestBase() { + @Test fun testReusable() = runTest { - testContinuationsCount(10, 1, ::suspendCancellableCoroutineReusable) + testContinuationsCount(10, 1, ::suspendAtomicCancellableCoroutineReusable) } @Test fun testRegular() = runTest { - testContinuationsCount(10, 10, ::suspendCancellableCoroutine) + testContinuationsCount(10, 10, ::suspendAtomicCancellableCoroutine) } private suspend inline fun CoroutineScope.testContinuationsCount( @@ -50,7 +51,7 @@ class ReusableCancellableContinuationTest : TestBase() { fun testCancelledOnClaimedCancel() = runTest { expect(1) try { - suspendCancellableCoroutineReusable { + suspendAtomicCancellableCoroutineReusable { it.cancel() } expectUnreached() @@ -64,7 +65,7 @@ class ReusableCancellableContinuationTest : TestBase() { expect(1) // Bind child at first var continuation: Continuation<*>? = null - suspendCancellableCoroutineReusable { + suspendAtomicCancellableCoroutineReusable { expect(2) continuation = it launch { // Attach to the parent, avoid fast path @@ -76,16 +77,13 @@ class ReusableCancellableContinuationTest : TestBase() { ensureActive() // Verify child was bound FieldWalker.assertReachableCount(1, coroutineContext[Job]) { it === continuation } - try { - suspendCancellableCoroutineReusable { - expect(5) - coroutineContext[Job]!!.cancel() - it.resume(Unit) // will not dispatch, will get CancellationException - } - } catch (e: CancellationException) { - assertFalse(isActive) - finish(6) + suspendAtomicCancellableCoroutineReusable { + expect(5) + coroutineContext[Job]!!.cancel() + it.resume(Unit) } + assertFalse(isActive) + finish(6) } @Test @@ -95,7 +93,7 @@ class ReusableCancellableContinuationTest : TestBase() { launch { cont!!.resumeWith(Result.success(Unit)) } - suspendCancellableCoroutineReusable { + suspendAtomicCancellableCoroutineReusable { cont = it } ensureActive() @@ -110,7 +108,7 @@ class ReusableCancellableContinuationTest : TestBase() { launch { // Attach to the parent, avoid fast path cont!!.resumeWith(Result.success(Unit)) } - suspendCancellableCoroutine { + suspendAtomicCancellableCoroutine { cont = it } ensureActive() @@ -123,7 +121,7 @@ class ReusableCancellableContinuationTest : TestBase() { expect(1) var cont: Continuation<*>? = null try { - suspendCancellableCoroutineReusable { + suspendAtomicCancellableCoroutineReusable { cont = it it.cancel() } @@ -139,7 +137,7 @@ class ReusableCancellableContinuationTest : TestBase() { val currentJob = coroutineContext[Job]!! expect(1) // Bind child at first - suspendCancellableCoroutineReusable { + suspendAtomicCancellableCoroutineReusable { expect(2) // Attach to the parent, avoid fast path launch { @@ -155,23 +153,15 @@ class ReusableCancellableContinuationTest : TestBase() { assertFalse(isActive) // Child detached FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - expect(5) - try { - // Resume is non-atomic, so it throws cancellation exception - suspendCancellableCoroutineReusable { - expect(6) // but the code inside the block is executed - it.resume(Unit) - } - } catch (e: CancellationException) { - FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - expect(7) - } + suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } + suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } + FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } + try { - // No resume -- still cancellation exception - suspendCancellableCoroutineReusable {} + suspendAtomicCancellableCoroutineReusable {} } catch (e: CancellationException) { FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - finish(8) + finish(5) } } diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index 17238e873c..bf462cc78f 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -69,8 +69,6 @@ public actual open class TestBase actual constructor() { throw makeError(message, cause) } - public fun hasError() = error.get() != null - private fun makeError(message: Any, cause: Throwable? = null): IllegalStateException = IllegalStateException(message.toString(), cause).also { setError(it) @@ -109,7 +107,7 @@ public actual open class TestBase actual constructor() { * Asserts that this line is never executed. */ public actual fun expectUnreached() { - error("Should not be reached, current action index is ${actionIndex.get()}") + error("Should not be reached") } /** diff --git a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt index 2e73b2432a..54ba7b639f 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt @@ -48,9 +48,8 @@ class BroadcastChannelMultiReceiveStressTest( launch(pool + CoroutineName("Sender")) { var i = 0L while (isActive) { - i++ - broadcast.send(i) // could be cancelled - sentTotal.set(i) // only was for it if it was not cancelled + broadcast.send(++i) + sentTotal.set(i) // set sentTotal only if `send` was not cancelled } } val receivers = mutableListOf() @@ -89,8 +88,10 @@ class BroadcastChannelMultiReceiveStressTest( try { withTimeout(5000) { receivers.forEachIndexed { index, receiver -> - if (lastReceived[index].get() >= total) receiver.cancel() - receiver.join() + if (lastReceived[index].get() == total) + receiver.cancel() + else + receiver.join() } } } catch (e: Exception) { @@ -111,7 +112,7 @@ class BroadcastChannelMultiReceiveStressTest( check(i == last + 1) { "Last was $last, got $i" } receivedTotal.incrementAndGet() lastReceived[receiverIndex].set(i) - return i >= stopOnReceive.get() + return i == stopOnReceive.get() } private suspend fun doReceive(channel: ReceiveChannel, receiverIndex: Int) { diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt new file mode 100644 index 0000000000..6556888a0f --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import org.junit.After +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import kotlin.random.Random +import java.util.concurrent.atomic.* +import kotlin.test.* + +/** + * Tests cancel atomicity for channel send & receive operations, including their select versions. + */ +@RunWith(Parameterized::class) +class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBase() { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun params(): Collection> = TestChannelKind.values().map { arrayOf(it) } + } + + private val TEST_DURATION = 1000L * stressTestMultiplier + + private val dispatcher = newFixedThreadPoolContext(2, "ChannelAtomicCancelStressTest") + private val scope = CoroutineScope(dispatcher) + + private val channel = kind.create() + private val senderDone = Channel(1) + private val receiverDone = Channel(1) + + private var lastSent = 0 + private var lastReceived = 0 + + private var stoppedSender = 0 + private var stoppedReceiver = 0 + + private var missedCnt = 0 + private var dupCnt = 0 + + val failed = AtomicReference() + + lateinit var sender: Job + lateinit var receiver: Job + + @After + fun tearDown() { + dispatcher.close() + } + + fun fail(e: Throwable) = failed.compareAndSet(null, e) + + private inline fun cancellable(done: Channel, block: () -> Unit) { + try { + block() + } finally { + if (!done.offer(true)) + fail(IllegalStateException("failed to offer to done channel")) + } + } + + @Test + fun testAtomicCancelStress() = runBlocking { + println("--- ChannelAtomicCancelStressTest $kind") + val deadline = System.currentTimeMillis() + TEST_DURATION + launchSender() + launchReceiver() + while (System.currentTimeMillis() < deadline && failed.get() == null) { + when (Random.nextInt(3)) { + 0 -> { // cancel & restart sender + stopSender() + launchSender() + } + 1 -> { // cancel & restart receiver + stopReceiver() + launchReceiver() + } + 2 -> yield() // just yield (burn a little time) + } + } + stopSender() + stopReceiver() + println(" Sent $lastSent ints to channel") + println(" Received $lastReceived ints from channel") + println(" Stopped sender $stoppedSender times") + println("Stopped receiver $stoppedReceiver times") + println(" Missed $missedCnt ints") + println(" Duplicated $dupCnt ints") + failed.get()?.let { throw it } + assertEquals(0, dupCnt) + if (!kind.isConflated) { + assertEquals(0, missedCnt) + assertEquals(lastSent, lastReceived) + } + } + + private fun launchSender() { + sender = scope.launch(start = CoroutineStart.ATOMIC) { + cancellable(senderDone) { + var counter = 0 + while (true) { + val trySend = lastSent + 1 + when (Random.nextInt(2)) { + 0 -> channel.send(trySend) + 1 -> select { channel.onSend(trySend) {} } + else -> error("cannot happen") + } + lastSent = trySend // update on success + when { + // must artificially slow down LINKED_LIST sender to avoid overwhelming receiver and going OOM + kind == TestChannelKind.LINKED_LIST -> while (lastSent > lastReceived + 100) yield() + // yield periodically to check cancellation on conflated channels + kind.isConflated -> if (counter++ % 100 == 0) yield() + } + } + } + } + } + + private suspend fun stopSender() { + stoppedSender++ + sender.cancel() + senderDone.receive() + } + + private fun launchReceiver() { + receiver = scope.launch(start = CoroutineStart.ATOMIC) { + cancellable(receiverDone) { + while (true) { + val received = when (Random.nextInt(2)) { + 0 -> channel.receive() + 1 -> select { channel.onReceive { it } } + else -> error("cannot happen") + } + val expected = lastReceived + 1 + if (received > expected) + missedCnt++ + if (received < expected) + dupCnt++ + lastReceived = received + } + } + } + } + + private suspend fun stopReceiver() { + stoppedReceiver++ + receiver.cancel() + receiverDone.receive() + } +} diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt deleted file mode 100644 index 76713aa173..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.coroutines.* -import kotlinx.coroutines.selects.* -import java.util.concurrent.atomic.* -import kotlin.random.* -import kotlin.test.* - -class ChannelCancelUndeliveredElementStressTest : TestBase() { - private val repeatTimes = 10_000 * stressTestMultiplier - - // total counters - private var sendCnt = 0 - private var offerFailedCnt = 0 - private var receivedCnt = 0 - private var undeliveredCnt = 0 - - // last operation - private var lastReceived = 0 - private var dSendCnt = 0 - private var dSendExceptionCnt = 0 - private var dOfferFailedCnt = 0 - private var dReceivedCnt = 0 - private val dUndeliveredCnt = AtomicInteger() - - @Test - fun testStress() = runTest { - repeat(repeatTimes) { - val channel = Channel(1) { dUndeliveredCnt.incrementAndGet() } - val j1 = launch(Dispatchers.Default) { - sendOne(channel) // send first - sendOne(channel) // send second - } - val j2 = launch(Dispatchers.Default) { - receiveOne(channel) // receive one element from the channel - channel.cancel() // cancel the channel - } - - joinAll(j1, j2) - - // All elements must be either received or undelivered (IN every run) - if (dSendCnt - dOfferFailedCnt != dReceivedCnt + dUndeliveredCnt.get()) { - println(" Send: $dSendCnt") - println("Send Exception: $dSendExceptionCnt") - println(" Offer failed: $dOfferFailedCnt") - println(" Received: $dReceivedCnt") - println(" Undelivered: ${dUndeliveredCnt.get()}") - error("Failed") - } - offerFailedCnt += dOfferFailedCnt - receivedCnt += dReceivedCnt - undeliveredCnt += dUndeliveredCnt.get() - // clear for next run - dSendCnt = 0 - dSendExceptionCnt = 0 - dOfferFailedCnt = 0 - dReceivedCnt = 0 - dUndeliveredCnt.set(0) - } - // Stats - println(" Send: $sendCnt") - println(" Offer failed: $offerFailedCnt") - println(" Received: $receivedCnt") - println(" Undelivered: $undeliveredCnt") - assertEquals(sendCnt - offerFailedCnt, receivedCnt + undeliveredCnt) - } - - private suspend fun sendOne(channel: Channel) { - dSendCnt++ - val i = ++sendCnt - try { - when (Random.nextInt(2)) { - 0 -> channel.send(i) - 1 -> if (!channel.offer(i)) { - dOfferFailedCnt++ - } - } - } catch(e: Throwable) { - assertTrue(e is CancellationException) // the only exception possible in this test - dSendExceptionCnt++ - throw e - } - } - - private suspend fun receiveOne(channel: Channel) { - val received = when (Random.nextInt(3)) { - 0 -> channel.receive() - 1 -> channel.receiveOrNull() ?: error("Cannot be closed yet") - 2 -> select { - channel.onReceive { it } - } - else -> error("Cannot happen") - } - assertTrue(received > lastReceived) - dReceivedCnt++ - lastReceived = received - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt index f414c33338..00c5a6090f 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt @@ -35,7 +35,7 @@ class ChannelSendReceiveStressTest( private val maxBuffer = 10_000 // artificial limit for LinkedListChannel - val channel = kind.create() + val channel = kind.create() private val sendersCompleted = AtomicInteger() private val receiversCompleted = AtomicInteger() private val dupes = AtomicInteger() diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt deleted file mode 100644 index 1188329a4c..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.atomicfu.* -import kotlinx.coroutines.* -import kotlinx.coroutines.selects.* -import org.junit.After -import org.junit.Test -import org.junit.runner.* -import org.junit.runners.* -import kotlin.random.Random -import kotlin.test.* - -/** - * Tests resource transfer via channel send & receive operations, including their select versions, - * using `onUndeliveredElement` to detect lost resources and close them properly. - */ -@RunWith(Parameterized::class) -class ChannelUndeliveredElementStressTest(private val kind: TestChannelKind) : TestBase() { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun params(): Collection> = - TestChannelKind.values() - .filter { !it.viaBroadcast } - .map { arrayOf(it) } - } - - private val iterationDurationMs = 100L - private val testIterations = 20 * stressTestMultiplier // 2 sec - - private val dispatcher = newFixedThreadPoolContext(2, "ChannelAtomicCancelStressTest") - private val scope = CoroutineScope(dispatcher) - - private val channel = kind.create { it.failedToDeliver() } - private val senderDone = Channel(1) - private val receiverDone = Channel(1) - - @Volatile - private var lastReceived = -1L - - private var stoppedSender = 0L - private var stoppedReceiver = 0L - - private var sentCnt = 0L // total number of send attempts - private var receivedCnt = 0L // actually received successfully - private var dupCnt = 0L // duplicates (should never happen) - private val failedToDeliverCnt = atomic(0L) // out of sent - - private val modulo = 1 shl 25 - private val mask = (modulo - 1).toLong() - private val sentStatus = ItemStatus() // 1 - send norm, 2 - send select, +2 - did not throw exception - private val receivedStatus = ItemStatus() // 1-6 received - private val failedStatus = ItemStatus() // 1 - failed - - lateinit var sender: Job - lateinit var receiver: Job - - @After - fun tearDown() { - dispatcher.close() - } - - private inline fun cancellable(done: Channel, block: () -> Unit) { - try { - block() - } finally { - if (!done.offer(true)) - error(IllegalStateException("failed to offer to done channel")) - } - } - - @Test - fun testAtomicCancelStress() = runBlocking { - println("=== ChannelAtomicCancelStressTest $kind") - var nextIterationTime = System.currentTimeMillis() + iterationDurationMs - var iteration = 0 - launchSender() - launchReceiver() - while (!hasError()) { - if (System.currentTimeMillis() >= nextIterationTime) { - nextIterationTime += iterationDurationMs - iteration++ - verify(iteration) - if (iteration % 10 == 0) printProgressSummary(iteration) - if (iteration >= testIterations) break - launchSender() - launchReceiver() - } - when (Random.nextInt(3)) { - 0 -> { // cancel & restart sender - stopSender() - launchSender() - } - 1 -> { // cancel & restart receiver - stopReceiver() - launchReceiver() - } - 2 -> yield() // just yield (burn a little time) - } - } - } - - private suspend fun verify(iteration: Int) { - stopSender() - drainReceiver() - stopReceiver() - try { - assertEquals(0, dupCnt) - assertEquals(sentCnt - failedToDeliverCnt.value, receivedCnt) - } catch (e: Throwable) { - printProgressSummary(iteration) - printErrorDetails() - throw e - } - sentStatus.clear() - receivedStatus.clear() - failedStatus.clear() - } - - private fun printProgressSummary(iteration: Int) { - println("--- ChannelAtomicCancelStressTest $kind -- $iteration of $testIterations") - println(" Sent $sentCnt times to channel") - println(" Received $receivedCnt times from channel") - println(" Failed to deliver ${failedToDeliverCnt.value} times") - println(" Stopped sender $stoppedSender times") - println(" Stopped receiver $stoppedReceiver times") - println(" Duplicated $dupCnt deliveries") - } - - private fun printErrorDetails() { - val min = minOf(sentStatus.min, receivedStatus.min, failedStatus.min) - val max = maxOf(sentStatus.max, receivedStatus.max, failedStatus.max) - for (x in min..max) { - val sentCnt = if (sentStatus[x] != 0) 1 else 0 - val receivedCnt = if (receivedStatus[x] != 0) 1 else 0 - val failedToDeliverCnt = failedStatus[x] - if (sentCnt - failedToDeliverCnt != receivedCnt) { - println("!!! Error for value $x: " + - "sentStatus=${sentStatus[x]}, " + - "receivedStatus=${receivedStatus[x]}, " + - "failedStatus=${failedStatus[x]}" - ) - } - } - } - - - private fun launchSender() { - sender = scope.launch(start = CoroutineStart.ATOMIC) { - cancellable(senderDone) { - var counter = 0 - while (true) { - val trySendData = Data(sentCnt++) - val sendMode = Random.nextInt(2) + 1 - sentStatus[trySendData.x] = sendMode - when (sendMode) { - 1 -> channel.send(trySendData) - 2 -> select { channel.onSend(trySendData) {} } - else -> error("cannot happen") - } - sentStatus[trySendData.x] = sendMode + 2 - when { - // must artificially slow down LINKED_LIST sender to avoid overwhelming receiver and going OOM - kind == TestChannelKind.LINKED_LIST -> while (sentCnt > lastReceived + 100) yield() - // yield periodically to check cancellation on conflated channels - kind.isConflated -> if (counter++ % 100 == 0) yield() - } - } - } - } - } - - private suspend fun stopSender() { - stoppedSender++ - sender.cancel() - senderDone.receive() - } - - private fun launchReceiver() { - receiver = scope.launch(start = CoroutineStart.ATOMIC) { - cancellable(receiverDone) { - while (true) { - val receiveMode = Random.nextInt(6) + 1 - val receivedData = when (receiveMode) { - 1 -> channel.receive() - 2 -> select { channel.onReceive { it } } - 3 -> channel.receiveOrNull() ?: error("Should not be closed") - 4 -> select { channel.onReceiveOrNull { it ?: error("Should not be closed") } } - 5 -> channel.receiveOrClosed().value - 6 -> { - val iterator = channel.iterator() - check(iterator.hasNext()) { "Should not be closed" } - iterator.next() - } - else -> error("cannot happen") - } - receivedCnt++ - val received = receivedData.x - if (received <= lastReceived) - dupCnt++ - lastReceived = received - receivedStatus[received] = receiveMode - } - } - } - } - - private suspend fun drainReceiver() { - while (!channel.isEmpty) yield() // burn time until receiver gets it all - } - - private suspend fun stopReceiver() { - stoppedReceiver++ - receiver.cancel() - receiverDone.receive() - } - - private inner class Data(val x: Long) { - private val failedToDeliver = atomic(false) - - fun failedToDeliver() { - check(failedToDeliver.compareAndSet(false, true)) { "onUndeliveredElement notified twice" } - failedToDeliverCnt.incrementAndGet() - failedStatus[x] = 1 - } - } - - inner class ItemStatus { - private val a = ByteArray(modulo) - private val _min = atomic(Long.MAX_VALUE) - private val _max = atomic(-1L) - - val min: Long get() = _min.value - val max: Long get() = _max.value - - operator fun set(x: Long, value: Int) { - a[(x and mask).toInt()] = value.toByte() - _min.update { y -> minOf(x, y) } - _max.update { y -> maxOf(x, y) } - } - - operator fun get(x: Long): Int = a[(x and mask).toInt()].toInt() - - fun clear() { - if (_max.value < 0) return - for (x in _min.value.._max.value) a[(x and mask).toInt()] = 0 - _min.value = Long.MAX_VALUE - _max.value = -1L - } - } -} diff --git a/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt index 888522c63c..864a0b4c2e 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt @@ -39,7 +39,7 @@ class InvokeOnCloseStressTest : TestBase(), CoroutineScope { private suspend fun runStressTest(kind: TestChannelKind) { repeat(iterations) { val counter = AtomicInteger(0) - val channel = kind.create() + val channel = kind.create() val latch = CountDownLatch(1) val j1 = async { diff --git a/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt b/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt index eeddfb5f49..07c431bb4d 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt @@ -28,7 +28,7 @@ class SimpleSendReceiveJvmTest( } } - val channel = kind.create() + val channel = kind.create() @Test fun testSimpleSendReceive() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt deleted file mode 100644 index d2a5d536b2..0000000000 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from Delay.kt by Knit tool. Do not edit. -package kotlinx.coroutines.examples.exampleDelay01 - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - -flow { - emit(1) - delay(90) - emit(2) - delay(90) - emit(3) - delay(1010) - emit(4) - delay(1010) - emit(5) -}.debounce(1000) -.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt deleted file mode 100644 index 1b6b12f041..0000000000 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from Delay.kt by Knit tool. Do not edit. -package kotlinx.coroutines.examples.exampleDelay02 - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - -flow { - repeat(10) { - emit(it) - delay(110) - } -}.sample(200) -.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt deleted file mode 100644 index a19e6cb181..0000000000 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt +++ /dev/null @@ -1,26 +0,0 @@ -@file:OptIn(ExperimentalTime::class) -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from Delay.kt by Knit tool. Do not edit. -package kotlinx.coroutines.examples.exampleDelayDuration01 - -import kotlin.time.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - -flow { - emit(1) - delay(90.milliseconds) - emit(2) - delay(90.milliseconds) - emit(3) - delay(1010.milliseconds) - emit(4) - delay(1010.milliseconds) - emit(5) -}.debounce(1000.milliseconds) -.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt deleted file mode 100644 index e43dfd1e05..0000000000 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt +++ /dev/null @@ -1,21 +0,0 @@ -@file:OptIn(ExperimentalTime::class) -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from Delay.kt by Knit tool. Do not edit. -package kotlinx.coroutines.examples.exampleDelayDuration02 - -import kotlin.time.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - -flow { - repeat(10) { - emit(it) - delay(110.milliseconds) - } -}.sample(200.milliseconds) -.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt deleted file mode 100644 index 226d31cc00..0000000000 --- a/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from Delay.kt by Knit tool. Do not edit. -package kotlinx.coroutines.examples.test - -import kotlinx.coroutines.knit.* -import org.junit.Test - -class FlowDelayTest { - @Test - fun testExampleDelay01() { - test("ExampleDelay01") { kotlinx.coroutines.examples.exampleDelay01.main() }.verifyLines( - "3, 4, 5" - ) - } - - @Test - fun testExampleDelayDuration01() { - test("ExampleDelayDuration01") { kotlinx.coroutines.examples.exampleDelayDuration01.main() }.verifyLines( - "3, 4, 5" - ) - } - - @Test - fun testExampleDelay02() { - test("ExampleDelay02") { kotlinx.coroutines.examples.exampleDelay02.main() }.verifyLines( - "1, 3, 5, 7, 9" - ) - } - - @Test - fun testExampleDelayDuration02() { - test("ExampleDelayDuration02") { kotlinx.coroutines.examples.exampleDelayDuration02.main() }.verifyLines( - "1, 3, 5, 7, 9" - ) - } -} diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt deleted file mode 100644 index 7d346bdc33..0000000000 --- a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import org.junit.* -import org.junit.Test -import java.util.* -import java.util.concurrent.atomic.* -import kotlin.random.* -import kotlin.test.* -import kotlin.time.* -import kotlin.time.TimeSource - -@OptIn(ExperimentalTime::class) -class SharingStressTest : TestBase() { - private val testDuration = 1000L * stressTestMultiplier - private val nSubscribers = 5 - private val testStarted = TimeSource.Monotonic.markNow() - - @get:Rule - val emitterDispatcher = ExecutorRule(1) - - @get:Rule - val subscriberDispatcher = ExecutorRule(nSubscribers) - - @Test - public fun testNoReplayLazy() = - testStress(0, started = SharingStarted.Lazily) - - @Test - public fun testNoReplayWhileSubscribed() = - testStress(0, started = SharingStarted.WhileSubscribed()) - - @Test - public fun testNoReplayWhileSubscribedTimeout() = - testStress(0, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 50L)) - - @Test - public fun testReplay100WhileSubscribed() = - testStress(100, started = SharingStarted.WhileSubscribed()) - - @Test - public fun testReplay100WhileSubscribedReset() = - testStress(100, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0L)) - - @Test - public fun testReplay100WhileSubscribedTimeout() = - testStress(100, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 50L)) - - @Test - public fun testStateLazy() = - testStress(1, started = SharingStarted.Lazily) - - @Test - public fun testStateWhileSubscribed() = - testStress(1, started = SharingStarted.WhileSubscribed()) - - @Test - public fun testStateWhileSubscribedReset() = - testStress(1, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0L)) - - private fun testStress(replay: Int, started: SharingStarted) = runTest { - log("-- Stress with replay=$replay, started=$started") - val random = Random(1) - val emitIndex = AtomicLong() - val cancelledEmits = HashSet() - val missingCollects = Collections.synchronizedSet(LinkedHashSet()) - // at most one copy of upstream can be running at any time - val isRunning = AtomicInteger(0) - val upstream = flow { - assertEquals(0, isRunning.getAndIncrement()) - try { - while (true) { - val value = emitIndex.getAndIncrement() - try { - emit(value) - } catch (e: CancellationException) { - // emission was cancelled -> could be missing - cancelledEmits.add(value) - throw e - } - } - } finally { - assertEquals(1, isRunning.getAndDecrement()) - } - } - val subCount = MutableStateFlow(0) - val sharingJob = Job() - val sharingScope = this + emitterDispatcher + sharingJob - val usingStateFlow = replay == 1 - val sharedFlow = if (usingStateFlow) - upstream.stateIn(sharingScope, started, 0L) - else - upstream.shareIn(sharingScope, started, replay) - try { - val subscribers = ArrayList() - withTimeoutOrNull(testDuration) { - // start and stop subscribers - while (true) { - log("Staring $nSubscribers subscribers") - repeat(nSubscribers) { - subscribers += launchSubscriber(sharedFlow, usingStateFlow, subCount, missingCollects) - } - // wait until they all subscribed - subCount.first { it == nSubscribers } - // let them work a bit more & make sure emitter did not hang - val fromEmitIndex = emitIndex.get() - val waitEmitIndex = fromEmitIndex + 100 // wait until 100 emitted - withTimeout(10000) { // wait for at most 10s for something to be emitted - do { - delay(random.nextLong(50L..100L)) - } while (emitIndex.get() < waitEmitIndex) // Ok, enough was emitted, wait more if not - } - // Stop all subscribers and ensure they collected something - log("Stopping subscribers (emitted = ${emitIndex.get() - fromEmitIndex})") - subscribers.forEach { - it.job.cancelAndJoin() - assertTrue { it.count > 0 } // something must be collected too - } - subscribers.clear() - log("Intermission") - delay(random.nextLong(10L..100L)) // wait a bit before starting them again - } - } - if (!subscribers.isEmpty()) { - log("Stopping subscribers") - subscribers.forEach { it.job.cancelAndJoin() } - } - } finally { - log("--- Finally: Cancelling sharing job") - sharingJob.cancel() - } - sharingJob.join() // make sure sharing job did not hang - log("Emitter was cancelled ${cancelledEmits.size} times") - log("Collectors missed ${missingCollects.size} values") - for (value in missingCollects) { - assertTrue(value in cancelledEmits, "Value $value is missing for no apparent reason") - } - } - - private fun CoroutineScope.launchSubscriber( - sharedFlow: SharedFlow, - usingStateFlow: Boolean, - subCount: MutableStateFlow, - missingCollects: MutableSet - ): SubJob { - val subJob = SubJob() - subJob.job = launch(subscriberDispatcher) { - var last = -1L - sharedFlow - .onSubscription { - subCount.increment(1) - } - .onCompletion { - subCount.increment(-1) - } - .collect { j -> - subJob.count++ - // last must grow sequentially, no jumping or losses - if (last == -1L) { - last = j - } else { - val expected = last + 1 - if (usingStateFlow) - assertTrue(expected <= j) - else { - if (expected != j) { - if (j == expected + 1) { - // if missing just one -- could be race with cancelled emit - missingCollects.add(expected) - } else { - // broken otherwise - assertEquals(expected, j) - } - } - } - last = j - } - } - } - return subJob - } - - private class SubJob { - lateinit var job: Job - var count = 0L - } - - private fun log(msg: String) = println("${testStarted.elapsedNow().toLongMilliseconds()} ms: $msg") -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt b/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt deleted file mode 100644 index fc4996c7c0..0000000000 --- a/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.flow - -import kotlinx.coroutines.* -import java.util.concurrent.* -import kotlin.test.* - -@Suppress("BlockingMethodInNonBlockingContext") -class StateFlowCancellabilityTest : TestBase() { - @Test - fun testCancellabilityNoConflation() = runTest { - expect(1) - val state = MutableStateFlow(0) - var subscribed = true - var lastReceived = -1 - val barrier = CyclicBarrier(2) - val job = state - .onSubscription { - subscribed = true - barrier.await() - } - .onEach { i -> - when (i) { - 0 -> expect(2) // initial value - 1 -> expect(3) - 2 -> { - expect(4) - currentCoroutineContext().cancel() - } - else -> expectUnreached() // shall check for cancellation - } - lastReceived = i - barrier.await() - barrier.await() - } - .launchIn(this + Dispatchers.Default) - barrier.await() - assertTrue(subscribed) // should have subscribed in the first barrier - barrier.await() - assertEquals(0, lastReceived) // should get initial value, too - for (i in 1..3) { // emit after subscription - state.value = i - barrier.await() // let it go - if (i < 3) { - barrier.await() // wait for receive - assertEquals(i, lastReceived) // shall receive it - } - } - job.join() - finish(5) - } -} - diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt deleted file mode 100644 index e7def132ae..0000000000 --- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. -package kotlinx.coroutines.guide.exampleCancel08 - -import kotlinx.coroutines.* - -var acquired = 0 - -class Resource { - init { acquired++ } // Acquire the resource - fun close() { acquired-- } // Release the resource -} - -fun main() { - runBlocking { - repeat(100_000) { // Launch 100K coroutines - launch { - val resource = withTimeout(60) { // Timeout of 60 ms - delay(50) // Delay for 50 ms - Resource() // Acquire a resource and return it from withTimeout block - } - resource.close() // Release the resource - } - } - } - // Outside of runBlocking all coroutines have completed - println(acquired) // Print the number of resources still acquired -} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt deleted file mode 100644 index 95424f5108..0000000000 --- a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. -package kotlinx.coroutines.guide.exampleCancel09 - -import kotlinx.coroutines.* - -var acquired = 0 - -class Resource { - init { acquired++ } // Acquire the resource - fun close() { acquired-- } // Release the resource -} - -fun main() { - runBlocking { - repeat(100_000) { // Launch 100K coroutines - launch { - var resource: Resource? = null // Not acquired yet - try { - withTimeout(60) { // Timeout of 60 ms - delay(50) // Delay for 50 ms - resource = Resource() // Store a resource to the variable if acquired - } - // We can do something else with the resource here - } finally { - resource?.close() // Release the resource if it was acquired - } - } - } - } - // Outside of runBlocking all coroutines have completed - println(acquired) // Print the number of resources still acquired -} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt index ea5003b0c7..7fc57c2ee3 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from basics.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class BasicsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt index 0cff63a834..a2e91de82d 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class CancellationGuideTest { @@ -88,11 +87,4 @@ class CancellationGuideTest { "Result is null" ) } - - @Test - fun testExampleCancel09() { - test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines( - "0" - ) - } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt index d15a550adb..209d439663 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from channels.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class ChannelsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt index 1f9692d56b..50c3fd7e62 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from composing-suspending-functions.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class ComposingGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt index d6f1c21dc0..c0c32410d5 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from coroutine-context-and-dispatchers.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class DispatcherGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt index f34fd3f83b..c42ba31d3a 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from exception-handling.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class ExceptionsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index c7d4a72082..7fa9cc84dd 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from flow.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class FlowGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt index 55650d4c6a..e3f47b9648 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from select-expression.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class SelectGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt index 3162b24cbc..8d534a09ea 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt @@ -5,7 +5,6 @@ // This file was automatically generated from shared-mutable-state-and-concurrency.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test -import kotlinx.coroutines.knit.* import org.junit.Test class SharedStateGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt b/kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt similarity index 98% rename from kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt rename to kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt index 7eda9043db..fb1c85bce1 100644 --- a/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt @@ -1,8 +1,8 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines.knit +package kotlinx.coroutines.guide.test import kotlinx.coroutines.* import kotlinx.coroutines.internal.* diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt index e4fa5e9bfe..ae4b5fce30 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt @@ -12,8 +12,8 @@ import org.junit.* class ConcurrentWeakMapTest : TestBase() { @Test fun testSimple() { - val expect = (1..1000).associate { it.toString().let { it to it } } - val m = ConcurrentWeakMap() + val expect = (1..1000).associateWith { it.toString() } + val m = ConcurrentWeakMap() // repeat adding/removing a few times repeat(5) { assertEquals(0, m.size) @@ -27,7 +27,7 @@ class ConcurrentWeakMapTest : TestBase() { assertEquals(expect.keys, m.keys) assertEquals(expect.entries, m.entries) for ((k, v) in expect) { - assertEquals(v, m[k]) + assertEquals(v, m.get(k)) } assertEquals(expect.size, m.size) if (it % 2 == 0) { @@ -38,9 +38,9 @@ class ConcurrentWeakMapTest : TestBase() { m.clear() } assertEquals(0, m.size) - for ((k, _) in expect) { - assertNull(m[k]) + for ((k, v) in expect) { + assertNull(m.get(k)) } } } -} +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt index bb713b258d..8ecb8fd741 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt @@ -5,7 +5,6 @@ package kotlinx.coroutines.sync import kotlinx.coroutines.* -import kotlinx.coroutines.selects.* import kotlin.test.* class MutexStressTest : TestBase() { @@ -27,67 +26,4 @@ class MutexStressTest : TestBase() { jobs.forEach { it.join() } assertEquals(n * k, shared) } - - @Test - fun stressUnlockCancelRace() = runTest { - val n = 10_000 * stressTestMultiplier - val mutex = Mutex(true) // create a locked mutex - newSingleThreadContext("SemaphoreStressTest").use { pool -> - repeat (n) { - // Initially, we hold the lock and no one else can `lock`, - // otherwise it's a bug. - assertTrue(mutex.isLocked) - var job1EnteredCriticalSection = false - val job1 = launch(start = CoroutineStart.UNDISPATCHED) { - mutex.lock() - job1EnteredCriticalSection = true - mutex.unlock() - } - // check that `job1` didn't finish the call to `acquire()` - assertEquals(false, job1EnteredCriticalSection) - val job2 = launch(pool) { - mutex.unlock() - } - // Because `job2` executes in a separate thread, this - // cancellation races with the call to `unlock()`. - job1.cancelAndJoin() - job2.join() - assertFalse(mutex.isLocked) - mutex.lock() - } - } - } - - @Test - fun stressUnlockCancelRaceWithSelect() = runTest { - val n = 10_000 * stressTestMultiplier - val mutex = Mutex(true) // create a locked mutex - newSingleThreadContext("SemaphoreStressTest").use { pool -> - repeat (n) { - // Initially, we hold the lock and no one else can `lock`, - // otherwise it's a bug. - assertTrue(mutex.isLocked) - var job1EnteredCriticalSection = false - val job1 = launch(start = CoroutineStart.UNDISPATCHED) { - select { - mutex.onLock { - job1EnteredCriticalSection = true - mutex.unlock() - } - } - } - // check that `job1` didn't finish the call to `acquire()` - assertEquals(false, job1EnteredCriticalSection) - val job2 = launch(pool) { - mutex.unlock() - } - // Because `job2` executes in a separate thread, this - // cancellation races with the call to `unlock()`. - job1.cancelAndJoin() - job2.join() - assertFalse(mutex.isLocked) - mutex.lock() - } - } - } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt index 374a1e3d7c..9c77990862 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt @@ -5,6 +5,7 @@ import org.junit.Test import kotlin.test.assertEquals class SemaphoreStressTest : TestBase() { + @Test fun stressTestAsMutex() = runBlocking(Dispatchers.Default) { val n = 10_000 * stressTestMultiplier @@ -70,14 +71,14 @@ class SemaphoreStressTest : TestBase() { // Initially, we hold the permit and no one else can `acquire`, // otherwise it's a bug. assertEquals(0, semaphore.availablePermits) - var job1EnteredCriticalSection = false + var job1_entered_critical_section = false val job1 = launch(start = CoroutineStart.UNDISPATCHED) { semaphore.acquire() - job1EnteredCriticalSection = true + job1_entered_critical_section = true semaphore.release() } // check that `job1` didn't finish the call to `acquire()` - assertEquals(false, job1EnteredCriticalSection) + assertEquals(false, job1_entered_critical_section) val job2 = launch(pool) { semaphore.release() } @@ -90,4 +91,5 @@ class SemaphoreStressTest : TestBase() { } } } + } diff --git a/kotlinx-coroutines-core/knit.properties b/kotlinx-coroutines-core/knit.properties deleted file mode 100644 index 93ce87db8f..0000000000 --- a/kotlinx-coroutines-core/knit.properties +++ /dev/null @@ -1,10 +0,0 @@ -# -# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. -# - -knit.package=kotlinx.coroutines.examples -knit.dir=jvm/test/examples/ - -test.package=kotlinx.coroutines.examples.test -test.dir=jvm/test/examples/test/ - diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt index 4ec1289ee7..bcc7f48963 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt @@ -16,8 +16,8 @@ internal actual object DefaultExecutor : CoroutineDispatcher(), Delay { takeEventLoop().dispatch(context, block) override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = - takeEventLoop().invokeOnTimeout(timeMillis, block, context) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + takeEventLoop().invokeOnTimeout(timeMillis, block) actual fun enqueue(task: Runnable): Unit = loopWasShutDown() } diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt index c06b7c2f0a..aca1cc0693 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt @@ -7,16 +7,24 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { + public actual val Default: CoroutineDispatcher = createDefaultDispatcher() + public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default) + public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing } private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native") + override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) + override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) + override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) + override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt index b397d6f182..d6c6525504 100644 --- a/kotlinx-coroutines-core/native/src/EventLoop.kt +++ b/kotlinx-coroutines-core/native/src/EventLoop.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines -import kotlin.coroutines.* import kotlin.system.* internal actual abstract class EventLoopImplPlatform: EventLoop() { @@ -14,7 +13,7 @@ internal actual abstract class EventLoopImplPlatform: EventLoop() { } internal class EventLoopImpl: EventLoopImplBase() { - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) } diff --git a/kotlinx-coroutines-core/native/src/WorkerMain.native.kt b/kotlinx-coroutines-core/native/src/WorkerMain.native.kt deleted file mode 100644 index 84cc9f42b9..0000000000 --- a/kotlinx-coroutines-core/native/src/WorkerMain.native.kt +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -// It is used in the main sources of native-mt branch -internal expect inline fun workerMain(block: () -> Unit) diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt index 99ab042f3c..9657830e35 100644 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt @@ -124,8 +124,6 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { return null } - actual open fun onRemoved(affected: Node) {} - actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode val failure = failure(affected) diff --git a/kotlinx-coroutines-core/native/test/WorkerTest.kt b/kotlinx-coroutines-core/native/test/WorkerTest.kt index d6b5fad182..84acedac94 100644 --- a/kotlinx-coroutines-core/native/test/WorkerTest.kt +++ b/kotlinx-coroutines-core/native/test/WorkerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -19,7 +19,6 @@ class WorkerTest : TestBase() { delay(1) } }.result - worker.requestTermination() } @Test @@ -32,6 +31,5 @@ class WorkerTest : TestBase() { }.join() } }.result - worker.requestTermination() } } diff --git a/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt deleted file mode 100644 index 3445cb9897..0000000000 --- a/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import kotlinx.cinterop.* - -internal actual inline fun workerMain(block: () -> Unit) { - autoreleasepool { - block() - } -} diff --git a/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt deleted file mode 100644 index 78ed765967..0000000000 --- a/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import platform.CoreFoundation.* -import kotlin.native.concurrent.* -import kotlin.native.internal.test.* -import kotlin.system.* - -// This is a separate entry point for tests in background -fun mainBackground(args: Array) { - val worker = Worker.start(name = "main-background") - worker.execute(TransferMode.SAFE, { args.freeze() }) { - val result = testLauncherEntryPoint(it) - exitProcess(result) - } - CFRunLoopRun() - error("CFRunLoopRun should never return") -} - -// This is a separate entry point for tests with leak checker -fun mainNoExit(args: Array) { - workerMain { // autoreleasepool to make sure interop objects are properly freed - testLauncherEntryPoint(args) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/nativeOther/test/Launcher.kt b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt deleted file mode 100644 index feddd4c097..0000000000 --- a/kotlinx-coroutines-core/nativeOther/test/Launcher.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines - -import kotlin.native.concurrent.* -import kotlin.native.internal.test.* -import kotlin.system.* - -// This is a separate entry point for tests in background -fun mainBackground(args: Array) { - val worker = Worker.start(name = "main-background") - worker.execute(TransferMode.SAFE, { args.freeze() }) { - val result = testLauncherEntryPoint(it) - exitProcess(result) - }.result // block main thread -} - -// This is a separate entry point for tests with leak checker -fun mainNoExit(args: Array) { - testLauncherEntryPoint(args) -} \ No newline at end of file diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 5518e00ef3..81e62e772a 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -23,7 +23,7 @@ https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.0-M1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.8' } ``` @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.0-M1.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.8.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. @@ -170,98 +170,6 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana at kotlinx.coroutines.debug.DebugProbes.install(DebugProbes.kt:49) --> -#### Build failures due to duplicate resource files - -Building an Android project that depends on `kotlinx-coroutines-debug` (usually introduced by being a transitive -dependency of `kotlinx-coroutines-test`) may fail with `DuplicateRelativeFileException` for `META-INF/AL2.0`, -`META-INF/LGPL2.1`, or `win32-x86/attach_hotspot_windows.dll` when trying to merge the Android resource. - -The problem is that Android merges the resources of all its dependencies into a single directory and complains about -conflicts, but: -* `kotlinx-coroutines-debug` transitively depends on JNA and JNA-platform, both of which include license files in their - META-INF directories. Trying to merge these files leads to conflicts, which means that any Android project that - depends on JNA and JNA-platform will experience build failures. -* Additionally, `kotlinx-coroutines-debug` embeds `byte-buddy-agent` and `byte-buddy`, along with their resource files. - Then, if the project separately depends on `byte-buddy`, merging the resources of `kotlinx-coroutines-debug` with ones - from `byte-buddy` and `byte-buddy-agent` will lead to conflicts as the resource files are duplicated. - -One possible workaround for these issues is to add the following to the `android` block in your gradle file for the -application subproject: -```groovy - packagingOptions { - // for JNA and JNA-platform - exclude "META-INF/AL2.0" - exclude "META-INF/LGPL2.1" - // for byte-buddy - exclude "META-INF/licenses/ASM" - pickFirst "win32-x86-64/attach_hotspot_windows.dll" - pickFirst "win32-x86/attach_hotspot_windows.dll" - } -``` -This will cause the resource merge algorithm to exclude the problematic license files altogether and only leave a single -copy of the files needed for `byte-buddy-agent` to work. - -Alternatively, avoid depending on `kotlinx-coroutines-debug`. In particular, if the only reason why this library a -dependency of your project is that `kotlinx-coroutines-test` in turn depends on it, you may change your dependency on -`kotlinx.coroutines.test` to exclude `kotlinx-coroutines-debug`. For example, you could replace -```kotlin -androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") -``` -with -```groovy -androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") { - exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" -} -``` - [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index fd0279123f..8507721e30 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -115,22 +115,16 @@ class CoroutinesDumpTest : DebugTestBase() { coroutineThread!!.interrupt() val expected = - "kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + - "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + - "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable\$default(Cancellable.kt)\n" + - "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt)\n" + - "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + - "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + - "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + - "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)" - if (!result.startsWith(expected)) { - println("=== Actual result") - println(result) - error("Does not start with expected lines") - } - + ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" + + "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" + + "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" + + "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)").trimStackTrace() + assertTrue(result.startsWith(expected)) } @Test diff --git a/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt b/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt new file mode 100644 index 0000000000..bf34917b77 --- /dev/null +++ b/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import org.junit.* + +/** + * This stress tests ensure that no actual [OutOfMemoryError] occurs when lots of coroutines are created and + * leaked in various ways under debugger. A faster but more fragile version of this test is in [DebugLeaksTest]. + */ +class DebugLeaksStressTest : DebugTestBase() { + private val nRepeat = 100_000 * stressTestMultiplier + private val nBytes = 100_000 + + @Test + fun testIteratorLeak() { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + iterator { yield(bytes) } + } + } + + @Test + fun testLazyGlobalCoroutineLeak() { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + GlobalScope.launch(start = CoroutineStart.LAZY) { println(bytes) } + } + } + + @Test + fun testLazyCancelledChildCoroutineLeak() = runTest { + coroutineScope { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + val child = launch(start = CoroutineStart.LAZY) { println(bytes) } + child.cancel() + } + } + } + + @Test + fun testAbandonedGlobalCoroutineLeak() { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + GlobalScope.launch { + suspendForever() + println(bytes) + } + } + } + + private suspend fun suspendForever() = suspendCancellableCoroutine { } +} diff --git a/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/kotlinx-coroutines-debug/test/DebugProbesTest.kt index 3b32db3a5a..24050e563c 100644 --- a/kotlinx-coroutines-debug/test/DebugProbesTest.kt +++ b/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -40,25 +40,24 @@ class DebugProbesTest : DebugTestBase() { val deferred = createDeferred() val traces = listOf( "java.util.concurrent.ExecutionException\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + "\t(Coroutine boundary)\n" + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + "\t(Coroutine creation stacktrace)\n" + - "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable\$default(Cancellable.kt)\n" + - "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt)\n" + - "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + - "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)\n" + "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)\n" + - "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt)\n" + - "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt)", + "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:138)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:19)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:38)", "Caused by: java.util.concurrent.ExecutionException\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt)\n") + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") nestedMethod(deferred, traces) deferred.join() } diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index b82fe8577e..97a1178f3c 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0-M1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8' } ``` diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index c99ec5cbf1..e3b1f73e4f 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -25,7 +25,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun getCurrentTime ()J - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public fun pauseDispatcher ()V public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun resumeDispatcher ()V diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt index cad2636f97..4706d627ef 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt @@ -65,7 +65,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val node = postDelayed(block, timeMillis) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt index baa1aa5fd2..c18e4108bb 100644 --- a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt +++ b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt @@ -46,8 +46,8 @@ internal class TestMainDispatcher(private val mainFactory: MainDispatcherFactory delay.delay(time) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - return delay.invokeOnTimeout(timeMillis, block, context) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + return delay.invokeOnTimeout(timeMillis, block) } public fun setDispatcher(dispatcher: CoroutineDispatcher) { diff --git a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt index e21c82b95c..0013a654a6 100644 --- a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt @@ -54,11 +54,11 @@ class TestRunBlockingOrderTest : TestBase() { } @Test - fun testVeryLongDelay() = runBlockingTest { + fun testInfiniteDelay() = runBlockingTest { expect(1) delay(100) // move time forward a bit some that naive time + delay gives an overflow launch { - delay(Long.MAX_VALUE / 2) // very long delay + delay(Long.MAX_VALUE) // infinite delay finish(4) } launch { diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index 1f5bdec7d0..d4bc1698ef 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -15,8 +15,6 @@ public final class kotlinx/coroutines/jdk9/PublishKt { public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; - public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; - public static synthetic fun asPublisher$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; public static final fun collect (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle b/reactive/kotlinx-coroutines-jdk9/build.gradle new file mode 100644 index 0000000000..8737e8ed6d --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/build.gradle @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +targetCompatibility = 9 + +dependencies { + compile project(":kotlinx-coroutines-reactive") + compile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "9" +} + +compileKotlin { + kotlinOptions.jvmTarget = "9" +} + +tasks.withType(dokka.getClass()) { + externalDocumentationLink { + url = new URL("https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle.kts b/reactive/kotlinx-coroutines-jdk9/build.gradle.kts deleted file mode 100644 index c721746f3b..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile(project(":kotlinx-coroutines-reactive")) - compile("org.reactivestreams:reactive-streams-flow-adapters:${version("reactive_streams")}") -} - -tasks { - compileKotlin { - kotlinOptions.jvmTarget = "9" - } - - compileTestKotlin { - kotlinOptions.jvmTarget = "9" - } -} - -externalDocumentationLink( - url = "https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html" -) diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index 5d546dffd3..89caf82c54 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -4,14 +4,12 @@ package kotlinx.coroutines.jdk9 -import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asPublisher import kotlinx.coroutines.reactive.collect -import org.reactivestreams.* -import kotlin.coroutines.* import java.util.concurrent.Flow as JFlow +import org.reactivestreams.FlowAdapters /** * Transforms the given reactive [Publisher] into [Flow]. @@ -27,15 +25,9 @@ public fun JFlow.Publisher.asFlow(): Flow = /** * Transforms the given flow to a reactive specification compliant [Publisher]. - * - * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -public fun Flow.asPublisher(context: CoroutineContext = EmptyCoroutineContext): JFlow.Publisher { - val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher(context) +public fun Flow.asPublisher(): JFlow.Publisher { + val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher() return FlowAdapters.toFlowPublisher(reactivePublisher) } diff --git a/reactive/kotlinx-coroutines-reactive/README.md b/reactive/kotlinx-coroutines-reactive/README.md index aed262263d..0a59b2c251 100644 --- a/reactive/kotlinx-coroutines-reactive/README.md +++ b/reactive/kotlinx-coroutines-reactive/README.md @@ -6,7 +6,7 @@ Coroutine builders: | **Name** | **Result** | **Scope** | **Description** | --------------- | ----------------------------- | ---------------- | --------------- -| [kotlinx.coroutines.reactive.publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe +| [publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe Integration with [Flow]: @@ -37,7 +37,7 @@ Suspending extension functions and suspending iteration: [ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html -[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html +[publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html [Publisher.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/as-flow.html [Flow.asPublisher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/kotlinx.coroutines.flow.-flow/as-publisher.html [org.reactivestreams.Publisher.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-first.html diff --git a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api index 961fdbe238..bed065d582 100644 --- a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api +++ b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api @@ -5,9 +5,6 @@ public final class kotlinx/coroutines/reactive/AwaitKt { public static final fun awaitFirstOrNull (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitLast (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitSingle (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun awaitSingleOrDefault (Lorg/reactivestreams/Publisher;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun awaitSingleOrElse (Lorg/reactivestreams/Publisher;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun awaitSingleOrNull (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class kotlinx/coroutines/reactive/ChannelKt { @@ -35,7 +32,7 @@ public final class kotlinx/coroutines/reactive/FlowKt { public final class kotlinx/coroutines/reactive/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, org/reactivestreams/Subscription { public final field flow Lkotlinx/coroutines/flow/Flow; public final field subscriber Lorg/reactivestreams/Subscriber; - public fun (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;Lkotlin/coroutines/CoroutineContext;)V + public fun (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;)V public fun cancel ()V public fun request (J)V } @@ -68,7 +65,5 @@ public final class kotlinx/coroutines/reactive/PublisherCoroutine : kotlinx/coro public final class kotlinx/coroutines/reactive/ReactiveFlowKt { public static final fun asFlow (Lorg/reactivestreams/Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Lorg/reactivestreams/Publisher; - public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher; - public static synthetic fun asPublisher$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; } diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts index 2ace4f9fcc..c69148fecf 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -2,6 +2,10 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import org.jetbrains.dokka.gradle.DokkaTask +import java.net.URL + val reactiveStreamsVersion = property("reactive_streams_version") dependencies { @@ -9,28 +13,31 @@ dependencies { testCompile("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion") } -val testNG by tasks.registering(Test::class) { - useTestNG() - reports.html.destination = file("$buildDir/reports/testng") - include("**/*ReactiveStreamTckTest.*") - // Skip testNG when tests are filtered with --tests, otherwise it simply fails - onlyIf { - filter.includePatterns.isEmpty() - } - doFirst { - // Classic gradle, nothing works without doFirst - println("TestNG tests: ($includes)") +tasks { + val testNG = register("testNG") { + useTestNG() + reports.html.destination = file("$buildDir/reports/testng") + include("**/*ReactiveStreamTckTest.*") + // Skip testNG when tests are filtered with --tests, otherwise it simply fails + onlyIf { + filter.includePatterns.isEmpty() + } + doFirst { + // Classic gradle, nothing works without doFirst + println("TestNG tests: ($includes)") + } } -} -tasks.test { - reports.html.destination = file("$buildDir/reports/junit") -} + named("test") { + reports.html.destination = file("$buildDir/reports/junit") -tasks.check { - dependsOn(testNG) -} + dependsOn(testNG) + } -externalDocumentationLink( - url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/" -) + withType().configureEach { + externalDocumentationLink(delegateClosureOf { + url = URL("https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + }) + } +} diff --git a/reactive/kotlinx-coroutines-reactive/src/Await.kt b/reactive/kotlinx-coroutines-reactive/src/Await.kt index 7956c26010..9ea2e3c50e 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Await.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Await.kt @@ -80,53 +80,13 @@ public suspend fun Publisher.awaitLast(): T = awaitOne(Mode.LAST) */ public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) -/** - * Awaits for the single value from the given publisher or the [default] value if none is emitted without blocking a thread and - * returns the resulting value or throws the corresponding exception if this publisher had produced error. - * - * This suspending function is cancellable. - * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function - * immediately resumes with [CancellationException]. - * - * @throws NoSuchElementException if publisher does not emit any value - * @throws IllegalArgumentException if publisher emits more than one value - */ -public suspend fun Publisher.awaitSingleOrDefault(default: T): T = awaitOne(Mode.SINGLE_OR_DEFAULT, default) - -/** - * Awaits for the single value from the given publisher or `null` value if none is emitted without blocking a thread and - * returns the resulting value or throws the corresponding exception if this publisher had produced error. - * - * This suspending function is cancellable. - * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function - * immediately resumes with [CancellationException]. - * - * @throws NoSuchElementException if publisher does not emit any value - * @throws IllegalArgumentException if publisher emits more than one value - */ -public suspend fun Publisher.awaitSingleOrNull(): T = awaitOne(Mode.SINGLE_OR_DEFAULT) - -/** - * Awaits for the single value from the given publisher or call [defaultValue] to get a value if none is emitted without blocking a thread and - * returns the resulting value or throws the corresponding exception if this publisher had produced error. - * - * This suspending function is cancellable. - * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function - * immediately resumes with [CancellationException]. - * - * @throws NoSuchElementException if publisher does not emit any value - * @throws IllegalArgumentException if publisher emits more than one value - */ -public suspend fun Publisher.awaitSingleOrElse(defaultValue: () -> T): T = awaitOne(Mode.SINGLE_OR_DEFAULT) ?: defaultValue() - // ------------------------ private ------------------------ private enum class Mode(val s: String) { FIRST("awaitFirst"), FIRST_OR_DEFAULT("awaitFirstOrDefault"), LAST("awaitLast"), - SINGLE("awaitSingle"), - SINGLE_OR_DEFAULT("awaitSingleOrDefault"); + SINGLE("awaitSingle"); override fun toString(): String = s } @@ -154,8 +114,8 @@ private suspend fun Publisher.awaitOne( cont.resume(t) } } - Mode.LAST, Mode.SINGLE, Mode.SINGLE_OR_DEFAULT -> { - if ((mode == Mode.SINGLE || mode == Mode.SINGLE_OR_DEFAULT) && seenValue) { + Mode.LAST, Mode.SINGLE -> { + if (mode == Mode.SINGLE && seenValue) { subscription.cancel() if (cont.isActive) cont.resumeWithException(IllegalArgumentException("More than one onNext value for $mode")) @@ -174,7 +134,7 @@ private suspend fun Publisher.awaitOne( return } when { - (mode == Mode.FIRST_OR_DEFAULT || mode == Mode.SINGLE_OR_DEFAULT) -> { + mode == Mode.FIRST_OR_DEFAULT -> { cont.resume(default as T) } cont.isActive -> { diff --git a/reactive/kotlinx-coroutines-reactive/src/Channel.kt b/reactive/kotlinx-coroutines-reactive/src/Channel.kt index 26f14ec63d..379fc4ed53 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Channel.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Channel.kt @@ -48,7 +48,7 @@ public suspend inline fun Publisher.collect(action: (T) -> Unit): Unit = @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") private class SubscriptionChannel( private val request: Int -) : LinkedListChannel(null), Subscriber { +) : LinkedListChannel(), Subscriber { init { require(request >= 0) { "Invalid request size: $request" } } diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index 5834220c40..efa9c9c9f1 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -34,24 +34,16 @@ public fun Publisher.asFlow(): Flow = * * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, * see its documentation for additional details. - * - * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -public fun Flow.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher = - FlowAsPublisher(this, Dispatchers.Unconfined + context) +public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) private class PublisherAsFlow( private val publisher: Publisher, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND -) : ChannelFlow(context, capacity, onBufferOverflow) { - override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = - PublisherAsFlow(publisher, context, capacity, onBufferOverflow) + capacity: Int = Channel.BUFFERED +) : ChannelFlow(context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + PublisherAsFlow(publisher, context, capacity) /* * Suppress for Channel.CHANNEL_DEFAULT_CAPACITY. @@ -60,15 +52,13 @@ private class PublisherAsFlow( */ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") private val requestSize: Long - get() = - if (onBufferOverflow != BufferOverflow.SUSPEND) { - Long.MAX_VALUE // request all, since buffering strategy is to never suspend - } else when (capacity) { - Channel.RENDEZVOUS -> 1L // need to request at least one anyway - Channel.UNLIMITED -> Long.MAX_VALUE // reactive streams way to say "give all", must be Long.MAX_VALUE - Channel.BUFFERED -> Channel.CHANNEL_DEFAULT_CAPACITY.toLong() - else -> capacity.toLong().also { check(it >= 1) } - } + get() = when (capacity) { + Channel.CONFLATED -> Long.MAX_VALUE // request all and conflate incoming + Channel.RENDEZVOUS -> 1L // need to request at least one anyway + Channel.UNLIMITED -> Long.MAX_VALUE // reactive streams way to say "give all" must be Long.MAX_VALUE + Channel.BUFFERED -> Channel.CHANNEL_DEFAULT_CAPACITY.toLong() + else -> capacity.toLong().also { check(it >= 1) } + } override suspend fun collect(collector: FlowCollector) { val collectContext = coroutineContext @@ -88,7 +78,7 @@ private class PublisherAsFlow( } private suspend fun collectImpl(injectContext: CoroutineContext, collector: FlowCollector) { - val subscriber = ReactiveSubscriber(capacity, onBufferOverflow, requestSize) + val subscriber = ReactiveSubscriber(capacity, requestSize) // inject subscribe context into publisher publisher.injectCoroutineContext(injectContext).subscribe(subscriber) try { @@ -115,14 +105,10 @@ private class PublisherAsFlow( @Suppress("SubscriberImplementation") private class ReactiveSubscriber( capacity: Int, - onBufferOverflow: BufferOverflow, private val requestSize: Long ) : Subscriber { private lateinit var subscription: Subscription - - // This implementation of ReactiveSubscriber always uses "offer" in its onNext implementation and it cannot - // be reliable with rendezvous channel, so a rendezvous channel is replaced with buffer=1 channel - private val channel = Channel(if (capacity == Channel.RENDEZVOUS) 1 else capacity, onBufferOverflow) + private val channel = Channel(capacity) suspend fun takeNextOrNull(): T? = channel.receiveOrNull() @@ -167,14 +153,11 @@ internal fun Publisher.injectCoroutineContext(coroutineContext: Coroutine * Adapter that transforms [Flow] into TCK-complaint [Publisher]. * [cancel] invocation cancels the original flow. */ -@Suppress("ReactiveStreamsPublisherImplementation") -private class FlowAsPublisher( - private val flow: Flow, - private val context: CoroutineContext -) : Publisher { +@Suppress("PublisherImplementation") +private class FlowAsPublisher(private val flow: Flow) : Publisher { override fun subscribe(subscriber: Subscriber?) { if (subscriber == null) throw NullPointerException() - subscriber.onSubscribe(FlowSubscription(flow, subscriber, context)) + subscriber.onSubscribe(FlowSubscription(flow, subscriber)) } } @@ -182,9 +165,8 @@ private class FlowAsPublisher( @InternalCoroutinesApi public class FlowSubscription( @JvmField public val flow: Flow, - @JvmField public val subscriber: Subscriber, - context: CoroutineContext -) : Subscription, AbstractCoroutine(context, true) { + @JvmField public val subscriber: Subscriber +) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, true) { private val requested = atomic(0L) private val producer = atomic?>(createInitialContinuation()) diff --git a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt index e7b8cb17ae..c044d92725 100644 --- a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt @@ -8,10 +8,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test import org.reactivestreams.* -import java.util.concurrent.* import kotlin.test.* class FlowAsPublisherTest : TestBase() { + @Test fun testErrorOnCancellationIsReported() { expect(1) @@ -75,78 +75,4 @@ class FlowAsPublisherTest : TestBase() { }) finish(4) } - - @Test - fun testUnconfinedDefaultContext() { - expect(1) - val thread = Thread.currentThread() - fun checkThread() { - assertSame(thread, Thread.currentThread()) - } - flowOf(42).asPublisher().subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - finish(5) - } - - @Test - fun testConfinedContext() { - expect(1) - val threadName = "FlowAsPublisherTest.testConfinedContext" - fun checkThread() { - val currentThread = Thread.currentThread() - assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") - } - val completed = CountDownLatch(1) - newSingleThreadContext(threadName).use { dispatcher -> - flowOf(42).asPublisher(dispatcher).subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - completed.countDown() - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - completed.await() - } - finish(5) - } } diff --git a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt index 18cd012d16..6f7d98480b 100644 --- a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt @@ -48,9 +48,6 @@ class IntegrationTest( assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) assertFailsWith { pub.awaitLast() } assertFailsWith { pub.awaitSingle() } - assertEquals("OK", pub.awaitSingleOrDefault("OK")) - assertNull(pub.awaitSingleOrNull()) - assertEquals("ELSE", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { cnt++ } assertEquals(0, cnt) @@ -68,9 +65,6 @@ class IntegrationTest( assertEquals("OK", pub.awaitFirstOrElse { "ELSE" }) assertEquals("OK", pub.awaitLast()) assertEquals("OK", pub.awaitSingle()) - assertEquals("OK", pub.awaitSingleOrDefault("!")) - assertEquals("OK", pub.awaitSingleOrNull()) - assertEquals("OK", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { assertEquals("OK", it) @@ -90,13 +84,10 @@ class IntegrationTest( } assertEquals(1, pub.awaitFirst()) assertEquals(1, pub.awaitFirstOrDefault(0)) + assertEquals(n, pub.awaitLast()) assertEquals(1, pub.awaitFirstOrNull()) assertEquals(1, pub.awaitFirstOrElse { 0 }) - assertEquals(n, pub.awaitLast()) assertFailsWith { pub.awaitSingle() } - assertFailsWith { pub.awaitSingleOrDefault(0) } - assertFailsWith { pub.awaitSingleOrNull() } - assertFailsWith { pub.awaitSingleOrElse { 0 } } checkNumbers(n, pub) val channel = pub.openSubscription() checkNumbers(n, channel.asPublisher(ctx(coroutineContext))) diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt index 04833e9814..61f88f6af3 100644 --- a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.reactive import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* -import org.reactivestreams.* import kotlin.test.* class PublisherAsFlowTest : TestBase() { @@ -182,85 +181,4 @@ class PublisherAsFlowTest : TestBase() { } finish(6) } - - @Test - fun testRequestRendezvous() = - testRequestSizeWithBuffer(Channel.RENDEZVOUS, BufferOverflow.SUSPEND, 1) - - @Test - fun testRequestBuffer1() = - testRequestSizeWithBuffer(1, BufferOverflow.SUSPEND, 1) - - @Test - fun testRequestBuffer10() = - testRequestSizeWithBuffer(10, BufferOverflow.SUSPEND, 10) - - @Test - fun testRequestBufferUnlimited() = - testRequestSizeWithBuffer(Channel.UNLIMITED, BufferOverflow.SUSPEND, Long.MAX_VALUE) - - @Test - fun testRequestBufferOverflowSuspend() = - testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.SUSPEND, 64) - - @Test - fun testRequestBufferOverflowDropOldest() = - testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.DROP_OLDEST, Long.MAX_VALUE) - - @Test - fun testRequestBufferOverflowDropLatest() = - testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.DROP_LATEST, Long.MAX_VALUE) - - @Test - fun testRequestBuffer10OverflowDropOldest() = - testRequestSizeWithBuffer(10, BufferOverflow.DROP_OLDEST, Long.MAX_VALUE) - - @Test - fun testRequestBuffer10OverflowDropLatest() = - testRequestSizeWithBuffer(10, BufferOverflow.DROP_LATEST, Long.MAX_VALUE) - - /** - * Tests `publisher.asFlow.buffer(...)` chain, verifying expected requests size and that only expected - * values are delivered. - */ - private fun testRequestSizeWithBuffer( - capacity: Int, - onBufferOverflow: BufferOverflow, - expectedRequestSize: Long - ) = runTest { - val m = 50 - // publishers numbers from 1 to m - val publisher = Publisher { s -> - s.onSubscribe(object : Subscription { - var lastSent = 0 - var remaining = 0L - override fun request(n: Long) { - assertEquals(expectedRequestSize, n) - remaining += n - check(remaining >= 0) - while (lastSent < m && remaining > 0) { - s.onNext(++lastSent) - remaining-- - } - if (lastSent == m) s.onComplete() - } - - override fun cancel() {} - }) - } - val flow = publisher - .asFlow() - .buffer(capacity, onBufferOverflow) - val list = flow.toList() - val runSize = if (capacity == Channel.BUFFERED) 1 else capacity - val expected = when (onBufferOverflow) { - // Everything is expected to be delivered - BufferOverflow.SUSPEND -> (1..m).toList() - // Only the last one (by default) or the last "capacity" items delivered - BufferOverflow.DROP_OLDEST -> (m - runSize + 1..m).toList() - // Only the first one (by default) or the first "capacity" items delivered - BufferOverflow.DROP_LATEST -> (1..runSize).toList() - } - assertEquals(expected, list) - } } diff --git a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api index b46fe338e5..422f36b1ea 100644 --- a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api +++ b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api @@ -38,8 +38,6 @@ public final class kotlinx/coroutines/reactor/ReactorContextKt { public final class kotlinx/coroutines/reactor/ReactorFlowKt { public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;)Lreactor/core/publisher/Flux; - public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lreactor/core/publisher/Flux; - public static synthetic fun asFlux$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lreactor/core/publisher/Flux; } public final class kotlinx/coroutines/reactor/SchedulerCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay { @@ -49,7 +47,7 @@ public final class kotlinx/coroutines/reactor/SchedulerCoroutineDispatcher : kot public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lreactor/core/scheduler/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle b/reactive/kotlinx-coroutines-reactor/build.gradle new file mode 100644 index 0000000000..3b640bd5cc --- /dev/null +++ b/reactive/kotlinx-coroutines-reactor/build.gradle @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + compile "io.projectreactor:reactor-core:$reactor_version" + compile project(':kotlinx-coroutines-reactive') +} + +tasks.withType(dokka.getClass()) { + externalDocumentationLink { + url = new URL("https://projectreactor.io/docs/core/$reactor_version/api/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + } +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle.kts b/reactive/kotlinx-coroutines-reactor/build.gradle.kts deleted file mode 100644 index d5fd208a27..0000000000 --- a/reactive/kotlinx-coroutines-reactor/build.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -val reactorVersion = version("reactor") - -dependencies { - compile("io.projectreactor:reactor-core:$reactorVersion") - compile(project(":kotlinx-coroutines-reactive")) -} - - -tasks { - compileKotlin { - kotlinOptions.jvmTarget = "1.8" - } - - compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" - } -} - -externalDocumentationLink( - url = "https://projectreactor.io/docs/core/$reactorVersion/api/" -) diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt index a478ab1ef8..d665c88d35 100644 --- a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt +++ b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt @@ -4,38 +4,25 @@ package kotlinx.coroutines.reactor -import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.reactive.FlowSubscription -import org.reactivestreams.* import reactor.core.CoreSubscriber import reactor.core.publisher.Flux -import kotlin.coroutines.* /** * Converts the given flow to a cold flux. * The original flow is cancelled when the flux subscriber is disposed. * * This function is integrated with [ReactorContext], see its documentation for additional details. - * - * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -public fun Flow.asFlux(context: CoroutineContext = EmptyCoroutineContext): Flux = - FlowAsFlux(this, Dispatchers.Unconfined + context) +public fun Flow.asFlux(): Flux = FlowAsFlux(this) -private class FlowAsFlux( - private val flow: Flow, - private val context: CoroutineContext -) : Flux() { +private class FlowAsFlux(private val flow: Flow) : Flux() { override fun subscribe(subscriber: CoreSubscriber?) { if (subscriber == null) throw NullPointerException() val hasContext = !subscriber.currentContext().isEmpty val source = if (hasContext) flow.flowOn(subscriber.currentContext().asCoroutineContext()) else flow - subscriber.onSubscribe(FlowSubscription(source, subscriber, context)) + subscriber.onSubscribe(FlowSubscription(source, subscriber)) } } diff --git a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt index 4fb5514322..e176c07bb9 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt @@ -39,7 +39,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = scheduler.schedule(block, timeMillis, TimeUnit.MILLISECONDS).asDisposableHandle() /** @suppress */ diff --git a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt index cecc89592e..e4bd8b315b 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt @@ -4,13 +4,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* import org.junit.Test -import org.reactivestreams.* import reactor.core.publisher.* import reactor.util.context.Context -import java.util.concurrent.* import kotlin.test.* -@Suppress("ReactiveStreamsSubscriberImplementation") class FlowAsFluxTest : TestBase() { @Test fun testFlowAsFluxContextPropagation() { @@ -71,78 +68,4 @@ class FlowAsFluxTest : TestBase() { } finish(4) } - - @Test - fun testUnconfinedDefaultContext() { - expect(1) - val thread = Thread.currentThread() - fun checkThread() { - assertSame(thread, Thread.currentThread()) - } - flowOf(42).asFlux().subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - finish(5) - } - - @Test - fun testConfinedContext() { - expect(1) - val threadName = "FlowAsFluxTest.testConfinedContext" - fun checkThread() { - val currentThread = Thread.currentThread() - assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") - } - val completed = CountDownLatch(1) - newSingleThreadContext(threadName).use { dispatcher -> - flowOf(42).asFlux(dispatcher).subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - completed.countDown() - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - completed.await() - } - finish(5) - } -} +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt index cc336ba6b5..3879c62c71 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt @@ -68,72 +68,6 @@ class FluxSingleTest : TestBase() { } } - @Test - fun testAwaitSingleOrDefault() { - val flux = flux { - send(Flux.empty().awaitSingleOrDefault("O") + "K") - } - - checkSingleValue(flux) { - assertEquals("OK", it) - } - } - - @Test - fun testAwaitSingleOrDefaultException() { - val flux = flux { - send(Flux.just("O", "#").awaitSingleOrDefault("!") + "K") - } - - checkErroneous(flux) { - assert(it is IllegalArgumentException) - } - } - - @Test - fun testAwaitSingleOrNull() { - val flux = flux { - send(Flux.empty().awaitSingleOrNull() ?: "OK") - } - - checkSingleValue(flux) { - assertEquals("OK", it) - } - } - - @Test - fun testAwaitSingleOrNullException() { - val flux = flux { - send((Flux.just("O", "#").awaitSingleOrNull() ?: "!") + "K") - } - - checkErroneous(flux) { - assert(it is IllegalArgumentException) - } - } - - @Test - fun testAwaitSingleOrElse() { - val flux = flux { - send(Flux.empty().awaitSingleOrElse { "O" } + "K") - } - - checkSingleValue(flux) { - assertEquals("OK", it) - } - } - - @Test - fun testAwaitSingleOrElseException() { - val flux = flux { - send(Flux.just("O", "#").awaitSingleOrElse { "!" } + "K") - } - - checkErroneous(flux) { - assert(it is IllegalArgumentException) - } - } - @Test fun testAwaitFirst() { val flux = flux { diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api index 4370325f58..22f40384f0 100644 --- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api +++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api @@ -30,19 +30,11 @@ public final class kotlinx/coroutines/rx2/RxCompletableKt { public final class kotlinx/coroutines/rx2/RxConvertKt { public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Completable; public static final fun asFlow (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/flow/Flow; - public static final fun asFlowable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; - public static synthetic fun asFlowable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Maybe; public static final fun asObservable (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; - public static final fun asObservable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; - public static synthetic fun asObservable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Single; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; - public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; - public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; } public final class kotlinx/coroutines/rx2/RxFlowableKt { @@ -84,7 +76,7 @@ public final class kotlinx/coroutines/rx2/SchedulerCoroutineDispatcher : kotlinx public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lio/reactivex/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt index 633693e756..e253161db0 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt @@ -64,7 +64,7 @@ public suspend inline fun ObservableSource.collect(action: (T) -> Unit): @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private class SubscriptionChannel : - LinkedListChannel(null), Observer, MaybeObserver + LinkedListChannel(), Observer, MaybeObserver { private val _subscription = atomic(null) diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index cf73ef2ea8..0be606ffc2 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -10,13 +10,12 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* -import org.reactivestreams.* import java.util.concurrent.atomic.* import kotlin.coroutines.* /** * Converts this job to the hot reactive completable that signals - * with [onCompleted][CompletableObserver.onComplete] when the corresponding job completes. + * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. * * Every subscriber gets the signal at the same time. * Unsubscribing from the resulting completable **does not** affect the original job in any way. @@ -50,7 +49,7 @@ public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMay /** * Converts this deferred value to the hot reactive single that signals either - * [onSuccess][SingleObserver.onSuccess] or [onError][SingleObserver.onError]. + * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. * * Every subscriber gets the same completion value. * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. @@ -65,6 +64,21 @@ public fun Deferred.asSingle(context: CoroutineContext): Single this@asSingle.await() } +/** + * Converts a stream of elements received from the channel to the hot reactive observable. + * + * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers, + * they'll receive values in round-robin way. + */ +@Deprecated( + message = "Deprecated in the favour of Flow", + level = DeprecationLevel.WARNING, replaceWith = ReplaceWith("this.consumeAsFlow().asObservable()") +) +public fun ReceiveChannel.asObservable(context: CoroutineContext): Observable = rxObservable(context) { + for (t in this@asObservable) + send(t) +} + /** * Transforms given cold [ObservableSource] into cold [Flow]. * @@ -92,19 +106,15 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { /** * Converts the given flow to a cold observable. * The original flow is cancelled when the observable subscriber is disposed. - * - * An optional [context] can be specified to control the execution context of calls to [Observer] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ +@JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> +public fun Flow.asObservable() : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ - val job = GlobalScope.launch(Dispatchers.Unconfined + context, start = CoroutineStart.ATOMIC) { + val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { try { collect { value -> emitter.onNext(value) } emitter.onComplete() @@ -125,35 +135,7 @@ public fun Flow.asObservable(context: CoroutineContext = EmptyCorout /** * Converts the given flow to a cold flowable. * The original flow is cancelled when the flowable subscriber is disposed. - * - * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ -@ExperimentalCoroutinesApi -public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = - Flowable.fromPublisher(asPublisher(context)) - -@Deprecated( - message = "Deprecated in the favour of Flow", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("this.consumeAsFlow().asObservable(context)", "kotlinx.coroutines.flow.consumeAsFlow") -) // Deprecated since 1.4.0 -public fun ReceiveChannel.asObservable(context: CoroutineContext): Observable = rxObservable(context) { - for (t in this@asObservable) - send(t) -} - -@Suppress("UNUSED") // KT-42513 -@JvmOverloads // binary compatibility @JvmName("from") -@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that -public fun Flow._asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = - asFlowable(context) - -@Suppress("UNUSED") // KT-42513 -@JvmOverloads // binary compatibility -@JvmName("from") -@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that -public fun Flow._asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = asObservable(context) +@ExperimentalCoroutinesApi +public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt index 9952eb91a0..3ddb67649e 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt @@ -38,7 +38,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) return DisposableHandle { disposable.dispose() } } diff --git a/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt b/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt index cfc3240741..a43366555e 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.rx2 import kotlinx.coroutines.* import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* import org.junit.Assert import org.junit.Test import kotlin.test.* @@ -127,7 +126,7 @@ class ConvertTest : TestBase() { delay(50) send("K") } - val observable = c.consumeAsFlow().asObservable(Dispatchers.Unconfined) + val observable = c.asObservable(Dispatchers.Unconfined) checkSingleValue(observable.reduce { t1, t2 -> t1 + t2 }.toSingle()) { assertEquals("OK", it) } @@ -141,7 +140,7 @@ class ConvertTest : TestBase() { delay(50) throw TestException("K") } - val observable = c.consumeAsFlow().asObservable(Dispatchers.Unconfined) + val observable = c.asObservable(Dispatchers.Unconfined) val single = rxSingle(Dispatchers.Unconfined) { var result = "" try { @@ -156,4 +155,4 @@ class ConvertTest : TestBase() { assertEquals("OK", it) } } -} +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt deleted file mode 100644 index 1cbded6dc3..0000000000 --- a/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.rx2 - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import org.junit.Test -import org.reactivestreams.* -import java.util.concurrent.* -import kotlin.test.* - -@Suppress("ReactiveStreamsSubscriberImplementation") -class FlowAsFlowableTest : TestBase() { - @Test - fun testUnconfinedDefaultContext() { - expect(1) - val thread = Thread.currentThread() - fun checkThread() { - assertSame(thread, Thread.currentThread()) - } - flowOf(42).asFlowable().subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - finish(5) - } - - @Test - fun testConfinedContext() { - expect(1) - val threadName = "FlowAsFlowableTest.testConfinedContext" - fun checkThread() { - val currentThread = Thread.currentThread() - assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") - } - val completed = CountDownLatch(1) - newSingleThreadContext(threadName).use { dispatcher -> - flowOf(42).asFlowable(dispatcher).subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - completed.countDown() - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - completed.await() - } - finish(5) - } -} diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt index 3cde182260..0908b34cf2 100644 --- a/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt @@ -4,12 +4,9 @@ package kotlinx.coroutines.rx2 -import io.reactivex.* -import io.reactivex.disposables.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test -import java.util.concurrent.* import kotlin.test.* class FlowAsObservableTest : TestBase() { @@ -142,70 +139,4 @@ class FlowAsObservableTest : TestBase() { observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) } - - @Test - fun testUnconfinedDefaultContext() { - expect(1) - val thread = Thread.currentThread() - fun checkThread() { - assertSame(thread, Thread.currentThread()) - } - flowOf(42).asObservable().subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - expect(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - } - - override fun onError(t: Throwable) { - expectUnreached() - } - }) - finish(5) - } - - @Test - fun testConfinedContext() { - expect(1) - val threadName = "FlowAsObservableTest.testConfinedContext" - fun checkThread() { - val currentThread = Thread.currentThread() - assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") - } - val completed = CountDownLatch(1) - newSingleThreadContext(threadName).use { dispatcher -> - flowOf(42).asObservable(dispatcher).subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - expect(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - completed.countDown() - } - - override fun onError(e: Throwable) { - expectUnreached() - } - }) - completed.await() - } - finish(5) - } } diff --git a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt index 540fa76b7e..22e0e72191 100644 --- a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* import org.junit.Test import org.junit.runner.* import org.junit.runners.* @@ -93,7 +92,7 @@ class IntegrationTest( assertFailsWith { observable.awaitSingle() } checkNumbers(n, observable) val channel = observable.openSubscription() - checkNumbers(n, channel.consumeAsFlow().asObservable(ctx(coroutineContext))) + checkNumbers(n, channel.asObservable(ctx(coroutineContext))) channel.cancel() } @@ -132,4 +131,4 @@ class IntegrationTest( assertEquals(n, last) } -} +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api index 6d2dd63d2c..27c3d3dfa0 100644 --- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -26,18 +26,10 @@ public final class kotlinx/coroutines/rx3/RxCompletableKt { public final class kotlinx/coroutines/rx3/RxConvertKt { public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Completable; public static final fun asFlow (Lio/reactivex/rxjava3/core/ObservableSource;)Lkotlinx/coroutines/flow/Flow; - public static final fun asFlowable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; - public static synthetic fun asFlowable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Maybe; - public static final fun asObservable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; - public static synthetic fun asObservable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Single; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; - public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; - public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; - public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; } public final class kotlinx/coroutines/rx3/RxFlowableKt { @@ -71,7 +63,7 @@ public final class kotlinx/coroutines/rx3/SchedulerCoroutineDispatcher : kotlinx public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lio/reactivex/rxjava3/core/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt index 737cf6710d..acb907b765 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt @@ -54,7 +54,7 @@ public suspend inline fun ObservableSource.collect(action: (T) -> Unit): @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private class SubscriptionChannel : - LinkedListChannel(null), Observer, MaybeObserver + LinkedListChannel(), Observer, MaybeObserver { private val _subscription = atomic(null) diff --git a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt index 9bb38c088f..f9e2e2158f 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt @@ -10,13 +10,12 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* -import org.reactivestreams.* import java.util.concurrent.atomic.* import kotlin.coroutines.* /** * Converts this job to the hot reactive completable that signals - * with [onCompleted][CompletableObserver.onComplete] when the corresponding job completes. + * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. * * Every subscriber gets the signal at the same time. * Unsubscribing from the resulting completable **does not** affect the original job in any way. @@ -50,7 +49,7 @@ public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMay /** * Converts this deferred value to the hot reactive single that signals either - * [onSuccess][SingleObserver.onSuccess] or [onError][SingleObserver.onError]. + * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. * * Every subscriber gets the same completion value. * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. @@ -92,19 +91,15 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { /** * Converts the given flow to a cold observable. * The original flow is cancelled when the observable subscriber is disposed. - * - * An optional [context] can be specified to control the execution context of calls to [Observer] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ +@JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> +public fun Flow.asObservable() : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ - val job = GlobalScope.launch(Dispatchers.Unconfined + context, start = CoroutineStart.ATOMIC) { + val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { try { collect { value -> emitter.onNext(value) } emitter.onComplete() @@ -125,25 +120,7 @@ public fun Flow.asObservable(context: CoroutineContext = EmptyCorout /** * Converts the given flow to a cold flowable. * The original flow is cancelled when the flowable subscriber is disposed. - * - * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. - * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to - * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher - * is used, so calls are performed from an arbitrary thread. */ -@ExperimentalCoroutinesApi -public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = - Flowable.fromPublisher(asPublisher(context)) - -@Suppress("UNUSED") // KT-42513 -@JvmOverloads // binary compatibility -@JvmName("from") -@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that -public fun Flow._asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = - asFlowable(context) - -@Suppress("UNUSED") // KT-42513 -@JvmOverloads // binary compatibility @JvmName("from") -@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that -public fun Flow._asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = asObservable(context) +@ExperimentalCoroutinesApi +public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt index a426aea6ba..6e91aeea85 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt @@ -38,7 +38,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) return DisposableHandle { disposable.dispose() } } diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt deleted file mode 100644 index a73fee469e..0000000000 --- a/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.rx3 - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import org.junit.Test -import org.reactivestreams.* -import java.util.concurrent.* -import kotlin.test.* - -@Suppress("ReactiveStreamsSubscriberImplementation") -class FlowAsFlowableTest : TestBase() { - @Test - fun testUnconfinedDefaultContext() { - expect(1) - val thread = Thread.currentThread() - fun checkThread() { - assertSame(thread, Thread.currentThread()) - } - flowOf(42).asFlowable().subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - finish(5) - } - - @Test - fun testConfinedContext() { - expect(1) - val threadName = "FlowAsFlowableTest.testConfinedContext" - fun checkThread() { - val currentThread = Thread.currentThread() - assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") - } - val completed = CountDownLatch(1) - newSingleThreadContext(threadName).use { dispatcher -> - flowOf(42).asFlowable(dispatcher).subscribe(object : Subscriber { - private lateinit var subscription: Subscription - - override fun onSubscribe(s: Subscription) { - expect(2) - subscription = s - subscription.request(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - completed.countDown() - } - - override fun onError(t: Throwable?) { - expectUnreached() - } - }) - completed.await() - } - finish(5) - } -} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt index 5759f9f426..50c4ae7dad 100644 --- a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt @@ -4,12 +4,9 @@ package kotlinx.coroutines.rx3 -import io.reactivex.rxjava3.core.* -import io.reactivex.rxjava3.disposables.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test -import java.util.concurrent.* import kotlin.test.* class FlowAsObservableTest : TestBase() { @@ -142,70 +139,4 @@ class FlowAsObservableTest : TestBase() { observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) } - - @Test - fun testUnconfinedDefaultContext() { - expect(1) - val thread = Thread.currentThread() - fun checkThread() { - assertSame(thread, Thread.currentThread()) - } - flowOf(42).asObservable().subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - expect(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - } - - override fun onError(t: Throwable) { - expectUnreached() - } - }) - finish(5) - } - - @Test - fun testConfinedContext() { - expect(1) - val threadName = "FlowAsObservableTest.testConfinedContext" - fun checkThread() { - val currentThread = Thread.currentThread() - assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") - } - val completed = CountDownLatch(1) - newSingleThreadContext(threadName).use { dispatcher -> - flowOf(42).asObservable(dispatcher).subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - expect(2) - } - - override fun onNext(t: Int) { - checkThread() - expect(3) - assertEquals(42, t) - } - - override fun onComplete() { - checkThread() - expect(4) - completed.countDown() - } - - override fun onError(e: Throwable) { - expectUnreached() - } - }) - completed.await() - } - finish(5) - } } diff --git a/site/build.gradle.kts b/site/build.gradle.kts index eba7b1a1bc..a18062a371 100644 --- a/site/build.gradle.kts +++ b/site/build.gradle.kts @@ -7,7 +7,7 @@ import groovy.lang.* val buildDocsDir = "$buildDir/docs" val jekyllDockerImage = "jekyll/jekyll:${version("jekyll")}" -val copyDocs by tasks.registering(Copy::class) { +val copyDocs = tasks.register("copyDocs") { val dokkaTasks = rootProject.getTasksByName("dokka", true) from(dokkaTasks.map { "${it.project.buildDir}/dokka" }) { @@ -21,12 +21,12 @@ val copyDocs by tasks.registering(Copy::class) { dependsOn(dokkaTasks) } -val copyExampleFrontendJs by tasks.registering(Copy::class) { +val copyExampleFrontendJs = tasks.register("copyExampleFrontendJs") { val srcBuildDir = project(":example-frontend-js").buildDir from("$srcBuildDir/dist") into("$buildDocsDir/example-frontend-js") - dependsOn(":example-frontend-js:browserDistribution") + dependsOn(":example-frontend-js:bundle") } tasks.register("site") { diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 5df8d7f0ed..071f794cb2 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your @@ -310,7 +310,7 @@ processing the previous one. The [actor] coroutine builder accepts an optional controls the implementation of the channel that this actor is using for its mailbox. The description of all the available choices is given in documentation of the [`Channel()`][Channel] factory function. -Let us change the code to use a conflated channel by passing [Channel.CONFLATED] capacity value. The +Let us change the code to use `ConflatedChannel` by passing [Channel.CONFLATED] capacity value. The change is only to the line that creates an actor: ```kotlin diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt index c677d9911a..9cf813bc3a 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt @@ -21,7 +21,7 @@ public class TestComponent { fun launchDelayed() { scope.launch { - delay(Long.MAX_VALUE / 2) + delay(Long.MAX_VALUE) delayedLaunchCompleted = true } } diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index cd34eb1285..0be3b9c1cb 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -20,8 +20,8 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.4.0 -coroutines_version=1.4.0-M1 +kotlin_version=1.3.71 +coroutines_version=1.3.8 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api b/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api index 090c14e09c..b97d8462c3 100644 --- a/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api +++ b/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api @@ -1,7 +1,7 @@ public abstract class kotlinx/coroutines/android/HandlerDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getImmediate ()Lkotlinx/coroutines/android/HandlerDispatcher; - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; } public final class kotlinx/coroutines/android/HandlerDispatcherKt { diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts index 4f24788359..4be32fc5c6 100644 --- a/ui/kotlinx-coroutines-android/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/build.gradle.kts @@ -6,6 +6,10 @@ import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink import org.jetbrains.dokka.gradle.DokkaTask import java.net.URL +repositories { + google() +} + configurations { create("r8") } @@ -21,20 +25,59 @@ dependencies { "r8"("com.android.tools.build:builder:4.0.0-alpha06") // Contains r8-2.0.4-dev } +open class RunR8Task : JavaExec() { + + @OutputDirectory + lateinit var outputDex: File + + @InputFile + lateinit var inputConfig: File + + @InputFile + val inputConfigCommon: File = File("testdata/r8-test-common.pro") + + @InputFiles + val jarFile: File = project.tasks.named("jar").get().archivePath + + init { + classpath = project.configurations["r8"] + main = "com.android.tools.r8.R8" + } + + override fun exec() { + // Resolve classpath only during execution + val arguments = mutableListOf( + "--release", + "--no-desugaring", + "--output", outputDex.absolutePath, + "--pg-conf", inputConfig.absolutePath + ) + arguments.addAll(project.configurations.runtimeClasspath.files.map { it.absolutePath }) + arguments.add(jarFile.absolutePath) + + args = arguments + + project.delete(outputDex) + outputDex.mkdirs() + + super.exec() + } +} + val optimizedDexDir = File(buildDir, "dex-optim/") val unOptimizedDexDir = File(buildDir, "dex-unoptim/") val optimizedDexFile = File(optimizedDexDir, "classes.dex") val unOptimizedDexFile = File(unOptimizedDexDir, "classes.dex") -val runR8 by tasks.registering(RunR8::class) { +val runR8 = tasks.register("runR8") { outputDex = optimizedDexDir inputConfig = file("testdata/r8-test-rules.pro") dependsOn("jar") } -val runR8NoOptim by tasks.registering(RunR8::class) { +val runR8NoOptim = tasks.register("runR8NoOptim") { outputDex = unOptimizedDexDir inputConfig = file("testdata/r8-test-rules-no-optim.pro") @@ -57,6 +100,9 @@ tasks.test { } } -externalDocumentationLink( - url = "https://developer.android.com/reference/" -) +tasks.withType().configureEach { + externalDocumentationLink(delegateClosureOf { + url = URL("https://developer.android.com/reference/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + }) +} diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index cd34eb1285..0be3b9c1cb 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -20,8 +20,8 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.4.0 -coroutines_version=1.4.0-M1 +kotlin_version=1.3.71 +coroutines_version=1.3.8 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro index 0d04990ad9..fd25b215c3 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro @@ -9,8 +9,6 @@ boolean ANDROID_DETECTED return true; } --keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} - # Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher -assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt { boolean SUPPORT_MISSING return false; diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index af79da7c97..1693409875 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -140,7 +140,7 @@ internal class HandlerContext private constructor( continuation.invokeOnCancellation { handler.removeCallbacks(block) } } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY)) return object : DisposableHandle { override fun dispose() { diff --git a/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api b/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api index e2c3b8f326..620e904612 100644 --- a/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api +++ b/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api @@ -5,7 +5,7 @@ public final class kotlinx/coroutines/javafx/JavaFxConvertKt { public abstract class kotlinx/coroutines/javafx/JavaFxDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } diff --git a/ui/kotlinx-coroutines-javafx/build.gradle b/ui/kotlinx-coroutines-javafx/build.gradle new file mode 100644 index 0000000000..77f1b09650 --- /dev/null +++ b/ui/kotlinx-coroutines-javafx/build.gradle @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id 'org.openjfx.javafxplugin' +} + +javafx { + version = javafx_version + modules = ['javafx.controls'] + configuration = 'compile' +} + +task checkJdk8() { + // only fail w/o JDK_18 when actually trying to test, not during project setup phase + doLast { + if (!System.env.JDK_18) { + throw new GradleException("JDK_18 environment variable is not defined. " + + "Can't run JDK 8 compatibility tests. " + + "Please ensure JDK 8 is installed and that JDK_18 points to it.") + } + } +} + +task jdk8Test(type: Test, dependsOn: [compileTestKotlin, checkJdk8]) { + classpath = files { test.classpath } + testClassesDirs = files { test.testClassesDirs } + executable = "$System.env.JDK_18/bin/java" +} + +// Run these tests only during nightly stress test +jdk8Test.onlyIf { project.properties['stressTest'] != null } +build.dependsOn jdk8Test diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts deleted file mode 100644 index 112441e0ed..0000000000 --- a/ui/kotlinx-coroutines-javafx/build.gradle.kts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -plugins { - id("org.openjfx.javafxplugin") -} - -javafx { - version = version("javafx") - modules = listOf("javafx.controls") - configuration = "compile" -} - -val JDK_18: String? by lazy { - System.getenv("JDK_18") -} - -val checkJdk8 by tasks.registering { - // only fail w/o JDK_18 when actually trying to test, not during project setup phase - doLast { - if (JDK_18 == null) { - throw GradleException( - """ - JDK_18 environment variable is not defined. - Can't run JDK 8 compatibility tests. - Please ensure JDK 8 is installed and that JDK_18 points to it. - """.trimIndent() - ) - } - } -} - -val jdk8Test by tasks.registering(Test::class) { - // Run these tests only during nightly stress test - onlyIf { project.properties["stressTest"] != null } - - val test = tasks.test.get() - - classpath = test.classpath - testClassesDirs = test.testClassesDirs - executable = "$JDK_18/bin/java" - - dependsOn("compileTestKotlin") - dependsOn(checkJdk8) -} - -tasks.build { - dependsOn(jdk8Test) -} diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index c3069d636f..a13a68368e 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -42,7 +42,7 @@ public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay { } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler { block.run() }) diff --git a/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api b/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api index d33191fd96..09556e807f 100644 --- a/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api +++ b/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api @@ -1,7 +1,7 @@ public abstract class kotlinx/coroutines/swing/SwingDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V - public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt index 054ed1f60e..77f109df91 100644 --- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt +++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt @@ -36,7 +36,7 @@ public sealed class SwingDispatcher : MainCoroutineDispatcher(), Delay { } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val timer = schedule(timeMillis, TimeUnit.MILLISECONDS, ActionListener { block.run() }) From ca53fb2ccee4ecbd32314e27f60b4a88f85fafac Mon Sep 17 00:00:00 2001 From: Miguel Kano Date: Sat, 17 Oct 2020 22:13:11 -0700 Subject: [PATCH 8/8] Revert "Revert "Merge branch 'master' of https://github.com/Kotlin/kotlinx.coroutines into debounce-selector"" This reverts commit 7c6fb412c2d85b322d50e045389319dff3d98a37. --- CHANGES.md | 40 + CONTRIBUTING.md | 113 +++ README.md | 80 +- RELEASE.md | 12 +- benchmarks/build.gradle.kts | 12 +- .../benchmarks/tailcall/SimpleChannel.kt | 8 +- build.gradle | 45 +- buildSrc/build.gradle.kts | 30 +- buildSrc/settings.gradle.kts | 17 + buildSrc/src/main/kotlin/CacheRedirector.kt | 152 ++++ buildSrc/src/main/kotlin/Dokka.kt | 26 + buildSrc/src/main/kotlin/Idea.kt | 1 + buildSrc/src/main/kotlin/MavenCentral.kt | 7 +- buildSrc/src/main/kotlin/Platform.kt | 1 + buildSrc/src/main/kotlin/Properties.kt | 11 + buildSrc/src/main/kotlin/RunR8.kt | 52 ++ buildSrc/src/main/kotlin/UnpackAar.kt | 32 + coroutines-guide.md | 3 + docs/cancellation-and-timeouts.md | 109 +++ docs/coroutine-context-and-dispatchers.md | 34 +- docs/coroutines-guide.md | 2 +- docs/exception-handling.md | 2 +- docs/flow.md | 16 +- docs/images/coroutine-idea-debugging-1.png | Bin 0 -> 248706 bytes docs/knit.properties | 12 - docs/knit.test.template | 1 + docs/shared-mutable-state-and-concurrency.md | 6 +- gradle.properties | 25 +- gradle/compile-common.gradle | 4 - gradle/compile-js-multiplatform.gradle | 4 - gradle/compile-js.gradle | 28 +- gradle/compile-jvm-multiplatform.gradle | 8 - gradle/compile-jvm.gradle | 7 +- gradle/compile-native-multiplatform.gradle | 40 +- gradle/targets.gradle | 28 - gradle/test-mocha-js.gradle | 4 +- .../kotlinx-coroutines-guava/build.gradle | 16 - .../kotlinx-coroutines-guava/build.gradle.kts | 13 + .../build.gradle | 34 - .../kotlinx-coroutines-slf4j/build.gradle | 17 - .../kotlinx-coroutines-slf4j/build.gradle.kts | 14 + js/example-frontend-js/README.md | 4 +- js/example-frontend-js/build.gradle | 69 +- js/example-frontend-js/npm/package.json | 22 - js/example-frontend-js/npm/webpack.config.js | 53 -- js/example-frontend-js/src/ExampleMain.kt | 3 + .../webpack.config.d/custom-config.js | 18 + knit.properties | 16 + .../api/kotlinx-coroutines-core.api | 114 ++- kotlinx-coroutines-core/build.gradle | 120 ++- kotlinx-coroutines-core/common/src/Await.kt | 15 +- .../common/src/Builders.common.kt | 5 +- .../common/src/CancellableContinuation.kt | 193 +++-- .../common/src/CancellableContinuationImpl.kt | 300 ++++--- .../common/src/CompletableDeferred.kt | 2 +- .../common/src/CompletableJob.kt | 2 +- ...tedExceptionally.kt => CompletionState.kt} | 18 +- .../common/src/CoroutineDispatcher.kt | 1 + .../common/src/CoroutineScope.kt | 4 +- .../common/src/Deferred.kt | 2 + kotlinx-coroutines-core/common/src/Delay.kt | 59 +- kotlinx-coroutines-core/common/src/Timeout.kt | 38 +- kotlinx-coroutines-core/common/src/Yield.kt | 3 + .../common/src/channels/AbstractChannel.kt | 163 +++- .../src/channels/ArrayBroadcastChannel.kt | 6 +- .../common/src/channels/ArrayChannel.kt | 160 ++-- .../common/src/channels/Broadcast.kt | 7 +- .../common/src/channels/BroadcastChannel.kt | 7 +- .../common/src/channels/BufferOverflow.kt | 36 + .../common/src/channels/Channel.kt | 205 +++-- .../common/src/channels/Channels.common.kt | 9 +- .../src/channels/ConflatedBroadcastChannel.kt | 8 +- .../common/src/channels/ConflatedChannel.kt | 27 +- .../common/src/channels/LinkedListChannel.kt | 2 +- .../common/src/channels/Produce.kt | 28 +- .../common/src/channels/RendezvousChannel.kt | 4 +- .../common/src/flow/Builders.kt | 61 +- .../common/src/flow/Channels.kt | 33 +- .../common/src/flow/Flow.kt | 22 +- .../common/src/flow/Migration.kt | 59 +- .../common/src/flow/SharedFlow.kt | 659 +++++++++++++++ .../common/src/flow/SharingStarted.kt | 216 +++++ .../common/src/flow/StateFlow.kt | 263 +++--- .../src/flow/internal/AbstractSharedFlow.kt | 101 +++ .../common/src/flow/internal/ChannelFlow.kt | 122 ++- .../common/src/flow/internal/Merge.kt | 30 +- .../common/src/flow/operators/Context.kt | 91 +- .../common/src/flow/operators/Delay.kt | 97 ++- .../common/src/flow/operators/Distinct.kt | 52 +- .../common/src/flow/operators/Emitters.kt | 11 +- .../common/src/flow/operators/Lint.kt | 90 +- .../common/src/flow/operators/Share.kt | 412 +++++++++ .../common/src/flow/operators/Transform.kt | 2 +- .../common/src/flow/terminal/Reduce.kt | 37 +- .../common/src/internal/Atomic.kt | 7 +- .../src/internal/DispatchedContinuation.kt | 57 +- .../common/src/internal/DispatchedTask.kt | 99 ++- .../src/internal/LockFreeLinkedList.common.kt | 1 + .../src/internal/OnUndeliveredElement.kt | 43 + .../common/src/intrinsics/Cancellable.kt | 8 +- .../common/src/selects/Select.kt | 12 +- .../common/src/sync/Mutex.kt | 37 +- .../common/src/sync/Semaphore.kt | 25 +- .../test/AtomicCancellationCommonTest.kt | 13 +- .../common/test/AwaitCancellationTest.kt | 27 + .../CancellableContinuationHandlersTest.kt | 72 +- .../test/CancellableContinuationTest.kt | 22 + .../common/test/CancellableResumeTest.kt | 156 +++- .../common/test/DispatchedContinuationTest.kt | 78 ++ .../test/channels/BasicOperationsTest.kt | 20 +- .../common/test/channels/BroadcastTest.kt | 4 +- .../channels/ChannelBufferOverflowTest.kt | 40 + .../test/channels/ChannelFactoryTest.kt | 17 +- .../ChannelUndeliveredElementFailureTest.kt | 143 ++++ .../channels/ChannelUndeliveredElementTest.kt | 104 +++ .../ConflatedChannelArrayModelTest.kt | 11 + .../test/channels/ConflatedChannelTest.kt | 19 +- .../common/test/channels/TestChannelKind.kt | 14 +- .../common/test/flow/VirtualTime.kt | 18 +- .../flow/operators/BufferConflationTest.kt | 146 ++++ .../common/test/flow/operators/BufferTest.kt | 33 +- .../common/test/flow/operators/CatchTest.kt | 7 +- .../common/test/flow/operators/CombineTest.kt | 18 +- .../operators/DistinctUntilChangedTest.kt | 29 + .../common/test/flow/operators/FlowOnTest.kt | 16 + .../test/flow/operators/OnCompletionTest.kt | 4 +- .../common/test/flow/operators/ZipTest.kt | 25 +- .../test/flow/sharing/ShareInBufferTest.kt | 98 +++ .../flow/sharing/ShareInConflationTest.kt | 162 ++++ .../test/flow/sharing/ShareInFusionTest.kt | 56 ++ .../common/test/flow/sharing/ShareInTest.kt | 215 +++++ .../flow/sharing/SharedFlowScenarioTest.kt | 331 ++++++++ .../test/flow/sharing/SharedFlowTest.kt | 798 ++++++++++++++++++ .../test/flow/sharing/SharingStartedTest.kt | 183 ++++ .../SharingStartedWhileSubscribedTest.kt | 42 + .../test/flow/{ => sharing}/StateFlowTest.kt | 85 +- .../common/test/flow/sharing/StateInTest.kt | 78 ++ .../common/test/flow/terminal/FirstTest.kt | 6 + .../common/test/flow/terminal/SingleTest.kt | 27 +- .../common/test/selects/SelectLoopTest.kt | 23 +- kotlinx-coroutines-core/js/src/Dispatchers.kt | 21 +- .../js/src/JSDispatcher.kt | 4 +- kotlinx-coroutines-core/js/src/Promise.kt | 2 + .../js/src/internal/LinkedList.kt | 2 + .../js/test/ImmediateDispatcherTest.kt | 32 + .../jvm/resources/DebugProbesKt.bin | Bin 1730 -> 1728 bytes .../META-INF/proguard/coroutines.pro | 6 + kotlinx-coroutines-core/jvm/src/CommonPool.kt | 2 + .../jvm/src/DebugStrings.kt | 1 + .../jvm/src/DefaultExecutor.kt | 3 +- .../jvm/src/Dispatchers.kt | 6 +- kotlinx-coroutines-core/jvm/src/Executors.kt | 38 +- .../jvm/src/ThreadPoolDispatcher.kt | 10 + .../jvm/src/internal/LockFreeLinkedList.kt | 19 +- .../jvm/src/internal/MainDispatchers.kt | 9 +- .../jvm/src/scheduling/Dispatcher.kt | 8 +- .../jvm/src/test_/TestCoroutineContext.kt | 2 +- .../jvm/test/AtomicCancellationTest.kt | 27 +- .../jvm/test/ExecutorsTest.kt | 6 +- .../test/FailingCoroutinesMachineryTest.kt | 39 +- .../jvm/test/JobStructuredJoinStressTest.kt | 41 +- .../jvm/test/RejectedExecutionTest.kt | 168 ++++ .../ReusableCancellableContinuationTest.kt | 52 +- kotlinx-coroutines-core/jvm/test/TestBase.kt | 4 +- .../BroadcastChannelMultiReceiveStressTest.kt | 13 +- .../channels/ChannelAtomicCancelStressTest.kt | 156 ---- ...annelCancelUndeliveredElementStressTest.kt | 102 +++ .../channels/ChannelSendReceiveStressTest.kt | 2 +- .../ChannelUndeliveredElementStressTest.kt | 255 ++++++ .../test/channels/InvokeOnCloseStressTest.kt | 2 +- .../test/channels/SimpleSendReceiveJvmTest.kt | 2 +- .../jvm/test/examples/example-delay-01.kt | 24 + .../jvm/test/examples/example-delay-02.kt | 19 + .../examples/example-delay-duration-01.kt | 26 + .../examples/example-delay-duration-02.kt | 21 + .../jvm/test/examples/test/FlowDelayTest.kt | 39 + .../jvm/test/flow/SharingStressTest.kt | 193 +++++ .../test/flow/StateFlowCancellabilityTest.kt | 56 ++ .../jvm/test/guide/example-cancel-08.kt | 31 + .../jvm/test/guide/example-cancel-09.kt | 36 + .../jvm/test/guide/test/BasicsGuideTest.kt | 1 + .../test/guide/test/CancellationGuideTest.kt | 8 + .../jvm/test/guide/test/ChannelsGuideTest.kt | 1 + .../jvm/test/guide/test/ComposingGuideTest.kt | 1 + .../test/guide/test/DispatcherGuideTest.kt | 1 + .../test/guide/test/ExceptionsGuideTest.kt | 1 + .../jvm/test/guide/test/FlowGuideTest.kt | 1 + .../jvm/test/guide/test/SelectGuideTest.kt | 1 + .../test/guide/test/SharedStateGuideTest.kt | 1 + .../test/internal/ConcurrentWeakMapTest.kt | 12 +- .../jvm/test/{guide/test => knit}/TestUtil.kt | 4 +- .../jvm/test/sync/MutexStressTest.kt | 64 ++ .../jvm/test/sync/SemaphoreStressTest.kt | 8 +- kotlinx-coroutines-core/knit.properties | 10 + .../native/src/CoroutineContext.kt | 4 +- .../native/src/Dispatchers.kt | 8 - .../native/src/EventLoop.kt | 3 +- .../native/src/WorkerMain.native.kt | 8 + .../native/src/internal/LinkedList.kt | 2 + .../native/test/WorkerTest.kt | 4 +- .../nativeDarwin/src/WorkerMain.kt | 13 + .../nativeDarwin/test/Launcher.kt | 28 + .../nativeOther/src/WorkerMain.kt | 5 +- .../nativeOther/test/Launcher.kt | 23 + kotlinx-coroutines-debug/README.md | 96 ++- .../test/CoroutinesDumpTest.kt | 26 +- .../test/DebugLeaksStressTest.kt | 56 -- .../test/DebugProbesTest.kt | 27 +- kotlinx-coroutines-test/README.md | 2 +- .../api/kotlinx-coroutines-test.api | 2 +- .../src/TestCoroutineDispatcher.kt | 2 +- .../src/internal/MainTestDispatcher.kt | 4 +- .../test/TestRunBlockingOrderTest.kt | 4 +- .../api/kotlinx-coroutines-jdk9.api | 2 + reactive/kotlinx-coroutines-jdk9/build.gradle | 24 - .../kotlinx-coroutines-jdk9/build.gradle.kts | 22 + .../src/ReactiveFlow.kt | 14 +- .../kotlinx-coroutines-reactive/README.md | 4 +- .../api/kotlinx-coroutines-reactive.api | 7 +- .../build.gradle.kts | 49 +- .../kotlinx-coroutines-reactive/src/Await.kt | 48 +- .../src/Channel.kt | 2 +- .../src/ReactiveFlow.kt | 56 +- .../test/FlowAsPublisherTest.kt | 76 +- .../test/IntegrationTest.kt | 11 +- .../test/PublisherAsFlowTest.kt | 82 ++ .../api/kotlinx-coroutines-reactor.api | 4 +- .../kotlinx-coroutines-reactor/build.gradle | 23 - .../build.gradle.kts | 25 + .../src/ReactorFlow.kt | 19 +- .../src/Scheduler.kt | 2 +- .../test/FlowAsFluxTest.kt | 79 +- .../test/FluxSingleTest.kt | 66 ++ .../api/kotlinx-coroutines-rx2.api | 14 +- .../kotlinx-coroutines-rx2/src/RxChannel.kt | 2 +- .../kotlinx-coroutines-rx2/src/RxConvert.kt | 62 +- .../kotlinx-coroutines-rx2/src/RxScheduler.kt | 2 +- .../test/ConvertTest.kt | 7 +- .../test/FlowAsFlowableTest.kt | 89 ++ .../test/FlowAsObservableTest.kt | 69 ++ .../test/IntegrationTest.kt | 5 +- .../api/kotlinx-coroutines-rx3.api | 14 +- .../kotlinx-coroutines-rx3/src/RxChannel.kt | 2 +- .../kotlinx-coroutines-rx3/src/RxConvert.kt | 37 +- .../kotlinx-coroutines-rx3/src/RxScheduler.kt | 2 +- .../test/FlowAsFlowableTest.kt | 89 ++ .../test/FlowAsObservableTest.kt | 69 ++ site/build.gradle.kts | 6 +- ui/coroutines-guide-ui.md | 4 +- .../test/ordered/tests/TestComponent.kt | 2 +- .../animation-app/gradle.properties | 4 +- .../api/kotlinx-coroutines-android.api | 2 +- .../build.gradle.kts | 56 +- .../example-app/gradle.properties | 4 +- .../r8-from-1.6.0/coroutines.pro | 2 + .../src/HandlerDispatcher.kt | 2 +- .../api/kotlinx-coroutines-javafx.api | 2 +- ui/kotlinx-coroutines-javafx/build.gradle | 34 - ui/kotlinx-coroutines-javafx/build.gradle.kts | 50 ++ .../src/JavaFxDispatcher.kt | 2 +- .../api/kotlinx-coroutines-swing.api | 2 +- .../src/SwingDispatcher.kt | 2 +- 262 files changed, 9795 insertions(+), 1943 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/CacheRedirector.kt create mode 100644 buildSrc/src/main/kotlin/Dokka.kt create mode 100644 buildSrc/src/main/kotlin/Properties.kt create mode 100644 buildSrc/src/main/kotlin/RunR8.kt create mode 100644 buildSrc/src/main/kotlin/UnpackAar.kt create mode 100644 docs/images/coroutine-idea-debugging-1.png delete mode 100644 gradle/targets.gradle delete mode 100644 integration/kotlinx-coroutines-guava/build.gradle create mode 100644 integration/kotlinx-coroutines-guava/build.gradle.kts delete mode 100644 integration/kotlinx-coroutines-slf4j/build.gradle create mode 100644 integration/kotlinx-coroutines-slf4j/build.gradle.kts delete mode 100644 js/example-frontend-js/npm/package.json delete mode 100644 js/example-frontend-js/npm/webpack.config.js create mode 100644 js/example-frontend-js/webpack.config.d/custom-config.js create mode 100644 knit.properties rename kotlinx-coroutines-core/common/src/{CompletedExceptionally.kt => CompletionState.kt} (78%) create mode 100644 kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/SharedFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/SharingStarted.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Share.kt create mode 100644 kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt create mode 100644 kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt rename kotlinx-coroutines-core/common/test/flow/{ => sharing}/StateFlowTest.kt (52%) create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt create mode 100644 kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt rename kotlinx-coroutines-core/jvm/test/{guide/test => knit}/TestUtil.kt (98%) create mode 100644 kotlinx-coroutines-core/knit.properties create mode 100644 kotlinx-coroutines-core/native/src/WorkerMain.native.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt rename js/example-frontend-js/src/main/web/main.js => kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt (51%) create mode 100644 kotlinx-coroutines-core/nativeOther/test/Launcher.kt delete mode 100644 kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt delete mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle create mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle.kts delete mode 100644 reactive/kotlinx-coroutines-reactor/build.gradle create mode 100644 reactive/kotlinx-coroutines-reactor/build.gradle.kts create mode 100644 reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt delete mode 100644 ui/kotlinx-coroutines-javafx/build.gradle create mode 100644 ui/kotlinx-coroutines-javafx/build.gradle.kts diff --git a/CHANGES.md b/CHANGES.md index a1e3953351..513c28fb33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,45 @@ # Change log for kotlinx.coroutines +## Version 1.4.0-M1 + +### Breaking changes + +* The concept of atomic cancellation in channels is removed. All operations in channels + and corresponding `Flow` operators are cancellable in non-atomic way (#1813). +* If `CoroutineDispatcher` throws `RejectedExecutionException`, cancel current `Job` and schedule its execution to `Dispatchers.IO` (#2003). +* `CancellableContinuation.invokeOnCancellation` is invoked if the continuation was cancelled while its resume has been dispatched (#1915). +* `Flow.singleOrNull` operator is aligned with standard library and does not longer throw `IllegalStateException` on multiple values (#2289). + +### New experimental features + +* `SharedFlow` primitive for managing hot sources of events with support of various subscription mechanisms, replay logs and buffering (#2034). +* `Flow.shareIn` and `Flow.stateIn` operators to transform cold instances of flow to hot `SharedFlow` and `StateFlow` respectively (#2047). + +### Other + +* Support leak-free closeable resources transfer via `onUndeliveredElement` in channels (#1936). +* Changed ABI in reactive integrations for Java interoperability (#2182). +* Fixed ProGuard rules for `kotlinx-coroutines-core` (#2046, #2266). +* Lint settings were added to `Flow` to avoid accidental capturing of outer `CoroutineScope` for cancellation check (#2038). + +### External contributions + +* Allow nullable types in `Flow.firstOrNull` and `Flow.singleOrNull` by @ansman (#2229). +* Add `Publisher.awaitSingleOrDefault|Null|Else` extensions by @sdeleuze (#1993). +* `awaitCancellation` top-level function by @LouisCAD (#2213). +* Significant part of our Gradle build scripts were migrated to `.kts` by @turansky. + +Thank you for your contributions and participation in the Kotlin community! + +## Version 1.3.9 + +* Support of `CoroutineContext` in `Flow.asPublisher` and similar reactive builders (#2155). +* Kotlin updated to 1.4.0. +* Transition to new HMPP publication scheme for multiplatform usages: + * Artifacts `kotlinx-coroutines-core-common` and `kotlinx-coroutines-core-native` are removed. + * For multiplatform usages, it's enough to [depend directly](README.md#multiplatform) on `kotlinx-coroutines-core` in `commonMain` source-set. + * The same artifact coordinates can be used to depend on platform-specific artifact in platform-specific source-set. + ## Version 1.3.8 ### New experimental features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..93f1330943 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,113 @@ +# Contributing Guidelines + +There are two main ways to contribute to the project — submitting issues and submitting +fixes/changes/improvements via pull requests. + +## Submitting issues + +Both bug reports and feature requests are welcome. +Submit issues [here](https://github.com/Kotlin/kotlinx.coroutines/issues). + +* Search for existing issues to avoid reporting duplicates. +* When submitting a bug report: + * Test it against the most recently released version. It might have been already fixed. + * By default, we assume that your problem reproduces in Kotlin/JVM. Please, mention if the problem is + specific to Kotlin/JS or Kotlin/Native. + * Include the code that reproduces the problem. Provide the complete reproducer code, yet minimize it as much as possible. + * However, don't put off reporting any weird or rarely appearing issues just because you cannot consistently + reproduce them. + * If the bug is in behavior, then explain what behavior you've expected and what you've got. +* When submitting a feature request: + * Explain why you need the feature — what's your use-case, what's your domain. + * Explaining the problem you face is more important than suggesting a solution. + Report your problem even if you don't have any proposed solution. + * If there is an alternative way to do what you need, then show the code of the alternative. + +## Submitting PRs + +We love PRs. Submit PRs [here](https://github.com/Kotlin/kotlinx.coroutines/pulls). +However, please keep in mind that maintainers will have to support the resulting code of the project, +so do familiarize yourself with the following guidelines. + +* All development (both new features and bug fixes) is performed in the `develop` branch. + * The `master` branch always contains sources of the most recently released version. + * Base PRs against the `develop` branch. + * The `develop` branch is pushed to the `master` branch during release. + * Documentation in markdown files can be updated directly in the `master` branch, + unless the documentation is in the source code, and the patch changes line numbers. +* If you fix documentation: + * After fixing/changing code examples in the [`docs`](docs) folder or updating any references in the markdown files + run the [Knit tool](#running-the-knit-tool) and commit the resulting changes as well. + It will not pass the tests otherwise. + * If you plan extensive rewrites/additions to the docs, then please [contact the maintainers](#contacting-maintainers) + to coordinate the work in advance. +* If you make any code changes: + * Follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html). + Use 4 spaces for indentation. + * [Build the project](#building) to make sure it all works and passes the tests. +* If you fix a bug: + * Write the test the reproduces the bug. + * Fixes without tests are accepted only in exceptional circumstances if it can be shown that writing the + corresponding test is too hard or otherwise impractical. + * Follow the style of writing tests that is used in this project: + name test functions as `testXxx`. Don't use backticks in test names. +* If you introduce any new public APIs: + * All new APIs must come with documentation and tests. + * All new APIs are initially released with `@ExperimentalCoroutineApi` annotation and are graduated later. + * [Update the public API dumps](#updating-the-public-api-dump) and commit the resulting changes as well. + It will not pass the tests otherwise. + * If you plan large API additions, then please start by submitting an issue with the proposed API design + to gather community feedback. + * [Contact the maintainers](#contacting-maintainers) to coordinate any big piece of work in advance. +* Comment on the existing issue if you want to work on it. Ensure that the issue not only describes a problem, + but also describes a solution that had received a positive feedback. Propose a solution if there isn't any. +* Steps for contributing new integration modules are explained [here](integration/README.md#Contributing). + +## Building + +This library is built with Gradle. + +* Run `./gradlew build` to build. It also runs all the tests. +* Run `./gradlew :test` to test the module you are looking at to speed + things up during development. +* Run `./gradlew jvmTest` to perform only fast JVM tests of the core multiplatform module. + +You can import this project into IDEA, but you have to delegate build actions +to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner) + +### Environment requirements + +* JDK >= 11 referred to by the `JAVA_HOME` environment variable. +* JDK 1.6 referred to by the `JDK_16` environment variable. + It is OK to have `JDK_16` pointing to a non 1.6 JDK (e.g. `JAVA_HOME`) for external contributions. +* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. + It is OK to have `JDK_18` to a non 1.8 JDK (e.g. `JAVA_HOME`) for external contributions. + +For external contributions you can for example add this to your shell startup scripts (e.g. `~/.zshrc`): +```shell +# This assumes JAVA_HOME is set already to a JDK >= 11 version +export JDK_16="$JAVA_HOME" +export JDK_18="$JAVA_HOME" +``` + +### Running the Knit tool + +* Use [Knit](https://github.com/Kotlin/kotlinx-knit/blob/master/README.md) for updates to documentation: + * Run `./gradlew knit` to update example files, links, tables of content. + * Commit updated documents and examples together with other changes. + +### Updating the public API dump + +* Use [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator/blob/master/README.md) for updates to public API: + * Run `./gradlew apiDump` to update API index files. + * Commit updated API indexes together with other changes. + +## Releases + +* Full release procedure checklist is [here](RELEASE.md). + +## Contacting maintainers + +* If something cannot be done, not convenient, or does not work — submit an [issue](#submitting-issues). +* "How to do something" questions — [StackOverflow](https://stackoverflow.com). +* Discussions and general inquiries — use `#coroutines` channel in [KotlinLang Slack](https://kotl.in/slack). diff --git a/README.md b/README.md index 9f8bae65ba..1e72cc1b3e 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.8) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.8) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0-M1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0-M1) +[![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.3.71` release. +This is a companion version for Kotlin `1.4.0` release. ```kotlin suspend fun main() = coroutineScope { @@ -44,7 +46,7 @@ suspend fun main() = coroutineScope { * [DebugProbes] API to probe, keep track of, print and dump active coroutines; * [CoroutinesTimeout] test rule to automatically dump coroutines on test timeout. * [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries: - * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [publish], etc), + * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [kotlinx.coroutines.reactive.publish], etc), * Flow (JDK 9) (the same interface as for Reactive Streams), * RxJava 2.x ([rxFlowable], [rxSingle], etc), and * RxJava 3.x ([rxFlowable], [rxSingle], etc), and @@ -84,7 +86,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.3.8 + 1.4.0-M1 ``` @@ -92,7 +94,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.3.71 + 1.4.0 ``` @@ -102,7 +104,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1' } ``` @@ -110,7 +112,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.3.71' + ext.kotlin_version = '1.4.0' } ``` @@ -128,7 +130,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") } ``` @@ -136,7 +138,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.3.71" + kotlin("jvm") version "1.4.0" } ``` @@ -146,9 +148,14 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re Core modules of `kotlinx.coroutines` are also available for [Kotlin/JS](#js) and [Kotlin/Native](#native). -In common code that should get compiled for different platforms, add dependency to -[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.8/jar) -(follow the link to get the dependency declaration snippet). +In common code that should get compiled for different platforms, you can add dependency to `kotlinx-coroutines-core` right to the `commonMain` source set: +```groovy +commonMain { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") + } +} +``` ### Android @@ -156,7 +163,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1' ``` This gives you access to Android [Dispatchers.Main] @@ -169,10 +176,21 @@ threads are handled by Android runtime. R8 and ProGuard rules are bundled into the [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module. For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-android/README.md#optimization). +#### Avoiding including the debug infrastructure in the resulting APK + +The `kotlinx-coroutines-core` artifact contains a resource file that is not required for the coroutines to operate +normally and is only used by the debugger. To exclude it at no loss of functionality, add the following snippet to the +`android` block in your gradle file for the application subproject: +```groovy +packagingOptions { + exclude "DebugProbesKt.bin" +} +``` + ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.8/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0-M1/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -180,7 +198,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.8/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0-M1/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. @@ -194,35 +212,9 @@ enableFeaturePreview('GRADLE_METADATA') Since Kotlin/Native does not generally provide binary compatibility between versions, you should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`. -## Building - -This library is built with Gradle. To build it, use `./gradlew build`. -You can import this project into IDEA, but you have to delegate build actions -to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner) - -### Requirements - -* JDK >= 11 referred to by the `JAVA_HOME` environment variable. -* JDK 1.6 referred to by the `JDK_16` environment variable. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. -* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is okay to have `JDK_18` pointing to `JAVA_HOME` for external contributions. - -## Contributions and releases - -All development (both new features and bug fixes) is performed in `develop` branch. -This way `master` sources always contain sources of the most recently released version. -Please send PRs with bug fixes to `develop` branch. -Fixes to documentation in markdown files are an exception to this rule. They are updated directly in `master`. - -The `develop` branch is pushed to `master` during release. +## Building and Contributing -* Full release procedure checklist is [here](RELEASE.md). -* Steps for contributing new integration modules are explained [here](integration/README.md#Contributing). -* Use [Knit](https://github.com/Kotlin/kotlinx-knit/blob/master/README.md) for updates to documentation: - * In project root directory run `./gradlew knit`. - * Commit updated documents and examples together with other changes. -* Use [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator/blob/master/README.md) for updates to public API: - * In project root directory run `./gradlew apiDump`. - * Commit updated API index together with other changes. +See [Contributing Guidelines](CONTRIBUTING.md). @@ -284,7 +276,7 @@ The `develop` branch is pushed to `master` during release. [Publisher.collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/collect.html [Publisher.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-single.html -[publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html +[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html [rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html diff --git a/RELEASE.md b/RELEASE.md index efb361f1e5..22cb61c42f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -64,18 +64,14 @@ To release new `` of `kotlinx-coroutines`: 5. Announce new release in [Slack](https://kotlinlang.slack.com) -6. Create a ticket to update coroutines version on [try.kotlinlang.org](try.kotlinlang.org). - * Use [KT-30870](https://youtrack.jetbrains.com/issue/KT-30870) as a template - * This step should be skipped for eap versions that are not merged to `master` - -7. Switch into `develop` branch:
+6. Switch into `develop` branch:
`git checkout develop` -8. Fetch the latest `master`:
+7. Fetch the latest `master`:
`git fetch` -9. Merge release from `master`:
+8. Merge release from `master`:
`git merge origin/master` -0. Push updates to `develop`:
+9. Push updates to `develop`:
`git push` diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 7df4510bf4..5da40f261e 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -2,6 +2,8 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("UnstableApiUsage") + import me.champeau.gradle.* import org.jetbrains.kotlin.gradle.tasks.* @@ -33,7 +35,7 @@ tasks.named("compileJmhKotlin") { * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. */ -val removeRedundantFiles = tasks.register("removeRedundantFiles") { +val removeRedundantFiles by tasks.registering(Delete::class) { delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class") delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class") @@ -71,10 +73,10 @@ extensions.configure("jmh") { } tasks.named("jmhJar") { - baseName = "benchmarks" - classifier = null - version = null - destinationDir = file("$rootDir") + archiveBaseName by "benchmarks" + archiveClassifier by null + archiveVersion by null + destinationDirectory.file("$rootDir") } dependencies { diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt index c217fcae91..d961dab8d9 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt @@ -70,12 +70,12 @@ class NonCancellableChannel : SimpleChannel() { } class CancellableChannel : SimpleChannel() { - override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutine { + override suspend fun suspendReceive(): Int = suspendCancellableCoroutine { consumer = it.intercepted() COROUTINE_SUSPENDED } - override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutine { + override suspend fun suspendSend(element: Int) = suspendCancellableCoroutine { enqueuedValue = element producer = it.intercepted() COROUTINE_SUSPENDED @@ -84,13 +84,13 @@ class CancellableChannel : SimpleChannel() { class CancellableReusableChannel : SimpleChannel() { @Suppress("INVISIBLE_MEMBER") - override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutineReusable { + override suspend fun suspendReceive(): Int = suspendCancellableCoroutineReusable { consumer = it.intercepted() COROUTINE_SUSPENDED } @Suppress("INVISIBLE_MEMBER") - override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutineReusable { + override suspend fun suspendSend(element: Int) = suspendCancellableCoroutineReusable { enqueuedValue = element producer = it.intercepted() COROUTINE_SUSPENDED diff --git a/build.gradle b/build.gradle index a758393b6c..79c7f3553e 100644 --- a/build.gradle +++ b/build.gradle @@ -46,13 +46,20 @@ buildscript { if (using_snapshot_version) { repositories { mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } } repositories { jcenter() - maven { url "https://kotlin.bintray.com/kotlinx" } + maven { + url "https://kotlin.bintray.com/kotlinx" + credentials { + username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" + password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" + } + } + // Future replacement for kotlin-dev, with cache redirector + maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { @@ -76,12 +83,14 @@ buildscript { // JMH plugins classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" } + + CacheRedirector.configureBuildScript(buildscript, rootProject) } import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -// Hierarchical project structures are not fully supported in 1.3.7x MPP, enable conditionally for 1.4.x -if (VersionNumber.parse(kotlin_version) > VersionNumber.parse("1.3.79")) { +// todo:KLUDGE: Hierarchical project structures are not fully supported in IDEA, enable only for a regular built +if (!Idea.active) { ext.set("kotlin.mpp.enableGranularSourceSetsMetadata", "true") } @@ -139,7 +148,6 @@ apiValidation { // Configure repositories allprojects { - String projectName = it.name repositories { /* * google should be first in the repository list because some of the play services @@ -147,6 +155,8 @@ allprojects { */ google() jcenter() + // Future replacement for kotlin-dev, with cache redirector + maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { @@ -155,7 +165,14 @@ allprojects { } } maven { url "https://kotlin.bintray.com/kotlin-eap" } - maven { url "https://kotlin.bintray.com/kotlinx" } + maven { + url "https://kotlin.bintray.com/kotlinx" + credentials { + username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" + password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" + } + } + mavenLocal() } } @@ -237,7 +254,14 @@ configure(subprojects.findAll { it.name != coreModule && it.name != rootModule } } // Redefine source sets because we are not using 'kotlin/main/fqn' folder convention -configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "benchmarks" && it.name != coreModule }) { +configure(subprojects.findAll { + !sourceless.contains(it.name) && + it.name != "benchmarks" && + it.name != coreModule && + it.name != "example-frontend-js" +}) { + // Pure JS and pure MPP doesn't have this notion and are configured separately + // TODO detect it via platformOf and migrate benchmarks to the same scheme sourceSets { main.kotlin.srcDirs = ['src'] test.kotlin.srcDirs = ['test'] @@ -273,7 +297,14 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) { // Report Kotlin compiler version when building project println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION") +// --------------- Cache redirector --------------- + +allprojects { + CacheRedirector.configure(project) +} + // --------------- Configure sub-projects that are published --------------- + def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm", true) task deploy(dependsOn: publishTasks) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 91b8bda92b..96b17a3d99 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,11 +1,39 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import java.util.* + plugins { `kotlin-dsl` } +val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + repositories { - gradlePluginPortal() + if (cacheRedirectorEnabled) { + maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") + maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-eap") + maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-dev") + } else { + maven("https://plugins.gradle.org/m2") + maven("https://dl.bintray.com/kotlin/kotlin-eap") + maven("https://dl.bintray.com/kotlin/kotlin-dev") + } } kotlinDslPluginOptions { experimentalWarning.set(false) } + +val props = Properties().apply { + file("../gradle.properties").inputStream().use { load(it) } +} + +fun version(target: String): String = + props.getProperty("${target}_version") + +dependencies { + implementation(kotlin("gradle-plugin", version("kotlin"))) + implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000000..e5267ea3e2 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +pluginManagement { + repositories { + val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + + if (cacheRedirectorEnabled) { + println("Redirecting repositories for buildSrc buildscript") + + maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") + } else { + maven("https://plugins.gradle.org/m2") + } + } +} diff --git a/buildSrc/src/main/kotlin/CacheRedirector.kt b/buildSrc/src/main/kotlin/CacheRedirector.kt new file mode 100644 index 0000000000..7cf01d8e76 --- /dev/null +++ b/buildSrc/src/main/kotlin/CacheRedirector.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.gradle.api.* +import org.gradle.api.artifacts.dsl.* +import org.gradle.api.artifacts.repositories.* +import org.gradle.api.initialization.dsl.* +import java.net.* + +/** + * Enabled via environment variable, so that it can be reliably accessed from any piece of the build script, + * including buildSrc within TeamCity CI. + */ +private val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + +/** + * The list of repositories supported by cache redirector should be synced with the list at https://cache-redirector.jetbrains.com/redirects_generated.html + * To add a repository to the list create an issue in ADM project (example issue https://youtrack.jetbrains.com/issue/IJI-149) + */ +private val mirroredUrls = listOf( + "https://cdn.azul.com/zulu/bin", + "https://clojars.org/repo", + "https://dl.bintray.com/d10xa/maven", + "https://dl.bintray.com/groovy/maven", + "https://dl.bintray.com/jetbrains/maven-patched", + "https://dl.bintray.com/jetbrains/scala-plugin-deps", + "https://dl.bintray.com/kodein-framework/Kodein-DI", + "https://dl.bintray.com/konsoletyper/teavm", + "https://dl.bintray.com/kotlin/kotlin-dev", + "https://dl.bintray.com/kotlin/kotlin-eap", + "https://dl.bintray.com/kotlin/kotlinx.html", + "https://dl.bintray.com/kotlin/kotlinx", + "https://dl.bintray.com/kotlin/ktor", + "https://dl.bintray.com/scalacenter/releases", + "https://dl.bintray.com/scalamacros/maven", + "https://dl.bintray.com/kotlin/exposed", + "https://dl.bintray.com/cy6ergn0m/maven", + "https://dl.bintray.com/kotlin/kotlin-js-wrappers", + "https://dl.google.com/android/repository", + "https://dl.google.com/dl/android/maven2", + "https://dl.google.com/dl/android/studio/ide-zips", + "https://dl.google.com/go", + "https://download.jetbrains.com", + "https://jcenter.bintray.com", + "https://jetbrains.bintray.com/dekaf", + "https://jetbrains.bintray.com/intellij-jbr", + "https://jetbrains.bintray.com/intellij-jdk", + "https://jetbrains.bintray.com/intellij-plugin-service", + "https://jetbrains.bintray.com/intellij-terraform", + "https://jetbrains.bintray.com/intellij-third-party-dependencies", + "https://jetbrains.bintray.com/jediterm", + "https://jetbrains.bintray.com/kotlin-native-dependencies", + "https://jetbrains.bintray.com/markdown", + "https://jetbrains.bintray.com/teamcity-rest-client", + "https://jetbrains.bintray.com/test-discovery", + "https://jetbrains.bintray.com/wormhole", + "https://jitpack.io", + "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev", + "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap", + "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/eap", + "https://kotlin.bintray.com/dukat", + "https://kotlin.bintray.com/kotlin-dependencies", + "https://oss.sonatype.org/content/repositories/releases", + "https://oss.sonatype.org/content/repositories/snapshots", + "https://oss.sonatype.org/content/repositories/staging", + "https://packages.confluent.io/maven/", + "https://plugins.gradle.org/m2", + "https://plugins.jetbrains.com/maven", + "https://repo1.maven.org/maven2", + "https://repo.grails.org/grails/core", + "https://repo.jenkins-ci.org/releases", + "https://repo.maven.apache.org/maven2", + "https://repo.spring.io/milestone", + "https://repo.typesafe.com/typesafe/ivy-releases", + "https://services.gradle.org", + "https://www.exasol.com/artifactory/exasol-releases", + "https://www.myget.org/F/intellij-go-snapshots/maven", + "https://www.myget.org/F/rd-model-snapshots/maven", + "https://www.myget.org/F/rd-snapshots/maven", + "https://www.python.org/ftp", + "https://www.jetbrains.com/intellij-repository/nightly", + "https://www.jetbrains.com/intellij-repository/releases", + "https://www.jetbrains.com/intellij-repository/snapshots" +) + +private val aliases = mapOf( + "https://repo.maven.apache.org/maven2" to "https://repo1.maven.org/maven2", // Maven Central + "https://kotlin.bintray.com/kotlin-dev" to "https://dl.bintray.com/kotlin/kotlin-dev", + "https://kotlin.bintray.com/kotlin-eap" to "https://dl.bintray.com/kotlin/kotlin-eap", + "https://kotlin.bintray.com/kotlinx" to "https://dl.bintray.com/kotlin/kotlinx" +) + +private fun URI.toCacheRedirectorUri() = URI("https://cache-redirector.jetbrains.com/$host/$path") + +private fun URI.maybeRedirect(): URI? { + val url = toString().trimEnd('/') + val dealiasedUrl = aliases.getOrDefault(url, url) + + return if (mirroredUrls.any { dealiasedUrl.startsWith(it) }) { + URI(dealiasedUrl).toCacheRedirectorUri() + } else { + null + } +} + +private fun URI.isCachedOrLocal() = scheme == "file" || + host == "cache-redirector.jetbrains.com" || + host == "teamcity.jetbrains.com" || + host == "buildserver.labs.intellij.net" + +private fun Project.checkRedirectUrl(url: URI, containerName: String): URI { + val redirected = url.maybeRedirect() + if (redirected == null && !url.isCachedOrLocal()) { + val msg = "Repository $url in $containerName should be cached with cache-redirector" + val details = "Using non cached repository may lead to download failures in CI builds." + + " Check buildSrc/src/main/kotlin/CacheRedirector.kt for details." + logger.warn("WARNING - $msg\n$details") + } + return if (cacheRedirectorEnabled) redirected ?: url else url +} + +private fun Project.checkRedirect(repositories: RepositoryHandler, containerName: String) { + if (cacheRedirectorEnabled) { + logger.info("Redirecting repositories for $containerName") + } + for (repository in repositories) { + when (repository) { + is MavenArtifactRepository -> repository.url = checkRedirectUrl(repository.url, containerName) + is IvyArtifactRepository -> repository.url = checkRedirectUrl(repository.url, containerName) + } + } +} + +// Used from Groovy scripts +object CacheRedirector { + /** + * Substitutes repositories in buildScript { } block. + */ + @JvmStatic + fun ScriptHandler.configureBuildScript(rootProject: Project) { + rootProject.checkRedirect(repositories, "${rootProject.displayName} buildscript") + } + + /** + * Substitutes repositories in a project. + */ + @JvmStatic + fun Project.configure() { + checkRedirect(repositories, displayName) + } +} diff --git a/buildSrc/src/main/kotlin/Dokka.kt b/buildSrc/src/main/kotlin/Dokka.kt new file mode 100644 index 0000000000..dd5f1ea48d --- /dev/null +++ b/buildSrc/src/main/kotlin/Dokka.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.delegateClosureOf +import org.gradle.kotlin.dsl.withType +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink.Builder +import org.jetbrains.dokka.gradle.DokkaTask +import java.io.File +import java.net.URL + +/** + * Package-list by external URL for documentation generation. + */ +fun Project.externalDocumentationLink( + url: String, + packageList: File = projectDir.resolve("package.list") +) { + tasks.withType().configureEach { + externalDocumentationLink(delegateClosureOf { + this.url = URL(url) + packageListUrl = packageList.toPath().toUri().toURL() + }) + } +} diff --git a/buildSrc/src/main/kotlin/Idea.kt b/buildSrc/src/main/kotlin/Idea.kt index 802b387b0d..615b8aad74 100644 --- a/buildSrc/src/main/kotlin/Idea.kt +++ b/buildSrc/src/main/kotlin/Idea.kt @@ -1,4 +1,5 @@ object Idea { + @JvmStatic // for Gradle val active: Boolean get() = System.getProperty("idea.active") == "true" } diff --git a/buildSrc/src/main/kotlin/MavenCentral.kt b/buildSrc/src/main/kotlin/MavenCentral.kt index 0d7e18cf15..3efaf33f1c 100644 --- a/buildSrc/src/main/kotlin/MavenCentral.kt +++ b/buildSrc/src/main/kotlin/MavenCentral.kt @@ -5,10 +5,9 @@ @file:Suppress("UnstableApiUsage") import org.gradle.api.Project -import org.gradle.api.provider.Property import org.gradle.api.publish.maven.MavenPom -// --------------- pom configuration --------------- +// Pom configuration fun MavenPom.configureMavenCentralMetadata(project: Project) { name by project.name @@ -36,7 +35,3 @@ fun MavenPom.configureMavenCentralMetadata(project: Project) { url by "https://github.com/Kotlin/kotlinx.coroutines" } } - -private infix fun Property.by(value: T) { - set(value) -} diff --git a/buildSrc/src/main/kotlin/Platform.kt b/buildSrc/src/main/kotlin/Platform.kt index 4cacd9e026..b667a138a8 100644 --- a/buildSrc/src/main/kotlin/Platform.kt +++ b/buildSrc/src/main/kotlin/Platform.kt @@ -1,5 +1,6 @@ import org.gradle.api.Project +// Use from Groovy for now fun platformOf(project: Project): String = when (project.name.substringAfterLast("-")) { "js" -> "js" diff --git a/buildSrc/src/main/kotlin/Properties.kt b/buildSrc/src/main/kotlin/Properties.kt new file mode 100644 index 0000000000..a0968ee699 --- /dev/null +++ b/buildSrc/src/main/kotlin/Properties.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UnstableApiUsage") + +import org.gradle.api.provider.* + +infix fun Property.by(value: T) { + set(value) +} diff --git a/buildSrc/src/main/kotlin/RunR8.kt b/buildSrc/src/main/kotlin/RunR8.kt new file mode 100644 index 0000000000..d9eba79bd4 --- /dev/null +++ b/buildSrc/src/main/kotlin/RunR8.kt @@ -0,0 +1,52 @@ +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.bundling.Zip +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.named +import java.io.File + +/* + * Task used by our ui/android tests to test minification results + * and keep track of size of the binary. + * TODO move back to kotlinx-coroutines-android when it's migrated to the kts + */ +open class RunR8 : JavaExec() { + + @OutputDirectory + lateinit var outputDex: File + + @InputFile + lateinit var inputConfig: File + + @InputFile + val inputConfigCommon: File = File("testdata/r8-test-common.pro") + + @InputFiles + val jarFile: File = project.tasks.named("jar").get().archivePath + + init { + classpath = project.configurations["r8"] + main = "com.android.tools.r8.R8" + } + + override fun exec() { + // Resolve classpath only during execution + val arguments = mutableListOf( + "--release", + "--no-desugaring", + "--output", outputDex.absolutePath, + "--pg-conf", inputConfig.absolutePath + ) + arguments.addAll(project.configurations["runtimeClasspath"].files.map { it.absolutePath }) + arguments.add(jarFile.absolutePath) + + args = arguments + + project.delete(outputDex) + outputDex.mkdirs() + + super.exec() + } +} diff --git a/buildSrc/src/main/kotlin/UnpackAar.kt b/buildSrc/src/main/kotlin/UnpackAar.kt new file mode 100644 index 0000000000..c7d0b53d04 --- /dev/null +++ b/buildSrc/src/main/kotlin/UnpackAar.kt @@ -0,0 +1,32 @@ +import org.gradle.api.artifacts.transform.InputArtifact +import org.gradle.api.artifacts.transform.TransformAction +import org.gradle.api.artifacts.transform.TransformOutputs +import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.provider.Provider +import java.io.File +import java.nio.file.Files +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +// TODO move back to kotlinx-coroutines-play-services when it's migrated to the kts +@Suppress("UnstableApiUsage") +abstract class UnpackAar : TransformAction { + @get:InputArtifact + abstract val inputArtifact: Provider + + override fun transform(outputs: TransformOutputs) { + ZipFile(inputArtifact.get().asFile).use { zip -> + zip.entries().asSequence() + .filter { !it.isDirectory } + .filter { it.name.endsWith(".jar") } + .forEach { zip.unzip(it, outputs.file(it.name)) } + } + } +} + +private fun ZipFile.unzip(entry: ZipEntry, output: File) { + getInputStream(entry).use { + Files.copy(it, output.toPath()) + } +} diff --git a/coroutines-guide.md b/coroutines-guide.md index 4b3c09c40f..09cfb93cab 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -20,6 +20,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m *
[Closing resources with `finally`](docs/cancellation-and-timeouts.md#closing-resources-with-finally) * [Run non-cancellable block](docs/cancellation-and-timeouts.md#run-non-cancellable-block) * [Timeout](docs/cancellation-and-timeouts.md#timeout) + * [Asynchronous timeout and resources](docs/cancellation-and-timeouts.md#asynchronous-timeout-and-resources) * [Composing Suspending Functions](docs/composing-suspending-functions.md#composing-suspending-functions) * [Sequential by default](docs/composing-suspending-functions.md#sequential-by-default) @@ -32,6 +33,8 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Dispatchers and threads](docs/coroutine-context-and-dispatchers.md#dispatchers-and-threads) * [Unconfined vs confined dispatcher](docs/coroutine-context-and-dispatchers.md#unconfined-vs-confined-dispatcher) * [Debugging coroutines and threads](docs/coroutine-context-and-dispatchers.md#debugging-coroutines-and-threads) + * [Debugging with IDEA](docs/coroutine-context-and-dispatchers.md#debugging-with-idea) + * [Debugging using logging](docs/coroutine-context-and-dispatchers.md#debugging-using-logging) * [Jumping between threads](docs/coroutine-context-and-dispatchers.md#jumping-between-threads) * [Job in the context](docs/coroutine-context-and-dispatchers.md#job-in-the-context) * [Children of a coroutine](docs/coroutine-context-and-dispatchers.md#children-of-a-coroutine) diff --git a/docs/cancellation-and-timeouts.md b/docs/cancellation-and-timeouts.md index d8d5b7bad4..b296bde493 100644 --- a/docs/cancellation-and-timeouts.md +++ b/docs/cancellation-and-timeouts.md @@ -11,6 +11,7 @@ * [Closing resources with `finally`](#closing-resources-with-finally) * [Run non-cancellable block](#run-non-cancellable-block) * [Timeout](#timeout) + * [Asynchronous timeout and resources](#asynchronous-timeout-and-resources) @@ -355,6 +356,114 @@ Result is null +### Asynchronous timeout and resources + + + +The timeout event in [withTimeout] is asynchronous with respect to the code running in its block and may happen at any time, +even right before the return from inside of the timeout block. Keep this in mind if you open or acquire some +resource inside the block that needs closing or release outside of the block. + +For example, here we imitate a closeable resource with the `Resource` class, that simply keeps track of how many times +it was created by incrementing the `acquired` counter and decrementing this counter from its `close` function. +Let us run a lot of coroutines with the small timeout try acquire this resource from inside +of the `withTimeout` block after a bit of delay and release it from outside. + +
+ +```kotlin +import kotlinx.coroutines.* + +//sampleStart +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + val resource = withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + Resource() // Acquire a resource and return it from withTimeout block + } + resource.close() // Release the resource + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +} +//sampleEnd +``` + +
+ +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). + + + +If you run the above code you'll see that it does not always print zero, though it may depend on the timings +of your machine you may need to tweak timeouts in this example to actually see non-zero values. + +> Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe, +> since it always happens from the same main thread. More on that will be explained in the next chapter +> on coroutine context. + +To workaround this problem you can store a reference to the resource in the variable as opposed to returning it +from the `withTimeout` block. + +
+ +```kotlin +import kotlinx.coroutines.* + +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { +//sampleStart + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + var resource: Resource? = null // Not acquired yet + try { + withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + resource = Resource() // Store a resource to the variable if acquired + } + // We can do something else with the resource here + } finally { + resource?.close() // Release the resource if it was acquired + } + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +//sampleEnd +} +``` + +
+ +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). + +This example always prints zero. Resources do not leak. + + + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index 4909206e17..36e049db89 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -8,6 +8,8 @@ * [Dispatchers and threads](#dispatchers-and-threads) * [Unconfined vs confined dispatcher](#unconfined-vs-confined-dispatcher) * [Debugging coroutines and threads](#debugging-coroutines-and-threads) + * [Debugging with IDEA](#debugging-with-idea) + * [Debugging using logging](#debugging-using-logging) * [Jumping between threads](#jumping-between-threads) * [Job in the context](#job-in-the-context) * [Children of a coroutine](#children-of-a-coroutine) @@ -155,8 +157,34 @@ The unconfined dispatcher should not be used in general code. Coroutines can suspend on one thread and resume on another thread. Even with a single-threaded dispatcher it might be hard to -figure out what the coroutine was doing, where, and when. The common approach to debugging applications with -threads is to print the thread name in the log file on each log statement. This feature is universally supported +figure out what the coroutine was doing, where, and when if you don't have special tooling. + +#### Debugging with IDEA + +The Coroutine Debugger of the Kotlin plugin simplifies debugging coroutines in IntelliJ IDEA. + +> Debugging works for versions 1.3.8 or later of `kotlinx-coroutines-core`. + +The **Debug** tool window contains the **Coroutines** tab. In this tab, you can find information about both currently running and suspended coroutines. +The coroutines are grouped by the dispatcher they are running on. + +![Debugging coroutines](images/coroutine-idea-debugging-1.png) + +With the coroutine debugger, you can: +* Check the state of each coroutine. +* See the values of local and captured variables for both running and suspended coroutines. +* See a full coroutine creation stack, as well as a call stack inside the coroutine. The stack includes all frames with +variable values, even those that would be lost during standard debugging. +* Get a full report that contains the state of each coroutine and its stack. To obtain it, right-click inside the **Coroutines** tab, and then click **Get Coroutines Dump**. + +To start coroutine debugging, you just need to set breakpoints and run the application in debug mode. + +Learn more about coroutines debugging in the [tutorial](https://kotlinlang.org/docs/tutorials/coroutines/debug-coroutines-with-idea.html). + +#### Debugging using logging + +Another approach to debugging applications with +threads without Coroutine Debugger is to print the thread name in the log file on each log statement. This feature is universally supported by logging frameworks. When using coroutines, the thread name alone does not give much of a context, so `kotlinx.coroutines` includes debugging facilities to make it easier. @@ -652,7 +680,7 @@ stored in a thread-local variable. However, in this case you are fully responsib potentially concurrent modifications to the variable in this mutable box. For advanced usage, for example for integration with logging MDC, transactional contexts or any other libraries -which internally use thread-locals for passing data, see documentation of the [ThreadContextElement] interface +which internally use thread-locals for passing data, see the documentation of the [ThreadContextElement] interface that should be implemented. diff --git a/docs/coroutines-guide.md b/docs/coroutines-guide.md index e3f18d208e..2d15a7bbff 100644 --- a/docs/coroutines-guide.md +++ b/docs/coroutines-guide.md @@ -10,7 +10,7 @@ coroutine-enabled primitives that this guide covers, including `launch`, `async` This is a guide on core features of `kotlinx.coroutines` with a series of examples, divided up into different topics. -In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on `kotlinx-coroutines-core` module as explained +In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on the `kotlinx-coroutines-core` module as explained [in the project README](../README.md#using-in-your-projects). ## Table of contents diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 5618cafbdc..d0b6b517a8 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -19,7 +19,7 @@ ## Exception Handling This section covers exception handling and cancellation on exceptions. -We already know that cancelled coroutine throws [CancellationException] in suspension points and that it +We already know that a cancelled coroutine throws [CancellationException] in suspension points and that it is ignored by the coroutines' machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same coroutine throw an exception. diff --git a/docs/flow.md b/docs/flow.md index 143f9e9300..2b1dfd59a9 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -50,7 +50,7 @@ ## Asynchronous Flow -Suspending functions asynchronously returns a single value, but how can we return +A suspending function asynchronously returns a single value, but how can we return multiple asynchronously computed values? This is where Kotlin Flows come in. ### Representing multiple values @@ -153,7 +153,7 @@ This code prints the numbers after waiting for a second. #### Flows Using the `List` result type, means we can only return all the values at once. To represent -the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would the `Sequence` type for synchronously computed values: +the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would use the `Sequence` type for synchronously computed values:
@@ -1463,13 +1463,15 @@ fun main() = runBlocking { A "Caught ..." message is not printed despite there being a `catch` operator: - +``` + + #### Catching declaratively @@ -1510,12 +1512,14 @@ fun main() = runBlocking { Now we can see that a "Caught ..." message is printed and so we can catch all the exceptions without explicitly using a `try/catch` block: - +``` + + ### Flow completion diff --git a/docs/images/coroutine-idea-debugging-1.png b/docs/images/coroutine-idea-debugging-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0afe9925157e44d5c03e5811e7703bb6d6642ce7 GIT binary patch literal 248706 zcmeEucT|(x_AMZ>pn!#P01+$*gr-ylX(A%MB%xO&ARVdFn}UJ@N|z22AoLbWC;}oF zszB&PPz3V zYU&#NnnFZXBqzfUg~;1675e%OH4FtIlDly;;ci#*uTsSDUZs5>{^&&ewRT6*#10>? zBzG69m6FcF?ZKIZnoc*#x^WBTPWQeNLk}C*rJA}00zB@Xn?sWNc_|W7^5g!K3q5(7 z)t7|qxc}&ppU#?8_;0=eLvdcMDdWHRiqqD#S#(_Aijl1Y2=43u#T#igjQetR)MsP^ z(HvBC#~p$5QYrjr&#NKgjJ!ISk3Vo}U?74gjY*IH4rmDpDI#hYKQnUS*yG2l;OqPU zZ|48~%t)2tJbxv4Op*=zXo^fEKKvNQgv_QVBpwO?Pm&UQH#pG zOrTZ#K3A=E1_h0#i8}6b?3M;I?)01%`jln4!=tk!$t76F@G<)%GxX=`thdM9sNtfd zf%}SyY&Kg#)$FIzmS`Jx_#88clJqA*v$7%XghTJK zG^$?+$kr?{v{~pc2)hfTJMJ#&)?r1;9Ja^aZgg;rde2eSn+$y7>lLwx0S|_4{x$KRXFMBu-xtZN%YC~JsIhRM(J6TymDR*Z|;4cG@ z6p^>0>ZqJYh3v+vD-6m9OU#D@DVcJ0%PcX?S3GDu>inT9vA61Zt5@&0;&8Yn|6&P<~9A{>%zqE!9O-lyr#i)dY1h}xWUK7nz%$x|5xJ@`>v_kka zDwr~f>rNIlsvbU%uA`#;wEdMmS-Zx4Gx%P>RBMdhc&(Ssk1ua|S63Da>UbTJ)1Yry zB*!36v&UkLyyJ>}p+4RsBlZCvd`(+2D*>b@P7ZWR9xeJeUh&?U$nu3E+cgRiZyANS z#l*y1T%(USS$+z9YEYR&POh*+wB~5venH)#Wtik)#pYa3^TW4{h`9+anolUCu=Ac| z-h3b;I!23YC+C7`8ihalm*q(xX%xDeH9{j{;z-z)@v)VdfgzLzC%oCGXGfc}G|?D@ zHAX*1@S3qfo;oiMe5-OIfyS%hDVC^v~WH!-Cf@eL6w?#&Y9ed)&4D z$%#``Z*+~`zid*B(NVctA6aCRv-}>}`b4Yb&GEL|$Cpw#e)w+58S{?8YPS;QWQoz6 z${$QGv8$9)OYV2|QSO?(RspUemIOSIOCs*HCRZMMeX>Aq2D>|!hWyTn~bK=|S29k

gYl8~3?t?Ee@ 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

xFmWv3f&Y;2fKQix{;PFy}WRA_96 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

V8Ybh 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�(8J?ow>@wIV_Obq!g(`!r~3I1b$%x#5Tb4S*^+_=VBdj=rC$u-oJ7=Q&`5dhHw>X` z*Kt2xL;Fu(3fbYqE%z2Xb`&ePhuh+uWYpu7G znu+bY9=22c9<5bE9NB;>KzTzk~OTA<$G?_jw#zPzVi^JwD?@| zhS(R;%Q>m`p1)fB8s)o}+J&)d$bmwO#u}PISR6D^zPs~at$f$jB70YR;UsE<$j;S9 zTXEz>4j*OXK=FQUs4XJSOij!;3mD2($2~WlchVTcg3!o|{tQ!s5isO_n=FiJP(=(X z_8kL-Qo;~XgLKV}O(fG!UT5j0yu;FH(C#Y}hMi6LtZyhW3E?VpQkiLe#m}P!X(<|} z0GTt@5w7{PH{{nD7%~c4PIT6wUp$?=1qFFq*OU!6?mO1xKD%ercMkng?Lv_3!jPjl znI@AYU|xT@0+pAS4{1*4TAFg!`_AmH3ezf_tkeJsmRD-K1`#=kuO7Tmn#yImXH+-; zaL@YhTsJf{Ec;IVIv|sTEIsNE_fn|p$@Il(!=0~R-GW4)bG;E-sB~+XAanSE;S}4J zamRtEN4GjXurxH34F%hV zyqiuTZhbEb?Q`@e{6wX5wK;DPfA?fG$Nm|qvVd$mU>ajtJrljU;3^TnUvSf@zMtwj zL8z!fd!&8VJM`iMrjPkWC|71nbAfJETpEkU#$z|O@4H>z@50OQwt}wf@aRGdIC8NA zYrYmBcI%YX5!7NbF`QFpEq+<^@Ai;WE~y#~O2-{;6_r@+Lsd>F+j5&X_#_U6ZMfEr@H1IOG}wgx%+^6ZK`Hlx;vHvlOH?@SWkXtD~sHB zIz8MF$@xZ#Mk)6Spk>{$z+i#cC7l6AvFZqKAO*z-&@pUY(7uK_Kap0M^?&W7(W&DjvB!uqyhJJQ9IP6hw`Y{oy) zaFt#xugH=)htULfRyyD-T7Ff?z_eZIodo#`^>&W8BkU4OH6K!KzWm>(mVf@etGVYZ zw)-ihRh0a{O-6tGjlWG?cigXeWPf3954iWMEA+>A(&7t#FP=FB^Z(ng8Dm_1bWip7 z0sng6TS5R?IFxWHnknhhc~5ZUKh327eDj&;Xv9ln-ZyUDo!3m&*h7KrCXKQ0EdTn+ z`)RM^@NIuW*etwBAPPP46aM3y|MO(+4#(_IxIE|E3{j-`!@c_|@+~#B0l4YA(5;j{ z7j{w7{lm-r`?Eob%jsUpuQc-;{U+Tvfj_(@^6OaNhk()!vOdhRH=wFJkQjvS_=qLq zM7*Wu+I<R&HgCIuSs>v(^aZ3TD@^&C-2Jh?2{)wmO%Uwz(e z%CF_#+1blzb|QbcGNt~X8YB~Vp)l=)6wZHrsXt#aFhzVL#+XjdCZ_B|`@<#suOH*; z&&Y9NP-bS6?1?h{_mBMNmvsL?`RUvL8?VO>BbhK8uHtjtFX47Q3osdn7e3^7{Dd<% zS>*%*RX0E)VEg;s`Q&ssJkTvTtm7&^%5ZbC$_UId#ufJFN!--XRepCQbSoCnVXugd z%(JL_DXSTsm%6mK`0)e9NY67n?Ld>p07+;lCBGJ^QT?-esQjS+YYpO#!@3vjQmak5 zx!7U0fg3X4A~BYrQ^gY#MCQM7T zKLg=*Ub=d5y2S6~N?e-c(8xW|cWecPu5ljb*jNxG+h`idagrfTqW<;Uhep57#X7Hc zlUt8i-b67rraPhV6cu`}!YN!#_CnOy&;XzEzQo;SefZ%i^!!AcS2i}^{RT1)1gfZv zviR;)pV>s^Ev=8VPij=?kAk2BkOQwr?)ovDKR-8=gww)7uf<&A{OaIJySw%lsLL(h zNS-RGyn2SRoT%6c)Gr^Yu&d#AyYNGRt&_<}k=KTI^ljq)`YGq&v|2|wi87Q}nUnk% z^3uu71;~bzi>~aQ<2cQ@=FRF>GM*9+;MUY)hZyv8Rz5d-BXuxp>K|Cmji+#jWN;`* zQ)C((^t@b_){x%cvQ?hl;`!~ru9`y6slnillol75$ja9v^W_=L9eFPwKs+!dZmbYz z)*N(WalUmg!d~K09HCCM;@XL>N%NY?7hot8}cmk zBXzH+PP94RWS_URFegWz(x8@m3cE0~$M+Non)@=CVyDh6dmvq3pEMG`wpy|d&a9>n#B$FE-$9^x*@})eF(^-1{sC-jM;%;Cd;qf zbw@btm60l3x9T_3CYhDY0DBvH>9KNPo3~XR{btRkH#IrkVJ{tE z1o|o%Pr8jPtB*sDTV)fOH%1B4`jhQ;cH6g3AEkq`W?b!Or`VK~TYMpt+{Lcvj0{H_ z*NXzZgHufDOrs!a?bWgHI0yD)UXo%F>oxHnKUJ`WClm!XjH! zUgbzR9INjwe^>5{;%a(H_CoTTKyHRsHSg6p)NR%&mX#SOb=@3)!f+8GS71K&NLK2_ zkRyfdbC3<+m-v9NoCE0H?OI8HadoeuT?eZ!`rH0eMTh*>$7h+Q`dR+x8~s@c4x5M( zI_)T9qmd#zj%V>`Dm-p8=yfdVpR$`gK}gTBUN*=!c?T318|4Cez)ZnFoT=NipOpm5 zW^M3>Nm9<|Te3u$Vg{q07b~(!+i2>6>!o#1CC*S6r>17S45}6!Kr1jXiG5M*=7wNO z?=5B!PAzu7;iM)jmtTF7974$8b&`uEJkS|e-T^ARAaZCGd9vCxdr}NpqS^z+{@o@> zyZH2z*I1@L!j$6QsPZztoOPPF=;Odp6;_P*2G;VKSinA?4WvD=GgYwa2+rc?gdw4O3~`dYx-JJ12LFHnn`&FXw<_(uNuFZ4bXv+udOY>FlV(c-2hQ=@WX@@$!`;DjFMsIp~qka_1^u&l&G|IIt( z;|vnaPvw<4{O7oboL=ikQc_a*@80!s%3J8&;!?{Zt5PMPQAq5WEf7&t8gRtw%Hr!- zIaTu^s7S{$VbG3=H5P;&L|E`4QU|;cIeuG_`uGc|&>ZSE>y=($cIR2g?z+SqtcIgp zdoqsHdZbZ@xY=rT_rK1O*U&;Sr<3Vp!~(UnYh3#rSwb(F; zOS|Nr#Rafq?)1cbsjy290#bmx{$xCtwSdIWUZ6f>({CbLQ>IlD7y06Li7Geig>-lB zh`5(BOGf6t$Zn1inULUPg)9>FWy-&oPORJkl9^Ug9@}1m(qqXtqp_`F-VYsaVvPbP z$G|#&6TYrVM^gmhNr%77;z)V*!B=n;n6nshPI9dn*-I?$!}rlb^<72qcs<{}*!yv# z^L*cy7LWRwdQlfXf5JXLk%_Zn`fWBqO)~Gc$!D5eJWlUjEEDCCc}tKhtCtDvew>mg z?P+|)!9MOoQ@}%G(Cdr~v95;X2mqwdsq8pPCw7Ttm2};YU(k9s{`eq&s&r{Ln_yr3 zz7EN*>&}RgCLh%(dYo?V{KjmESW^s*G;th=5sM2JoLu&YmZ((Nnph?}+h{Z~2g!dF zFBvE@+iV(35oSvnfb<0A&+kK zlk@3@Y$%!}>;}@Ftjr_+TV~@#Ur!(2(PWB}?C>CL;(enC9+{^!Nj@GAoq=}2g%hf} z$OCVIiAiw7m!M67PeWp!iYl=32$c0c&tz|uHarIIjeDm{tP;#LJzcz8xh3TH)1^sP zEJi9Abr(l6V zJcui7$UR+E_S)4j<5xuQYqY5UGBJr<&vwb+z4w7xwro#Cy`c&yqoiDVPG2Q1 z&NpsRT<(x0PH(!WQh`a&W2^G57r|Do-D&U1FDq%YegAEOp)56A!xu zUVGKDilwg7xDBFtVic4?4H`ZrIdYQJ&F|pbPulYMSrUuB?VFvVqN-{+&LeH5k9xO% zb$X_Z0mp0u@&VVhpU=8Z6@iIg?otMbG#p`;!P5~}THyOM-^Y#lF_LNOEjkmPG#u9! zk8XX^rQi8zMq{n;!_I8!BIvQOnh#L8Ro13Uc6{5`1#9KLC~Y%ePlpUc+c01Rm(IUw z`>DMmf#8W47pv%4Ep|NI@|ELpbU}2lk3bqCRRxu5__VokFhLw*sZN+oQuW*Um^X*Z z-@~MU1Z&;F>V|e=eth(slrT|vDIdK1!k-_YRQcN=3y_=5M8ZB+WznNIzf#XZ{ z*r0FpuHQ;{jT|{W(tJ#qY~69_>=wH6;p<4Zc`BKd*9Y1hE{-3b3P5F1V!SuR~JEX()0<<4qiU>G(gsvcRI_fpx7i};g@!T@%Ii(>~ zr06~P{&R_MJiQ^t@m9;6cD@c4@?$r?=q6{5s%7CG-Mh){85c!9lUEM6MAc1aE|zS_ z2pOs%hA+SDvpVY9FaI3H5?h#&nN0^70WrY>8kYRZ3%yJWzIBhf4pOU)&ymoS%8`|a zKV#)G)%gh>Y1a_IsVeLE0#!4ENfDQ+=C@FszI(ix${tQdm2}9a6ryUAyvEQ~IMgt{ zfZp1!MKJYz$XmpX39)NUdhl#bJ)|>9kYynuYu|1SZwr@SccS76AIB>I4SWih@a@DWOa@q|PlKQBzIkGuFep3xxWZg1d$g!z*bof*qFeB`h$-F17f-`^O%umK&;QKM zQYJpRqQv(>nMBx|PD!TDa<`ZUrCl?SC=9ofC&BCV3YA}G#Z>SBfUz&3{fD6QFxrTM zK+=(JKk;yK1Kh5tm5RzrkpOB787YDerfwA+e)iE*l9f_|N=0kfPYdvP!o-hGa!X2r z%dMwcbQ1WT=oYFafAVAMd${4XoB{W?#&o)*MXSMFr7yKC%hhNvg!^8j`m-=)CGct_ zgr=VbA?)l39K&yA7W|aHE>s>lKhO?BV@U8K5GBk*U~ts%y*pnwl8w zyOfMi2Vqmc*K6N!VFDTPD({UhvG0p%;O~0u+VLLD+y&)NvfmhG=P2hqxFA+Mt>u2s z$jFK}g8s^HWtHwRz;FBn?3TX@+d?68g7+Nk>V?b#SX_kv&1R1RQ6NlP^46P}r_6 z3N5i%L-!UcD#n6MT?lv)K(Y3MnryjEKt zuga;37y%P2)JgQV_(HFTn3r)VEk35ic#`L9yioCH;rZxQBnRv2J;87JcSU!$>%V(Q z&@D|MY3n9Z$G7qB4@y4@0u8zhq0 zdlpFO#8g1HmN9cKgb|BiANSF&Qznl zWcZR)|8(mU*PsSG|5%5xEn6`|Pq&1~qF5{#i{+bVHr5mMzs&{oU~dXl-GbZF;3y~X z5{?xlHwUX?6x2Cjt1Hg%q-YJ z?!L~jg0s4yTrW@44tbxkEQZO-6eo(cw=tsm|MDP6%;%03A@m|;7-%Dx1~Ti~zjm=< zLbD4s2PXS{@qO>Sb5IM#eG(?&i8p@K70wh5vyuhkjDe@|{v5rJ^{n3=&L8XVFS`TV zXURUt->eT($HF0~H*e@081TLX67_UX6g7k9dTh<5OYt2BqH?Qx2O2v1wyx({FHIP= zJ~pzK^RZ|wbd5bOS{Z$z9-fB#1rK{=Qr7L{W8yM<5rc1^?_7U)bj$NiWi--K~pS(&peb?cVRJa$5D>Urq#JM3H06pGM< z32pB#m>$~uKOivFx&ZCDVZW>fF{<Q_gg- z1{GTno)t0%^i&o+V{mI|=%)$aSWwrsXjEEh`})TQ>c6)^hZS7-}i#(ZWH%m`(ca#YpDZ(Jm{$R>w8IRm%l; z;IZJXj-0A3xVoq#CU9e(e!+D*HbHq5G}J&wx~m0Ng>39Tuo&^Bn!Ky`{<@m(SB8@0 zWbp^QQ3VXhp)y5+R#B+=$d(Q>C!FatMDkS3+Lu2T!UNJIC#J&#kCb&f;o3()nAq?qqhC z-6zhW^9r(mq@$U{G^;J2&l*9_*!+E+R1+F=98a0Qr?MALvN&OMQcH+whm4nUh=@%0 z)fnP7Jv-L%P{JoN?`lIJ_J1CK+ajI8xTHK7xPEPrC9@i&(IJzlIS0b~>%|vx9s90X z3X2W4_)`eNZeE>3qcdG#36|Zp^ZQd%XJ?FISEG+~*T?Ivc?U{oLmi9y9rx$S|8+9> z*T?5c?-lHKs>I?SYyhJVl9Ji2D9^q1{QHDXRylSOE{+mBG!MlO!X=>bS$77BWb6)5 z$BJGICK5q37`W?WkIRZ4O$692`#dTW5qz&8zjD>&cvrC;$B$DxGuk>H6}Fzi;l~!| zus+cpKbF!M(LSGKvcFnXccqEO<>Fn=@Pds5t+&lEpV`jPXp$K1q-o#QP~P~WKWq88 zu-}{Pj`<>^+2*8jUg(l$RWTE!U(O#?Bby>oWR;v;;!nDNCf#^$d$cO$yJD1P)! z7G#%JLNQLNKns}mw`JQMr{xLVADnZ zaJa399BH?%_I*w=-06i%ewoZ8P((($#PlW(I$r;V^OAZZAU{ zQ$CbC{l^5R%9|FV0T-z!>&@!!W7)}Y^z}ulQy3P=+C|;B4x-} zYoj#f0~?bJSylC3jgl74dwGpQ{NN?o=o%qChjmGp^T}J82(0W#R=ss7QUh&7dod0_ zM&;Mks0wCRHn&xR?A+KLGI;TeW;QT93N< z89BakdmS|*J0kNqfVISQr+ERhT+LNIZS8qt?9ViI+`tm={Yye{M-Wx7hRJh{NkcL_ z7)5(EOJ6&`3w=Nd=glbvanU55Fx#n2#h2=dloAZyS}Z97*t}t%^Xz96fBFxDKE~RZ zY*ei#$PNwM6wJugshQNS493bMun?fTsb2gJGRFIc4hHT?r`2gNi#eV3>4u!_Lvo#- zs+78)P}B=K*@8S}R_z%fCyl&TI)MR2sP-5fS}CDJo}mEdBH-h zKc&3t>(BT4!&%z25~h5*`||XhSe*ktz0_WzA0fs*jH}3d>nVI{owv7?S(>lISa-ca zzz3$bKS^IYrQPuoDlDO+r`W37Zx_Wlm))thw2_wS&gXd^s`s)ru)Y4Lz1YFz#^)kK z%)@B{D5<^f?_Dj*Ftz9PwsMy=7GGSa!mCL{^(Kie_RBMJxgCHQlk>w?VHe$C1`LV_ z8&&k(V3MumFigwHVrd9G8ZAECgXtd#Ze-;o|2f@}xDKhjtlqKXvJ% zI9|aR(T(gqTk4`O-n9IVkJ!oGOQ%$Gtf4rpk?rWF-JV>^ZC)QAjT*;T5D08k_uw69 z(4OCn&uN1G2-%#UbW$6C_GxTW%~|Z54YGT6=fN#uAj0w>O;(7F=w=MYcpD_Z-p!Ta z;(1VG1Yo+yk+GuV?>Th6h%{_$%zG&1)m(LTF5ZXPq6R|1%Au-N=O!lAzx|GSCPIcx z?LLdPG4iE;4WaH@zdX0rjCV5=e%ju0|IdPy-YS(gK3dc-x4MgSlJW8}yaG^@hpL_~ zaBKU}DmrIs`QOhghJ#He_4!22I%0g_cZ1+sTC)@w{lWG^7rBDV$|ISM8eM7XN}=J? z(1&&oOPMBvK2h351|=G0*`3qgk((Q!WpUEuvvY-*X&Tgu%Tt0gJ{j=S+@m!s;b#oU z!DD%~q2)Ha4pn1LMcQ7FKy@7vVU5r*G)6p2$ga_ZT5 z1tf+n_c8q|hr-~EEi;;~~d&ZeC#qN5U9Gtu)2=nl={lv(3mmJX!Y z{b+&Zgt$)FxHGXd7Hync2y>yr3pt1_#C!z3#pkqM%z6cDILBjeW4epr?PHE?u}1Py z;G+}#P)MNYDvuGux@UY;h4IC``b4MuYI;DF(WxXa>l8KU68B8M@HRJ{{s(dWFq+$( z_yZWSNlq-Wsg6x}6T-gRg z5a(-kX{r;n8?xw@Yi#UEC|q~B%9?aAKbQZ{NQ^DOJ1|2oey3SyW4sE|ml{HDsQ{(U zj}~-|!}c|@bpW%hp&h=^5%DOsSWy|#*Vu%67Sg@c6fbCOBFCH;V;-hw(rKg;?D4Xu zPB*YCPF+!`@*sE)vp$#~#aT6)G}F;Uq{ zfRV6w9$NnyLAlj~o{hn~Ie!^hM0PmQmbb7gD=(e!qK>~Bu5$7^J+xLew@m`KlVcw? zy^1_g#fN40*XZJCj8?sv(pO zJ_3{A?=c)EW5V%_dF)D)W+_53@S6fBt1>Cp27GtNfP-4+U60L7;y0{Ck~A}~dcE2j zPf0k<-gO9_Za$dS@*_~9Kh>D7)9DKG*mZt2^Lb1WhD=D9bLF&!Fs_0`pr+20AURt} zNeS#{9*0dghH^qN7`yRdo>E;-bsQbIf>E#U-X1mLL{F#*C?Z@aOurwQHf%8{8a#*0 zO7Rdq*SY(6Z2a`FHRy!wqu0UX92p3s)l1mB#cJeY`+}n8$aBlRt{B{qrTodQcG?UQ zXgGGD(t&**8-U>DN1@h74gu^H_zvzT<0WuwjkPnIOgTRSs^(~3y8xCvpH^aR&j!0F zJqa}$0X3E&#wb5`3IXMVaQdcz=nkc?7_$0d5-ETLSt%l0{A_4XHXg3#<<-nl^aJFk zvuRJ%uaB05sr5XX6V1*F{EE!>i?_r6%^tJz8Ae`VNu(*CH1TdWqfpjmJ)AZ^UML=` zA;Y1!#tCV!!62)|k9%KW$&WDO#1NbKjUM&VF{m`9p_p`5;mC=+>w^RW!(|*5k@oTQ z@<|(41~ry6?arhsd^sll8>QSG_R7uG-ql^49SzNCZpJ4$<%n~F#%gea+Cp5__VRJA zfKM^j5ILwitCfQURCukdS#&lg!R~t;S&+GOHENaolR1_1I+zDN!c0r+P#SfjJ=Db= zBFbmgnM4pUg|UFo1k0JUyFa^QMAYB_D3fR~c`AL*Mk=O(xfGq z8~oV}TbxN9ab^0`Wk0n;ofe=L%-B|~%jK?TZupY2V+}y(gzrG%;zI!S7kP&+#SiRW?I6PZM z*zv0QAf~GwT3rOgD)Q}V6g$$S)w;^ex$|CAPa3;c+m_MG&J8>ObV$U(1KarVP3N*H zum_xnG;$T`>P@s*^61HG6E}*Hc8nc5#;%MWGh3;E?rK?V7tpd7K26s~+*naQ5~oAd zMCwJctO-UQ)yAS3t&=2Vj@ajN)k-QGH7?h=Z$_^udI@iC|0G5lb$s6u^a%_b=JDwb z50)})=d_3;@jH$~PW-y{Fa>@cN5lMKb%>I!cJ6ay9}~FiLC#1Wu}#sJY8VDPPX_*P z1c!=I&nr2CU@;n-%+;DOG0u=;!efuX>bltnuCm=aR7=qNfhPxVU|r*@fG1(cw~&_E zlfT%V6wTi32#=s{u2wwf|51>8Q{HiX!c>b~*u(AizE}Fc0Wlxlu)-rEMppsf-InnH z`fvYs6{}eeoobQ8bJAVg5izGInqlR!6KnkA!&T#7$uAed{z{UXdIT)HCU2UR5mX-| z)@!#BjPw-OLG`i@m3MB$J>U|^sylW}>M}qwKUptozpeCPL9-6>TbuOw=uT)LfCywzN!OU2ixM;La< zs(1DGaz8O73}@qhlw@aGy(lxvPWOFHzBVXrD9pRcwA$EMR7pw%2!5LWIib(f>Fh`% z{CJf2C;i4=87<(tqxYyb0kt&9eD?f7oBG~qX`kJ${&S5E*xj@Nnk1F-iyfVVa9WG(oI3xkM#DuC(4cq`{Q(Y!Kh~o(2OHCZZn5TnYTNfO zFf_}guqLgh<-ET^UWl1wUd9`*dmvus@Y$^hKF~R6%qS)*!v3JpRbWAa;P3%D~Sr{?2>PWd&$Hx$;r{pU8L7kfhO{UmPra)q{1R zCjkGlk72o&85-;foI2SURdZj@6q@vq0b}-$;r3C9t_T9I!8E?hQ02dnx+;=SR7D2C zl%Z4ae4BXZv+!xdM5PU7+BBMWf}X5Z03TXRrFDAQKevFiL$+nv$4f}#zm&q6IM<4N zPl0wkbhOgOaL9w92jos@alHL;r5{)V#;WO`&&M#tM>D9$pkTnmei;-(&tmK@X_x%v zn-t-FQBj~y>IYipF3_(pc9d((0z}t%eeOdsZWV5WY&=4i|5yk9OBd!}f9=N+tY(sJ z`Pxv-tP>z+DZf+wRXGFIa>=&mFE1WtqK_jtRDk0>J9t^_oQ&6ddV;9mp6@^@b7UG3 zPgj7*%;gx&s{Y@v-ODFK!JheHCB|LFy4k*W$t}U=*&<9@TDl87B3-D#oUWh##5zN% zhOu0bPP8>_Wwp%|MHz)Jh8l{@-#E_ff?}D zlyp}a1LMl~EkWc~=SK+_yvwkfLv#qs_vfawENhBhOtc88>V_MafqDP&DIcS;o5{8d ziq9pV4JNQVvtIHHMdv)FSRCdP93vPt5!|HeYq!(SMPjp*OCjQHz z{2O@xpHDlGbASfA;PFz*E6>qR>{JE<6^eAks}R@!{K5bG1iTQ!Fr%8+%YPi6Ddm+V z;~ULD@UZWHyzxb`2FyevZiY)L`$T64FQ?!7-!5J7w-?{MPdkQ^><=1eF7TAnQp7dI zC<^tPSnLkg1^e@~B>KzDPt<+gL8}u$c`52BL|N$f_Z&LYfX6f^;RYsP87mSHxsLM1 z;z@t@^vu0i%zE#qSH@ky>DGbs9$y?a%(oR+*cHMk^KSR)5Sgyfh6;L4rU|3nvWA7A`{D1 z{8nUep<_6Js|?sl8(x5|nyHl3Sj_sQbmxzclU<1-f_4VBXqp?iERH7t#2DQ{W{<3~ znTw?D9Elt-2EDb_^i6(IGx3yZ6x{*rru3~yKhy31OS zyBKCX1;<`IKgnUiUk2^$PV)23qL2?v?}7N4yT*yU$P=F)xH|HhQu0Sf*&wn7L3m*@Em3hn0yc>2rr_lfn9S?8}~{ z7fcwa@I;2ldrH_CB+ss5f7gXBT({dz35lAKo5#(+cYx%>p2cwzx zBbFd>eH=ckg(*?w{5 zCOwMsGG!CK(#l48nu2eA?{1inl|z=Hj5R1|Hy;FWhRc)n=J%O3jnH~N;B5T8PMqu( z(E!iK<3boH71p8Aj3rD9@yzkB#mL4NyW`*1R>{UiGi_#WNq*`5=qs>~HDczVh#jcw zc_zjfZ8l!sMxfMHHih{q9^|BC)H1PHGBuUZ|HBIa0%8P7;3{dz<2&g7-^RErV-EML z;d(Ht0!ae5zSj){`um_|U_vHZq1K@At?;E($!#CB-i^IKfZ!S#2+R3b#NF;8TXMof z_S7h(q*=`EnF@!Ms|5Fz>BIZRtvn6lGH&@OgBUjz|Po5ik@kV@-d9l}QVWMq#&;ROOG%b;p111WDBhOBd_8PBMG(@9>(zbGU!T|u(21VM*63c9=@WN^6J2Ky1DTr1F?6aI8B`= zeJNR&`0ScCzdrFqD$NvQ4Lzv#9#w&6_<1aw5EhAAf6RlnuO$9)Clp}!xnZL)` ziW3BeZZWy;UG5P_JKJIn6{U2 ztbOlLRsk;uS%8;!PSv({g6ApkeO4x2Od8xW@9!9Y!7KCf)YLq%9f%I8C@Azn>P<<3 zFP1}uSN#oI#^JPnm`joLoE6RQgPXJ8*VA^5PF%S_H$E4Wpi7a@{{$*I{E84?2Ad-DV&d-|KQLMtATZ70qvgQ0)n0Lmrn~yW<%1aHICCf0PH~KLeC77 z%+0{7hbz}e!D4GjtD_bvxb?#mP-{o#3d#ANXTv`G?$4~Ino|`Feabg(;w_NQq1v3> ze{wnwdpPvOn{$t|39#CDcs4nx4LUK6A2uHB-X4S_=4JEjkh9kJS7K~BBG0&L0hea_ zD_P?WnV%0eBJ`R3!TAii@k4jqIMY0jW78O-La9UM{sYa831CRF_ib;HKqS$1LY=r zMMUK`iw^+dx4B1w=i8iBx)G`mj5doJQ#RePIe_-Wl| zVO*)yw?a%{Z=FltUf9FBOJi2Ka2(c|)ExorfL2y|2rVd1fhg`7AFmoC!JEu_=V^>) zK~5V}CA!tRu4`gDpU4Hi4~=`XztRsK_&rRlGP?10a$zB3Z84Wi>JtrHh_?yQ*{}x* zMpj=X>5N@>1A!2oz|5|s5evHf;8|i#f~)~Hq{W!SX4oYdJn9_yD>r2KWD|t?+H#ch z)4L!L!j2RW0lFaRZtDO*>=BsMi>G(r*nEfEw}r*Zz(mF>_upYD?d7>MA}3Fib5FZMe#Z%@pkY=GR@oflQY@WGdLP zzrSi;c_uofR#^Ae^(b&MB5xh!uNw2;tZF$syFH|%GF@b0h&vrt0Z<2-hf)VBZ%7&9U$0p`xCd?V*?@>-EDmrp=*m2X>C;$c2r^ z@0Qo9RkU}dqQhp8W)sa}!g2cxxmBGH%wEb4tX%*(f z2$`7BRf;!H3&CDg^RAx?#XGs(51yCQIWH!u(UQ#0cnsR@-yHglcq_{UI3x;lPlG0i&u%f`K!7^j znbeCmS3xv3e7pP!m%hzcgt&h4c7%q?p4OVj>DJA0xHtz62d&kclLPV{aby0xLkZHN zc>anU7jTXPv}R+px+mYfD*?vXRUq)qr#7*Yi~4)Npddl53w`sfp!}_}>FLs_AJ&JP zb@Z^(c_M(iQboTZWhe*^`EKQBMMb7W-h@VQSS>Bq9}qJN;ueFkKn!{wd5|n_xII8? z(+}{0PIOoXEL(fL>Jjb&nn@NeJD;r4(LLI|^O0xH7~%lVmfT-=r!-%__U?Ov{?E{cK5Q<7ikIHeV=KDi;8-8W05v9Q1pQaymS)CgfQ2V+pB`NM}uSs56 z?A(*z*ek4Hgi?mzILSBn3*YmwcHCd3UP)5uDfi%~_f8aGM(na+<7NXHv+)g;8z7V4c_CRJ)5M(74nG-1JUl0n>Sm0b;z~BMxG_L8hbqmJ2H$vDpRD~{U zWj}|c8#59tw;Fo_b~hMe zH8wxZ0W0WsNIT;%AnxUmTYdAUuZ2`eZAb57kJeSMF}v`k5q>`Fq=U*X|44VUUmCjH z@(67yP_P5*x2t0vp~q_js;%}x-`gvOHypP;*Z>Nj`;+R{(Nc=HN#PE%U_>EoOg$o6 zURQ`p;7gNoLbD*+@(ZoqCOXOV#K)_4$m&`Ep5CL@PBx_vbH&I4<0N~Df3j#-&O*#+ z8zr;q$F!Z&kNQhedhOXE3Lr+We^QPQ1y%bZ0}1BuH?D*GRR+UAT!c(@0VnI#&$%i& zm=@5-Mq}Q?Zf8e5j7)}IdGPM414#z~)^f^^p0fsN3`F~KNhI+OtLhe`7bvw2%_Nv8 zt<*b`J9f+4pMw;g0Ut*ARrV=wOfwPwJ06OJsZW~1fR#@U`#UQ?Gc&T{HWa>%a(6q~ z*QsYIvfFqs%oY^pHrA6e7NGG@Y@Ras7B_0viK$89O(=DnPMMj%>@fe+{*!$m%Z52W zqsV=XX6EB5DUMpcVY7w8%B7BBp;)dfg(}COzsrVG7;eP=pAV#RZ;l1 z*(YbzfHyaVUrOl~*>hS=#qV6}ja;U0vY^XpNBgXnY!PF9BaN|N#6Iuu`)KLZl*^B{ z4ZKPvP;Xxm9Z6=#9*$bCW;k9O3&|+twQH%8zQ}y$%JMD*E|0G5>!UjS1Ze&N&&Q;8 z_1PO0XjkbsJ)$27;fkeB4>g?t653S8H`d?1MbY-<%@p4(9-A>%viSS^=k~IaZ>i_U zS2;2l&E%c}t$$aU`k347)0vcWFW@Q(op1Bt3J=Q&9+k>-KK5Vt?__1tACy$8w^N3RKJ(2p>bMUk z&(cv9b=u50K|I4sK=57P@m+~FeVd>*?C*H}*`38-PzrMfZHe#|lqC?>sw_nV9(VBP zhI+o;Q;`tdSd#tDhq+BqXR|4I77@!iO3mMmZ4gLcYpvbo59_(LdHp&U&Xl-p-(3*0 zMf6eMUk41!8pEMrzn}?DJ8i#9#1egc)%o&4)ELfpxJ>)NL1EkIWEhWz7dQ~xg*`!s z>4E#qp-l%2K?1+3rZq6{k~#h4`TyC+-Bl@jNB<6ADa*Ct+c@xKaG}UYD7m<=sQMgy zoNMhlq=1AM0pACC@4*q=yLPQRiweeKVeK_?U~vBwSLO|0D2)iIpML+%L;;gF%1CkX zWf|S~RCnw|6W+oyWxO1W_zJ#y)RaG`wM^+xt1=8CBhp0xv(ydfx7YR9E8tn&Rt@(r zgN!3y%g~1XUIxt{-O8UzLro!E+2p_|B1XtvnMK8@D+SLU2Rbic+Gh9aVEwV6h=Lt| z{dOt{lYL2QSqL`L(BtLLB|}jgUFos}Pgx?Aq6>M}W`8p3#&a_XvkfG`txUV5CZgN5 z+p;##K=0vLlG|!(A2>tq4&1+ky}*u&>4W>^@xh|3V`o-GH5^dNtNyG_4*N@rTb3ao zn1_SI|124Z4*3(YK7NY$cCiTW2RJhhr_MfC7~?8}If?Qq5e~8ydP~O)_MHw!>nM~H zQxx=Fmx~Qf)UUni3)tq(Oed6KkYF^6 zzpR~CzWv6@*-6Kmlgh)UsWfe$hI4wVw+aLKR_VBN{I>HSQmuVmh%2qU;Gt69WL$nS zlic!T@V?OxR|Qv{4o8ZR8rE};0Emw%K3dkltFo=R zIUlC0rEa<|DW*u0&ux8VE1#fUF(mv^04@+{2d8`YF(2? z@t?A&>BhErp3b#$Jx}9mqYlMJ+CXu6&E_UXL0D`qCX16!+t&6%&XnCR#dQu)?Cps^C9{ml9oZtpYlOj(#SM6;rxvZds$;Mz@qYsV0KAzD`-s3GgoN;Tl% z^%UENW3=U4O;~fDT%sUkY%{=s0oyIUwPvR2Y$E1Gh-r<)Vba&8uKq6Rh6@pb^m*((i*VxlmWJQt{hz zXC=F&rEU}Swq(Fhl#O4$B)WzV;3DYuRPyaQX081bSVAd9rHez1tXgGdkzpA3uJ+NG zOF(1leqdIziRDhk(=@Z5(z-g}jn~z|deOyIs^7H!+KT}qBMtcYF)7;cKI0P6rL*z+ zT2sNuc1nqAxAM?y=h(g?e9&#ttDMS&A-$^$QPUhQmT{7=`-+V?7;S&46Eb8ote&3` zG&X_tA?YEmX$oILRHgdMdv3}tnRsM2`5A~~HJf@-`(uZ9-CwnC?=qp;`9r|Y-*D(t z*?Md@mG#^+Ev>dsE@jMt;vih)sX-noC_3r_renaD8WlJ;*e-Y(CmGyFIZI4ls%DhjnblrzK!vN-amh)(j4b=HR(2co$)wi zi|&T`e_&BBFSa9wz1DALj}dj^w3-_5l>l{wLAnKR8aclsqG!0?llwxzExF6sr#76J z4BGzYFja=)UXu}H4FHBxZb#UI$U?i_CHkJZR>2-aiN&oPAt2>YI zN|L*r?#EacFofx+Q@!(rJ*pr&si3B6M`kv3x11ub^d798QlYxsY!>S0KiD3IEuvd( z(>@2or5>%aTQE>klORN2JhZx(v%uN|tyh&ckqaLwhbU@piC42bFf&hE$!M?SD9Cl{ zbh)=aH(?>v{d@?Yil^HJyI-jWgH4I&HWs81{`^m}(@~Q&+Wo_&i@p zw9+!Iy=fSjhwP12re5BZdvg2ZYPQ$&8?UoHuE{A=QndX_F)>*Fz_pHf8)YhZ9aR5_ zA^Lm%xJfDAAPD#nLSOoZ57)O_%BeoPFWXB}Wsg&*Rw4-2O;;VV)T)8(UYjtTpF2NW zfQ=wIPl$Kd@8zpsBZ5{0tI!DGMBUcItfjB|wq{M{w6>1fQR%7}h?6l|peNdyb{q8v zdyj{G`Umzdu<>{$Ou!$wi#A58WOk00w9@!x*;9_gjUmrJ$KqS_@Y^q?^2_o8!_;w* z-RO8itN*>^yR+`B0kud(OGJ0`xJDF9jL%(`ApTb~pxg?5-K}BG#3CzEd!Tbz^qT~0 z(Qj_Tk9jVWahNaJ&2Sbl@#cSH;u$w8cZ^)8RR*1841*85Yp3U-9AQ<3&iDer!E(r> zhj4ae)c3Yuurr{~qFk--rq;Z+1A?=(gs?@ub~&wlLJyPt3wgNcRYLDm29 zF#fIa{TiJ|gYrKM--LQqqRin~& z;rnQ5*3;*KjB1ZtGi9<r5|&@ zO(gE221n5iTmtFIZ47Sy&fg8SeK_f*_f|!p8|`%8%jNHf_MWr?K~ra2$eGem0)kA) zt?ytASR1a4z?G^zz!0;~cecOsxucKTo6OWgQ%X00y?4cMV6*DuC_Qi+0_K-yEG}|2 z;qp%dfRWgd3f&rrG&vr&Nxz&Pd`fZ*ifoCYEeh|+YCSJnu+gthg&ieq(43U$ils(nb>{9T)OgrTot0yoyLP^IOP|RGYaI$QWu+3o`N`s-7=MF-F-$kE3i4BnIcI%aX$JUpy92w*(dM@-nH}9yX37E z(m(GfKQEl1k&k}XoPS{rvlzw~mWh5gFc|{P67h9^ zQ@xLUPR|<`DxlM@72H@-d$Mj;lQapOI9Hfo8ZbZ{)gmNU58Ei`R(BZKerdC(wY01}vI@!Q>2ocJLorB5S zU(5xUtD-*th+yP~iqbr<3c+{uWQ=>7#< zUnk)4TTlB)(oGn6$t?>es9fwkL2%Om+?j9xJFLcjM}xo=>??9C9+~v$?w2gN2Y2r> zhe-ycPBVA!PCn$yEx7HJdvLhFhS~My-q{4i)ndH->FQRa4|$B0n_)4nkeOjZJEwWM zOV5ISth_;fE1B!FtX!73Cu+#!wSg1~+LL(Du#XAN1%=qd2mIxY=8=}w)*t=q)ZCgJ zdZ1;YbTV_VO}cATN{nSgThgap(RU#MU=|_AH~QZeOAad@g{l{n>}WtBxXMfw$K79sVFvVsMQ|c^ zDbXvmWVlg^q&)&0ls~VB3;4aSmByl(`A$$!wY`0f#wLv5xN^5L5Lhl9EN*01WYsmf zJi&H(Zwo0k8*CKV8?bj;CeJwm+m?tHIB*0xfiJ~i$9i)D=?Yhz_Ob(5-*RakY+C|; zIo6_4d7-d{g&d`0mcN1W0-c8 ztAbFVXP^L#`ON11(x5vyteA+Ll_W9O*o2Ey`u++lRC;t{=-F?~pzoi_$>v;bY`}w^ znbnDg@HMS4x1a5ctKWSudE2DYI&Ok3uB$A|Msk%> z$yvb1Hq2CDdzXR?5!Nl2Am{zNUvqu3#;%$vB0Veq1}@od^V9x3wS4;<18UH`KRkR4 z=|WU5>a(!++gedc??tAWuABkfEZ&asA9AUmk+f~J&g-b&UtM|K91AhRp2(ZEGqE02cyTkRFEA4wP_u7;qyaPn~UCr8pr=~dgTM2vw@Zd&0XmFmabBTwJS%(Z- z95zDP#&u{ZEhO)ufw_VUL-u(^^-_u%+(4Gn}Wub$ga~pVPKyq|%ps#Lqy~Ho1 z6-?38HL6gRw9lN!$+g3saqY7W~gI+%;{8>86Wb(+jvt z=AQW&g>utFbj?$=DT24LNi#T+qVmA1=Bt3!QJ|zKPc0_kP-6~U6RXihu4yN5a%Feg zV1S!4PM=8Ctu`Lb{l4cQm?hu98}UqjV-x}VwyBO&d;b$KvKbM=cuD0mr1rW>w4P0I zSnZKvL3fKp-rb0WAL|i7Wd*_SXtL zR}ZFX^L^oB=T%;h%mY7Qu#SzGYbDaiOr4gT^E)E@i?YR`MAlfw9K=kC9)vwL5FaWm@IJ9a{>0f}qf{3-y= z!`aSNC69SVgZ`bjBR&YcYVXi@aLzQ#40ygvS^pl)S2i}H$5wB0jtITdJz>`u?9#)fLFS^ z5kL;#q2X$A6$guZzjXem(>y?Yt?ZRTnSRqvppdu2%3(3$qu!){vWS}^7W4`gT1eY zX!e0L9I!KSTtNgX?i;St93W^?HukeP)^m(&pxwj;lwEA!NV&Oys`fR>Hh}p$$I8aL zs_de==G(<&ZERvwPqb5M|CPkvIO`K~PbZP-;zDwcFpuv7$pl&=fV2K;VEYF5BL-)* z|9A|8+oL2DYQd-+Ni|+e_iQ$kJCJYtv6#CSu+JXv3vPvNRMQGy^0a0aV3;W@H~^Tr zNga)q#a&@SU4uNQ#LI9UZhAlOvXn7lGdDC|V?C5A6PAV%U?5(-7{L-F_dhVGU#^qQc~bwz$fA)W#s#7K2E~2EJp@Stv&7A{V2W>L`nbShU%yC4=GYmwJ8Yr^;RSAomGGZ2i% zE$Ct|uJDuevY-g%dml*%l;e498gqeLJyM?{8}X3nA?bsw&_U4o@Y<{7u$les!gdUN zn_^XR5Uc9*yjUVCyQYF}hp9)i{%cS*klS}QNvvc_21Mp&-?^>K_SXd$eat7#Yu##( z79|#Gg`th5=4#<@JO{SviS7pj$*(M1SRFaArCYiP>Z7TP-0YUMv4ZwzN`_z<3els}~!SPSZM&x?>chlJf3{ zGe%bVeV&N?bhPERpTs+WB6R^nDsPZ25K>Eq?oEWDUAH@dm39{h=;BEQeGF%&g5QP49U3lL)P`vUCTM}dOkhc?`~C^4Nn+vRL;xHg!1 z7>7ZR7zY?abv+Vv83V`s2v`Z~BxCuEzro4}Fi$LKNQqu`sxMC2W>oH6?kDa7&niO) z*~%HCTK;%B_2z36;DNsqFV*u!Dh8l?hM?ZHJ(-k7lC;u+hC zackMWGcl=v^CRmYG%;};SmLjq;Dx>pZF^XM4qNHxo2=LvtetdMJU-7B&ma&+?k=zJ z>0Nku1n?g_9N9)te1Ve2 zflRL4f|tp1632~|ot@7Q2_q#RxJx))@0qV&_|{j0KwM>}Gl+N1F@BSMxVC%PT1HfG z=Mxd4g~UrCFNloSQ&@D6eBGl|z<@Jhm+fcGSBHjgRq;xeC|iUQ`Q8af|JEohzf3{!uKf2|eXbBk$@5xks%w$jb>XJ*R6Z0^T&Alug5=JyZD zB9F7HN{1gj-WjER1q_Do?SsV#NrlhJaQzKhcog`L;$Itkxzr5|Fe$0@>e@P1?iKHX zG!FkV^8>6eZ|oZs>pgf4S^^X)c4n?}O}D9i=B0iw|Ipy}KIHE@ptsUs;_HB+^eNXK0Jt{mGi7B(dbjJ99jdf3+i4k z1*@h6c)>`zXo-^STAdz>7&LlQT-zD6)J4Lg*d(-`2(0!}$l0%sG)z73;KVQ(FMk|) z@H;-sYTD9ZIKNu#;-k5#XYSmLKS7VY&(m1wdka`AsFUtc-p3s4aOh~-IP!M1n9`=&Un|MiQ@1-l=wYi{ zp}e(WExEr~15^5yTQpP0tcxs-nkL41Rl3trP?vL@us@RJ()4(34!_H)fLPa=Li?B7 zt4nTNaA$V~zL2@0lDHg|nGKA~TNamZDJm%Zrn7t>KzT6VE}w2a-vYz?XN0_CMW^O#+FK0LZO5i`Dh$@m?!FX>$Rl+t^6vOsq+hdT z;u!mPU_v@HHy$xO?t22_?OU5CYwlveTL5n^ZxkW%^Q~DGUplM6r2+b1+uAzL z`pJk*+kq48z~8yJ$qHO>8uzWAujmIpZgyF|-8w>&G8s@uEH?Y?__iU7jkE_i9GZg} z_EDLzs)tVVgC{Dw9zXGzh;BeTy=d{j%&$+nK3VJ{&B%Tpgg4BjQ$1u6>vBa1ZPPK}3o0DMEmDYfgN0U8rA18fTHd3!oPk6{ZmGHVp8 zmcOiF?ybK~dgluYr6rQI8r&(%BKr*$-JMt;7oLw2&le!Kym|))-s*p? zJp>oArOi%jDln)5XKSLEbssOpa=ISefi0I>p@E|vD zJ3piS=CogaYDwRa220Pk;}$rD$-g(ubbR{_#0|&oiXV082{AU=qug!zGZgYQ<@dG+ z1l_;gK7iP0)n_a96bxs<<}xdYkkwT&dM;2GjbHmZ3+`iv8K~P%169>WYee}LLX=nW zpZwo{J}$m`vgpNdWsf17TnI#+g-ajayerCKvgp6M zYA^OenV!4j|FQR$QB}6>+NhF~l8H1(qev(c64H%;fTSQy1Vlhu1STC$Qc~&emXz*E zNq0zxfOL25i)X#>`quZZz4qg~$N0wBKlT{>fB|E2&wHNtbzSFi9zie&H6Se-kmAjK z0gKM&ybu)EK^wq6i;2V0pCFXzSEa8*)o&(AEe0bb496cJJ2Zqjb8U)W%gJ?Z^jqnH znoqriT1IWEnPL&G7TnsjkaWC0Go+hbdVzkMj z0_L`+oC3N&j22Bd5_i&J<5dn3qo^{f*z4>n*PS_L=D~PQqsm=NBOLhRGlv54H#)Q= z&9(=R1O(K}Z(qX`oMG*?l_&A$ILpLmbNc!F3`-E2)_%ljI&!20szgQCv z-a*U6rROn%?dN@{y;rD{1K_*qfR-W>en)OwfviotYF(zqp!nT=eZ|xq5d{tckq-h57aPv5fx0aoZi-}^ zR)qoB#`7-U4>^UfLT~-e7dJ7Al9V#~E2#RTS4E;1t4i)44ua>zp&;i$UZnF6g(Q6{2s-;Jb3G9R^*I&C-4)Da*F$p5xdH z6lCJa6kIY%MF>9T0FI=<6Zn!?HkSyV0f-_prZ6b<5XD7ee5a%icmuKcZt^lk*~|Cf zfRWk!bSSAlw2v;oBu?|e(cVR68yWou9Zy>X9WH|;Ka0ZRNZ#0^zPvoE==m|IHtwmw zN580_O{nujK;iWbS1hwL@Zk4KB`MJ_a$Do?T2Ci@`b}tDio|;#K@iZh7v61oIaNjn zTys?>z#L8mn!?GzzD$OMH_eSeQttOu?g)$T*PrZx4O26PC)9L+O#Cj(`f!d0cg9u@LmTJgf&`AfZTS$}wfeD( z+RqI|8e;1^0*;_&Vayx7{xH3Ff5w% zriKs(pJeY~yj+O0>gD!!Il(Y^A!#B3k*1?==6!o{(iE&4_)bx(wC$P3vNc!i8C8jl zYuDz*Ufwpyq#f`III7T;lA>j_{eJl5Pl`t!tTiJ`@BE)ka!>?W%u#E6Qw1?|J2x<1@@&GGgDJ_kX(@}T1UxwPij$B55 zZs$Z)Kmg0(4=8@{QxN_0V8$D$)7}O;9!$tIv{Au&-u2$ghU4QeT1l^+$A%SPF4bXeS6Rqal!fE)t?zb`ssW1-e(mcQvD{N9l<$YLL~?{= zL;$58=DfI4T3bu)lbjM!JGhbzD~vmSxm`HP;oDvaNb=~r=d%q4kjePwOFA}Mq-QGr zjvVPXF-SJ9L4);Zv7sm$zIqdcVc$|;OxQ3HvEV&l@m@JpW z>u}k}lxo}>mZK(xc_WK$_b0m^5ibA1j*}@%>lEQ5k6mNG$thkz75tF5Rz*yCB%Uoi z8XNbZM*u?78_$d{zCS0{h)PVYrl#|h=X$7{Tel`tUaU7wCZuKbn-u3Sidu}rjTj>Y z@xcSmC#+buUOh@RbhUJ~OpOdAXpa;zlo+ADbonvUwVcIKEA3^yI_ygLQTEqY5|?59 z-nSslzu)d$eGLnH8-uC1x^a9#;SioCsPuDP)I{7UB4R{Z02P)qEMzA+&H{Jv{2_fQ z#^zJ(?aR%Z>HJtCT7jwgUK|kW4KeJ)#tO=|1045P2m`4ktm`~LDS;6e)#fQn2Hcop>cju}lq|4v7MvVQ#km0T9_9A0qVq$4{ zVFbY#P=nE$YqpTbii+#{z1a?~cdYohs(rqgY*ypTf)I}tarb&v_CAV$ezkyzImg;; zCxF8RuE$}ilbGfkn2gk&@_@6%i@hTJ=4f=+!Zj>iGv#c}{N4HnxZIOEf24M@~d1+Z^(Vze)$0e3w9#r|`dq?yYn*v&eG_Fnt>x01IIMGZ=_}lwK3jr`UzSMDXflN1PDu!*jV*K4X z_V!&rD$i(~8*^Gq^Knh|Lt1urrCQ));?wT!{8yuocB?EkSK}MfnH!rW-+%T;r`%U+ zmZF@iqsdZToL<_y%w%M++w)sr&}w)DcNvr{z}={-oBd8 zanB2P2?PICN^N@f(W%|Rvtt9H<<2;Qv-8Vi0i-?Ub|a9`L~(*N-bFNVPnKr5L4jd@ zZk~j5c5Y6NYi8eVu=)$s-n0F{nJ$ZqEG{YO8gwQ_W_xqNwQJxU&|r&34;|YX;UIph zT;0DKUy9rp^IPYwYl9hcvv+b~@YiZy?e}rkt z>AOmzga?mdo2+Vh>Rkk@YQ{^MZfBi;TpZXhi2SUGkf1h zc>5P!?7aTOtmQZ84aLHUwoPG@?K5+Rd(=)b>o8z!XS-IrM9K=u4d;F8oN?w96QN!c zJTdgMuIb^r0!iq#VJY^b~1sy8ltj3n9WGDE@X=CiKGxePidEYw; zB*wkrlW|PYY7p<}_IM-duH8J9rYYu~4QjQ>*lp+`$PMND?WjIt_w$aRtdbHo_Vcx; z?#kYeEfoNJ@nQx@s=J{c33hDg`yZyOUEftoB18a=qoKBDrzoI7G5_MP%_peHKk}_x zakcw(%E{UKqsS7Q+aVv}D-k)T`322Xb$dHz!mFDe9!idrgOl~|M;>#%1xv)HQ&yIZ zGj#{WdijRz-pyxg=jOQS>>8rW=n~`e_Ya_Kqkv~L;zEL~!Kf=zOOkeVb=8MA9i2o; z&1pX(dyRqNf=1$M^(|t)HSbgO6Met&>M~hu_sGY(tZjiEgwxeVe(Pga{$IX)U^e%x z$oTauI8`QuP5yR*KvpzLq*2$XKN88Ia^dkr0CIf17~*W?giz?}*)(7F=H-BAdmiKZ zeTNF;(tiSJH=WJ^+~W}9jfqJd^w)gA&!5l=Vu*0X#+j?aE_d7Wa5GWyG852wRtj}% z=)ijW79lC9kI1j3b4K45xY6U4`7$#l`_S>_=^jN&MuAQafiHg4k0N_Z z7CV70MZ3)@EjHQ(G01lW;>ZUx_d~JS7>8;yubD=Dzfe~bD^xH~)Ng;YVp7u0&3$oN z>F&;5bdHgqrSo-OtNKhK_m~NGaD5yT!iSr19kF(2L8k4m3-88-uS%m7hX-F*b8{`_ zEXnj<$;rtytG+B(9ZRiWvD-T3Sccwa(eK~yQwp`!vP0YW=(bkx78qj1#6Ag}013?3 zOs#{f{EzRs@LD`$9DA29v@?a4LOGEl2w_%oa3Y88r?DYwE;dT-O2!A$Fn^RO6{Z!~ z`q4;-P@8m*c~nS3J{D!-(aem!Vkb<0gPs^#ZYjj3Rw)TVYY!xU!#uGbUrt1_n7D!cJ zaD=E*R2;wSd;FwDbp%C!otE!xtHvTSmDa5TVCpS1Uw+IZyDHaPQ?}A%`uttb6@Q3a zlK094{wDqYQ^W;M!_N+k@mRe@_awRPFKYDfP4*RrWT7pz;}^2ecAGQ!C?6r;~0}b%->sRd_~qaKcbmMW>EH2 zg%$^2aeZu?cQl$L@(|VbO@DHt*o&Q58gvZ}7*{b!#S$0lvcGbv!dnfuu^l=86D!5 za|7ExARdavxD{;u4B1~%g6^~tkdp2^PD@;*n6I93-bNoNH7+8hhF>_XB=8&T{UXR@ z!YB6m2BtHd_((T?Ec)t?&v_A&L+PZH)2IYI4#i$J>+3dO`N4P-8`x74?Z_Rl?2(y7XTU4h-%asv09@^Hijr1v-HiD|n!bZpp zku$i?)=&Q$&&aR~mwGmmvOl!G$GKHf0>GSjr~RIo!PhZvH1qv!!BmVOqA+bXas`@k zv8HL8QSlJYrGi1HCnlB%zdSVcF=4S589ou;!LX7&a~V6&?L_cl#Z5vLSAEQCNw@3I z<9|;))pcj=BhFJkWO!*2JK>;C{L5&|k!m04?w+&1jIMxkNtxy9J<>)vR=GVUnwcVb z9M3FNF;9Y4Shg-h*+DJdtGxVFM6j&sNfIys#8rPWe*SxVs%jBeY=-Nd$OicwIyNkC z#?=AVuOp+)ZmaEX1>P;*dj)HDHHr#bDx>Oz!dzX&?dRFXuvaPHa;JX%k_0GWsnqoq zf27^5b%^9s_6ETYh0iY4Ih^qmK_4w0Y1uoEo+T;Y!Ch)pB^H!i^Lc8}=&`joEw6xk zCA?}YDKgfjjBDM;{6g0(zkX)T=x3KL3#I9SprYy2RsGJhhJGG)R~ffguE7as-;%kc z<39GD@h38?Iv(t*_rV$epV95Vew%&2e*u_rk~k;}IUQuXQY>j{X~lr#82rGVqjn!& zZ~H-3_3NuEpB(>^=VBdwi9)Q~c;ti3zqJgjc7Jsc+AOwNFsMzVGSk~knB$E_0Y3{l zD^1ApNRFyOWqPDBG2Aj{JnC>ZApnrkVA$=i&Ry|^=g(`YmTasN_>UMOZ^r*Z+ZCP`=EnR z9|tONmIH>^A{*Gs$I5c4z3wiZ<7avdrb&ry>>9thkcGl-f&JxJgP#*%A(OTuaCm}} zNsVwOG;ZN?&-N0(9TS&c%dk0O{xD{8794q_Xj}{XiuXHp`}EX^poLDS+qdd!&z_06 zT~y{j6A@{sk8N~%{=av|sf@~MYK*qHr=Kd_LcxZlJgXW;G_9+w^me@D#f__piD7=Z;!Hta%jwC7yTE)glI3Y5 zEp1L#L*grZqFP%GMDpeqzB;>?7cG+&LifTWL`9M{WP7s;&+V<;p zXT*BRGlau_7Z`$IT~s|Sw4Jc1V@djJFU{qVbUOGDWL9qXSbks?PtK^{eS1RC2wB4Rs|?%YEnVa41s#5BzVVoakr4DBt*F3 zK^s&2JaiCBp4;UGbkHZMwf+Fxlz!Oz-KYOvx#u68RsW-8l4GDdQpzL9@x#pM+?Z}M z`%*2a$xvq5LpC-O5?3Q8%EzAh`D(y8>?@@u>QBzUM-Tqd0C_%mo^14WgIGZ7U#ss= zKDNgYW#uIliqgNGfBg49;C_KZr1rGO#;EXbu%iF{k@=55)`2D`ccOE@qx*x^@LxJT zlUTHe+3#Z)J7IsaS^vv*SJH}#Mc|lpi~svKtDhT7S$TxJJ)(~O-@j;UF%K^m(*9`g zKdzVmylcs(caxKIO@mpT|6hE$^8@R@EI_eFlbnONSvyC9{l&6LH6cLcM&H(OG|nwq+hYXZ!R$%Jc_K2cHf zxzuRDLwFjt3D8?d~Q@3;cKkk;}@-L3)1;CA^5c z;~yIvM-TyqHJBVmojZPmJQ0!sO4`~o9Bvn(nSbZJ~ zP+*&BYChw@In$sP77@V$gn`vb39+%0cbg8^E8>;;W^mdIT+jIiiVbUtL_TgOnnI|x z`6B1rd#o4NWlr&m18@2kz~Ax%hlFR?xyYaw>!aTSPvhxtm*~!!80; zOcmv&$>dVnzbg^X2V%*qTjsp}^QIRGuPQ7X#w)fFKM5clNUYpB=J_F|F@J#Q#5r1f zLA6hSDEvF%9x-C$$jFC<-yi5B1%eS0`N(_%VE)5RW4O2jyifMFe}vt=S^5pjo18Yr zwQN>Z%CsCR3E{=2EM6?;om=BC*=hvLUs*AG*{;PSP~U&FIoG6GfTkW3)4_2*!+S6QRM*ufzrK#B0K&jo zsuY)$i;KX|jfDkU6$S6l7Z_zb>`fFN_{(ePnRAmqq8&Q>*zF@hD@S8yijpWSQ%35!v}-P_Mo+& zJ#h}LU>H}@voqn<^IChKzP~`1vP<9)(2-JWFLJ9)@%vr?IX3=6fEWs3WYLQPh9 zWP-w(nfBo;XCevDo+fPP&lFbo=H*)Yg^0lJkLf*m_1z@$cR$@scR4`&9TUF{zC2iq zcoY!uzS~&vPc~AhmlsQ~S2WZ^CfllE`hnZoDigDx*F$V%fVGB-ie*5~oVU?v{=mDv zde<6%;1=h1eO9QB0?UWyc0A4&*VS{Y>X@>Ya+{9B5qxq_K!XKjnSC-JJ3BmYowVxX zSl>BE^9qytjD7hyaw$yo)BVt}FgBRGYjuVzoSmSa(4Yobgm)bi!WN!6P@6s<8-EPF zf|_9=k|C|Yu|wJB^h1zN9GEHK;yXQh%U1=&Sh8U&*B42Jz6ZI!^}gEdv{OF;f-_#Z zo9t19jY}X7MD$GDJa2=_>&W5&j*eD#eRb(;usI<#h{Mo^wSq(1i!Ikqc?E@UmYIs7 zi@iByzb=uSp%JW%iWaW3DP$F+`sj#C%^-3GFKawQP5JrSs^^Q0`n_5c@eM4>r@2Yh zJq&-a0eMI#XV~`+VTjLx^)hnjW$}9w9{R4X!k8Jui+tmuOo?~n^#J+tudFP$Bf+;~ z?|h(mFIBOJK~k&P_c(H_!g4yof|rJZ3Ho-rmfTN^>!XR)=V-YM$|tmA0Aq}b+IfGw zQ2oUS?OV?kB0sJ7?=9sxq`$icE#31ke{^*4o2HFYTnORka_~D$==r!=;pt1h6ENbr zFXaZUIkAXd89(t69q5=V{+7C+{bNPZak%bPh3*JyF@a!Y-Nhaw*Rb024ynWOT}aqn zadq@FYAVe@)IB^%@dMqX9$Fyy0_7PG-}w~NGq7Hys{W**q17%T$r zQwes`Dy-S|1@Ue(9K_VaV)4I!H91c+!gF3TGDni=v`esGHcccif2AmWmizfPhhWM4 zd1h^hSTah;^OSCRnyKD(Wefi!H?~oFcEMDO2303^+p7BZ<{Xi;x~FDGKek0i_55@; zVbyc1gc#ci(7d5PIc<7jeZN!RDxmcFmLTL}9>43Y9S%u1HxhFMAgX-GIZrf7U1*@o z$vn0ryIOR0XD7=p&Gqg#1yp=sp!BxG_QJ4m5#NvczFLj#`Ld>{pYfNyvV}M638Bu z#2jGHH(XgUmmX;!zTznSLV94g5lI~qithS}G;DQ9#veH5d`87o3^-iJm)&N72k&-w z$`J!h3m3c0gne?mJnc$0-)?=B^PZ;GPhy+v`qF-^)E_-@_}GXoz-Ppn zSf%1qFq99Zo@lCRXrr4z2ziV;AC%oACha?YI;2d@03D5J%(`FL6e@%qPv!_#T;}8v3AQX{UIH z!rKl;`s{E$iUAnet7}FvxCQUgW_pQY&YImXQbC8WN_^j+*q+64E0A*AQ8t3}OAACD zR;NvhuQR2uQn#WTfjBa+c$adJWpYZX4 zxY=0-mx5SELIBI1|)1RsD! zS(Y*%hr2{vs}-=J#3Do<^_yV4H2%?@cIC3#?woB&oDC&qaG^D?MPR$T7|t-z6)ZLR z2oKBlc5w$Fvl(I6%%So2CFBtS@e!VOLqD(Iok;dsHlYGha}(ZtQ$UdA7!$qZ9~Fdr zhnBn`jm_mwS(d0gMt7X7C_Joo*!zWkdpCQUw&YyzdL8fFVr*=&9iL~pjiU;d#BV$B zs*nZ(xo>AXNmGY=UKLkdTxgskUabGES;sfJrEj-M;8SQIG-@1ApODVuLvCLp&lfY@ zxs0G78Zk$RM-U8o86ZYo)emCY^udKpA+&p6}c(z!!I2qbK zXyj_SQ5T)60(!BiSd(bSZORI$sy^50&ODcoHLI9HqM&%+`$QQ5nf~CZDFudfjUW$W zmhpIgL&-vz?k|4vr)Ngds5`xApJX0%qjoRm=B|IRY@EbH-UIRn>DtSq(l+PeHkGom zi-)+2s1nnc=PPm{77+ak=VKdkZJX;WLLZsXxAXRC6i6wAF;XBlEX*8XUqcJ5};o6fVw*z-iT9V0&olI2+}57bxYWiEz+Y`Mi-h`^wbSlB-_xR#p;j z=e%8a>Qv_Qr1$dt%h#`_k=`YA7fvVrEQ%b2C-5%`2`q~7m5FiODIjqDFJ9xuA^6t<*1^=994vLZcnXfB)2fxzI5cuWN zF7Gve40&(QQ8+0Kj~R12z%ptvc=VyGIYihorpqk^6ZwUOgSZ8;!xNK`uv`^4_xX?n zLrh>)wFcN=h|eH-)x4Qo&uJ0Y4g8yS`>}uD_frsh&Lu>)4WB(2)-ZQ!67c-mPA5|1 z?^I;t8^mn{xs6%)j79QZ?T_DmVU{4tQYCaKRSJj0`b|OSdw%f^RDD!j18PKeK-*l( zZ7)m+d+kbn zkJ751e~4%#BZ~G&GoJzeG!JbSnws~7Rf5X?g!vfZE-96TZfe zDr^o+G%w3#;udjm7Ue!oG2cP>J|OGV`JSyjCyeX5J;3l95Q+7En8ANz8yA}kRI+Q8 zOypDBqR_K1VcnV0T<&HLg_U{7G zt9;d+<%wGJSxH2fRWYgFWZQCh4ASHThO6Da+nJBE_XaU5_MffYL3GR=Yya|=iGC-2 z@uN%U$X2jOap847^5T)sE3yMi$tmVXbZ&ibHa!owlMJ<()Bz7gPU1r65&QoBdDl~d zRMm^ePV?TRYMj*n0=WO*ZnL;38pk}6DpqLFna86BEFTMu`!;BciMEqm_fQd_7*?-j<4M1+48L{!sJWaFjSz$>AkBUAI(B66}PV1)<9xWGv z4QM|Oo7y|)d4sLhn-FI|5I2v%z;r)ToN~vFqEIK)ZuQG!^bBakngL4$G%=Vq5}~f9 zmNU7R-LMGX-sE|wkO*J@_H$2(Lz2SPtP1D zVU*nL?c3N7wW`pwHTRSHi-zmth+1F8WQA^q>Dq&)&DzoRW2dc~D)Akwr)`L;+8Bd3 z?x&A%MTi)%9N?!)UG#c-?}=hr`96WSBF|5+&SzgqF7MG@|4FACibg;Af-ygu5KaN= zK^!Z1gGx?r?aj;$OU1Du>FwK(gY1u1Am7jmRh}RyOL4^6{3Uc~*@iJ`T$t*3XtB^F zbZg#`q2%!~Ux~fCkElGm*vCVePl35Peo%gXGmySmaW4o@8H2`x!(0Yu;2oYCx@Ugp_{#zz}KXT0~Zg!&8`_BfF#c8S84V+#AZ<;#ia`; zk$hzDyfYW(h@(%Cu>5IA=xxB;;Arpmx%65=sN7wtb^YFh+_@AME-oo2sod;r;x}|b zfc{QByvg{x*%m>hs-dMt^-Ett2nQSi8>Z(n@Ma8XUtupDLgwk{zI>8>5)P^ZS|ECn zj{CxslmwyQGSy1qDUweBF(vB^r8c1RqZ*-^(LZ?M$gScyzyvYev9y}jW|TlfcB%Iz zYSBM>bP&5VcJ5UT@`+eye<}&*9QJx2>DQg@b-hqgTm25p5tg(au2a!MHz?bm?mE}g ze4R>88J;o9Rp7e)*yxDZtOsPW~H9w=K*9b%#3Ap6DSQS5|7m6{%8Ra1%NXKCqDgQ_gMya8Z+Z2NKHhugnGV$WNlZEu`&py2L zI|%sl?}599Ln5?-P0|n1?$2?-t-qN_sVC$30IE`+rLUmRo-*-g{<|dvF9nt3cK!V~ z@wO>8#h1H($nr=&H5%l)D@n=n4OD}8R?z!HHlaF=M%0&>;)zmbc6Fc0+$%`d%bPz< z1(Kfz_ek)uzz>_wE5T8W`J?1379-%+NkYj>{bhcGmO5~Fr?)RjM9eAa)0|r z5yGNMH64t%*St7>Ji8q_gVH4CeThT5g@0OmkX6D-`&eGqgx%ZkVP6NWxZ1kckgXm5 z%w7dq|C}ze6VCI6BA=hkztdL)->gWEK2p)>2RYRs@C8ZQ;8cVowud z`j`2}kaNrdas}^O-5smu5c#|6`=5d>&(da`T@^8j&8M>0t2$8&9jn;|UT}frWiUAI}?m5=>ar0wGCG<9qJ%G5(H!FtFX0JBoW8Tix+GIA)EwpO0tJFFB-udd=Ba4th@ZC<-hF-C%ojipv zHtC6ZmPw8OP=h`HV9S+{Px*tmg$zndyD=NVIx<+JXi?cP6Z{Q;A76E}%gyO#+?>mC zKP^WWh0P$0DFe>|xcs&2x9~pCaelrV41r9 zUSOdW#OvmOYYuwL!p;Z_nB4+{wEIx>uM6aqd{`Kn6ZvDJt~~hBoIT4@N97ah60NsC z34ObC>I|Rfbh%&O@dlwBb+l6qJ4;^sY!AzG;p6!ZK$D&RbJEQ!8rz>;@VU61^%61`AAGq&*1i6-<2QPG}pU57d?GUEp`W9BUDsn%yZ&=;RIjBs3wk-~Hs2%rH~jVBRO{ z7@fWs&=zqJ-zIBML#q51LTve)+KH=a2 z?*JRV=_Pgo8u6SBOtcb5$LS^4a}n4Xs#rvcvKhLhL-j3{jF1DrDYy0f43H2c4-bav}Ds=JVFlTexkER3phfC0Z%#l^x;+JP>$(#ET% zh|cHqb=)+^RuaR{EGA^h*T67Qo9_43W!X$#{NDbbET}R!3#!*kr57(aB^IfKv?+!c z0Gdyh;#+I+1D*JNji<8fPIP*0!0!+Ox%K~`CQr;s>#1oYJXJ81R63#3&%GgP$HTi9 zr}84sDjrNe!6e&i5CRP08ha{YK%VAlhUV7tiJO<&u5dWA($T?uMu(Dk)AvEjO~>** zA#DSnb_%{C8QDJ7Y7djcHQiv`Vw>|75%wr|kW6w=R`X4J#HqqVhPvU!_G|$0Wb1Uz z@uysm$0DiSHVi=Z+D_OUG(4eS&&&lR1M@UC zdCX4M<`$Ub19BH{<;D01waI|d)tM|GR!hxm(~4yj9jaTxA(+r~GVb^ji? zYy|+!+w8PH;mJ3M@{X@`RXg|S>_M+=giQnHF7GW(7OFTA^{0T7y!Qe4NBCGbX@|&Z zjuBW0yYSWaVr?}7b;^pA%1J$zFiJli`V-o`u8UU*mEUVl!v#-+#qUiZ2J zm-$Ten(oB2=stf{ng=PH$crRYnc#50;bBp#=g+C`Nxcc%dH!PaZ7(T&)T1H=fZ?52 zjj#KFxilEW`eYZ>->;higqp$nD1 zTZ)Stb*lHVrtT_dICxl#&&-t>Tj_r7d9RQ`rZH;t1F~MWCn6YDY zx>;yB)csY|^^UJmCfUY%VFd(1riSc)tUDN3X6lhUv1{T^1B9XUbz4pQAU)P76N-nD z4p_lN2a1lHA%+zqemcg7hXzEf>VLH^z{$PH-9H@5 z`V0u@Z~RnQf|M21flRADDm5I;N61qHecwGDf0D>y7~T&CKfcgMv?6GCGz&*%ZG4ib=1^VTL7jyx!Z~| z6@u}d0Y`~ou`?1BFC#Q~;L={?Mw`AedtEQarp=w#TZV)s);|{ikx$v8eF8&*^xFq_ z2?c(KENwq5-1f*2h5avT&Hu!`5hbLbD@;wr@^qxoHT_v)Z>uNbkV{GV9jvR|tKVcu zk)qt3u4l4U`h|NxrBI!Io>j?(q$Rd9_kf#$8}jg5IxjUuI`yf#+h>xLtb1tK04BVd zlzoZY%toEB2jXHQ+W4Luu7GL$L_Z({NhokhD8zEB%}rU3xA+zrY)#o5$t*t=jBW(n zn)}rbecanJPN^YEx{hmu+&NFQ9%53%=!-!sOqN)ML7Cz*q4i~vLZ4(7kk9q;MKVLG z+zLpO+n~qd8J@?S@Yccga?@9-6KtdFY8Fl7O@RA+XBA@$72_lCa=(J%P-WfKj}Kc< zMaK2-tm(77yuD1hyajN2w!>A@_m<=58T-S1hqWvr30ZP!d){%Xu!({-2Nu0U`7$XH zdK$zhD88t|?pDn{0{eFEpqTl7o?cG`Y^bRBq1i}o#>0m{2n!p8dv+dxxST?o?Nu-Z zBKXP_5(=9L)%4Gq>%nrBj($13{nm1ZRQ=9bP-s@e^(8AOBI><>;BM_+C(&-m^+`#w za0s@*N^|k2-4r?j zwHVbbU}dWBgS zCH1pD<&~7Sml?;LQFVd7u_8xhIGbL4I<6t|@~o|GL73{c48r2v=n_I_6{N>={x7`S zd;0K?zW6=~BquMU;|%lt1OFUGDD7W7aB1aC?e&2)nN2A|K>=qTXu0K&P{p8f2gp;v zgu*ji%nv%)OXXQNn5HwI&~H|;;B2q1kK^5z?V|@|>#tt85S%&F2PzUKHF=s=vR0Z( zi5G7n1b6Xl!~>>o;cK>KB^40gkX&rqQ>DD;zwrDvhNd;GgF_L1y6B_$A3wH(leYki zyJN?TWh!mf?v2Yp1K@ni7vl-iNC}A0_u%(E2kz;dKfN9I+i>8$Uiy|Zl{W3ss*f@- zIn1khxy{Ti+NWO9<*TUNO)ui0$b`N@%2Ogg+pmR_*=UH&L~TyuQ_jyGQJ}>7``qIE z2=DcyT%t>%xh&S)wlfD((>K`CZs(JyVyTkOu4iRYS95V%;F$98&bH~tb;Jxt;R@_* z&K@>4w(TwZ%5k3H`RV*1XfnZup%XVHEJ3CNIi<}!o4r2At7!F0P4$SbExvYW++Dnl zUvPSQTA=mhUsxWEL-%h;-K_UG-~Jf-7G3tQoF8gr~W&smI#u- z41E{6RAdt6ek_e0GLPblBJJsLx#8Aaot@dvgz+&o$#X5YTK1h-3@e@Dy9CmM_z`U( zSJ@@Q0H~h=wlC(hFt*QY0}QNd9D9Q_E?kseq}1%L)&hm=>&ITPT=T&&ID(b;i!p4O zv(rI+aUI9~TUOLJ(o-rBbr@u-%NCC& zn`}zpp_zMjq%hL&7a5@&217aC64Hl%bAA}-8=AlkXmsunMn*KrI(!<88FqI$Bj!KZ ziHX!~?u2?si#ld`Ws&p3EWfA1rJCAEG|lZCV3?F{(b6iU}``nV6QwK6YYg2OKX4vP|MHPyCQA`Hck)z zMBL#Q2w;jZ8-v_;;a`bpzupbLw&zkov6%Qy0*uYV9#|=_QrJ#X63Ozk5qlob z#BHs=GAq=-_>zoRk!C*k(Ld}AVe!>yK0YbyWy&F{Zmz%VPQmNjA# zR;K^*ivL31{-1vQ|Nrs7b;JKp-|FR;>%RZ=0{9;h)c?~L@&Cnt>^XX-fr)D)pIUuh8EvXqwLRo!A?|H&_6^9;xzCxM2wi6KrP}+|Ph`>poEaGUj#mY_f53 zs&v2ai)SWPS09s_EVpRsPpRuU$6JytD|`_*Y5j1_6m*RQvJK*Aeva$+y0 z0E`wN0pmO9N0H&6+o&I;fRIpJh-X{GiACkw4iJ|Kw@n zwnUlQG@Duk7PB6W#fHYM8S*0~zfMld7h;TkyuU3CX0Y6#*)bSiX+E^s$hL!Es@9Sh zU~)&2lk+^-wf}rATC^r6ax|v{*pV-#*@^sO7W~Tgx?E8ZzSI8&LtNlO%;XS&qmn8r zUQ7J7vB^|n+A1pNerW2z2bT2wDB;OxZ(2qTz{#c2_hVgiYpAyCGO_n!dCD8BJ*(c( zP0H4SHFJE!?A}EZ(VKp4a9!%O+wEY=%ZSFF1$FmMTQU<^GIWz9{iI3jz{En&UH%*>yQ9Za>Pz3D{MjvGxqLX)9!ua*0-3fvu` zsy%n9p0&JwvN7$f1ItSPNOUt$Ol;jlnYpOHcJsXfP5!bMWf>9n`*n7(`u@4^4g#Qh zPtS;q;S4U-22bQA;9`HE44M-FJDrP6CraF1Phh+JLs_!na?>`|Zawq7`c1R@Kq@km z`=e3@yYq44lU6mWS`LYi+c4ImhY!gf0?u?ApIfckRITId#;+07CEH+BKJv4T#%-J# zT^*|GJ-H(z2DNiN-{jdHQWEiRX*t@%M+wQ&j~7w;#JLXkNlwl$t?PwK72H_c)Z%X9 z-A)TFj;m!L3GA+f^3%aD1ywqYsI%=fg*<374rtG5pR-w>Dq;$|)mPo+q zX&8esC?evemDfT_Ih~uEr8ut%|1ZwYI;^U$UH8(9UNi`jBGLlVU4pcfpoD;gl>Fy3CrMm^ByXy?zeeHAh`Of*y`=0$D0vFC}%`wM(#{JyS@5Wu}D%AX@X7i6a zreC4naq@Yr@;QuTDb=a+UI{EYF zN2LPx$<44Iv$*CD^!+DOPR03kBx)3G2&BmnD#U%<&tGTVo>RC1kT&TjHN8>iP1h+O z(o1pS+NCCX))pg|pkPkiN)l@NDPXo`(r1`6n=a2z;Icqe$W9S*f0Odct`R){Q!T>K zdTf0LKP?R7gPedmv?PAVch0sAUEG)(1F2Y+ z@Qq#~lLb(F;8U?4NVp*{4<2Pyg8R*yqws{PG$S`jP*^Oq%>XN+bR#VdS}7WgSvimt zipYgq=3Rub5_xxPoAu}^Ev?-JgE^w*w2k!6w82265>H)SN_K7l@P(?D#nEg9{6Sc@ zwZl$L-1(17vO_2tpq199sT5!*bV*WI@Ld&egn4?fppb7{NnMq zmYyufjD@C#KV4F8O=Vu=y%VhgP z(Drv%dlV(?Pv!0TGgsFK0zV1`;Q+Y#V=ipOga>}_CuGbm`i-q(ZAIY|-Kkfm*h!-| z!}U*2>DMdbPiHJVuK5XfYv{G4b`3jRhtGBcv-1!x514&&Gl7BnCo~cCr}4KZDWY^D z4#@UZ)YS_Tggy0@o~yV{dsLI!OPL-RC48NEgO;?v=ty`+t@USp07dfCCT)er3A12R zG&pcX5wmB-J1=^yfR$&|R5kfzlXE8Ml<>jE^P^u5wbuoJ;;b)V!Htpo%3wT4_A4Mf z5bLiCDhq@g13^#TZaYpd$D7g(7<1_{0++A?7UeFO*+Hw|bM)x6+;;^oOM&NgtM5>g zC_Ar&?>AaL;=1)6$op>YlDJm~~7P zEgc6%e?efmg7~N$Bt5Cz?AdQvI<*D|6$o0c#v9Y0I?<^YPEqv*zU>4-VkAJmQRZrcYQ8R}OoFx>VAWp?}2b}mIarkudSsv#f zEwdTy%^d=>Uvq3bAtZLe)ye}T6v9)w%!zcm{X)eHs&FFqLhL%fNdS!LLE2&nwij#Cx zV4<+@XYg$GnLy1G9+-?!fCxjfBSRSTI{Z~YUAomyAVW*@K6m`(YOHPEInBt!8oqsE zdu)L=Y1Ondn%ft4ex6q+L`88`?Rw4z=GPItoy#rfg=DRr(w4j*W(dKI)Vf9^`KICZ zyrnG7R&A@}&DQufdo0T%j1NMOW57764xO0RADRGN0vV5PvCPD0i=%Te@V~EWW=_EY&FRy)d34J9hQP6Br zi``+BqmtYE+bYp*smgsK$}12%jjhRR%UOxgZ#SXm z#f^?K73Lb|D#cA|$L;Umh+sw$YCkI=265P#;@z~yx%HNoo8Ft3lK;#u_M`cz_v7jF zOq%vLGmG>eRd>VlpwIq(+}GPk>@Nq=FF-hnXZF7M=6SERiHVZo1C(->E`lk<{(iY? zbG|M4z<3mh*Mn`liM=%mu>7S2gQrTDz36j%0mMA53P;d|TpWT=Y=4q*B}i_;}v|S%q#$!3z|2_Cj*$CAi(2ORsZ+ zN_6rUcD8+ztO)wzK|xDxn6|@Hgj{~^wCk_sh>)(f_XgReM(aBG+j*R%MRI}n$s1o@~`}kXmp|2e`hwbCrF1ws9 zrVW!;(F{GCkTUA_*5%s);GFV%L6mha4#7DT^wJX=N=~$L+6JH%`q)>*{n5%y2qnT7 zd%VFBQJ@5Pb(w(ynt;MPF_|QV{0Cgwq@7|HuU2lm0SK9nu0zA%-H*G~q{w~JnMf<9 z32sB3W;2h%nt~_xPu|2|35hP{Pw2Wf3%bIunBS0Zj?l^rb#sH##L2{`hsCLQjKpX5 zp-gLGz7PZnaK(q=+YC*TMVb}wivxdPXKVu9zf$%3Y}OW|Y{6ap`E5!gHrrU%>yF>N zTfUFv5=|*Q_VcW)b}K|`MVfY6#_4lv>QgXy`#C8dDyLnq%G_}TqN6o(dW4}pAAHH zJ6elR>ikSY8<3Fu&Lv#gLR6=!F8tu-LuDP(5=ErrL0S5)(J6cG>0DNccL-X)nB zqd9d4utj6Mk;xJ0c_z?lFs0Rn%p`aIdxBkv1mo3!-19MSKCYv8yyOGh;_3ZuU z6naGf$Yk~V6&qKe=k@HyM;>vFo40l#1fembDza@k zSGs|Ef(pqK%7sam@Vy}DPf*lb69kM5dKv-)kS#Qi5sSn|?B|z(K}N;nw=5W9rxU<1 zwCg^YcCi1pQmd(Ca$&;N)wRhjX~%GL5{k~O#WSjvb}-Q4f`l#qBIpIqAjY}gh5G5A z&&+oxxvalA1sDPd&~fZ`!H1?r z5DzPcc#qkh;4K6__{^!4z~z z!O~~5(VSMRQtMUZU=og_Lmqu!$2X^fVmuN~m@3r9P9pt%+m<&T4LH`$a?<}`3442u z8_L=Ku}0XXk-+0HjzBZjc5x{uVL?g7F4ieRmGLEonmvb$R`EGHGF#sTCIn#DI?l0X z{|hgy>s)95?On?&589lO;|{zL@Pvl9WCR$e%+R?kfT6oYefn5S5@o) z=gi5)vy05CVAaI={%%vxdJa^Ok!C@kpq3DwmJd@quYx+H`(+p32z6R5p><4IEoglV zaP&P85sXF^xbclrJcz^D!xnoquhfEDOQn@HKPK3(M{A<|snO=mnn>P9R?;C#31xM) zHIHcG1p|-s2BOY=&-;)~T8UWFJxIobiXQL7-rr}^{b@AEmz`pL9zZ-X--ctL2A7$Q zqb<`HQHgdKom^6FM7D_h$3ko3)@hM<(LA$ttJ%>fgh-6P!`kPSk;^_3gMQmu04!#>)jn^-nuto-U9S2HI z)vaXnY#|}42`clmS%SBzf^cP@w6xw|rnuhUTFJ4E{~1Q;YSYD!xlSgdqD^lEv98yWIW4rxC`MvfEMP&PrgQgr-Ni~70q z-%I)4>vCS?1&@FHp}CRC!z{9#=SwOoEG)!2Li7tyDgFQ1- zZ*N8uHb~R4E%_NE7_o#LWV4V^Ve(VeeXAST&`7XWK}}lXT?2E^Bfe;UTBfV~szR*p zZw;xR;($T0i>$`5<>?1}A89>9O1uGZq>IN8LC}c{$QlmdHcN*w6y>070I5VAI;+3i(GV=$nDm!V{SHJ? zjC>~-*j~n7Bsjr@H<+LEn7nysbz1Gm7nqI~P$e7ev-4@04V{{T_^RRK&&SicFZ|!W za(u#_YaQ>a1I4!9Qs7z8LpZlJwD;K3YLPCTiE zJB*h3E}%sz%*FAI&E3{>bEL}NXk1XW4SnS15)V-fPA?j7NR{WQ9~1Culn`^Xu>M$$ zn{``$jIu|ksPlqLWvCbP7@$KTf=rnzI;`%KeZFsBIElO=ErW<6?#p?Qv@`yP8QqsV z@j-v;!j_7RBlaoqf;~!FgCdF$dzJb*9EYNFEu~;&hF!fdul79pX*%jl!qw_}8se7B zWjSYMgT9f1<^u-{6qV-GYfr!WQ>;n15gFVbkGH>VF9O|mXqx0-KAp{qHxw?}7A%r2 zHDy(7i2%1-QAS2zT2U0@dJcwMyZq7PyW*GwB^(^SN(vwhuXTq9e*D8*m9uSuZNCRj zypt}Z1=?F4@&ocJN->~kH;=_f&I22vCRAy!4aP$OJ$_O1g7%%|B={0H1&576K^5rU zNw_L`)&c)BRnpV5wT<%%RbS&s3KX%enKb$m^@Pc9Qt!p#3dEr5Bmrl$g1Y%HdD^eP zq3woNg82^Kj~5bxX}4r6LVln9!^x&6sj(_@*Zv6{(koWPi6DZLxnwK2b!gc0Uy&6Rk~Te+_hajRjTLNskbzDrac~x`A*yI(LOU~8?+cl?>p8L zJ`2cz3?{ZAoM`NkvJa+35T97$Tb7L8qR4CF=xo_Zi1|$i^eg>bKwVQf6usa@Fm&Jl zn5XK2ihXAbsMwoogF2~SX&`tx?d1AQFN`p|csv^;{El6oIOi`M3C~-p{B9~>G%TFO zTps;iqJRUs{>J6>xr*_LJ`8?HHwX6>=Q(X}v_93}!@=njL7n92Da)*Iuc^7b zp+N)pZVHzJe6g`rMzN}2Qn|%cS?Gq2`cx9vcp}rzeGSp}Sy3V4vz4zW;f)~fQ8O&6 zPR=E?*UpSX?k7#@l#}+53rrgNbEQWt3SSt?vVO-r=E!M1tsfatIXlFm5o>sPiKr-K z%$j>aCRPL39-A&oftL!gy#se`;dquNM$(JNNA)N?koHfqWC+%gbAB5z*-l~{B%~%^ zzoii@2?w)@oiEzs6~^(kovL{v-J!cw;z!_B{|`E#L0?3+3JayCmjmAQ+ZozK;N*`z z&BS^7TJxBLzaiQoRg6!V>88qjx?RA+O1H^H+=_-Xtd24vWnQv@LoWl{Gb7o1CnPL< zZ=Qdpi>0P|GI!ENn|38#$i9E~GJ*Io zWZizxtIa10zTLdLs&ilj+-t@XU*80D;O(oK*91{IIKxf-)AEoLs5BmrvrWdaU28K> z_wv%w+ARDauOsEb6|m^YP`vuel+`bny>x@xOuCRZxsZBp6#3=Wl6_X@w}bY1gT?Gb zTta0y;o{fD$#By1SzwG0Om#8U@G3?2NxeVzBt+uj;jQbu()nJrQIyp#&Zmo+6{3um&I5v4k~iV8$#^m-btJ@~W_;M~p8435D-> z=iBti8>83p~yDB+qqj%~<*G=c|YZccc{vs@A$!n7nld3PZ7v18rA&MH(Kex`iyUtc;^way#DvBg5 zC+%A(fZsmX#2-Cc!uis$DLAHz!YKDG9R#x~spU1<7>EX-UYhPPL};NCx;a1*3{Xg6ADgQ) zIHk`b2H;=nF8wQf%N6!52et)aA{M86i*3k`QL__uh=?!#cuHh}7T~}``|IV1+^t3m+&pJHSTzb^eAc;nQOv{$3 z=dIB!Fp-x&dECFfTni#Kna4eI6|_qgsdfjh_dQF5?wtI$?GqF4{7spZWIJ6`v?w<5 z(A%`@FFGyG_pf`l8VCEP-Vp7L)+|~MkLza;>OZN)pjyJOHcxEefAj9EaO`BAt%qo8 z^9$5_Y18bEK*iZl+sN31=$o(BV?!sYB>GSyXkd$c_Pum*HfBWUBFd-XBpe#KSGJ5q zds6|J9*zaMsc8EO{w`4~M<5aw{hE%2MI#};T)|tUR%m>^E`ZN-#n8kfd=r&!{)r4mE_fs~5jA z+T_B9h8&695^u$2M1q>0GBAFx9DR1Sd_*TkyVId9d-dC{xZN4i?+*+n zQOm0#Dt$gIC9L6III*V#J0|0+jATZ`v#j^gy=%MSwaL=D*Bh+!&*Jps&xfoJ*uZ$) z?r--CU&bsZ1=h*`{VDS42OP+4%N3qt5#ChuZIl9uZ{k7g}f> z^av#NJ~n#;^HRvSUG097f(5(x&@D*m?>|Gsg5rYs$2WL%SSm@}@qdt-xsZH`@}^&w&1T!8a6Lrr8q@nIaX$XybEci|4_-cI;z|jax7PrDi*haG?EbJ$ofo4oEp4h}f82gNyWX3>zG|Ys^-s;tS@-tHeIlSy ztdhP$<|iU8#W-AJ72$zRZNCc!B+3@PAuwmy1vohRB%rM%E8*lfeEq_J!eQ0$bK5QP`cBd>2a5d>Z-J89=h9(>*~C`7W~PyFD$<%&s)#EYTt?C z6EYx`*1m768|LM@i34qb)2nj|zfqth%PeBgXpje#4Vxycj1Z2o-$- z1e#+(3&7@Ap}2fj_MF##OZxp7L4kRrL0}Y${&U3S$fp)~XU2m}6X+Sxnq{HiYxbkm z0?Fb(sD%hksT{n~kyg5h=um=7U^f8Jr9M-iR!YLY6hR0w>xi<*1?&IVspUP&q=dAz zOEzk?kCW`ynOutOJoUCvQ9{ZfmGHp@27x!S@d3s>A4C+#GbNWCw|giIY;(Vy;T}3) z9@)F^gB@6}vF%}%aoT+JlvUzvYev#Zo75?Zj~MH2cs(QY+qnGN9Wa??AN=+4heAM? z*sRV&@?UmCoa7gXKMW&k%TbL6>BGX zQPaFSne<^GlQ0#^{R7z-$ddKL!%xnlL~}&;pvfRh-5-=zJm>XY=0PmH5Z_vuu5hQO zt?-1lic&IqFOpL8IG#NjRQyt0Rr+GF%Azs%GaAtwjVac2jDBd5UT%V$8y zyZ#yMcbJX~vJw_=tLC$61 z;Y);@^ampx?*}0jg-8t(>-k;}%9{ptCQh6a-GR%F!JECN^VHGyuU?@Z(}}{KnU94% z@?vM`1eac(S?@m39@tfAGL%39%hhQ~nz~9(*jVu^o@e?DOnjLFBZ<7)R4Ywbr?Lzb zwW<;i?j(&tjrkq@j6Lx}gOYq_lhE|i=h*8;t?rNrLr4~xM1iDn>l;+`?q^mTQ6CEj z&F0UF=yAD9m6+givrVYgS@K3dnp-CACZ2;~8j}mTgcqGQCo-e@oI#HfXpU|mOeOFV zo;;45Blf$$0xWo35<@R|)@l7jqn~A1X-WDIRUs|LFErgA^UOH#dg7knez`U*Kfh`` za}m749cn-*?ft9M8af?`kTQ&D_>*jZC$Wxv8MWKyBVEA;6IFLg*L%Csb9 zj&Bsn#sNS4plVJ68j^gE0_-!G`!(vod3kaBn-SIehOtDhs`j*elwk#fNAx6YF@?41 zC8t+@Wt-yP8(K|v5WhYIV#}z42+T%-dj*6!2FBy;{l{E|;wh0>SOoWHR%zQFlqsZS zGSKnyb6`OlkBj=2?{L*dZJLR&scX5|Y*wZAjmU~>#@f~%GG6Q~*r~cKp)tr;>J(JH zKku6EuUV#ro`I8G9M#Q4c=)<;lJ|PgUO28y*yTi;I}qtcDfB`a86CLaP-ol#)j=Wu z`KP7E2%6!^o_1Oii@;Tcj7NxM8GE9c*D6V;?q}1FcgK7rT_LzzW?4r!uMt&_ozcs} z<2USIPEM{h{6sIgny~7B|2j54ahmHlhX8)G)1ukWi)PA{7s}^ia;Jg9jpPM=sJs#T z2xM@F3!!U{UN0jmeVlRvadltUT^(F5wRPn6z*)l1eSSx@}KH+b5)NDIEECLSYVM&xt~xW6ZI0tfpC& zorD_xy)8VQ3JOUJ-M@;rrmhgkpMETewpn1Y%RX zQb{)aztU*_^_!Gys1nt!uP>=e|N0Xm@MXAA#{w`i*0pv;Xy(eK2>(aehL6wO(qpM@Vt!qqMGA; zY-9?WysryV#vc}qhzb0BgQGG!kpil=m+@m}@2e}zf|6UyO1yA{e`Evq#((zfrT=wz z@!uc2{_7h3kFQ0FZ~+=y-MWJ9@Arm(J)V23J{;m07N4Aj|D%uKf4e8?>Hx&1_PutR z_}`k8|E&X6gaHqzz_I&S*Z*V$=wDv0O5YzCf@*W-VgLUZO&JR=I=0s_(EaZtFaHny zDo+hKL&q5`x&9j$9n}jiT4t@GqxrvalgAi<*V2@LoAkeN(b9(CqO*_Y^%noPyZ2w7 zqW{PDO=H0WRuz&I{@=LhEOv0w%9nekR{zAg{V%^$B?HVP@45!M{~s6k?|!75C{m%i zrzO5W3rtmo4toNHV2-`0JB%cY&v7rRluFq7NMd)&T4TP}S_+^uH4H)HyKqlJ4Inqz zPA?AeVxyz8%PaV43*HT#c8-=@wepj{h@=o!|Mcn8q1L;Sl3-)3v2L%`-M8*3X_4fD z>VO~Ed>J29r>eW}%w*8wb{PQvpOEIK+%?~Odwa)bft*pf;2v8b8M|{F!EeoMDFyk zhY>$j6L88&Mx@>%;rs#gwlQ1dRxcfTx;*yl$r9bjN224v#5*>UExc*{Fbvjos=l%? zduX$6UnS*r-g2f_^LgAP-}!0)+pskp zHGYM;!F0ec^Vw=~ms&ta9?g{6nA+|TbUfpoxeW=CIR)PL$^~~c%lkpUM zs>NNZQzV?GN|kEQva~*JezHH^l8;SY(2`H)>8~1Wy~D(K4nz|RSBh2rmt^q6{g_~R z*|zZF{@thJ9RMO5!19WO-6V1thtYmG%)XKR_;%?xX#1P&9JxwHkj7mxSZ&sNwua@re;(mm5fyij_9CmFU+k+x zH9@-MI8eY;1FOPx^1@;H^{;A;Epbi-|Qdk%q8Fra3u4q z;}ixZ;V>!+!Z;Pc&vZkxg^&Zi2I%+R!F4(T7Uc>Z&*t2 z1c*dh4P%a}CeJV;rA(deMloR)CG9Mr%;3)Z5em0mB85&IA`=EJCzQjMKk_4uZjKVI z6e;L#*zc^L>U6Jvg0c{an*+#rtv2Nv0pSKv*LRd{cLJB7PuV^x!wMLJG5{;3mOIy*itoA-@TRmDM zMce+7Mm7F;&P_yr*U%k;Lb# zA?7Y@LR8PId9ywQ$Ec=5ja1{Y4$ibvJ26<@fH1B9w8^ zW!**k4d$yf6iB|c=dhHM+D@BH6eh+76f_Z1kB#~1%-PkA3E}TgWy3SQw3@MNl~E!3 zft4R;W7}X#7X<<(We)R%$~3MYwz?$HPF0>_v;T>QvCD?>5tP&>v&(u`0iAo9-fkDpOKQKxo|_-ZbusF*VpcT*W#~ z8P(?W<5J5!enNXw)>Cf^_8gi9hpt|7i}H>gQxdGNXNViRP`|WVkON~6Fa3vvAyG5vO1utca*pkxZT9G~**Xi$cLPYJUe};#9s=uUgST6~qXDRzKJ)VC*#3&ws~w2U)oIfid%V58J!tWzP783v!hX@x z27!FEBct#005azNel9iAp%Qgw;m()#3mn++_5?FW`pnm_fd!Q(l|t%DQO4*l*}qZ+ zdrpq=^pn(!M#e!?`fY+DZY^sPSFFh}`$!V?yGoIFv&E(>oq7dtONy1p?&eDMT24I{ zO3v3(MS@O;?r(_P%>n$}bRPlFSNa0nC$!;Q&--)MlLn`;JkdCWC1Q8XJ8$Vt zMbCEyo^&6u#FkLvMf!0ngEu(4(T5A(FKI`*MyJfbzCEiMLME67+BkTkl8V1O?pvy( zb@7TCwm*5!+ENI}Kv@wK!nxZbnm0lKL_4}Q>X&}~ae@HbRx9!(4WKm8w8ukPg|cO2 z6!JmjarG~^yPMZ&o5xRAEKtOHAAI)3$U6gk*_J^N;hSIQ?p%GH?A%|R@3Ye`?w0tm z2#>V8&eN?H$xe4FR!_C!%WwYP!UTtVfCFi=kr#VS_s9U1Nt)*O==5_!zOtoFjF_HG z2Et>H<0NA{KF$ic+WIts%W0hpiGrFZ{8-bQ)md1zdhVBN$Y$Q>89K!AtHU1p-Ul;P zGTL)ml^}>VCe(3fq#N{pnW$#irmKXP2bGkS4Vim&##!iKUBS;vy_&$Ah&jjS!z2Bd zLHW?1>*01L53S@;&DWk~=@DZgpq!Wp-I|IY`=pGV2HIp}p$jWJU(yyk1d527s9t%k z0!^PN_E-RoLfAuH*SG5k*8eMTzh$vt<0YZFytRnPH~ZREdyk3SI@JD*bz|yOB%SqV zWQI#7em@3wje=kJc4b^;9S)(h3K(jy;x$&95-N7`~ zwe;TLZgIX1jaBA(Gk!yL#I8(LTrPPtOVFTJA+! zHuxOOzdw_MI1u?kcYb`f*<}bi6nXlr%yvVsO1}%M34M55mR0_<${Tm;s(mn|N71xahpyF@iWUN(MkA59+b7FBPtQ*%H-{+0A%>4?WPo(`X@mG%TXJIuqWcW$%gug zq(2+)Yd4_!4Y!2#jwP>hKXzY3;l=;a)=CMELh#O6V+)bOdlg-(KrR?*&zYOsi!+eS zb}$B!9)1d*l}?AIr-S@Mc_()FVxKAzrt7Oz8|>whxJGba+SRbQ>Ia7_%!c-9Fg@X` zRMeOxHh%wS5nI^2hA@cY&vth>=?W9B_2Tx_@?&cBAm}|l5%$=suk8pZkSmEX#jt|9 zfeZ2y=Ne%-FerZs;v>1TvonZOVoMkle_n3*e%JpbIeBnGi}(X?&c{uBDOe$&%So7^ zj&ahIuw9-OMRz-E_ddQH0Rd(yA~eu`i}Qu(3@1~m!DnJi=2{9EqFAf*v*Ze*In$*b z?*u_tyQ%VeCFXmu^lU7Vo4FeM2SP%U&gS=Ls=hTnu|%+C9lJSpvC7g8LSLRho)m#7 zQ>O3CP1B{uum_vUZiTMnSz19;)l}z{VL4&~wtUQL?$6q86+4@u^q@&Q zWkb7_l@bji4Qz4zs1p+{#$)Jd40hE;oGbnJnc?B(xrr*Y8{AejPvC~65j%6BDs7)p z6f|M|?2A&+JEnJ)GW_Hj0-Y_s$ZPKeTuo~7fa_z+n4~yR`ecCHI3wZ7e&cZ^;Z*be z-T4)9r&2T>_T;Py_6jTNCdGo3LJcFt4{EenhV9L`aEuFjU=u^C{ct>LqWuUpT!^Vd zmOceHH)PCt?G+S*3b$96ot4li0QGvWi@rfCa7+~!&IQP72Db{&hr6f}e1b4pG~!w2 zf4K)}km{2+2Fi=ZzFmhZic&rU;a41Hd*NgItCf{6%`oH(_boY?#g1*PsJMJgo_Jqx zon50DcIpjjKC7<#oO~U5iSUNBa|zNodU)3@&|&pBX!xM19T>hvYa5X2n>Rx*u>Zam zc&QKX&A1v)EKz{jR8TOZK@M+bAAwEZ4wFVl3@xH?F6$^zY+qy{XYfy zmbO0gmNX8^cp8qEoYQ}-jI|50br`1AuqlKlLL0fQuE>m+7EVq^4chBTrin;Nb$TMk zf}s4AGu0MFJ%NgdgcVdW7hB)MUEpwhA~=kTHK_fIuNr5Ci8sK2KfI&O-~B1BttQ9r8t9cV1R={g}VX)=1_r}7Y5bso#L zpS#_E~IUefi*nhB_ubqtAvq93KGNYkRhP%*&huE(=XWRT{j8B$_d$!1A zR}#tGreh+^zcuS@0%KeScS6{RWRw7N<6FwZ zPI-a#W9}>4x91|Tzx)!s7M(_NF2Qdsou2IY# zADoS=6J#QmZTIHNs%d_L4u8MO8>{op=v{_5AI8|uaMBt>jwQT3JZWq?PmKgZHUhME z+D`VaAl}A^`Hyf8)H&?@@NeUPMoVdU`p3b32;kLXN_`hGE&%kuJYMnzZ-H)Lv#Q9I z)-%kpVIR_SohW>><#{(mG2VfbPgtjnhh2^Jk~%z$dO zhC3ZMZ3)f@=W<6aRym5SSh%vHd2kt+@DUPF?ru^jL9o}en9*1sfi^)X{BMv#Wo7YT z2lp+qb00zhG^kz#I%Y(_GzB(-2yv5{Z-GO*y>5gn$%x8FR5mvCe)w6^_XlAPA#%&? z{vwN1XPn}LqEmoZJ!;Mt8`D;yk=g?^o2U-BG#E1;m8SD0ZI#i0<$F0!gICu5`V2f| zAql1z2X4DpwC+&>Wx|HNuv?EOf-|hR$Gv3xPnR*hd6n_&gYz4!eXXVWVUs=y1I+#G z?Ndy28y!R8zvn#NtU9yt6kIL$455!@L|zR1{mBmw7xGjjut0>IzQXY^lhyDs-^ZLj zHvS-_7fdHqr$gDY9&mA*&qk`VUfIO;{V*}0C;%Nwt0YnYj43A!yq*c9O!ZND1!4pp z+Tsf3#X+`bLoBX@8 z8j32KiFSV#pBZ|_h7;Kt%Iy8RBo*0TlCK4#!Y+T-kz6nx@NvJdJP1C2ff?1?C+pP2 zZ8Qr^@z3KQc8+a*{xJ^PIsR7snE4jnZ<1Am&B9Jgw$&Y=6J0$0=^}ZJdRJDa3b?h* z^`e|H=gKLML`CqgHgmr-Q|%9-E#3p9*(V~pibiv*sU=&J_J*ZtWARX7$$lig1dgbQ zy(@cN2C>|Rm>ACMz)D!o0m0Dw*FEnv5-6g2p^MDU@omfK5mJL**HLa(OA7=HN5`&P z8yEiQ1nMaSGwx9OJIbIC+SW&(yl_CdF}g`4u5A@ko*njf9Vm^HEv!lu9x6*sJA~l* zcPF5#8+DeM@r@GDN;Fx2#lC+JB$pAjZJnyBF10gJGx2q-h-K}ef4Brh)hHulrlXO} zYGrGAUr0-Reuf?z1`B9!cc?A53BL@MG_LWYd6ng5@Fq`HRC?5}(i(bU{&0r;JgMpb z{(y7#h|j?q4xXPWu7?y>7bUG|RZK){D7-M_M8hf=lSdqwQL?7Sh6e1Olyg}H0*JT-Wz zYZhLY*pA)^wPIooDjfz)x*Al^7HVF+HK3klpDhZtI3`cn4Rl3G@F@qoRYH87WzRNn zQ{)DPfP6Qt?FDc|a%lS!4OO9fn6L(^g4TIiVx)YPCA+PS2Gs8&S^Od zoe1$N%**qfI{$opCGmpj87zg2tz8=BV z`Y&h&Wk?7zA|(6wV z5kq(!OG2mT7hz#u?txhV;#(1t)~Z`c5-v|`R6KW$PChfODEb{nsZ|91AjU+xS)$GF zxNKQlZ>=zwU#hJX6&mUFLZx-tZ*xUe*@hv#uukROmN1b_g71+~Tl7#O+Mcq}QVI-A z%DO}Be>Ai+8JuBT25V}1=dV7c)zZ;%L_%Rfb<5?%2-arvu~MEK%nYXRJB}Z&+UQ<4 zgzUohZw-%pdnGuyf9-#qzaJT44@X)YYV&oKy6Kq1T2tNl-RJ&y;w>Dmi5z14EbQeo zTzamFogIZ4XSB_m(%PBwY*$lKoc7kH9~Q$^EP+Tw!4F!^%GU<<5GK#Z&C>2id}lj5 zEG}hu=sz#e*IIG&DWqM`*OMy7-s)PYN-z4xIKW&Uiu^2DJeOtwA!E!|t8<|25G$e= zVCMK&q)H@_G1~sJpx5CQHL$8!xy?UR*HR-H#cD}=# z!F^ch_2*}pplie|{OpSUxpBgD%gT!P`q~y4+@I0JMM?R~Fs&CVFP@#u2)xn~SzCqg zAN!=+4`+_6^Vh6Jt?6rI0Awuwq+o+hSNuC-frBKPxPx9YOD$-g=u`Z?6d>8%UMtQ- z7}H4AH+-;|RyIBF+Fg+1c+fTa`dByL`(ECT$f_9B$ur6R6Ava7(qdzQ|J!I6*Xz0T z%8hfHlgQZ`-Ujtm2oh}0&wqVAfS11^|0q<8$AD}Mv zFK}^7KxP`9O{k_o&&r%?WGug6}2dCqnhC3PkdT-zSVyI{=4;F;m z+S@L9q5)b?B~R0-W%Q%!@P$;``R;M>Y%4=d9`Ua8CzMGbFe6i3rC8MmJXF95(Kfn= zJ2q&2nQ5d_cCkNe@2P)C=o0@+ck}=H&%=k7xQD~*rV)H3Q32S#j=Yj9XzBgRABVxy z$1Bq>JuL%v;@Qm%#k8Hgh6lYH{sf%=C$TgKyUR3KJjch|%mK@JXSM)3MxD_&EyRi{bf${>v`k;d>D$ivf7MT|idp>2$9r0Eq7|Nl< zgS9wSOlL%5o+juj$>02;!FJ>9tex}Bc;IpNg8O-0spowC<%qW4=Xbq)lE``Axu_TZ zy8Vv0Y}|D643%9Bz^jiceQZ>XY~2dvmYuOL(u(wp=rQ2L-(O8_;ra>vSXY8J5c|55 z)W}s$ZQS(U!&8RuIN3ZmoAa}LpY@OZ7seEFM*+NdYTq4dg*Ye$Zb#GNwuE1g6Ux@e zz0C${ylDJNCCi6)icc~nx+NF|uDdhy@61E-!~@STZ6w-lD+_w* z2nT^h+VwAs^r-U#jDg=R!=sit7y`M5iNy%(j zwmh+FZ?Cmx#n?VqH&*)c0YkREn&l^%Wr%6co&C1#OIw=hb6pLu#H3%3^O*p{#q~CD z5eeh(cXLWONBE3glDWQP6J{I{nUvhr$Hg93pIWL~-)0Nl+Ql+3JkE=v-d-PI*v%{j z;kfgp5Ph-io7YB;%RZErSSBcDrJS7DcAnVeOf~-48cP`QGK14nB?eaW_1WN{rlI$S z3`;Z3t-M<%|D{N-dA;>&gaRJYGL)L5qG>G(Pjd%}?U(J4{I5M8jr4n4KM$E zf-OeyTwpqYEdQ*R50W8dewW`x1N+(5&QCbGyfj>HSg(NoXX5OkH|NhVBrN;}lVVTN zFeZ9b^7PNJWP?_lHe&I!U?X-g~_yEqtPpa zWX?3g_131Ji7c$jw>+M=^^41Swi{vTvJU%abv&YBh#t63Mi1R=kou^a+b8H&tA$BA z@m67b2V2cMDlisE9_s@F9pXq0V)r3N~!jF%0n} zN8c@b5D{EzSHKjM+b^&7JPB8mYbiwW1FUQPYu{(*a$`RxRsQLI*yWPK4jnqES@>#^ zi_qQZ3*8#aKUS~u&NpkhvGKhX@?7@M_1TgX{csImyqxV{H68caf{z0NzX(kMXE8^?cVCcua(h!u_ASPHd}!(Cz)BqIANoa zp33%3I+wRYpG%oxSrH!VIaD>n%2F>UytA4ln7o9&UD2DO;6`T8z~WP@dKXfj&4Q+edTg+P7By z6RLnj;RDdVWJ9f_PJ<+Qaq-S=6od~FYuOpJ!>nNL!T0d?mm=O&lDyIv(4ST;!^*@# zH%64UXYhi0kpe{~pz zg#0S8m)&yu9yat1|Da=wZl`l3JEem6j@&1Qrto>ptESbJr*_iX^77eN_Vn}g*qJJh ze8;n>C)B7sVutEiBMu^HB|!(P7!hBVo)V%YHZ;GCKnt-MtSX~(5YKjsqfO6RIIj~MW|1q*=EKJzq&wcpI0RbxI9m3`4t@4LJSG1^#)UGiv*rcmVy{#8}=flLKf>+BeQgniI(-0l(xxttEV!630B?^3l_VewDEZffhc1E=>WMlAQ>5zq;4~xE5(*H;5)u zK+luA+Y+AQB3iv{k5_^KDq-Vd)8=W)O%+jomf*5CcL_#qR_~A{^vpk_Q*}a=Pl2%V z?T6&<8JF2UC<%q> z&V!)M_@9I&R{Hu$2GqD%l40d?V!@s;PmiN<8y(!9#xr3K7xQRoYaaMa?8g#=kPx5B zTX?EE5I>pkwsns}HtEnG{N!-^gTGG4LuflkjEs!BCM{VsD^G*&ZoJ)?S{-L??d;fQ z=@9l=dJFSIx#|T5LcN6mD|R_c#4~3vh0_}T>eX$nZHdrNuc1UGs_tu&M_QRKKGBBN z%a$DTXhJc+ zg`F^kk5`3|FDNM~qh=ndSbp>4q54|T0}Cur1#O_UU=bwNzFBW-r+s^XBvm=KYwx7l z6f(wvjtfx>CZX<;M<~@g|ES}i-!a7dNK+wHQnd=aU5baJIrT350O~%H9rs^!gqUEO zAh}I>Y&yx1Vwh2E9e!JyUfUC@p~n2;ss(qa%QJiBieV$kiL|9$#Y_M7<<=kt1I5QV zsS(b`3_~A?+$BNaj1-;@$$q4Kone?RuqSV)Gf{iuhPKfrR_n4E4TUGEYM_L)ZAobb zud&F6$fVKzsKa{Xu)HXDrX>sekycv-jq|bEB|M9N-MQPD@B7z}D&2yYm;;kfAA`MIz z(7*0|K2Vnx^Wr&nfT(;$z^_|2s`m|bi&kQlaEWcH%f2S`|1?>PpV)jOTxGqXIyU26 zg+WYr)gWj8>1YHR{1R~zE(nVazO7=6d3J+a1IwYB%?!Ia5zR(7&7TBkuC;MX|6K2kGXELddoDy@e}9-(_NGT^zcH(e@On0lH)r{$29q zmoN?8@iDP42`8It>CbzLceF$?bs;sKFpw+p%69g#A2mAV=!`RdTK$8&{?5f=fFQqH z%c%1k!}Xuzc`KL+;99;uybapoIv`%HYMMgQH;m~8emw)6PY-o8hb+JO%BA?NM&W%V z-+3*Jg=ap8gjK*YGA(ywps7Z)(q>dCm!e3J#-W48p3SM8cz#KQLd599!fWz~a+5UY zxTyao1Nb{`{qHS&ibxi?)qWfFUs&#aE+4sFsf|P;;qYpab&QlEHFVG8evRu*zs7gK z^DmPoA65jGK@ORkq0))E8J$|UVzO~Y=c7Q=l}F4sM<}3+M^N)ZfUjofXgj6&YM;*3 z2C=;4|2y*|u+Ih-=r3<9NJt-Q%!hEg_!kcg;!Na(;N@Bqy6w(sg2=7{mrZCqSE+H3 z`DmjTlUF2x<)bJwSd=+jAaMFK{cnC}fOB5}lQ$MsjGK8c&|zk+bV2AEQ8(5s3ucLr zU;Ou*>VK;hg6%LJRF%h#{wx11Ncj7kyvsz=?wND?(GkD>dH;DAm?b!8osp5`|0|dN z@3Z^&9r(1sYx&1S`yBr=Jp6svzi;Vy2r$aTSNo{{Jl6lUN*)l3B%wn+t%N!LKVF;& zXm>JR9+SV#fBtcTbmz+>@Y?Z5yKLR^|2&=g>)Szi!3Ak?zYFg-FW~=sH%cVzPPvh` zQuBXr{mtJkkb;K(&+aJy+~of2X_As<1h18CET<;_|9Ejf7@J;FP;mbB{QVtq{M#1m zJoIG#u>eP-dC*K{dxSIz^xQ6ZJ!P9CuPD(vr%jHA#r zc0qp#QArUXEVj%NhV0I~K6ktF4ykkgk!%ZCJ4$W_Q^JGtS2wW*=Y=LV=Cf-4g{AMf z!{NGb-@eVSR!gh1Zq#Mb21B{Fxp{LORpMbOQrU94*RrPZpodvo$MpdjIZ?&jQJQAS z?iS>?4vL&IxLU0r@c(=ca#bGz*{EZ7Z2Z?T1MHo5buOD(b#A*^A3twES#`D^A<_AN zvF3B2x9F);Y|^MqbhJ7!y#erxdIHhx7{E&?fRZk!1)bMX?M34-Ozn6RFfCVG8A-a&}~=i7l()C*S7%0t({MAt?^2c1>To*vXvu9EAWW}W8A_W1aevRtdy7Rh#URM&9e*3K%6i?BC7dy4^hj@<52QkaX z+w~j8)f@Ao-0{1&`+jGX<$H#@)0+?~ZW+x%hzhdQ@>zA^_Zx05B~6%Sz(wul8s5>8 zTLXVJ@twxf&3ZRFZBWu-h(?Q82e=7nS{8s%@?tPgvYrU|M9!hL-i)kZfagG7L&MJT zFFE+wQNuHAC)lf#cKw8zjCApG8C>x1GV215;Rlx&2SWu<`e0+keh^+y zf~xbYt#Xp$xTx0m%=P|{(M~Ra;Cae|1-tJd6rm%1mJ5vD71-nTn|(`H#11Py#CL3e z9d9Vwb%Hs%!H$y`+)pN&O8_H*)~#!jlmkmtI?5}o#pFnJ*dK`2O0q*38dg}pWfUpU z0j>>Y!8zqexskY+!JBmNYux>*OWqCzVf8%=*v?bmK{dC!PK~y)pRO8VST^{U+N&~- zR?TRF=-mqy4|Ca>sVz(^EiojhGMcZr)vFGB@kDAO&=7ejItgtqVV}PQQ2v7@M zE1J=Z#S8hIy>C*e!7RB0O!hV?`0nYdq>IVyri0RXy-rW<)ak>+hKH6J+}$g*%f8(t zr*kdzgiBo@2{aE3U>!y(d_%Nj5w&4gUDLXUPWwya1}y>Shf5J^`fxE}?h`?6js>G- zqXH((-?4x8F&v(Se%N~3A;K*@Z|KyA~3fu#!ZKxX5jDf;Xm9iT| zYY=_XZW(W|P#Et|>IAhHJlmDtG=q*~wd=#=O)f;tD_%m5MfyYVeXZ>9WX zKVHAqL^*OAY~ z4E}7!y~Wd&ngF7ZlUx1hw!(3Blb}2X(SP>2N6q-raf=tK-_L>Nmm@ zTu{z~C?}Qtx>UQK#GFew)i)URV=TG`pTM;;9?(wOx?gEMCy3&OoLgQUiRPtcE_ zKZ(+xBFS;?pTN=LXc5XDn^RqwEaaHD0bEZ=$1|t|wyNi9&yT9GQ($oyYkpS|5&|w8 z*!(scnR5c?Kti(X&lj>j+I)RZZUDn^YXeK+b-O(xIVHcdQH@LSE(c2QT_QS;>h#-! zhujUi+;ae#nx06cI*kfmBO4h3Sj|v!?%{EhPxttzr_;{HAl{^0p%CrQfq7vLS*af4sUfagLZq-cDPbXzKd^A=!%V38=gKl~3-u#_h5Lj% zWeO&He!TaxQ{Bo;OwYgb{k((kL8R?ONo9H1oN1GH3S*ZD)R_=2#K@?TpU}{v+Bh$~FUuY}(R(`V#$5Pa4y%9&B`q-#6$4;<8P7 z>#YA;Xlpi_2F+M! zi{$J5wLxE$84%&!uUT0|V`Gwh!t!-jw?-IZ=D8!VB(%vp&-MB7L8I^L7b> z-+)+vu_ZaSsTA{Q;hA!Ul2+cw;|0dYy!WG*NrvK(9OryY67hM2*ZwyvJJQrO$Y@TK zmc0B<{y>XDx=r04=k@2ZM<=~(hSSqMsdl%N8^ASN3}2%hFEA*w!)O&3ms+8j zCj&TgbEFH+a3CFZJMUr$`Ca`X>t;phkt}uPQ)QbkZkQ!uNH}SC=w!}Ir<&6RSQW74 zi487_hPt(VH22BQJ0j#i7yKd-9R-~4sy^g+5DZG+;n-DZ$Y9K-AEe*oXx)#dnJxfR z!(W7y*D~m?-xX_JM?3UjkT;XH2OkZ}EMClod_2ic$oZU~Cn4y-Q6~2uz~t|nPaF4v zsnR{{v9x@}T$l02%@f50DF9_Ev0IrrL>UMIFL+YN|FSseUCgJV_aCv*oKQs6(+-1Crdysz+IQZkwYxWbN z&!p@{J9m{BOK@|gXS=W7-oy7##3fWgBn3^U9?ZVn1%5e&Y^{4_J_JoEiq%uq4n*6x zDm{->w<*I#KqOD0=fQw}9rcyRT3T+U+3Dp;^F%i+G+E4}X70Wh#P0#XIDK72jHO}0 zT<6g~S$UoHhD&Eh(^Jeh=j{M({yE5v2zA#(P+Hs9HyWz7C?3jvytzh#DJxdxx()YX zH%dtNf(HhcYgLrFZqshMsfBY8wvx&{L$rdWV(1QU@Oubi(Jh2I?`IoT6mXplNCY8& z14V1cikIgM?Cb-SvLu_;?;L#HLA~D@^5HhZA+0$DLH(4W(dtq3Z=_X3y^`81FnOF6 zi~6_=m6bX+Hkm0Yz!gTwgTbYj6`~xRg&))x=P|cM6nsC!x2{*>aBuN(BNxk#ECS(q z_JfYCE zeq!+`!TTQo!LqhusnM0zBI5|f?-_khDHMgo$RE-(PjH!FkEaHB)j6#Zvs-&y9wiy< zF+GLt$TaGG4~01ZRCc0s%jf(e;)kMi2*#2gv@yF;F{39j;nmdP^DD5Xjn|wpH^xY{ z+)H}~v}kiLYHarw=B9ccbYQ~m&F4x@ca?L)v~*X+WnL;^7jeZbR(!J2%zyi{kZXZf zDeYCdIOM6cR*00Xaz)5G(5*uKm(pcU0}vvg*OAZ(6U3>787tt5-u`KgR5RE0e&nkb z7a^U5*4nOGXx~M_QZO78CR86mG#MIf zSBRqH>69Js)?xW+)NrY3w5nGmPv!`Egg>rJT*5iJHsaY-&G5+}kSK*6u~CGKs$+Gu zpV9%#lkB^6B0i9I!`cB8ce=%-{| zu?8vQ0U{34-s+5f|3J-qC9LiH(?oT0#R9QEJB!OuIMGc+B=$DY4hkS90&XW{qv28@ zDgvjmpzc0{T5}y**7tN){{@kcD*ifjxL$So!WEM;U2YjQB9j}>o-!RVx#}T=PY-jI zPNo-nbGSR-ezP>&W|%_m#sUg?c4_m~10XllI)5&dw`lU1nI|7?wn?N+IN zxiSba8EL}Yma1X05I&bf6?o0fN%EyQR=Wj59@R;wL+d%dMJ1R38lIfu*-6hZS-y7q zX)}2KV>BYDLj%|mvEA6GkyC@9h95+Y1q>+;(E=ioZHHeDVi`$e_->D!gG;R=33mm? z6VQ%-raK?#6y?MoFN~-D3fo$ywj;cqWKI}Dc0J+eQz(}OlIzUaC4z&pd10Y}Bs8cT zDo6}{S%3C}g4N|i!R9`p7g95)2FJ)-MaUBif#|6s!AgN@3$m z*YbYJW!4Ne%zxluj{F*DY;J+DKF_4VE7S zihU9dJ!lE-TCHAE$}*k{JG83ov-i%7wv`jg`C=+q=kIKOy0ryz?uN$)g{m z#y*F`xOVCkHsnL-wM31FBU(PRHmGHaJ{PAJX;s)3u&_25dYjMY@NH#0$45?iosUfm z_pehTJt+Jf7)+U?Iqrp%8pBa0UKbVN4~%R#FgMiuHFs&RpDqNLMuv<^pWID(&5*?vH? z=$!ESx#e}N0O2kzPQy845?c(FQn& z!ciQ(vv}YW`@i8&!v_V`rR>jXp=Ox^s1X(L#Z})$9S+`dr#a=bRC9RxQqd@Djo zrBAG`SpcHhh_{xqWnwg~o~W4{dCmS7B4Y!z}@@o-X|-u)eyYb5^%3@)kxYG8A4&=4%*9OOuHE zh((&x6y1n8ZBQ4eTHRfx`opF4g+yz!RmU*eE}T~T$qc1*w8kX2&d2ptEZomW@Rw>O zE#$1Z^%$mq&Q8w_F0T93Ub{z=lO9&hjk_83?Zf{0X~3t0+M_PWr;{AzJp(aGv{_V5 z7t1hXxH9r-E)w261KxWyen0PY83l1Pl9*F;n1ke#7=yz_4YvFja`!N+Un7Wrc-PoD z66R+wBrn~~d2Oe1l2vY1+G~DJ;&Rb0$@Wo>`(Qj(_`}hAF%7MO6hV{As}sDjcha@E z?@@ydS?<@rzXQMHXVNw2k<=WYq&perOiSKQ%qCjz)Y0s^9VqW)P9>-+^K75!`s1Tq z+8|BpDLJX4vZu=7XU*3q%8X0c%RF8r2j9nat*oVKHdr{0ao84A52~B(u_1Aj`bA7$JF5eBE9fV@n`UL{t=6Qeb7`?NR9y4AsF|6*DzOlq z@$*aap5TG|EdPc{6`&^V@tg?*Z2lG(*}iG0hX*j zAlb=6`Y>1y(b~r;U0I&&BPB%kyi`jn`wJUwdN`@!d;_84@xmlvuS_kI^wz* zms@5YB`7`(Y37c`S6Xitid|W)%dNs2H(`iT5aqPnJjYPKiTXgrY+q?mUiS_Gmd{K4n^ zD~c2TeEz{&sqKq2aG9<18G%fe=O#LwC&CjA^Hrfd6L)B6H6Dv}D!xOP#!G({hSc!c zEW8|B>MT5}#~M=czMdUHfA6jTP2StuoGqOx(_$!{`_icrX z=oKoV-wUFI>FzWtgu*LQokp*7@`JRM*Nt>=ldqj8eWX%}kt&2W3c96pJVY4JWN(DE zxGi7&TJMNJA#!rr>sJ54_2#cnQM6D(PN5N0CU8PRu1IkzJf0>GT%7M#PvR|G);_U+EXKeGx)vFg z?%u-hn1zzr00a-rgj4xk7a-3US{|?t#18dz2xS|$c%B^NWD(MFlpAaG^FR)t&3Y=* zDJd)C>D}r^6h_uGy-M>Aj1BE+D7k99X^woCj$d1UN6^HEE@MaqCTmA#sfxsClSV1$Suld=sprj{g73Ma1TA-JD$f%%m}i&^oV~nGXeV3(f_TLta@4$*q^xYP+bi=-8AWAK zcC7Wo`=E+8Z1eVb_oP-9*}Fr04&2Q>pj1B=m}Hfa_73>s{0MQv*8N&K|5*6WuMsv2ZbRq?b$aL*g&5jA&B}nG*Nl<| z%!RmdY=gI-GN3;L#`qm}ptnAS%3t2BXoKN+%i?fd+IDLa^}u;-+GO>qv3E1rO2N#&Jr@YMUNZgg1;-fzmNYz=30U$sb_p*1iZY>9iUwWP)b|-h$NQ zoO4{=(gUN3lBD>LJMSbScUwU>MH>`m4AuiGOi$3Ywbjpb*(<)Y^U^7T!x1uyy37#ME5(2UGX zajflEVF5ACJ!*g0EkfNt9mVIaAu_;UXs*E3S!&$Wx3*OvP3-NB8#ucUBLRBz-|6huK;FIU#ZY3QO$L^1E0A&x&S3LQGCxaF=sC8M5%1lHw`{`| zcMNQvMO&}o326(XaXiqojuQBIE3(PVk+S5GsrUW|;B@>A1x`KumCR0L3TX<#<$X(Cl?Fmm;_EysN5DEVYgXy%Y@)J6n}Bjm)LNPrgWf;T z3kv;SCUrgQ)_Q`!0)tBFR|7Joj`7AG4$+JTNv-j+YyT#41f2jp^`TLS6d`lsLa-%f z2LZ5hm7aGg#~%UTmRjZma#AFDAXY{HEK$(GXyyRgR-Yysqjyj$Q$GaSQXJ#&MR_l2 z(c^#_Ulq-hR3GeWqT@U&8651bc+U9}*o`=dyY!N-kshisw8+N{QxQ?cpg;`|W0W6YY~H%&8_^cV?vP)y-OeB7L|K?$1Lj%qgDNU_!YU zwJ>t2c)tk9iG$m#&%hmSpN=~jDuvO;#&#Ri&Q>}7I?3t{Uv<`?iMb=hljY zROf9L3@k3AHT()9;{H*!7y^SY3i?^@Z7!hc?@G4Zg0UPWXj#zn25*}*#FA5>R}%3iZ=e=a{U+*3XKJ+&CIIK{k>GrEu)cni+dcGz93i}k z+k3M>6=P!yoa?4rak>{Lu_rP?E$V}D;T<2g!3SX6s7$AjC8ef%I+w_ywRnaleN#>y z+iG4}xo<1384>P`SF_vdL9A}HMLyN0vcFmCH?p=6hi%28`7q>XP9JTJ;=9xa$pxt& z$4#>O7bZY|m=7&CF|cNt2vRRKF|PFA1T8bny*QrzpZBMX(O1(o$Jks%C711$Hv$ax zf!#IAu06Kh{<)II#o{HQxj@iwkex8TfV)_P)E{40{kqM`In+FQqq?`(oB2y9!R8|e z1uVCK%?SrXyug7r%+Y-N(HQTA!YX2x4ssuKD50uf43>0o` zjC^bB(U{=irily>(*XEgUVSVDmulZ@4!aGtF7F9fUg(edD$$XFZ$k_ZCGWn4(im+O@|x5%1gBwV8kXvd%O;>m!$)0H<$doNU8^$zH4-dphg&h1BPI*f|MI%Krum zDmsXP`^zs%cVL+Xa)QcvIy)4k!QZAOZijMZJ2fIOq&&DlF?gGXmt z%vA~F`R^MS%2;UMqAJB2V!YS>ZiZ-fnL`hFj3d{XXI-C^tF%yN;bQzhebIk^8oB_6 z>fJXz)2kne|DU`4XMb!hwS@{#^4R1LKaC_Fj%5 i&;Rof{pZAeL*13gle?B0ZtuJSejdoH%9Y5N1^yo?IIjf& literal 0 HcmV?d00001 diff --git a/docs/knit.properties b/docs/knit.properties index ab2508a114..2028ecb416 100644 --- a/docs/knit.properties +++ b/docs/knit.properties @@ -4,19 +4,7 @@ knit.package=kotlinx.coroutines.guide knit.dir=../kotlinx-coroutines-core/jvm/test/guide/ -knit.pattern=example-[a-zA-Z0-9-]+-##\\.kt -knit.include=knit.code.include test.package=kotlinx.coroutines.guide.test test.dir=../kotlinx-coroutines-core/jvm/test/guide/test/ -test.template=knit.test.template -# Various test validation modes and their corresponding methods from TestUtil -test.mode.=verifyLines -test.mode.STARTS_WITH=verifyLinesStartWith -test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime -test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime -test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread -test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered -test.mode.LINES_START=verifyLinesStart -test.mode.EXCEPTION=verifyExceptions \ No newline at end of file diff --git a/docs/knit.test.template b/docs/knit.test.template index a912555a43..727493c662 100644 --- a/docs/knit.test.template +++ b/docs/knit.test.template @@ -5,6 +5,7 @@ // This file was automatically generated from ${file.name} by Knit tool. Do not edit. package ${test.package} +import kotlinx.coroutines.knit.* import org.junit.Test class ${test.name} { diff --git a/docs/shared-mutable-state-and-concurrency.md b/docs/shared-mutable-state-and-concurrency.md index 316d56e5bc..8b83ad0b20 100644 --- a/docs/shared-mutable-state-and-concurrency.md +++ b/docs/shared-mutable-state-and-concurrency.md @@ -24,7 +24,7 @@ but others are unique. ### The problem -Let us launch a hundred coroutines all doing the same action thousand times. +Let us launch a hundred coroutines all doing the same action a thousand times. We'll also measure their completion time for further comparisons:

@@ -102,7 +102,7 @@ increment the `counter` concurrently from multiple threads without any synchroni ### Volatiles are of no help -There is common misconception that making a variable `volatile` solves concurrency problem. Let us try it: +There is a common misconception that making a variable `volatile` solves concurrency problem. Let us try it: @@ -158,7 +158,7 @@ do not provide atomicity of larger actions (increment in our case). ### Thread-safe data structures The general solution that works both for threads and for coroutines is to use a thread-safe (aka synchronized, -linearizable, or atomic) data structure that provides all the necessarily synchronization for the corresponding +linearizable, or atomic) data structure that provides all the necessary synchronization for the corresponding operations that needs to be performed on a shared state. In the case of a simple counter we can use `AtomicInteger` class which has atomic `incrementAndGet` operations: diff --git a/gradle.properties b/gradle.properties index 6a1ae653f1..18b95166d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,14 @@ # # Kotlin -version=1.3.8-SNAPSHOT +version=1.4.0-M1-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.3.71 +kotlin_version=1.4.0 # Dependencies junit_version=4.12 -atomicfu_version=0.14.2 -knit_version=0.1.3 +atomicfu_version=0.14.4 +knit_version=0.2.0 html_version=0.6.8 lincheck_version=2.7.1 dokka_version=0.9.16-rdev-2-mpp-hacks @@ -36,19 +36,21 @@ kotlin.js.compiler=both gradle_node_version=1.2.0 node_version=8.9.3 npm_version=5.7.1 -mocha_version=4.1.0 +mocha_version=6.2.2 mocha_headless_chrome_version=1.8.2 -mocha_teamcity_reporter_version=2.2.2 -source_map_support_version=0.5.3 +mocha_teamcity_reporter_version=3.0.0 +source_map_support_version=0.5.16 +jsdom_version=15.2.1 +jsdom_global_version=3.0.2 # Settings kotlin.incremental.multiplatform=true kotlin.native.ignoreDisabledTargets=true -# Site deneration +# Site generation jekyll_version=4.0 -# JS IR baceknd sometimes crashes with out-of-memory +# JS IR backend sometimes crashes with out-of-memory # TODO: Remove once KT-37187 is fixed org.gradle.jvmargs=-Xmx2g @@ -56,7 +58,6 @@ org.gradle.jvmargs=-Xmx2g # https://github.com/gradle/gradle/issues/11412 systemProp.org.gradle.internal.publish.checksums.insecure=true -# This is commented out, and the property is set conditionally in build.gradle, because 1.3.71 doesn't work with it. -# Once this property is set by default in new versions or 1.3.71 is dropped, either uncomment or remove this line. +# todo:KLUDGE: This is commented out, and the property is set conditionally in build.gradle, because IDEA doesn't work with it. #kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.mpp.enableCompatibilityMetadataVariant=true \ No newline at end of file +kotlin.mpp.enableCompatibilityMetadataVariant=true diff --git a/gradle/compile-common.gradle b/gradle/compile-common.gradle index bee61429df..0dc1b5c014 100644 --- a/gradle/compile-common.gradle +++ b/gradle/compile-common.gradle @@ -3,10 +3,6 @@ */ kotlin.sourceSets { - commonMain.dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" - } - commonTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" api "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version" diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle index 93d371a21f..b52cfc5230 100644 --- a/gradle/compile-js-multiplatform.gradle +++ b/gradle/compile-js-multiplatform.gradle @@ -26,10 +26,6 @@ kotlin { } sourceSets { - jsMain.dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" - } - jsTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } diff --git a/gradle/compile-js.gradle b/gradle/compile-js.gradle index d0697cfd3a..55c81fe56e 100644 --- a/gradle/compile-js.gradle +++ b/gradle/compile-js.gradle @@ -4,25 +4,29 @@ // Platform-specific configuration to compile JS modules -apply plugin: 'kotlin2js' +apply plugin: 'org.jetbrains.kotlin.js' dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" - testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } -tasks.withType(compileKotlin2Js.getClass()) { - kotlinOptions { - moduleKind = "umd" - sourceMap = true - metaInfo = true +kotlin { + js(LEGACY) { + moduleName = project.name - "-js" + } + + sourceSets { + main.kotlin.srcDirs = ['src'] + test.kotlin.srcDirs = ['test'] + main.resources.srcDirs = ['resources'] + test.resources.srcDirs = ['test-resources'] } } -compileKotlin2Js { +tasks.withType(compileKotlinJs.getClass()) { kotlinOptions { - // drop -js suffix from outputFile - def baseName = project.name - "-js" - outputFile = new File(outputFile.parent, baseName + ".js") + moduleKind = "umd" + sourceMap = true + metaInfo = true } } diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle index b226c97a57..e72d30511e 100644 --- a/gradle/compile-jvm-multiplatform.gradle +++ b/gradle/compile-jvm-multiplatform.gradle @@ -5,19 +5,11 @@ sourceCompatibility = 1.6 targetCompatibility = 1.6 -repositories { - maven { url "https://dl.bintray.com/devexperts/Maven/" } -} - kotlin { targets { fromPreset(presets.jvm, 'jvm') } sourceSets { - jvmMain.dependencies { - api 'org.jetbrains.kotlin:kotlin-stdlib' - } - jvmTest.dependencies { api "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index a8116595f5..caa5c45f60 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -4,13 +4,12 @@ // Platform-specific configuration to compile JVM modules -apply plugin: 'kotlin' +apply plugin: 'org.jetbrains.kotlin.jvm' sourceCompatibility = 1.6 targetCompatibility = 1.6 dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" @@ -19,10 +18,6 @@ dependencies { testCompile "junit:junit:$junit_version" } -repositories { - maven { url "https://dl.bintray.com/devexperts/Maven/" } -} - compileKotlin { kotlinOptions { freeCompilerArgs += ['-Xexplicit-api=strict'] diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle index 378e4f5f98..4487446799 100644 --- a/gradle/compile-native-multiplatform.gradle +++ b/gradle/compile-native-multiplatform.gradle @@ -13,36 +13,24 @@ kotlin { } targets { - if (project.ext.ideaActive) { - fromPreset(project.ext.ideaPreset, 'native') - } else { - addTarget(presets.linuxX64) - addTarget(presets.iosArm64) - addTarget(presets.iosArm32) - addTarget(presets.iosX64) - addTarget(presets.macosX64) - addTarget(presets.mingwX64) - addTarget(presets.tvosArm64) - addTarget(presets.tvosX64) - addTarget(presets.watchosArm32) - addTarget(presets.watchosArm64) - addTarget(presets.watchosX86) - } + addTarget(presets.linuxX64) + addTarget(presets.iosArm64) + addTarget(presets.iosArm32) + addTarget(presets.iosX64) + addTarget(presets.macosX64) + addTarget(presets.mingwX64) + addTarget(presets.tvosArm64) + addTarget(presets.tvosX64) + addTarget(presets.watchosArm32) + addTarget(presets.watchosArm64) + addTarget(presets.watchosX86) } sourceSets { nativeMain { dependsOn commonMain } - // Empty source set is required in order to have native tests task - nativeTest {} + nativeTest { dependsOn commonTest } - if (!project.ext.ideaActive) { - configure(nativeMainSets) { - dependsOn nativeMain - } - - configure(nativeTestSets) { - dependsOn nativeTest - } - } + configure(nativeMainSets) { dependsOn nativeMain } + configure(nativeTestSets) { dependsOn nativeTest } } } diff --git a/gradle/targets.gradle b/gradle/targets.gradle deleted file mode 100644 index 08f3d989aa..0000000000 --- a/gradle/targets.gradle +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -/* - * This is a hack to avoid creating unsupported native source sets when importing project into IDEA - */ -project.ext.ideaActive = System.getProperty('idea.active') == 'true' - -kotlin { - targets { - def manager = project.ext.hostManager - def linuxEnabled = manager.isEnabled(presets.linuxX64.konanTarget) - def macosEnabled = manager.isEnabled(presets.macosX64.konanTarget) - def winEnabled = manager.isEnabled(presets.mingwX64.konanTarget) - - project.ext.isLinuxHost = linuxEnabled - project.ext.isMacosHost = macosEnabled - project.ext.isWinHost = winEnabled - - if (project.ext.ideaActive) { - def ideaPreset = presets.linuxX64 - if (macosEnabled) ideaPreset = presets.macosX64 - if (winEnabled) ideaPreset = presets.mingwX64 - project.ext.ideaPreset = ideaPreset - } - } -} diff --git a/gradle/test-mocha-js.gradle b/gradle/test-mocha-js.gradle index 6676dc9268..7de79b9939 100644 --- a/gradle/test-mocha-js.gradle +++ b/gradle/test-mocha-js.gradle @@ -86,8 +86,8 @@ task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) { task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) { args = ['install', "mocha@$mocha_version", - 'jsdom@15.2.1', - 'jsdom-global@3.0.2', + "jsdom@$jsdom_version", + "jsdom-global@$jsdom_global_version", "source-map-support@$source_map_support_version", '--no-save'] if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"] diff --git a/integration/kotlinx-coroutines-guava/build.gradle b/integration/kotlinx-coroutines-guava/build.gradle deleted file mode 100644 index 16bdea50fd..0000000000 --- a/integration/kotlinx-coroutines-guava/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -ext.guava_version = '28.0-jre' - -dependencies { - compile "com.google.guava:guava:$guava_version" -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://google.github.io/guava/releases/$guava_version/api/docs/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} diff --git a/integration/kotlinx-coroutines-guava/build.gradle.kts b/integration/kotlinx-coroutines-guava/build.gradle.kts new file mode 100644 index 0000000000..53e91add44 --- /dev/null +++ b/integration/kotlinx-coroutines-guava/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +val guavaVersion = "28.0-jre" + +dependencies { + compile("com.google.guava:guava:$guavaVersion") +} + +externalDocumentationLink( + url = "https://google.github.io/guava/releases/$guavaVersion/api/docs/" +) diff --git a/integration/kotlinx-coroutines-play-services/build.gradle b/integration/kotlinx-coroutines-play-services/build.gradle index eb554866ed..29ce3d606f 100644 --- a/integration/kotlinx-coroutines-play-services/build.gradle +++ b/integration/kotlinx-coroutines-play-services/build.gradle @@ -2,12 +2,6 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -import org.gradle.api.artifacts.transform.* - -import java.nio.file.Files -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - ext.tasks_version = '16.0.1' def artifactType = Attribute.of("artifactType", String) @@ -49,31 +43,3 @@ tasks.withType(dokka.getClass()) { packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } - -abstract class UnpackAar implements TransformAction { - @InputArtifact - abstract Provider getInputArtifact() - - @Override - void transform(TransformOutputs outputs) { - ZipFile zip = new ZipFile(inputArtifact.get().asFile) - try { - for (entry in zip.entries()) { - if (!entry.isDirectory() && entry.name.endsWith(".jar")) { - unzipEntryTo(zip, entry, outputs.file(entry.name)) - } - } - } finally { - zip.close() - } - } - - private static void unzipEntryTo(ZipFile zip, ZipEntry entry, File output) { - InputStream stream = zip.getInputStream(entry) - try { - Files.copy(stream, output.toPath()) - } finally { - stream.close() - } - } -} diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle b/integration/kotlinx-coroutines-slf4j/build.gradle deleted file mode 100644 index 05accb75d3..0000000000 --- a/integration/kotlinx-coroutines-slf4j/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile 'org.slf4j:slf4j-api:1.7.25' - testCompile 'io.github.microutils:kotlin-logging:1.5.4' - testRuntime 'ch.qos.logback:logback-classic:1.2.3' - testRuntime 'ch.qos.logback:logback-core:1.2.3' -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - url = new URL("https://www.slf4j.org/apidocs/") - } -} diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle.kts b/integration/kotlinx-coroutines-slf4j/build.gradle.kts new file mode 100644 index 0000000000..c7d0d82d62 --- /dev/null +++ b/integration/kotlinx-coroutines-slf4j/build.gradle.kts @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + compile("org.slf4j:slf4j-api:1.7.25") + testCompile("io.github.microutils:kotlin-logging:1.5.4") + testRuntime("ch.qos.logback:logback-classic:1.2.3") + testRuntime("ch.qos.logback:logback-core:1.2.3") +} + +externalDocumentationLink( + url = "https://www.slf4j.org/apidocs/" +) diff --git a/js/example-frontend-js/README.md b/js/example-frontend-js/README.md index 4e534e427a..ad61372dc9 100644 --- a/js/example-frontend-js/README.md +++ b/js/example-frontend-js/README.md @@ -3,7 +3,7 @@ Build application with ``` -gradlew :example-frontend-js:bundle +gradlew :example-frontend-js:build ``` The resulting application can be found in `build/dist` subdirectory. @@ -11,7 +11,7 @@ The resulting application can be found in `build/dist` subdirectory. You can start application with webpack-dev-server using: ``` -gradlew :example-frontend-js:start +gradlew :example-frontend-js:run ``` Built and deployed application is available at the library documentation site diff --git a/js/example-frontend-js/build.gradle b/js/example-frontend-js/build.gradle index 735a70d55b..7abde63964 100644 --- a/js/example-frontend-js/build.gradle +++ b/js/example-frontend-js/build.gradle @@ -2,53 +2,32 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -apply plugin: 'kotlin-dce-js' -apply from: rootProject.file('gradle/node-js.gradle') - -// Workaround resolving new Gradle metadata with kotlin2js -// TODO: Remove once KT-37188 is fixed -try { - def jsCompilerType = Class.forName("org.jetbrains.kotlin.gradle.targets.js.KotlinJsCompilerAttribute") - def jsCompilerAttr = Attribute.of("org.jetbrains.kotlin.js.compiler", jsCompilerType) - project.dependencies.attributesSchema.attribute(jsCompilerAttr) - configurations { - matching { - it.name.endsWith("Classpath") - }.forEach { - it.attributes.attribute(jsCompilerAttr, jsCompilerType.legacy) +project.kotlin { + js(LEGACY) { + binaries.executable() + browser { + distribution { + directory = new File(directory.parentFile, "dist") + } + webpackTask { + cssSupport.enabled = true + } + runTask { + cssSupport.enabled = true + } + testTask { + useKarma { + useChromeHeadless() + webpackConfig.cssSupport.enabled = true + } + } } } -} catch (java.lang.ClassNotFoundException e) { - // org.jetbrains.kotlin.gradle.targets.js.JsCompilerType is missing in 1.3.x - // But 1.3.x doesn't generate Gradle metadata, so this workaround is not needed -} - -dependencies { - compile "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" -} - -compileKotlin2Js { - kotlinOptions { - main = "call" - } -} - -task bundle(type: NpmTask, dependsOn: [npmInstall, runDceKotlinJs]) { - inputs.files(fileTree("$buildDir/kotlin-js-min/main")) - inputs.files(fileTree(file("src/main/web"))) - inputs.file("npm/webpack.config.js") - outputs.dir("$buildDir/dist") - args = ["run", "bundle"] -} -task start(type: NpmTask, dependsOn: bundle) { - args = ["run", "start"] -} - -// we have not tests but kotlin-dce-js still tries to work with them and crashed. -// todo: Remove when KT-22028 is fixed -afterEvaluate { - if (tasks.findByName('unpackDependenciesTestKotlinJs')) { - tasks.unpackDependenciesTestKotlinJs.enabled = false + sourceSets { + main.dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" + implementation(npm("html-webpack-plugin", "3.2.0")) + } } } diff --git a/js/example-frontend-js/npm/package.json b/js/example-frontend-js/npm/package.json deleted file mode 100644 index 7668cefba3..0000000000 --- a/js/example-frontend-js/npm/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "example-frontend-js", - "version": "$version", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/Kotlin/kotlinx.coroutines.git" - }, - "devDependencies": { - "webpack": "4.29.1", - "webpack-cli": "3.2.3", - "webpack-dev-server": "3.1.14", - "html-webpack-plugin": "3.2.0", - "uglifyjs-webpack-plugin": "2.1.1", - "style-loader": "0.23.1", - "css-loader": "2.1.0" - }, - "scripts": { - "bundle": "webpack", - "start": "webpack-dev-server --open --no-inline" - } -} diff --git a/js/example-frontend-js/npm/webpack.config.js b/js/example-frontend-js/npm/webpack.config.js deleted file mode 100644 index a208d047b3..0000000000 --- a/js/example-frontend-js/npm/webpack.config.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This script is copied to "build" directory and run from there - -const webpack = require("webpack"); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); -const path = require("path"); - -const dist = path.resolve(__dirname, "dist"); - -module.exports = { - mode: "production", - entry: { - main: "main" - }, - output: { - filename: "[name].bundle.js", - path: dist, - publicPath: "" - }, - devServer: { - contentBase: dist - }, - module: { - rules: [ - { - test: /\.css$/, - use: [ - 'style-loader', - 'css-loader' - ] - } - ] - }, - resolve: { - modules: [ - path.resolve(__dirname, "kotlin-js-min/main"), - path.resolve(__dirname, "../src/main/web/") - ] - }, - devtool: 'source-map', - plugins: [ - new HtmlWebpackPlugin({ - title: 'Kotlin Coroutines JS Example' - }), - new UglifyJSPlugin({ - sourceMap: true - }) - ] -}; diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt index 25a73c61c0..da6e4196a6 100644 --- a/js/example-frontend-js/src/ExampleMain.kt +++ b/js/example-frontend-js/src/ExampleMain.kt @@ -13,7 +13,10 @@ import kotlin.coroutines.* import kotlin.math.* import kotlin.random.Random +external fun require(resource: String) + fun main() { + require("style.css") println("Starting example application...") document.addEventListener("DOMContentLoaded", { Application().start() diff --git a/js/example-frontend-js/webpack.config.d/custom-config.js b/js/example-frontend-js/webpack.config.d/custom-config.js new file mode 100644 index 0000000000..21939bece0 --- /dev/null +++ b/js/example-frontend-js/webpack.config.d/custom-config.js @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +;(function (config) { + const HtmlWebpackPlugin = require('html-webpack-plugin'); + + config.output.filename = "[name].bundle.js" + + config.plugins.push( + new HtmlWebpackPlugin({ + title: 'Kotlin Coroutines JS Example' + }) + ) + + // path from /js/packages/example-frontend-js to src/main/web + config.resolve.modules.push("../../../../js/example-frontend-js/src/main/web"); +})(config); diff --git a/knit.properties b/knit.properties new file mode 100644 index 0000000000..bc177ce44c --- /dev/null +++ b/knit.properties @@ -0,0 +1,16 @@ +# +# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +knit.include=docs/knit.code.include +test.template=docs/knit.test.template + +# Various test validation modes and their corresponding methods from TestUtil +test.mode.=verifyLines +test.mode.STARTS_WITH=verifyLinesStartWith +test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime +test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime +test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread +test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered +test.mode.LINES_START=verifyLinesStart +test.mode.EXCEPTION=verifyExceptions \ No newline at end of file diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 4609c68e36..bb1c0f36ab 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -46,6 +46,7 @@ public abstract interface class kotlinx/coroutines/CancellableContinuation : kot public abstract fun resumeUndispatched (Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Object;)V public abstract fun resumeUndispatchedWithException (Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Throwable;)V public abstract fun tryResume (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun tryResume (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun tryResumeWithException (Ljava/lang/Throwable;)Ljava/lang/Object; } @@ -56,6 +57,8 @@ public final class kotlinx/coroutines/CancellableContinuation$DefaultImpls { public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/jvm/internal/CoroutineStackFrame, kotlinx/coroutines/CancellableContinuation { public fun (Lkotlin/coroutines/Continuation;I)V + public final fun callCancelHandler (Lkotlinx/coroutines/CancelHandler;Ljava/lang/Throwable;)V + public final fun callOnCancellation (Lkotlin/jvm/functions/Function1;Ljava/lang/Throwable;)V public fun cancel (Ljava/lang/Throwable;)Z public fun completeResume (Ljava/lang/Object;)V public fun getCallerFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; @@ -75,14 +78,12 @@ public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/ public fun resumeWith (Ljava/lang/Object;)V public fun toString ()Ljava/lang/String; public fun tryResume (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun tryResume (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun tryResumeWithException (Ljava/lang/Throwable;)Ljava/lang/Object; } public final class kotlinx/coroutines/CancellableContinuationKt { public static final fun disposeOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Lkotlinx/coroutines/DisposableHandle;)V - public static final fun suspendAtomicCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun suspendAtomicCancellableCoroutine (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun suspendAtomicCancellableCoroutine$default (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun suspendCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -257,24 +258,21 @@ public final class kotlinx/coroutines/Deferred$DefaultImpls { public abstract interface class kotlinx/coroutines/Delay { public abstract fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public abstract fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public abstract fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } public final class kotlinx/coroutines/Delay$DefaultImpls { public static fun delay (Lkotlinx/coroutines/Delay;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; } public final class kotlinx/coroutines/DelayKt { + public static final fun awaitCancellation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun delay-p9JZ4hM (DLkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class kotlinx/coroutines/DispatchedContinuationKt { - public static final fun resumeCancellableWith (Lkotlin/coroutines/Continuation;Ljava/lang/Object;)V -} - public final class kotlinx/coroutines/Dispatchers { public static final field INSTANCE Lkotlinx/coroutines/Dispatchers; public static final fun getDefault ()Lkotlinx/coroutines/CoroutineDispatcher; @@ -580,6 +578,14 @@ public final class kotlinx/coroutines/channels/BroadcastKt { public static synthetic fun broadcast$default (Lkotlinx/coroutines/channels/ReceiveChannel;ILkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; } +public final class kotlinx/coroutines/channels/BufferOverflow : java/lang/Enum { + public static final field DROP_LATEST Lkotlinx/coroutines/channels/BufferOverflow; + public static final field DROP_OLDEST Lkotlinx/coroutines/channels/BufferOverflow; + public static final field SUSPEND Lkotlinx/coroutines/channels/BufferOverflow; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/channels/BufferOverflow; + public static fun values ()[Lkotlinx/coroutines/channels/BufferOverflow; +} + public abstract interface class kotlinx/coroutines/channels/Channel : kotlinx/coroutines/channels/ReceiveChannel, kotlinx/coroutines/channels/SendChannel { public static final field BUFFERED I public static final field CONFLATED I @@ -612,8 +618,10 @@ public final class kotlinx/coroutines/channels/ChannelIterator$DefaultImpls { } public final class kotlinx/coroutines/channels/ChannelKt { - public static final fun Channel (I)Lkotlinx/coroutines/channels/Channel; + public static final synthetic fun Channel (I)Lkotlinx/coroutines/channels/Channel; + public static final fun Channel (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/channels/Channel; public static synthetic fun Channel$default (IILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; + public static synthetic fun Channel$default (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; } public final class kotlinx/coroutines/channels/ChannelsKt { @@ -787,7 +795,7 @@ public abstract interface class kotlinx/coroutines/channels/ReceiveChannel { public abstract fun iterator ()Lkotlinx/coroutines/channels/ChannelIterator; public abstract fun poll ()Ljava/lang/Object; public abstract fun receive (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun receiveOrClosed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun receiveOrClosed-ZYPwvRU (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun receiveOrNull (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -868,7 +876,7 @@ public final class kotlinx/coroutines/debug/internal/DebuggerInfo : java/io/Seri public final fun getState ()Ljava/lang/String; } -public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { +public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/CancellableFlow, kotlinx/coroutines/flow/Flow { public fun ()V public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun collectSafely (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -895,10 +903,15 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun asFlow ([I)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([J)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun asSharedFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)Lkotlinx/coroutines/flow/SharedFlow; + public static final fun asStateFlow (Lkotlinx/coroutines/flow/MutableStateFlow;)Lkotlinx/coroutines/flow/StateFlow; public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static final fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static final synthetic fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static final fun buffer (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun cache (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun callbackFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun cancellable (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun catch (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; @@ -988,10 +1001,15 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun onErrorReturn$default (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun onStart (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun onSubscription (Lkotlinx/coroutines/flow/SharedFlow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun produceIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final fun publish (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun publish (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun publishOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static final fun receiveAsFlow (Lkotlinx/coroutines/channels/ReceiveChannel;)Lkotlinx/coroutines/flow/Flow; public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun replay (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun replay (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final synthetic fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun retry (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; @@ -1003,11 +1021,15 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; + public static final fun shareIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;I)Lkotlinx/coroutines/flow/SharedFlow; + public static synthetic fun shareIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;IILjava/lang/Object;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun skip (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V @@ -1029,17 +1051,63 @@ public final class kotlinx/coroutines/flow/FlowKt { } public final class kotlinx/coroutines/flow/LintKt { + public static final fun cancel (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;)V + public static synthetic fun cancel$default (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V + public static final fun cancellable (Lkotlinx/coroutines/flow/SharedFlow;)Lkotlinx/coroutines/flow/Flow; public static final fun conflate (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; - public static final fun flowOn (Lkotlinx/coroutines/flow/StateFlow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; + public static final fun flowOn (Lkotlinx/coroutines/flow/SharedFlow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; + public static final fun getCoroutineContext (Lkotlinx/coroutines/flow/FlowCollector;)Lkotlin/coroutines/CoroutineContext; + public static final fun isActive (Lkotlinx/coroutines/flow/FlowCollector;)Z } -public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/StateFlow { +public abstract interface class kotlinx/coroutines/flow/MutableSharedFlow : kotlinx/coroutines/flow/FlowCollector, kotlinx/coroutines/flow/SharedFlow { + public abstract fun getSubscriptionCount ()Lkotlinx/coroutines/flow/StateFlow; + public abstract fun resetReplayCache ()V + public abstract fun tryEmit (Ljava/lang/Object;)Z +} + +public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/MutableSharedFlow, kotlinx/coroutines/flow/StateFlow { + public abstract fun compareAndSet (Ljava/lang/Object;Ljava/lang/Object;)Z public abstract fun getValue ()Ljava/lang/Object; public abstract fun setValue (Ljava/lang/Object;)V } -public abstract interface class kotlinx/coroutines/flow/StateFlow : kotlinx/coroutines/flow/Flow { +public abstract interface class kotlinx/coroutines/flow/SharedFlow : kotlinx/coroutines/flow/Flow { + public abstract fun getReplayCache ()Ljava/util/List; +} + +public final class kotlinx/coroutines/flow/SharedFlowKt { + public static final fun MutableSharedFlow (IILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/MutableSharedFlow; + public static synthetic fun MutableSharedFlow$default (IILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/MutableSharedFlow; +} + +public final class kotlinx/coroutines/flow/SharingCommand : java/lang/Enum { + public static final field START Lkotlinx/coroutines/flow/SharingCommand; + public static final field STOP Lkotlinx/coroutines/flow/SharingCommand; + public static final field STOP_AND_RESET_REPLAY_CACHE Lkotlinx/coroutines/flow/SharingCommand; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/flow/SharingCommand; + public static fun values ()[Lkotlinx/coroutines/flow/SharingCommand; +} + +public abstract interface class kotlinx/coroutines/flow/SharingStarted { + public static final field Companion Lkotlinx/coroutines/flow/SharingStarted$Companion; + public abstract fun command (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; +} + +public final class kotlinx/coroutines/flow/SharingStarted$Companion { + public final fun WhileSubscribed (JJ)Lkotlinx/coroutines/flow/SharingStarted; + public static synthetic fun WhileSubscribed$default (Lkotlinx/coroutines/flow/SharingStarted$Companion;JJILjava/lang/Object;)Lkotlinx/coroutines/flow/SharingStarted; + public final fun getEagerly ()Lkotlinx/coroutines/flow/SharingStarted; + public final fun getLazily ()Lkotlinx/coroutines/flow/SharingStarted; +} + +public final class kotlinx/coroutines/flow/SharingStartedKt { + public static final fun WhileSubscribed-9tZugJw (Lkotlinx/coroutines/flow/SharingStarted$Companion;DD)Lkotlinx/coroutines/flow/SharingStarted; + public static synthetic fun WhileSubscribed-9tZugJw$default (Lkotlinx/coroutines/flow/SharingStarted$Companion;DDILjava/lang/Object;)Lkotlinx/coroutines/flow/SharingStarted; +} + +public abstract interface class kotlinx/coroutines/flow/StateFlow : kotlinx/coroutines/flow/SharedFlow { public abstract fun getValue ()Ljava/lang/Object; } @@ -1050,13 +1118,15 @@ public final class kotlinx/coroutines/flow/StateFlowKt { public abstract class kotlinx/coroutines/flow/internal/ChannelFlow : kotlinx/coroutines/flow/internal/FusibleFlow { public final field capacity I public final field context Lkotlin/coroutines/CoroutineContext; - public fun (Lkotlin/coroutines/CoroutineContext;I)V - public fun additionalToStringProps ()Ljava/lang/String; + public final field onBufferOverflow Lkotlinx/coroutines/channels/BufferOverflow; + public fun (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)V + protected fun additionalToStringProps ()Ljava/lang/String; public fun broadcastImpl (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected abstract fun collectTo (Lkotlinx/coroutines/channels/ProducerScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - protected abstract fun create (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/ChannelFlow; - public fun fuse (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/FusibleFlow; + protected abstract fun create (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/internal/ChannelFlow; + public fun dropChannelOperators ()Lkotlinx/coroutines/flow/Flow; + public fun fuse (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; public fun produceImpl (Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; public fun toString ()Ljava/lang/String; } @@ -1070,11 +1140,11 @@ public final class kotlinx/coroutines/flow/internal/FlowExceptions_commonKt { } public abstract interface class kotlinx/coroutines/flow/internal/FusibleFlow : kotlinx/coroutines/flow/Flow { - public abstract fun fuse (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/FusibleFlow; + public abstract fun fuse (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; } public final class kotlinx/coroutines/flow/internal/FusibleFlow$DefaultImpls { - public static synthetic fun fuse$default (Lkotlinx/coroutines/flow/internal/FusibleFlow;Lkotlin/coroutines/CoroutineContext;IILjava/lang/Object;)Lkotlinx/coroutines/flow/internal/FusibleFlow; + public static synthetic fun fuse$default (Lkotlinx/coroutines/flow/internal/FusibleFlow;Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; } public final class kotlinx/coroutines/flow/internal/SafeCollector_commonKt { diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 59dc5da894..f98f6a529c 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -2,14 +2,61 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -apply plugin: 'kotlin-multiplatform' -apply from: rootProject.file("gradle/targets.gradle") +apply plugin: 'org.jetbrains.kotlin.multiplatform' apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle") apply from: rootProject.file("gradle/compile-common.gradle") apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") apply from: rootProject.file('gradle/publish-npm-js.gradle') +/* ========================================================================== + Configure source sets structure for kotlinx-coroutines-core: + + TARGETS SOURCE SETS + ------- ---------------------------------------------- + + js -----------------------------------------------------+ + | + V + jvm -------------------------------> concurrent ---> common + ^ + ios \ | + macos | ---> nativeDarwin ---> native --+ + tvos | ^ + watchos / | + | + linux \ ---> nativeOther -------+ + mingw / + + ========================================================================== */ + +project.ext.sourceSetSuffixes = ["Main", "Test"] + +void defineSourceSet(newName, dependsOn, includedInPred) { + for (suffix in project.ext.sourceSetSuffixes) { + def newSS = kotlin.sourceSets.maybeCreate(newName + suffix) + for (dep in dependsOn) { + newSS.dependsOn(kotlin.sourceSets[dep + suffix]) + } + for (curSS in kotlin.sourceSets) { + def curName = curSS.name + if (curName.endsWith(suffix)) { + def prefix = curName.substring(0, curName.length() - suffix.length()) + if (includedInPred(prefix)) curSS.dependsOn(newSS) + } + } + } +} + +static boolean isNativeDarwin(String name) { return ["ios", "macos", "tvos", "watchos"].any { name.startsWith(it) } } +static boolean isNativeOther(String name) { return ["linux", "mingw"].any { name.startsWith(it) } } + +defineSourceSet("concurrent", ["common"]) { it in ["jvm", "native"] } +defineSourceSet("nativeDarwin", ["native"]) { isNativeDarwin(it) } +defineSourceSet("nativeOther", ["native"]) { isNativeOther(it) } + +/* ========================================================================== */ + /* * All platform plugins and configuration magic happens here instead of build.gradle * because JMV-only projects depend on core, thus core should always be initialized before configuration. @@ -18,7 +65,7 @@ kotlin { configure(sourceSets) { def srcDir = name.endsWith('Main') ? 'src' : 'test' def platform = name[0..-5] - kotlin.srcDir "$platform/$srcDir" + kotlin.srcDirs = ["$platform/$srcDir"] if (name == "jvmMain") { resources.srcDirs = ["$platform/resources"] } else if (name == "jvmTest") { @@ -31,12 +78,18 @@ kotlin { } configure(targets) { - def targetName = it.name - compilations.all { compilation -> - def compileTask = tasks.getByName(compilation.compileKotlinTaskName) - // binary compatibility support - if (targetName.contains("jvm") && compilation.compilationName == "main") { - compileTask.kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"] + // Configure additional binaries and test runs -- one for each OS + if (["macos", "linux", "mingw"].any { name.startsWith(it) }) { + binaries { + // Test for memory leaks using a special entry point that does not exit but returns from main + binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"] + // Configure a separate test where code runs in background + test("background", [org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG]) { + freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"] + } + } + testRuns { + background { setExecutionSourceFrom(binaries.backgroundDebugTest) } } } } @@ -54,23 +107,52 @@ compileKotlinMetadata { } } +// :KLUDGE: Idea.active: This is needed to workaround resolve problems after importing this project to IDEA +def configureNativeSourceSetPreset(name, preset) { + def hostMainCompilation = project.kotlin.targetFromPreset(preset).compilations.main + // Look for platform libraries in "implementation" for default source set + def implementationConfiguration = configurations[hostMainCompilation.defaultSourceSet.implementationMetadataConfigurationName] + // Now find the libraries: Finds platform libs & stdlib, but platform declarations are still not resolved due to IDE bugs + def hostNativePlatformLibs = files( + provider { + implementationConfiguration.findAll { + it.path.endsWith(".klib") || it.absolutePath.contains("klib${File.separator}platform") || it.absolutePath.contains("stdlib") + } + } + ) + // Add all those dependencies + for (suffix in sourceSetSuffixes) { + configure(kotlin.sourceSets[name + suffix]) { + dependencies.add(implementationMetadataConfigurationName, hostNativePlatformLibs) + } + } +} + +// :KLUDGE: Idea.active: Configure platform libraries for native source sets when working in IDEA +if (Idea.active) { + def manager = project.ext.hostManager + def linuxPreset = kotlin.presets.linuxX64 + def macosPreset = kotlin.presets.macosX64 + // linux should be always available (cross-compilation capable) -- use it as default + assert manager.isEnabled(linuxPreset.konanTarget) + // use macOS libs for nativeDarwin if available + def macosAvailable = manager.isEnabled(macosPreset.konanTarget) + // configure source sets + configureNativeSourceSetPreset("native", linuxPreset) + configureNativeSourceSetPreset("nativeOther", linuxPreset) + configureNativeSourceSetPreset("nativeDarwin", macosAvailable ? macosPreset : linuxPreset) +} + kotlin.sourceSets { jvmMain.dependencies { compileOnly "com.google.android:annotations:4.1.1.4" } jvmTest.dependencies { - // This is a workaround for https://youtrack.jetbrains.com/issue/KT-39037 - def excludingCurrentProject = { dependency -> - def result = project.dependencies.create(dependency) - result.exclude(module: project.name) - return result - } - - api(excludingCurrentProject("org.jetbrains.kotlinx:lincheck:$lincheck_version")) + api "org.jetbrains.kotlinx:lincheck:$lincheck_version" api "org.jetbrains.kotlinx:kotlinx-knit-test:$knit_version" api "com.esotericsoftware:kryo:4.0.0" - implementation(excludingCurrentProject(project(":android-unit-tests"))) + implementation project(":android-unit-tests") } } @@ -97,7 +179,7 @@ jvmTest { enableAssertions = true systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' // 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true" - if (!project.ext.ideaActive && rootProject.properties['stress'] == null) { + if (!Idea.active && rootProject.properties['stress'] == null) { exclude '**/*StressTest.*' } systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt index dd1e1771f2..7189349024 100644 --- a/kotlinx-coroutines-core/common/src/Await.kt +++ b/kotlinx-coroutines-core/common/src/Await.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import kotlinx.atomicfu.* +import kotlinx.coroutines.channels.* import kotlin.coroutines.* /** @@ -18,6 +19,8 @@ import kotlin.coroutines.* * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun awaitAll(vararg deferreds: Deferred): List = if (deferreds.isEmpty()) emptyList() else AwaitAll(deferreds).await() @@ -33,6 +36,8 @@ public suspend fun awaitAll(vararg deferreds: Deferred): List = * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Collection>.awaitAll(): List = if (isEmpty()) emptyList() else AwaitAll(toTypedArray()).await() @@ -41,8 +46,11 @@ public suspend fun Collection>.awaitAll(): List = * Suspends current coroutine until all given jobs are complete. * This method is semantically equivalent to joining all given jobs one by one with `jobs.forEach { it.join() }`. * - * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() } @@ -50,8 +58,11 @@ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() } * Suspends current coroutine until all given jobs are complete. * This method is semantically equivalent to joining all given jobs one by one with `forEach { it.join() }`. * - * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Collection.joinAll(): Unit = forEach { it.join() } diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 64bff500dc..c0924a0238 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -129,8 +129,9 @@ private class LazyDeferredCoroutine( * This function uses dispatcher from the new context, shifting execution of the [block] into the * different thread if a new dispatcher is specified, and back to the original dispatcher * when it completes. Note that the result of `withContext` invocation is - * dispatched into the original context in a cancellable way, which means that if the original [coroutineContext], - * in which `withContext` was invoked, is cancelled by the time its dispatcher starts to execute the code, + * dispatched into the original context in a cancellable way with a **prompt cancellation guarantee**, + * which means that if the original [coroutineContext], in which `withContext` was invoked, + * is cancelled by the time its dispatcher starts to execute the code, * it discards the result of `withContext` and throws [CancellationException]. */ public suspend fun withContext( diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 0d3fe847dc..7d9315afbf 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -15,6 +15,8 @@ import kotlin.coroutines.intrinsics.* * When the [cancel] function is explicitly invoked, this continuation immediately resumes with a [CancellationException] or * the specified cancel cause. * + * An instance of `CancellableContinuation` is created by the [suspendCancellableCoroutine] function. + * * Cancellable continuation has three states (as subset of [Job] states): * * | **State** | [isActive] | [isCompleted] | [isCancelled] | @@ -24,14 +26,12 @@ import kotlin.coroutines.intrinsics.* * | _Canceled_ (final _completed_ state)| `false` | `true` | `true` | * * Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while - * invocation of [resume] or [resumeWithException] transitions it from _active_ to _resumed_ state. + * invocation of [Continuation.resume] or [Continuation.resumeWithException] transitions it from _active_ to _resumed_ state. * * A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted]. * - * Invocation of [resume] or [resumeWithException] in _resumed_ state produces an [IllegalStateException]. - * Invocation of [resume] in _cancelled_ state is ignored (it is a trivial race between resume from the continuation owner and - * outer job's cancellation, and the cancellation wins). - * Invocation of [resumeWithException] in _cancelled_ state triggers exception handling of the passed exception. + * Invocation of [Continuation.resume] or [Continuation.resumeWithException] in _resumed_ state produces an [IllegalStateException], + * but is ignored in _cancelled_ state. * * ``` * +-----------+ resume +---------+ @@ -43,7 +43,6 @@ import kotlin.coroutines.intrinsics.* * +-----------+ * | Cancelled | * +-----------+ - * * ``` */ public interface CancellableContinuation : Continuation { @@ -78,6 +77,14 @@ public interface CancellableContinuation : Continuation { @InternalCoroutinesApi public fun tryResume(value: T, idempotent: Any? = null): Any? + /** + * Same as [tryResume] but with [onCancellation] handler that called if and only if the value is not + * delivered to the caller because of the dispatch in the process, so that atomicity delivery + * guaranteed can be provided by having a cancellation fallback. + */ + @InternalCoroutinesApi + public fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? + /** * Tries to resume this continuation with the specified [exception] and returns a non-null object token if successful, * or `null` otherwise (it was already resumed or cancelled). When a non-null object is returned, @@ -112,8 +119,8 @@ public interface CancellableContinuation : Continuation { public fun cancel(cause: Throwable? = null): Boolean /** - * Registers a [handler] to be **synchronously** invoked on cancellation (regular or exceptional) of this continuation. - * When the continuation is already cancelled, the handler will be immediately invoked + * Registers a [handler] to be **synchronously** invoked on [cancellation][cancel] (regular or exceptional) of this continuation. + * When the continuation is already cancelled, the handler is immediately invoked * with the cancellation exception. Otherwise, the handler will be invoked as soon as this * continuation is cancelled. * @@ -122,7 +129,15 @@ public interface CancellableContinuation : Continuation { * processed as an uncaught exception in the context of the current coroutine * (see [CoroutineExceptionHandler]). * - * At most one [handler] can be installed on a continuation. + * At most one [handler] can be installed on a continuation. Attempt to call `invokeOnCancellation` second + * time produces [IllegalStateException]. + * + * This handler is also called when this continuation [resumes][Continuation.resume] normally (with a value) and then + * is cancelled while waiting to be dispatched. More generally speaking, this handler is called whenever + * the caller of [suspendCancellableCoroutine] is getting a [CancellationException]. + * + * A typical example for `invokeOnCancellation` usage is given in + * the documentation for the [suspendCancellableCoroutine] function. * * **Note**: Implementation of `CompletionHandler` must be fast, non-blocking, and thread-safe. * This `handler` can be invoked concurrently with the surrounding code. @@ -165,7 +180,7 @@ public interface CancellableContinuation : Continuation { * (see [CoroutineExceptionHandler]). * * This function shall be used when resuming with a resource that must be closed by - * code that called the corresponding suspending function, e.g.: + * code that called the corresponding suspending function, for example: * * ``` * continuation.resume(resource) { @@ -173,17 +188,119 @@ public interface CancellableContinuation : Continuation { * } * ``` * + * A more complete example and further details are given in + * the documentation for the [suspendCancellableCoroutine] function. + * * **Note**: The [onCancellation] handler must be fast, non-blocking, and thread-safe. * It can be invoked concurrently with the surrounding code. * There is no guarantee on the execution context of its invocation. */ @ExperimentalCoroutinesApi // since 1.2.0 - public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) + public fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) } /** * Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to - * the [block]. This function throws a [CancellationException] if the coroutine is cancelled or completed while suspended. + * the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is + * cancelled or completed while it is suspended. + * + * A typical use of this function is to suspend a coroutine while waiting for a result + * from a single-shot callback API and to return the result to the caller. + * For multi-shot callback APIs see [callbackFlow][kotlinx.coroutines.flow.callbackFlow]. + * + * ``` + * suspend fun awaitCallback(): T = suspendCancellableCoroutine { continuation -> + * val callback = object : Callback { // Implementation of some callback interface + * override fun onCompleted(value: T) { + * // Resume coroutine with a value provided by the callback + * continuation.resume(value) + * } + * override fun onApiError(cause: Throwable) { + * // Resume coroutine with an exception provided by the callback + * continuation.resumeWithException(cause) + * } + * } + * // Register callback with an API + * api.register(callback) + * // Remove callback on cancellation + * continuation.invokeOnCancellation { api.unregister(callback) } + * // At this point the coroutine is suspended by suspendCancellableCoroutine until callback fires + * } + * ``` + * + * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because + * > `invokeOnCancellation` block can be called at any time due to asynchronous nature of cancellation, even + * > concurrently with the call of the callback. + * + * ### Prompt cancellation guarantee + * + * This function provides **prompt cancellation guarantee**. + * If the [Job] of the current coroutine was cancelled while this function was suspended it will not resume + * successfully. + * + * The cancellation of the coroutine's job is generally asynchronous with respect to the suspended coroutine. + * The suspended coroutine is resumed with the call it to its [Continuation.resumeWith] member function or to + * [resume][Continuation.resume] extension function. + * However, when coroutine is resumed, it does not immediately start executing, but is passed to its + * [CoroutineDispatcher] to schedule its execution when dispatcher's resources become available for execution. + * The job's cancellation can happen both before, after, and concurrently with the call to `resume`. In any + * case, prompt cancellation guarantees that the the coroutine will not resume its code successfully. + * + * If the coroutine was resumed with an exception (for example, using [Continuation.resumeWithException] extension + * function) and cancelled, then the resulting exception of the `suspendCancellableCoroutine` function is determined + * by whichever action (exceptional resume or cancellation) that happened first. + * + * ### Returning resources from a suspended coroutine + * + * As a result of a prompt cancellation guarantee, when a closeable resource + * (like open file or a handle to another native resource) is returned from a suspended coroutine as a value + * it can be lost when the coroutine is cancelled. In order to ensure that the resource can be properly closed + * in this case, the [CancellableContinuation] interface provides two functions. + * + * * [invokeOnCancellation][CancellableContinuation.invokeOnCancellation] installs a handler that is called + * whenever a suspend coroutine is being cancelled. In addition to the example at the beginning, it can be + * used to ensure that a resource that was opened before the call to + * `suspendCancellableCoroutine` or in its body is closed in case of cancellation. + * + * ``` + * suspendCancellableCoroutine { continuation -> + * val resource = openResource() // Opens some resource + * continuation.invokeOnCancellation { + * resource.close() // Ensures the resource is closed on cancellation + * } + * // ... + * } + * ``` + * + * * [resume(value) { ... }][CancellableContinuation.resume] method on a [CancellableContinuation] takes + * an optional `onCancellation` block. It can be used when resuming with a resource that must be closed by + * the code that called the corresponding suspending function. + * + * ``` + * suspendCancellableCoroutine { continuation -> + * val callback = object : Callback { // Implementation of some callback interface + * // A callback provides a reference to some closeable resource + * override fun onCompleted(resource: T) { + * // Resume coroutine with a value provided by the callback and ensure the resource is closed in case + * // when the coroutine is cancelled before the caller gets a reference to the resource. + * continuation.resume(resource) { + * resource.close() // Close the resource on cancellation + * } + * } + * // ... + * } + * ``` + * + * ### Implementation details and custom continuation interceptors + * + * The prompt cancellation guarantee is the result of a coordinated implementation inside `suspendCancellableCoroutine` + * function and the [CoroutineDispatcher] class. The coroutine dispatcher checks for the status of the [Job] immediately + * before continuing its normal execution and aborts this normal execution, calling all the corresponding + * cancellation handlers, if the job was cancelled. + * + * If a custom implementation of [ContinuationInterceptor] is used in a coroutine's context that does not extend + * [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor + * can resume execution of a previously suspended coroutine even if its job was already cancelled. */ public suspend inline fun suspendCancellableCoroutine( crossinline block: (CancellableContinuation) -> Unit @@ -201,29 +318,10 @@ public suspend inline fun suspendCancellableCoroutine( } /** - * Suspends the coroutine like [suspendCancellableCoroutine], but with *atomic cancellation*. - * - * When the suspended function throws a [CancellationException], it means that the continuation was not resumed. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when the continuation - * was already resumed and was posted for execution to the thread's queue. - * - * @suppress **This an internal API and should not be used from general code.** + * Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of + * [CancellableContinuationImpl] is reused. */ -@InternalCoroutinesApi -public suspend inline fun suspendAtomicCancellableCoroutine( - crossinline block: (CancellableContinuation) -> Unit -): T = - suspendCoroutineUninterceptedOrReturn { uCont -> - val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_ATOMIC_DEFAULT) - block(cancellable) - cancellable.getResult() - } - -/** - * Suspends coroutine similar to [suspendAtomicCancellableCoroutine], but an instance of [CancellableContinuationImpl] is reused if possible. - */ -internal suspend inline fun suspendAtomicCancellableCoroutineReusable( +internal suspend inline fun suspendCancellableCoroutineReusable( crossinline block: (CancellableContinuation) -> Unit ): T = suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = getOrCreateCancellableContinuation(uCont.intercepted()) @@ -234,12 +332,12 @@ internal suspend inline fun suspendAtomicCancellableCoroutineReusable( internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl { // If used outside of our dispatcher if (delegate !is DispatchedContinuation) { - return CancellableContinuationImpl(delegate, resumeMode = MODE_ATOMIC_DEFAULT) + return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE) } /* * Attempt to claim reusable instance. * - * suspendAtomicCancellableCoroutineReusable { // <- claimed + * suspendCancellableCoroutineReusable { // <- claimed * // Any asynchronous cancellation is "postponed" while this block * // is being executed * } // postponed cancellation is checked here. @@ -250,26 +348,13 @@ internal fun getOrCreateCancellableContinuation(delegate: Continuation): * thus leaking CC instance for indefinite time. * 2) Continuation was cancelled. Then we should prevent any further reuse and bail out. */ - return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetState() } - ?: return CancellableContinuationImpl(delegate, MODE_ATOMIC_DEFAULT) + return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetStateReusable() } + ?: return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE) } /** - * @suppress **Deprecated** - */ -@Deprecated( - message = "holdCancellability parameter is deprecated and is no longer used", - replaceWith = ReplaceWith("suspendAtomicCancellableCoroutine(block)") -) -@InternalCoroutinesApi -public suspend inline fun suspendAtomicCancellableCoroutine( - holdCancellability: Boolean = false, - crossinline block: (CancellableContinuation) -> Unit -): T = - suspendAtomicCancellableCoroutine(block) - -/** - * Removes the specified [node] on cancellation. + * Removes the specified [node] on cancellation. This function assumes that this node is already + * removed on successful resume and does not try to remove it if the continuation is cancelled during dispatch. */ internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) = invokeOnCancellation(handler = RemoveOnCancel(node).asHandler) @@ -290,7 +375,7 @@ public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHa // --------------- implementation details --------------- -private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : CancelHandler() { +private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : BeforeResumeCancelHandler() { override fun invoke(cause: Throwable?) { node.remove() } override fun toString() = "RemoveOnCancel[$node]" } diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index e25ebd3a37..cdb1b78882 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -27,6 +27,10 @@ internal open class CancellableContinuationImpl( final override val delegate: Continuation, resumeMode: Int ) : DispatchedTask(resumeMode), CancellableContinuation, CoroutineStackFrame { + init { + assert { resumeMode != MODE_UNINITIALIZED } // invalid mode for CancellableContinuationImpl + } + public override val context: CoroutineContext = delegate.context /* @@ -88,15 +92,17 @@ internal open class CancellableContinuationImpl( private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this) /** - * Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work. - * Invariant: used only by [suspendAtomicCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. + * Resets cancellability state in order to [suspendCancellableCoroutineReusable] to work. + * Invariant: used only by [suspendCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. */ - @JvmName("resetState") // Prettier stack traces - internal fun resetState(): Boolean { + @JvmName("resetStateReusable") // Prettier stack traces + internal fun resetStateReusable(): Boolean { + assert { resumeMode == MODE_CANCELLABLE_REUSABLE } // invalid mode for CancellableContinuationImpl assert { parentHandle !== NonDisposableHandle } val state = _state.value assert { state !is NotCompleted } - if (state is CompletedIdempotentResult) { + if (state is CompletedContinuation && state.idempotentResume != null) { + // Cannot reuse continuation that was resumed with idempotent marker detachChild() return false } @@ -114,7 +120,6 @@ internal open class CancellableContinuationImpl( if (checkCompleted()) return if (parentHandle !== null) return // fast path 2 -- was already initialized val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent - parent.start() // make sure the parent is started val handle = parent.invokeOnCompletion( onCancelling = true, handler = ChildContinuation(parent, this).asHandler @@ -130,7 +135,7 @@ internal open class CancellableContinuationImpl( private fun checkCompleted(): Boolean { val completed = isCompleted - if (resumeMode != MODE_ATOMIC_DEFAULT) return completed // Do not check postponed cancellation for non-reusable continuations + if (!resumeMode.isReusableMode) return completed // Do not check postponed cancellation for non-reusable continuations val dispatched = delegate as? DispatchedContinuation<*> ?: return completed val cause = dispatched.checkPostponedCancellation(this) ?: return completed if (!completed) { @@ -147,10 +152,26 @@ internal open class CancellableContinuationImpl( override fun takeState(): Any? = state - override fun cancelResult(state: Any?, cause: Throwable) { - if (state is CompletedWithCancellation) { - invokeHandlerSafely { - state.onCancellation(cause) + // Note: takeState does not clear the state so we don't use takenState + // and we use the actual current state where in CAS-loop + override fun cancelCompletedResult(takenState: Any?, cause: Throwable): Unit = _state.loop { state -> + when (state) { + is NotCompleted -> error("Not completed") + is CompletedExceptionally -> return // already completed exception or cancelled, nothing to do + is CompletedContinuation -> { + check(!state.cancelled) { "Must be called at most once" } + val update = state.copy(cancelCause = cause) + if (_state.compareAndSet(state, update)) { + state.invokeHandlers(this, cause) + return // done + } + } + else -> { + // completed normally without marker class, promote to CompletedContinuation in case + // if invokeOnCancellation if called later + if (_state.compareAndSet(state, CompletedContinuation(state, cancelCause = cause))) { + return // done + } } } } @@ -159,7 +180,7 @@ internal open class CancellableContinuationImpl( * Attempt to postpone cancellation for reusable cancellable continuation */ private fun cancelLater(cause: Throwable): Boolean { - if (resumeMode != MODE_ATOMIC_DEFAULT) return false + if (!resumeMode.isReusableMode) return false val dispatched = (delegate as? DispatchedContinuation<*>) ?: return false return dispatched.postponeCancellation(cause) } @@ -171,10 +192,10 @@ internal open class CancellableContinuationImpl( val update = CancelledContinuation(this, cause, handled = state is CancelHandler) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure // Invoke cancel handler if it was present - if (state is CancelHandler) invokeHandlerSafely { state.invoke(cause) } + (state as? CancelHandler)?.let { callCancelHandler(it, cause) } // Complete state update detachChildIfNonResuable() - dispatchResume(mode = MODE_ATOMIC_DEFAULT) + dispatchResume(resumeMode) // no need for additional cancellation checks return true } } @@ -186,14 +207,36 @@ internal open class CancellableContinuationImpl( detachChildIfNonResuable() } - private inline fun invokeHandlerSafely(block: () -> Unit) { + private inline fun callCancelHandlerSafely(block: () -> Unit) { + try { + block() + } catch (ex: Throwable) { + // Handler should never fail, if it does -- it is an unhandled exception + handleCoroutineException( + context, + CompletionHandlerException("Exception in invokeOnCancellation handler for $this", ex) + ) + } + } + + private fun callCancelHandler(handler: CompletionHandler, cause: Throwable?) = + /* + * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, + * because we play type tricks on Kotlin/JS and handler is not necessarily a function there + */ + callCancelHandlerSafely { handler.invokeIt(cause) } + + fun callCancelHandler(handler: CancelHandler, cause: Throwable?) = + callCancelHandlerSafely { handler.invoke(cause) } + + fun callOnCancellation(onCancellation: (cause: Throwable) -> Unit, cause: Throwable) { try { - block() + onCancellation.invoke(cause) } catch (ex: Throwable) { // Handler should never fail, if it does -- it is an unhandled exception handleCoroutineException( context, - CompletionHandlerException("Exception in cancellation handler for $this", ex) + CompletionHandlerException("Exception in resume onCancellation handler for $this", ex) ) } } @@ -232,64 +275,75 @@ internal open class CancellableContinuationImpl( val state = this.state if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this) // if the parent job was already cancelled, then throw the corresponding cancellation exception - // otherwise, there is a race is suspendCancellableCoroutine { cont -> ... } does cont.resume(...) + // otherwise, there is a race if suspendCancellableCoroutine { cont -> ... } does cont.resume(...) // before the block returns. This getResult would return a result as opposed to cancellation // exception that should have happened if the continuation is dispatched for execution later. - if (resumeMode == MODE_CANCELLABLE) { + if (resumeMode.isCancellableMode) { val job = context[Job] if (job != null && !job.isActive) { val cause = job.getCancellationException() - cancelResult(state, cause) + cancelCompletedResult(state, cause) throw recoverStackTrace(cause, this) } } return getSuccessfulResult(state) } - override fun resumeWith(result: Result) { + override fun resumeWith(result: Result) = resumeImpl(result.toState(this), resumeMode) - } - override fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) { - val cancelled = resumeImpl(CompletedWithCancellation(value, onCancellation), resumeMode) - if (cancelled != null) { - // too late to resume (was cancelled) -- call handler - invokeHandlerSafely { - onCancellation(cancelled.cause) - } - } - } + override fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) = + resumeImpl(value, resumeMode, onCancellation) public override fun invokeOnCancellation(handler: CompletionHandler) { - var handleCache: CancelHandler? = null + val cancelHandler = makeCancelHandler(handler) _state.loop { state -> when (state) { is Active -> { - val node = handleCache ?: makeHandler(handler).also { handleCache = it } - if (_state.compareAndSet(state, node)) return // quit on cas success + if (_state.compareAndSet(state, cancelHandler)) return // quit on cas success } is CancelHandler -> multipleHandlersError(handler, state) - is CancelledContinuation -> { + is CompletedExceptionally -> { /* - * Continuation was already cancelled, invoke directly. + * Continuation was already cancelled or completed exceptionally. * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed, - * so we check to make sure that handler was installed just once. + * so we check to make sure handler was installed just once. */ if (!state.makeHandled()) multipleHandlersError(handler, state) /* + * Call the handler only if it was cancelled (not called when completed exceptionally). * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, * because we play type tricks on Kotlin/JS and handler is not necessarily a function there */ - invokeHandlerSafely { handler.invokeIt((state as? CompletedExceptionally)?.cause) } + if (state is CancelledContinuation) { + callCancelHandler(handler, (state as? CompletedExceptionally)?.cause) + } return } + is CompletedContinuation -> { + /* + * Continuation was already completed, and might already have cancel handler. + */ + if (state.cancelHandler != null) multipleHandlersError(handler, state) + // BeforeResumeCancelHandler does not need to be called on a completed continuation + if (cancelHandler is BeforeResumeCancelHandler) return + if (state.cancelled) { + // Was already cancelled while being dispatched -- invoke the handler directly + callCancelHandler(handler, state.cancelCause) + return + } + val update = state.copy(cancelHandler = cancelHandler) + if (_state.compareAndSet(state, update)) return // quit on cas success + } else -> { /* - * Continuation was already completed, do nothing. - * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed, - * but we have no way to check that it was installed just once in this case. + * Continuation was already completed normally, but might get cancelled while being dispatched. + * Change its state to CompletedContinuation, unless we have BeforeResumeCancelHandler which + * does not need to be called in this case. */ - return + if (cancelHandler is BeforeResumeCancelHandler) return + val update = CompletedContinuation(state, cancelHandler = cancelHandler) + if (_state.compareAndSet(state, update)) return // quit on cas success } } } @@ -299,7 +353,7 @@ internal open class CancellableContinuationImpl( error("It's prohibited to register multiple handlers, tried to register $handler, already has $state") } - private fun makeHandler(handler: CompletionHandler): CancelHandler = + private fun makeCancelHandler(handler: CompletionHandler): CancelHandler = if (handler is CancelHandler) handler else InvokeOnCancel(handler) private fun dispatchResume(mode: Int) { @@ -308,15 +362,39 @@ internal open class CancellableContinuationImpl( dispatch(mode) } - // returns null when successfully dispatched resumed, CancelledContinuation if too late (was already cancelled) - private fun resumeImpl(proposedUpdate: Any?, resumeMode: Int): CancelledContinuation? { + private fun resumedState( + state: NotCompleted, + proposedUpdate: Any?, + resumeMode: Int, + onCancellation: ((cause: Throwable) -> Unit)?, + idempotent: Any? + ): Any? = when { + proposedUpdate is CompletedExceptionally -> { + assert { idempotent == null } // there are no idempotent exceptional resumes + assert { onCancellation == null } // only successful results can be cancelled + proposedUpdate + } + !resumeMode.isCancellableMode && idempotent == null -> proposedUpdate // cannot be cancelled in process, all is fine + onCancellation != null || (state is CancelHandler && state !is BeforeResumeCancelHandler) || idempotent != null -> + // mark as CompletedContinuation if special cases are present: + // Cancellation handlers that shall be called after resume or idempotent resume + CompletedContinuation(proposedUpdate, state as? CancelHandler, onCancellation, idempotent) + else -> proposedUpdate // simple case -- use the value directly + } + + private fun resumeImpl( + proposedUpdate: Any?, + resumeMode: Int, + onCancellation: ((cause: Throwable) -> Unit)? = null + ) { _state.loop { state -> when (state) { is NotCompleted -> { - if (!_state.compareAndSet(state, proposedUpdate)) return@loop // retry on cas failure + val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent = null) + if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure detachChildIfNonResuable() - dispatchResume(resumeMode) - return null + dispatchResume(resumeMode) // dispatch resume, but it might get cancelled in process + return // done } is CancelledContinuation -> { /* @@ -324,14 +402,48 @@ internal open class CancellableContinuationImpl( * because cancellation is asynchronous and may race with resume. * Racy exceptions will be lost, too. */ - if (state.makeResumed()) return state // tried to resume just once, but was cancelled + if (state.makeResumed()) { // check if trying to resume one (otherwise error) + // call onCancellation + onCancellation?.let { callOnCancellation(it, state.cause) } + return // done + } + } + } + alreadyResumedError(proposedUpdate) // otherwise, an error (second resume attempt) + } + } + + /** + * Similar to [tryResume], but does not actually completes resume (needs [completeResume] call). + * Returns [RESUME_TOKEN] when resumed, `null` when it was already resumed or cancelled. + */ + private fun tryResumeImpl( + proposedUpdate: Any?, + idempotent: Any?, + onCancellation: ((cause: Throwable) -> Unit)? + ): Symbol? { + _state.loop { state -> + when (state) { + is NotCompleted -> { + val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent) + if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure + detachChildIfNonResuable() + return RESUME_TOKEN + } + is CompletedContinuation -> { + return if (idempotent != null && state.idempotentResume === idempotent) { + assert { state.result == proposedUpdate } // "Non-idempotent resume" + RESUME_TOKEN // resumed with the same token -- ok + } else { + null // resumed with a different token or non-idempotent -- too late + } } + else -> return null // cannot resume -- not active anymore } - alreadyResumedError(proposedUpdate) // otherwise -- an error (second resume attempt) } } - private fun alreadyResumedError(proposedUpdate: Any?) { + private fun alreadyResumedError(proposedUpdate: Any?): Nothing { error("Already resumed, but proposed with update $proposedUpdate") } @@ -343,7 +455,7 @@ internal open class CancellableContinuationImpl( /** * Detaches from the parent. - * Invariant: used used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` + * Invariant: used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` */ internal fun detachChild() { val handle = parentHandle @@ -352,42 +464,14 @@ internal open class CancellableContinuationImpl( } // Note: Always returns RESUME_TOKEN | null - override fun tryResume(value: T, idempotent: Any?): Any? { - _state.loop { state -> - when (state) { - is NotCompleted -> { - val update: Any? = if (idempotent == null) value else - CompletedIdempotentResult(idempotent, value) - if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - detachChildIfNonResuable() - return RESUME_TOKEN - } - is CompletedIdempotentResult -> { - return if (state.idempotentResume === idempotent) { - assert { state.result === value } // "Non-idempotent resume" - RESUME_TOKEN - } else { - null - } - } - else -> return null // cannot resume -- not active anymore - } - } - } + override fun tryResume(value: T, idempotent: Any?): Any? = + tryResumeImpl(value, idempotent, onCancellation = null) - override fun tryResumeWithException(exception: Throwable): Any? { - _state.loop { state -> - when (state) { - is NotCompleted -> { - val update = CompletedExceptionally(exception) - if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - detachChildIfNonResuable() - return RESUME_TOKEN - } - else -> return null // cannot resume -- not active anymore - } - } - } + override fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? = + tryResumeImpl(value, idempotent, onCancellation) + + override fun tryResumeWithException(exception: Throwable): Any? = + tryResumeImpl(CompletedExceptionally(exception), idempotent = null, onCancellation = null) // note: token is always RESUME_TOKEN override fun completeResume(token: Any) { @@ -408,11 +492,15 @@ internal open class CancellableContinuationImpl( @Suppress("UNCHECKED_CAST") override fun getSuccessfulResult(state: Any?): T = when (state) { - is CompletedIdempotentResult -> state.result as T - is CompletedWithCancellation -> state.result as T + is CompletedContinuation -> state.result as T else -> state as T } + // The exceptional state in CancellableContinuationImpl is stored directly and it is not recovered yet. + // The stacktrace recovery is invoked here. + override fun getExceptionalResult(state: Any?): Throwable? = + super.getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } + // For nicer debugging public override fun toString(): String = "${nameString()}(${delegate.toDebugString()}){$state}@$hexAddress" @@ -429,8 +517,20 @@ private object Active : NotCompleted { override fun toString(): String = "Active" } +/** + * Base class for all [CancellableContinuation.invokeOnCancellation] handlers to avoid an extra instance + * on JVM, yet support JS where you cannot extend from a functional type. + */ internal abstract class CancelHandler : CancelHandlerBase(), NotCompleted +/** + * Base class for all [CancellableContinuation.invokeOnCancellation] handlers that don't need to be invoked + * if continuation is cancelled after resumption, during dispatch, because the corresponding resources + * were already released before calling `resume`. This cancel handler is called only before `resume`. + * It avoids allocation of [CompletedContinuation] instance during resume on JVM. + */ +internal abstract class BeforeResumeCancelHandler : CancelHandler() + // Wrapper for lambdas, for the performance sake CancelHandler can be subclassed directly private class InvokeOnCancel( // Clashes with InvokeOnCancellation private val handler: CompletionHandler @@ -441,16 +541,18 @@ private class InvokeOnCancel( // Clashes with InvokeOnCancellation override fun toString() = "InvokeOnCancel[${handler.classSimpleName}@$hexAddress]" } -private class CompletedIdempotentResult( - @JvmField val idempotentResume: Any?, - @JvmField val result: Any? -) { - override fun toString(): String = "CompletedIdempotentResult[$result]" -} - -private class CompletedWithCancellation( +// Completed with additional metadata +private data class CompletedContinuation( @JvmField val result: Any?, - @JvmField val onCancellation: (cause: Throwable) -> Unit + @JvmField val cancelHandler: CancelHandler? = null, // installed via invokeOnCancellation + @JvmField val onCancellation: ((cause: Throwable) -> Unit)? = null, // installed via resume block + @JvmField val idempotentResume: Any? = null, + @JvmField val cancelCause: Throwable? = null ) { - override fun toString(): String = "CompletedWithCancellation[$result]" + val cancelled: Boolean get() = cancelCause != null + + fun invokeHandlers(cont: CancellableContinuationImpl<*>, cause: Throwable) { + cancelHandler?.let { cont.callCancelHandler(it, cause) } + onCancellation?.let { cont.callOnCancellation(it, cause) } + } } diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index d24f1837cd..0605817afa 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.selects.* * All functions on this interface are **thread-safe** and can * be safely invoked from concurrent coroutines without external synchronization. * - * **`CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**, + * **The `CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**, * as new methods might be added to this interface in the future, but is stable for use. */ public interface CompletableDeferred : Deferred { diff --git a/kotlinx-coroutines-core/common/src/CompletableJob.kt b/kotlinx-coroutines-core/common/src/CompletableJob.kt index 8e6b1ab02f..74a92e36e5 100644 --- a/kotlinx-coroutines-core/common/src/CompletableJob.kt +++ b/kotlinx-coroutines-core/common/src/CompletableJob.kt @@ -11,7 +11,7 @@ package kotlinx.coroutines * All functions on this interface are **thread-safe** and can * be safely invoked from concurrent coroutines without external synchronization. * - * **`CompletableJob` interface is not stable for inheritance in 3rd party libraries**, + * **The `CompletableJob` interface is not stable for inheritance in 3rd party libraries**, * as new methods might be added to this interface in the future, but is stable for use. */ public interface CompletableJob : Job { diff --git a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt b/kotlinx-coroutines-core/common/src/CompletionState.kt similarity index 78% rename from kotlinx-coroutines-core/common/src/CompletedExceptionally.kt rename to kotlinx-coroutines-core/common/src/CompletionState.kt index b426785bd7..f09aa3ccd9 100644 --- a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt +++ b/kotlinx-coroutines-core/common/src/CompletionState.kt @@ -9,10 +9,17 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -internal fun Result.toState(): Any? = fold({ it }, { CompletedExceptionally(it) }) +internal fun Result.toState( + onCancellation: ((cause: Throwable) -> Unit)? = null +): Any? = fold( + onSuccess = { if (onCancellation != null) CompletedWithCancellation(it, onCancellation) else it }, + onFailure = { CompletedExceptionally(it) } +) -internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold({ it }, - { CompletedExceptionally(recoverStackTrace(it, caller)) }) +internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold( + onSuccess = { it }, + onFailure = { CompletedExceptionally(recoverStackTrace(it, caller)) } +) @Suppress("RESULT_CLASS_IN_RETURN_TYPE", "UNCHECKED_CAST") internal fun recoverResult(state: Any?, uCont: Continuation): Result = @@ -21,6 +28,11 @@ internal fun recoverResult(state: Any?, uCont: Continuation): Result = else Result.success(state as T) +internal data class CompletedWithCancellation( + @JvmField val result: Any?, + @JvmField val onCancellation: (cause: Throwable) -> Unit +) + /** * Class for an internal state of a job that was cancelled (completed exceptionally). * diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 1b6e7eb00f..ab1e814b8a 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlin.coroutines.* /** diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 7dbd6a6d7b..0dde6c9352 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -226,10 +226,10 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni /** * Ensures that current scope is [active][CoroutineScope.isActive]. - * Throws [IllegalStateException] if the context does not have a job in it. * * If the job is no longer active, throws [CancellationException]. * If the job was cancelled, thrown exception contains the original cancellation cause. + * This function does not do anything if there is no [Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext]. * * This method is a drop-in replacement for the following code, but with more precise exception: * ``` @@ -237,6 +237,8 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni * throw CancellationException() * } * ``` + * + * @see CoroutineContext.ensureActive */ public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive() diff --git a/kotlinx-coroutines-core/common/src/Deferred.kt b/kotlinx-coroutines-core/common/src/Deferred.kt index 72f3fde141..ff996756a3 100644 --- a/kotlinx-coroutines-core/common/src/Deferred.kt +++ b/kotlinx-coroutines-core/common/src/Deferred.kt @@ -43,6 +43,8 @@ public interface Deferred : Job { * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * This function can be used in [select] invocation with [onAwait] clause. * Use [isCompleted] to check for completion of this deferred value without waiting. diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index ab80912269..f7948443fa 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -21,9 +21,12 @@ import kotlin.time.* public interface Delay { /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. + * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun delay(time: Long) { if (time <= 0) return // don't delay @@ -54,15 +57,57 @@ public interface Delay { * * This implementation uses a built-in single-threaded scheduled executor service. */ - public fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = - DefaultDelay.invokeOnTimeout(timeMillis, block) + public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + DefaultDelay.invokeOnTimeout(timeMillis, block, context) } +/** + * Suspends until cancellation, in which case it will throw a [CancellationException]. + * + * This function returns [Nothing], so it can be used in any coroutine, + * regardless of the required return type. + * + * Usage example in callback adapting code: + * + * ```kotlin + * fun currentTemperature(): Flow = callbackFlow { + * val callback = SensorCallback { degreesCelsius: Double -> + * trySend(Temperature.celsius(degreesCelsius)) + * } + * try { + * registerSensorCallback(callback) + * awaitCancellation() // Suspends to keep getting updates until cancellation. + * } finally { + * unregisterSensorCallback(callback) + * } + * } + * ``` + * + * Usage example in (non declarative) UI code: + * + * ```kotlin + * suspend fun showStuffUntilCancelled(content: Stuff): Nothing { + * someSubView.text = content.title + * anotherSubView.text = content.description + * someView.visibleInScope { + * awaitCancellation() // Suspends so the view stays visible. + * } + * } + * ``` + */ +@ExperimentalCoroutinesApi +public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} + /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. + * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * + * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. * * Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * @@ -72,15 +117,23 @@ public interface Delay { public suspend fun delay(timeMillis: Long) { if (timeMillis <= 0) return // don't delay return suspendCancellableCoroutine sc@ { cont: CancellableContinuation -> - cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) + // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule. + if (timeMillis < Long.MAX_VALUE) { + cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) + } } } /** * Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time. + * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * + * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. * * Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index c8e4455c92..4bfff118e8 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -24,7 +24,14 @@ import kotlin.time.* * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -48,7 +55,14 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { @@ -68,7 +82,14 @@ public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineSc * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -101,7 +122,14 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? = @@ -114,7 +142,7 @@ private fun setupTimeout( // schedule cancellation of this coroutine on time val cont = coroutine.uCont val context = cont.context - coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine)) + coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context)) // restart the block using a new coroutine with a new job, // however, start it undispatched, because we already are in the proper context return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block) diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index e0af04ddb7..0d8bd3bc2f 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -13,6 +14,8 @@ import kotlin.coroutines.intrinsics.* * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed when this suspending function is invoked or while * this function is waiting for dispatch, it resumes with a [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend. * diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 28c7ceabe1..53ecf06a2c 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -16,7 +16,9 @@ import kotlin.native.concurrent.* /** * Abstract send channel. It is a base class for all send channel implementations. */ -internal abstract class AbstractSendChannel : SendChannel { +internal abstract class AbstractSendChannel( + @JvmField protected val onUndeliveredElement: OnUndeliveredElement? +) : SendChannel { /** @suppress **This is unstable API and it is subject to change.** */ protected val queue = LockFreeLinkedListHead() @@ -151,24 +153,34 @@ internal abstract class AbstractSendChannel : SendChannel { // We should check for closed token on offer as well, otherwise offer won't be linearizable // in the face of concurrent close() // See https://github.com/Kotlin/kotlinx.coroutines/issues/359 - throw recoverStackTrace(helpCloseAndGetSendException(closedForSend ?: return false)) + throw recoverStackTrace(helpCloseAndGetSendException(element, closedForSend ?: return false)) + } + result is Closed<*> -> { + throw recoverStackTrace(helpCloseAndGetSendException(element, result)) } - result is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(result)) else -> error("offerInternal returned $result") } } - private fun helpCloseAndGetSendException(closed: Closed<*>): Throwable { + private fun helpCloseAndGetSendException(element: E, closed: Closed<*>): Throwable { // To ensure linearizablity we must ALWAYS help close the channel when we observe that it was closed // See https://github.com/Kotlin/kotlinx.coroutines/issues/1419 helpClose(closed) + // Element was not delivered -> cals onUndeliveredElement + onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let { + // If it crashes, add send exception as suppressed for better diagnostics + it.addSuppressed(closed.sendException) + throw it + } return closed.sendException } - private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont -> loop@ while (true) { if (isFullImpl) { - val send = SendElement(element, cont) + val send = if (onUndeliveredElement == null) + SendElement(element, cont) else + SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement) val enqueueResult = enqueueSend(send) when { enqueueResult == null -> { // enqueued successfully @@ -176,7 +188,7 @@ internal abstract class AbstractSendChannel : SendChannel { return@sc } enqueueResult is Closed<*> -> { - cont.helpCloseAndResumeWithSendException(enqueueResult) + cont.helpCloseAndResumeWithSendException(element, enqueueResult) return@sc } enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead @@ -193,7 +205,7 @@ internal abstract class AbstractSendChannel : SendChannel { } offerResult === OFFER_FAILED -> continue@loop offerResult is Closed<*> -> { - cont.helpCloseAndResumeWithSendException(offerResult) + cont.helpCloseAndResumeWithSendException(element, offerResult) return@sc } else -> error("offerInternal returned $offerResult") @@ -201,9 +213,15 @@ internal abstract class AbstractSendChannel : SendChannel { } } - private fun Continuation<*>.helpCloseAndResumeWithSendException(closed: Closed<*>) { + private fun Continuation<*>.helpCloseAndResumeWithSendException(element: E, closed: Closed<*>) { helpClose(closed) - resumeWithException(closed.sendException) + val sendException = closed.sendException + onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let { + it.addSuppressed(sendException) + resumeWithException(it) + return + } + resumeWithException(sendException) } /** @@ -375,7 +393,7 @@ internal abstract class AbstractSendChannel : SendChannel { select.disposeOnSelect(node) return } - enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(enqueueResult)) + enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, enqueueResult)) enqueueResult === ENQUEUE_FAILED -> {} // try to offer enqueueResult is Receive<*> -> {} // try to offer else -> error("enqueueSend returned $enqueueResult ") @@ -391,7 +409,7 @@ internal abstract class AbstractSendChannel : SendChannel { block.startCoroutineUnintercepted(receiver = this, completion = select.completion) return } - offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(offerResult)) + offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, offerResult)) else -> error("offerSelectInternal returned $offerResult") } } @@ -431,7 +449,7 @@ internal abstract class AbstractSendChannel : SendChannel { // ------ private ------ private class SendSelect( - override val pollResult: Any?, + override val pollResult: E, // E | Closed - the result pollInternal returns when it rendezvous with this node @JvmField val channel: AbstractSendChannel, @JvmField val select: SelectInstance, @JvmField val block: suspend (SendChannel) -> R @@ -440,11 +458,13 @@ internal abstract class AbstractSendChannel : SendChannel { select.trySelectOther(otherOp) as Symbol? // must return symbol override fun completeResumeSend() { - block.startCoroutine(receiver = channel, completion = select.completion) + block.startCoroutineCancellable(receiver = channel, completion = select.completion) } override fun dispose() { // invoked on select completion - remove() + if (!remove()) return + // if the node was successfully removed (meaning it was added but was not received) then element not delivered + undeliveredElement() } override fun resumeSendClosed(closed: Closed<*>) { @@ -452,6 +472,10 @@ internal abstract class AbstractSendChannel : SendChannel { select.resumeSelectWithException(closed.sendException) } + override fun undeliveredElement() { + channel.onUndeliveredElement?.callUndeliveredElement(pollResult, select.completion.context) + } + override fun toString(): String = "SendSelect@$hexAddress($pollResult)[$channel, $select]" } @@ -469,7 +493,9 @@ internal abstract class AbstractSendChannel : SendChannel { /** * Abstract send/receive channel. It is a base class for all channel implementations. */ -internal abstract class AbstractChannel : AbstractSendChannel(), Channel { +internal abstract class AbstractChannel( + onUndeliveredElement: OnUndeliveredElement? +) : AbstractSendChannel(onUndeliveredElement), Channel { // ------ extension points for buffered channels ------ /** @@ -501,6 +527,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutineReusable sc@ { cont -> - val receive = ReceiveElement(cont as CancellableContinuation, receiveMode) + private suspend fun receiveSuspend(receiveMode: Int): R = suspendCancellableCoroutineReusable sc@ { cont -> + val receive = if (onUndeliveredElement == null) + ReceiveElement(cont as CancellableContinuation, receiveMode) else + ReceiveElementWithUndeliveredHandler(cont as CancellableContinuation, receiveMode, onUndeliveredElement) while (true) { if (enqueueReceive(receive)) { removeReceiveOnCancel(cont, receive) @@ -561,7 +591,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel @@ -785,7 +820,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel, receive: Receive<*>) = cont.invokeOnCancellation(handler = RemoveReceiveOnCancel(receive).asHandler) - private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : CancelHandler() { + private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : BeforeResumeCancelHandler() { override fun invoke(cause: Throwable?) { if (receive.remove()) onReceiveDequeued() @@ -793,7 +828,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel(val channel: AbstractChannel) : ChannelIterator { + private class Itr(@JvmField val channel: AbstractChannel) : ChannelIterator { var result: Any? = POLL_FAILED // E | POLL_FAILED | Closed override suspend fun hasNext(): Boolean { @@ -814,7 +849,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel + private suspend fun hasNextSuspend(): Boolean = suspendCancellableCoroutineReusable sc@ { cont -> val receive = ReceiveHasNext(this, cont) while (true) { if (channel.enqueueReceive(receive)) { @@ -832,7 +867,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel( + private open class ReceiveElement( @JvmField val cont: CancellableContinuation, @JvmField val receiveMode: Int ) : Receive() { @@ -860,9 +896,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel value } - @Suppress("IMPLICIT_CAST_TO_ANY") override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(resumeValue(value), otherOp?.desc) ?: return null + val token = cont.tryResume(resumeValue(value), otherOp?.desc, resumeOnCancellationFun(value)) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -881,12 +916,22 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( + private class ReceiveElementWithUndeliveredHandler( + cont: CancellableContinuation, + receiveMode: Int, + @JvmField val onUndeliveredElement: OnUndeliveredElement + ) : ReceiveElement(cont, receiveMode) { + override fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = + onUndeliveredElement.bindCancellationFun(value, cont.context) + } + + private open class ReceiveHasNext( @JvmField val iterator: Itr, @JvmField val cont: CancellableContinuation ) : Receive() { override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(true, otherOp?.desc) ?: return null + val token = cont.tryResume(true, otherOp?.desc, resumeOnCancellationFun(value)) + ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -906,13 +951,17 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel Unit)? = + iterator.channel.onUndeliveredElement?.bindCancellationFun(value, cont.context) + override fun toString(): String = "ReceiveHasNext@$hexAddress" } @@ -927,16 +976,20 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { if (!select.trySelect()) return when (receiveMode) { RECEIVE_THROWS_ON_CLOSE -> select.resumeSelectWithException(closed.receiveException) - RECEIVE_RESULT -> block.startCoroutine(ValueOrClosed.closed(closed.closeCause), select.completion) + RECEIVE_RESULT -> block.startCoroutineCancellable(ValueOrClosed.closed(closed.closeCause), select.completion) RECEIVE_NULL_ON_CLOSE -> if (closed.closeCause == null) { - block.startCoroutine(null, select.completion) + block.startCoroutineCancellable(null, select.completion) } else { select.resumeSelectWithException(closed.receiveException) } @@ -948,6 +1001,9 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel Unit)? = + channel.onUndeliveredElement?.bindCancellationFun(value, select.completion.context) + override fun toString(): String = "ReceiveSelect@$hexAddress[$select,receiveMode=$receiveMode]" } } @@ -959,23 +1015,27 @@ internal const val RECEIVE_RESULT = 2 @JvmField @SharedImmutable -internal val OFFER_SUCCESS: Any = Symbol("OFFER_SUCCESS") +internal val EMPTY = Symbol("EMPTY") // marker for Conflated & Buffered channels + +@JvmField +@SharedImmutable +internal val OFFER_SUCCESS = Symbol("OFFER_SUCCESS") @JvmField @SharedImmutable -internal val OFFER_FAILED: Any = Symbol("OFFER_FAILED") +internal val OFFER_FAILED = Symbol("OFFER_FAILED") @JvmField @SharedImmutable -internal val POLL_FAILED: Any = Symbol("POLL_FAILED") +internal val POLL_FAILED = Symbol("POLL_FAILED") @JvmField @SharedImmutable -internal val ENQUEUE_FAILED: Any = Symbol("ENQUEUE_FAILED") +internal val ENQUEUE_FAILED = Symbol("ENQUEUE_FAILED") @JvmField @SharedImmutable -internal val HANDLER_INVOKED: Any = Symbol("ON_CLOSE_HANDLER_INVOKED") +internal val HANDLER_INVOKED = Symbol("ON_CLOSE_HANDLER_INVOKED") internal typealias Handler = (Throwable?) -> Unit @@ -983,7 +1043,7 @@ internal typealias Handler = (Throwable?) -> Unit * Represents sending waiter in the queue. */ internal abstract class Send : LockFreeLinkedListNode() { - abstract val pollResult: Any? // E | Closed + abstract val pollResult: Any? // E | Closed - the result pollInternal returns when it rendezvous with this node // Returns: null - failure, // RETRY_ATOMIC for retry (only when otherOp != null), // RESUME_TOKEN on success (call completeResumeSend) @@ -991,6 +1051,7 @@ internal abstract class Send : LockFreeLinkedListNode() { abstract fun tryResumeSend(otherOp: PrepareOp?): Symbol? abstract fun completeResumeSend() abstract fun resumeSendClosed(closed: Closed<*>) + open fun undeliveredElement() {} } /** @@ -1009,9 +1070,8 @@ internal interface ReceiveOrClosed { /** * Represents sender for a specific element. */ -@Suppress("UNCHECKED_CAST") -internal class SendElement( - override val pollResult: Any?, +internal open class SendElement( + override val pollResult: E, @JvmField val cont: CancellableContinuation ) : Send() { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { @@ -1021,9 +1081,27 @@ internal class SendElement( otherOp?.finishPrepare() // finish preparations return RESUME_TOKEN } + override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) - override fun toString(): String = "SendElement@$hexAddress($pollResult)" + override fun toString(): String = "$classSimpleName@$hexAddress($pollResult)" +} + +internal class SendElementWithUndeliveredHandler( + pollResult: E, + cont: CancellableContinuation, + @JvmField val onUndeliveredElement: OnUndeliveredElement +) : SendElement(pollResult, cont) { + override fun remove(): Boolean { + if (!super.remove()) return false + // if the node was successfully removed (meaning it was added but was not received) then we have undelivered element + undeliveredElement() + return true + } + + override fun undeliveredElement() { + onUndeliveredElement.callUndeliveredElement(pollResult, cont.context) + } } /** @@ -1048,6 +1126,7 @@ internal class Closed( internal abstract class Receive : LockFreeLinkedListNode(), ReceiveOrClosed { override val offerResult get() = OFFER_SUCCESS abstract fun resumeReceiveClosed(closed: Closed<*>) + open fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = null } @Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index 155652fd6f..91b5473c41 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -28,7 +28,7 @@ internal class ArrayBroadcastChannel( * Buffer capacity. */ val capacity: Int -) : AbstractSendChannel(), BroadcastChannel { +) : AbstractSendChannel(null), BroadcastChannel { init { require(capacity >= 1) { "ArrayBroadcastChannel capacity must be at least 1, but $capacity was specified" } } @@ -180,6 +180,8 @@ internal class ArrayBroadcastChannel( this.tail = tail + 1 return@withLock // go out of lock to wakeup this sender } + // Too late, already cancelled, but we removed it from the queue and need to release resources. + // However, ArrayBroadcastChannel does not support onUndeliveredElement, so nothing to do } } } @@ -205,7 +207,7 @@ internal class ArrayBroadcastChannel( private class Subscriber( private val broadcastChannel: ArrayBroadcastChannel - ) : AbstractChannel(), ReceiveChannel { + ) : AbstractChannel(null), ReceiveChannel { private val subLock = ReentrantLock() private val _subHead = atomic(0L) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index e26579eff7..80cb8aa011 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -23,25 +23,31 @@ internal open class ArrayChannel( /** * Buffer capacity. */ - val capacity: Int -) : AbstractChannel() { + private val capacity: Int, + private val onBufferOverflow: BufferOverflow, + onUndeliveredElement: OnUndeliveredElement? +) : AbstractChannel(onUndeliveredElement) { init { + // This check is actually used by the Channel(...) constructor function which checks only for known + // capacities and calls ArrayChannel constructor for everything else. require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } } private val lock = ReentrantLock() + /* * Guarded by lock. * Allocate minimum of capacity and 16 to avoid excess memory pressure for large channels when it's not necessary. */ - private var buffer: Array = arrayOfNulls(min(capacity, 8)) + private var buffer: Array = arrayOfNulls(min(capacity, 8)).apply { fill(EMPTY) } + private var head: Int = 0 private val size = atomic(0) // Invariant: size <= capacity protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = size.value == 0 protected final override val isBufferAlwaysFull: Boolean get() = false - protected final override val isBufferFull: Boolean get() = size.value == capacity + protected final override val isBufferFull: Boolean get() = size.value == capacity && onBufferOverflow == BufferOverflow.SUSPEND override val isFull: Boolean get() = lock.withLock { isFullImpl } override val isEmpty: Boolean get() = lock.withLock { isEmptyImpl } @@ -53,31 +59,26 @@ internal open class ArrayChannel( lock.withLock { val size = this.size.value closedForSend?.let { return it } - if (size < capacity) { - // tentatively put element to buffer - this.size.value = size + 1 // update size before checking queue (!!!) - // check for receivers that were waiting on empty queue - if (size == 0) { - loop@ while (true) { - receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued - if (receive is Closed) { - this.size.value = size // restore size - return receive!! - } - val token = receive!!.tryResumeReceive(element, null) - if (token != null) { - assert { token === RESUME_TOKEN } - this.size.value = size // restore size - return@withLock - } + // update size before checking queue (!!!) + updateBufferSize(size)?.let { return it } + // check for receivers that were waiting on empty queue + if (size == 0) { + loop@ while (true) { + receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued + if (receive is Closed) { + this.size.value = size // restore size + return receive!! + } + val token = receive!!.tryResumeReceive(element, null) + if (token != null) { + assert { token === RESUME_TOKEN } + this.size.value = size // restore size + return@withLock } } - ensureCapacity(size) - buffer[(head + size) % buffer.size] = element // actually queue element - return OFFER_SUCCESS } - // size == capacity: full - return OFFER_FAILED + enqueueElement(size, element) + return OFFER_SUCCESS } // breaks here if offer meets receiver receive!!.completeResumeReceive(element) @@ -90,41 +91,36 @@ internal open class ArrayChannel( lock.withLock { val size = this.size.value closedForSend?.let { return it } - if (size < capacity) { - // tentatively put element to buffer - this.size.value = size + 1 // update size before checking queue (!!!) - // check for receivers that were waiting on empty queue - if (size == 0) { - loop@ while (true) { - val offerOp = describeTryOffer(element) - val failure = select.performAtomicTrySelect(offerOp) - when { - failure == null -> { // offered successfully - this.size.value = size // restore size - receive = offerOp.result - return@withLock - } - failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer - failure === RETRY_ATOMIC -> {} // retry - failure === ALREADY_SELECTED || failure is Closed<*> -> { - this.size.value = size // restore size - return failure - } - else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") + // update size before checking queue (!!!) + updateBufferSize(size)?.let { return it } + // check for receivers that were waiting on empty queue + if (size == 0) { + loop@ while (true) { + val offerOp = describeTryOffer(element) + val failure = select.performAtomicTrySelect(offerOp) + when { + failure == null -> { // offered successfully + this.size.value = size // restore size + receive = offerOp.result + return@withLock + } + failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer + failure === RETRY_ATOMIC -> {} // retry + failure === ALREADY_SELECTED || failure is Closed<*> -> { + this.size.value = size // restore size + return failure } + else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") } } - // let's try to select sending this element to buffer - if (!select.trySelect()) { // :todo: move trySelect completion outside of lock - this.size.value = size // restore size - return ALREADY_SELECTED - } - ensureCapacity(size) - buffer[(head + size) % buffer.size] = element // actually queue element - return OFFER_SUCCESS } - // size == capacity: full - return OFFER_FAILED + // let's try to select sending this element to buffer + if (!select.trySelect()) { // :todo: move trySelect completion outside of lock + this.size.value = size // restore size + return ALREADY_SELECTED + } + enqueueElement(size, element) + return OFFER_SUCCESS } // breaks here if offer meets receiver receive!!.completeResumeReceive(element) @@ -135,6 +131,35 @@ internal open class ArrayChannel( super.enqueueSend(send) } + // Guarded by lock + // Result is `OFFER_SUCCESS | OFFER_FAILED | null` + private fun updateBufferSize(currentSize: Int): Symbol? { + if (currentSize < capacity) { + size.value = currentSize + 1 // tentatively put it into the buffer + return null // proceed + } + // buffer is full + return when (onBufferOverflow) { + BufferOverflow.SUSPEND -> OFFER_FAILED + BufferOverflow.DROP_LATEST -> OFFER_SUCCESS + BufferOverflow.DROP_OLDEST -> null // proceed, will drop oldest in enqueueElement + } + } + + // Guarded by lock + private fun enqueueElement(currentSize: Int, element: E) { + if (currentSize < capacity) { + ensureCapacity(currentSize) + buffer[(head + currentSize) % buffer.size] = element // actually queue element + } else { + // buffer is full + assert { onBufferOverflow == BufferOverflow.DROP_OLDEST } // the only way we can get here + buffer[head % buffer.size] = null // drop oldest element + buffer[(head + currentSize) % buffer.size] = element // actually queue element + head = (head + 1) % buffer.size + } + } + // Guarded by lock private fun ensureCapacity(currentSize: Int) { if (currentSize >= buffer.size) { @@ -143,6 +168,7 @@ internal open class ArrayChannel( for (i in 0 until currentSize) { newBuffer[i] = buffer[(head + i) % buffer.size] } + newBuffer.fill(EMPTY, currentSize, newSize) buffer = newBuffer head = 0 } @@ -172,6 +198,8 @@ internal open class ArrayChannel( replacement = send!!.pollResult break@loop } + // too late, already cancelled, but we removed it from the queue and need to notify on undelivered element + send!!.undeliveredElement() } } if (replacement !== POLL_FAILED && replacement !is Closed<*>) { @@ -254,17 +282,23 @@ internal open class ArrayChannel( // Note: this function is invoked when channel is already closed override fun onCancelIdempotent(wasClosed: Boolean) { // clear buffer first, but do not wait for it in helpers - if (wasClosed) { - lock.withLock { - repeat(size.value) { - buffer[head] = 0 - head = (head + 1) % buffer.size + val onUndeliveredElement = onUndeliveredElement + var undeliveredElementException: UndeliveredElementException? = null // first cancel exception, others suppressed + lock.withLock { + repeat(size.value) { + val value = buffer[head] + if (onUndeliveredElement != null && value !== EMPTY) { + @Suppress("UNCHECKED_CAST") + undeliveredElementException = onUndeliveredElement.callUndeliveredElementCatchingException(value as E, undeliveredElementException) } - size.value = 0 + buffer[head] = EMPTY + head = (head + 1) % buffer.size } + size.value = 0 } // then clean all queued senders super.onCancelIdempotent(wasClosed) + undeliveredElementException?.let { throw it } // throw cancel exception at the end if there was one } // ------ debug ------ diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index 863d1387fc..790580e0a3 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* -import kotlin.native.concurrent.* /** * Broadcasts all elements of the channel. @@ -34,8 +33,10 @@ import kotlin.native.concurrent.* * * This function has an inappropriate result type of [BroadcastChannel] which provides * [send][BroadcastChannel.send] and [close][BroadcastChannel.close] operations that interfere with - * the broadcasting coroutine in hard-to-specify ways. It will be replaced with - * sharing operators on [Flow][kotlinx.coroutines.flow.Flow] in the future. + * the broadcasting coroutine in hard-to-specify ways. + * + * **Note: This API is obsolete.** It will be deprecated and replaced with the + * [Flow.shareIn][kotlinx.coroutines.flow.shareIn] operator when it becomes stable. * * @param start coroutine start option. The default value is [CoroutineStart.LAZY]. */ diff --git a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt index 312480f943..d356566f17 100644 --- a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt @@ -7,9 +7,9 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.channels.Channel.Factory.CHANNEL_DEFAULT_CAPACITY +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED /** @@ -20,9 +20,10 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED * See `BroadcastChannel()` factory function for the description of available * broadcast channel implementations. * - * **Note: This is an experimental api.** It may be changed in the future updates. + * **Note: This API is obsolete.** It will be deprecated and replaced by [SharedFlow][kotlinx.coroutines.flow.SharedFlow] + * when it becomes stable. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // not @ObsoleteCoroutinesApi to reduce burden for people who are still using it public interface BroadcastChannel : SendChannel { /** * Subscribes to this [BroadcastChannel] and returns a channel to receive elements from it. diff --git a/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt new file mode 100644 index 0000000000..99994ea81b --- /dev/null +++ b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* + +/** + * A strategy for buffer overflow handling in [channels][Channel] and [flows][kotlinx.coroutines.flow.Flow] that + * controls what is going to be sacrificed on buffer overflow: + * + * * [SUSPEND] — the upstream that is [sending][SendChannel.send] or + * is [emitting][kotlinx.coroutines.flow.FlowCollector.emit] a value is **suspended** while the buffer is full. + * * [DROP_OLDEST] — drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. + * * [DROP_LATEST] — drop **the latest** value that is being added to the buffer right now on buffer overflow + * (so that buffer contents stay the same), do not suspend. + */ +@ExperimentalCoroutinesApi +public enum class BufferOverflow { + /** + * Suspend on buffer overflow. + */ + SUSPEND, + + /** + * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. + */ + DROP_OLDEST, + + /** + * Drop **the latest** value that is being added to the buffer right now on buffer overflow + * (so that buffer contents stay the same), do not suspend. + */ + DROP_LATEST +} diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index c4b4a9b25e..72c08e1acd 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -44,19 +44,17 @@ public interface SendChannel { * Sends the specified [element] to this channel, suspending the caller while the buffer of this channel is full * or if it does not exist, or throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). * - * Note that closing a channel _after_ this function has suspended does not cause this suspended [send] invocation + * [Closing][close] a channel _after_ this function has suspended does not cause this suspended [send] invocation * to abort, because closing a channel is conceptually like sending a special "close token" over this channel. * All elements sent over the channel are delivered in first-in first-out order. The sent element * will be delivered to receivers before the close token. * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `send` is atomic*: when this function - * throws a [CancellationException], it means that the [element] was not sent to this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this `send` operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `send` call can send the element to the channel, + * but then throw [CancellationException], thus an exception should not be treated as a failure to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -81,6 +79,11 @@ public interface SendChannel { * in situations when `send` suspends. * * Throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). + * + * When `offer` call returns `false` it guarantees that the element was not delivered to the consumer and it + * it does not call `onUndeliveredElement` that was installed for this channel. If the channel was closed, + * then it calls `onUndeliveredElement` before throwing an exception. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. */ public fun offer(element: E): Boolean @@ -170,12 +173,10 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this `receive` operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `receive` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -200,12 +201,10 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this `receive` operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `receiveOrNull` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -250,12 +249,10 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `receiveOrClosed` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -332,7 +329,7 @@ public interface ReceiveChannel { * @suppress *This is an internal API, do not use*: Inline classes ABI is not stable yet and * [KT-27524](https://youtrack.jetbrains.com/issue/KT-27524) needs to be fixed. */ -@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") +@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS", "EXPERIMENTAL_FEATURE_WARNING") @InternalCoroutinesApi // until https://youtrack.jetbrains.com/issue/KT-27524 is fixed public inline class ValueOrClosed internal constructor(private val holder: Any?) { @@ -439,12 +436,10 @@ public interface ChannelIterator { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `hasNext` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -486,28 +481,108 @@ public interface ChannelIterator { * Conceptually, a channel is similar to Java's [BlockingQueue][java.util.concurrent.BlockingQueue], * but it has suspending operations instead of blocking ones and can be [closed][SendChannel.close]. * + * ### Creating channels + * * The `Channel(capacity)` factory function is used to create channels of different kinds depending on * the value of the `capacity` integer: * - * * When `capacity` is 0 — it creates a `RendezvousChannel`. + * * When `capacity` is 0 — it creates a _rendezvous_ channel. * This channel does not have any buffer at all. An element is transferred from the sender * to the receiver only when [send] and [receive] invocations meet in time (rendezvous), so [send] suspends * until another coroutine invokes [receive], and [receive] suspends until another coroutine invokes [send]. * - * * When `capacity` is [Channel.UNLIMITED] — it creates a `LinkedListChannel`. + * * When `capacity` is [Channel.UNLIMITED] — it creates a channel with effectively unlimited buffer. * This channel has a linked-list buffer of unlimited capacity (limited only by available memory). * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * - * * When `capacity` is [Channel.CONFLATED] — it creates a `ConflatedChannel`. + * * When `capacity` is [Channel.CONFLATED] — it creates a _conflated_ channel * This channel buffers at most one element and conflates all subsequent `send` and `offer` invocations, * so that the receiver always gets the last element sent. - * Back-to-send sent elements are _conflated_ — only the last sent element is received, + * Back-to-send sent elements are conflated — only the last sent element is received, * while previously sent elements **are lost**. * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * * * When `capacity` is positive but less than [UNLIMITED] — it creates an array-based channel with the specified capacity. * This channel has an array buffer of a fixed `capacity`. * [Sending][send] suspends only when the buffer is full, and [receiving][receive] suspends only when the buffer is empty. + * + * Buffered channels can be configured with an additional [`onBufferOverflow`][BufferOverflow] parameter. It controls the behaviour + * of the channel's [send][Channel.send] function on buffer overflow: + * + * * [SUSPEND][BufferOverflow.SUSPEND] — the default, suspend `send` on buffer overflow until there is + * free space in the buffer. + * * [DROP_OLDEST][BufferOverflow.DROP_OLDEST] — do not suspend the `send`, add the latest value to the buffer, + * drop the oldest one from the buffer. + * A channel with `capacity = 1` and `onBufferOverflow = DROP_OLDEST` is a _conflated_ channel. + * * [DROP_LATEST][BufferOverflow.DROP_LATEST] — do not suspend the `send`, drop the value that is being sent, + * keep the buffer contents intact. + * + * A non-default `onBufferOverflow` implicitly creates a channel with at least one buffered element and + * is ignored for a channel with unlimited buffer. It cannot be specified for `capacity = CONFLATED`, which + * is a shortcut by itself. + * + * ### Prompt cancellation guarantee + * + * All suspending functions with channels provide **prompt cancellation guarantee**. + * If the job was cancelled while send or receive function was suspended, it will not resume successfully, + * but throws a [CancellationException]. + * With a single-threaded [dispatcher][CoroutineDispatcher] like [Dispatchers.Main] this gives a + * guarantee that if a piece code running in this thread cancels a [Job], then a coroutine running this job cannot + * resume successfully and continue to run, ensuring a prompt response to its cancellation. + * + * > **Prompt cancellation guarantee** for channel operations was added since `kotlinx.coroutines` version `1.4.0` + * > and had replaced a channel-specific atomic-cancellation that was not consistent with other suspending functions. + * > The low-level mechanics of prompt cancellation are explained in [suspendCancellableCoroutine] function. + * + * ### Undelivered elements + * + * As a result of a prompt cancellation guarantee, when a closeable resource + * (like open file or a handle to another native resource) is transferred via channel from one coroutine to another + * it can fail to be delivered and will be lost if either send or receive operations are cancelled in transit. + * + * A `Channel()` constructor function has an `onUndeliveredElement` optional parameter. + * When `onUndeliveredElement` parameter is set, the corresponding function is called once for each element + * that was sent to the channel with the call to the [send][SendChannel.send] function but failed to be delivered, + * which can happen in the following cases: + * + * * When [send][SendChannel.send] operation throws an exception because it was cancelled before it had a chance to actually + * send the element or because the channel was [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel]. + * * When [offer][SendChannel.offer] operation throws an exception when + * the channel was [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel]. + * * When [receive][ReceiveChannel.receive], [receiveOrNull][ReceiveChannel.receiveOrNull], or [hasNext][ChannelIterator.hasNext] + * operation throws an exception when it had retrieved the element from the + * channel but was cancelled before the code following the receive call resumed. + * * The channel was [cancelled][ReceiveChannel.cancel], in which case `onUndeliveredElement` is called on every + * remaining element in the channel's buffer. + * + * Note, that `onUndeliveredElement` function is called synchronously in an arbitrary context. It should be fast, non-blocking, + * and should not throw exceptions. Any exception thrown by `onUndeliveredElement` is wrapped into an internal runtime + * exception which is either rethrown from the caller method or handed off to the exception handler in the current context + * (see [CoroutineExceptionHandler]) when one is available. + * + * A typical usage for `onDeliveredElement` is to close a resource that is being transferred via the channel. The + * following code pattern guarantees that opened resources are closed even if producer, consumer, and/or channel + * are cancelled. Resources are never lost. + * + * ``` + * // Create the channel with onUndeliveredElement block that closes a resource + * val channel = Channel(capacity) { resource -> resource.close() } + * + * // Producer code + * val resourceToSend = openResource() + * channel.send(resourceToSend) + * + * // Consumer code + * val resourceReceived = channel.receive() + * try { + * // work with received resource + * } finally { + * resourceReceived.close() + * } + * ``` + * + * > Note, that if you do any kind of work in between `openResource()` and `channel.send(...)`, then you should + * > ensure that resource gets closed in case this additional code fails. */ public interface Channel : SendChannel, ReceiveChannel { /** @@ -515,25 +590,26 @@ public interface Channel : SendChannel, ReceiveChannel { */ public companion object Factory { /** - * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function + * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function. */ public const val UNLIMITED: Int = Int.MAX_VALUE /** - * Requests a rendezvous channel in the `Channel(...)` factory function — a `RendezvousChannel` gets created. + * Requests a rendezvous channel in the `Channel(...)` factory function — a channel that does not have a buffer. */ public const val RENDEZVOUS: Int = 0 /** - * Requests a conflated channel in the `Channel(...)` factory function — a `ConflatedChannel` gets created. + * Requests a conflated channel in the `Channel(...)` factory function. This is a shortcut to creating + * a channel with [`onBufferOverflow = DROP_OLDEST`][BufferOverflow.DROP_OLDEST]. */ public const val CONFLATED: Int = -1 /** - * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function — - * an `ArrayChannel` gets created with the default capacity. - * The default capacity is 64 and can be overridden by setting - * [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. + * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function. + * The default capacity for a channel that [suspends][BufferOverflow.SUSPEND] on overflow + * is 64 and can be overridden by setting [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. + * For non-suspending channels, a buffer of capacity 1 is used. */ public const val BUFFERED: Int = -2 @@ -557,17 +633,48 @@ public interface Channel : SendChannel, ReceiveChannel { * See [Channel] interface documentation for details. * * @param capacity either a positive channel capacity or one of the constants defined in [Channel.Factory]. + * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to + * a [suspending][BufferOverflow.SUSPEND] attempt to [send][Channel.send] a value, + * supported only when `capacity >= 0` or `capacity == Channel.BUFFERED`, + * implicitly creates a channel with at least one buffered element). + * @param onUndeliveredElement an optional function that is called when element was sent but was not delivered to the consumer. + * See "Undelivered elements" section in [Channel] documentation. * @throws IllegalArgumentException when [capacity] < -2 */ -public fun Channel(capacity: Int = RENDEZVOUS): Channel = +public fun Channel( + capacity: Int = RENDEZVOUS, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + onUndeliveredElement: ((E) -> Unit)? = null +): Channel = when (capacity) { - RENDEZVOUS -> RendezvousChannel() - UNLIMITED -> LinkedListChannel() - CONFLATED -> ConflatedChannel() - BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) - else -> ArrayChannel(capacity) + RENDEZVOUS -> { + if (onBufferOverflow == BufferOverflow.SUSPEND) + RendezvousChannel(onUndeliveredElement) // an efficient implementation of rendezvous channel + else + ArrayChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel + } + CONFLATED -> { + require(onBufferOverflow == BufferOverflow.SUSPEND) { + "CONFLATED capacity cannot be used with non-default onBufferOverflow" + } + ConflatedChannel(onUndeliveredElement) + } + UNLIMITED -> LinkedListChannel(onUndeliveredElement) // ignores onBufferOverflow: it has buffer, but it never overflows + BUFFERED -> ArrayChannel( // uses default capacity with SUSPEND + if (onBufferOverflow == BufferOverflow.SUSPEND) CHANNEL_DEFAULT_CAPACITY else 1, + onBufferOverflow, onUndeliveredElement + ) + else -> { + if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST) + ConflatedChannel(onUndeliveredElement) // conflated implementation is more efficient but appears to work in the same way + else + ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement) + } } +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") +public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity) + /** * Indicates an attempt to [send][SendChannel.send] to a [isClosedForSend][SendChannel.isClosedForSend] channel * that was closed without a cause. A _failed_ channel rethrows the original [close][SendChannel.close] cause diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index 8c61928aa4..d19028bf63 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -40,12 +40,9 @@ public inline fun BroadcastChannel.consume(block: ReceiveChannel.() * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. If the `receiveOrNull` call threw [CancellationException] there is no way + * to tell if some element was already received from the channel or not. See [Channel] documentation for details. * * Note, that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 2b9375ddec..5986dae3d4 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlin.jvm.* -import kotlin.native.concurrent.* /** * Broadcasts the most recently sent element (aka [value]) to all [openSubscription] subscribers. @@ -27,9 +26,10 @@ import kotlin.native.concurrent.* * [opening][openSubscription] and [closing][ReceiveChannel.cancel] subscription takes O(N) time, where N is the * number of subscribers. * - * **Note: This API is experimental.** It may be changed in the future updates. + * **Note: This API is obsolete.** It will be deprecated and replaced by [StateFlow][kotlinx.coroutines.flow.StateFlow] + * when it becomes stable. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // not @ObsoleteCoroutinesApi to reduce burden for people who are still using it public class ConflatedBroadcastChannel() : BroadcastChannel { /** * Creates an instance of this class that already holds a value. @@ -282,7 +282,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { private class Subscriber( private val broadcastChannel: ConflatedBroadcastChannel - ) : ConflatedChannel(), ReceiveChannel { + ) : ConflatedChannel(null), ReceiveChannel { override fun onCancelIdempotent(wasClosed: Boolean) { if (wasClosed) { diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 4734766914..75e421c6e7 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* -import kotlin.native.concurrent.* /** * Channel that buffers at most one element and conflates all subsequent `send` and `offer` invocations, @@ -18,7 +17,7 @@ import kotlin.native.concurrent.* * * This channel is created by `Channel(Channel.CONFLATED)` factory function invocation. */ -internal open class ConflatedChannel : AbstractChannel() { +internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = value === EMPTY protected final override val isBufferAlwaysFull: Boolean get() = false @@ -30,10 +29,6 @@ internal open class ConflatedChannel : AbstractChannel() { private var value: Any? = EMPTY - private companion object { - private val EMPTY = Symbol("EMPTY") - } - // result is `OFFER_SUCCESS | Closed` protected override fun offerInternal(element: E): Any { var receive: ReceiveOrClosed? = null @@ -54,7 +49,7 @@ internal open class ConflatedChannel : AbstractChannel() { } } } - value = element + updateValueLocked(element)?.let { throw it } return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -87,7 +82,7 @@ internal open class ConflatedChannel : AbstractChannel() { if (!select.trySelect()) { return ALREADY_SELECTED } - value = element + updateValueLocked(element)?.let { throw it } return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -120,12 +115,20 @@ internal open class ConflatedChannel : AbstractChannel() { } protected override fun onCancelIdempotent(wasClosed: Boolean) { - if (wasClosed) { - lock.withLock { - value = EMPTY - } + var undeliveredElementException: UndeliveredElementException? = null // resource cancel exception + lock.withLock { + undeliveredElementException = updateValueLocked(EMPTY) } super.onCancelIdempotent(wasClosed) + undeliveredElementException?.let { throw it } // throw exception at the end if there was one + } + + private fun updateValueLocked(element: Any?): UndeliveredElementException? { + val old = value + val undeliveredElementException = if (old === EMPTY) null else + onUndeliveredElement?.callUndeliveredElementCatchingException(old as E) + value = element + return undeliveredElementException } override fun enqueueReceiveInternal(receive: Receive): Boolean = lock.withLock { diff --git a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt index e66bbb2279..2f46421344 100644 --- a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.selects.* * * @suppress **This an internal API and should not be used from general code.** */ -internal open class LinkedListChannel : AbstractChannel() { +internal open class LinkedListChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = true protected final override val isBufferEmpty: Boolean get() = true protected final override val isBufferAlwaysFull: Boolean get() = false diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 1b1581a99e..10a15e2a93 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -27,7 +27,11 @@ public interface ProducerScope : CoroutineScope, SendChannel { /** * Suspends the current coroutine until the channel is either [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel] - * and invokes the given [block] before resuming the coroutine. This suspending function is cancellable. + * and invokes the given [block] before resuming the coroutine. + * + * This suspending function is cancellable. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * Note that when the producer channel is cancelled, this function resumes with a cancellation exception. * Therefore, in case of cancellation, no code after the call to this function will be executed. @@ -91,13 +95,8 @@ public fun CoroutineScope.produce( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, @BuilderInference block: suspend ProducerScope.() -> Unit -): ReceiveChannel { - val channel = Channel(capacity) - val newContext = newCoroutineContext(context) - val coroutine = ProducerCoroutine(newContext, channel) - coroutine.start(CoroutineStart.DEFAULT, coroutine, block) - return coroutine -} +): ReceiveChannel = + produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block) /** * **This is an internal API and should not be used from general code.** @@ -118,8 +117,19 @@ public fun CoroutineScope.produce( start: CoroutineStart = CoroutineStart.DEFAULT, onCompletion: CompletionHandler? = null, @BuilderInference block: suspend ProducerScope.() -> Unit +): ReceiveChannel = + produce(context, capacity, BufferOverflow.SUSPEND, start, onCompletion, block) + +// Internal version of produce that is maximally flexible, but is not exposed through public API (too many params) +internal fun CoroutineScope.produce( + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = 0, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + start: CoroutineStart = CoroutineStart.DEFAULT, + onCompletion: CompletionHandler? = null, + @BuilderInference block: suspend ProducerScope.() -> Unit ): ReceiveChannel { - val channel = Channel(capacity) + val channel = Channel(capacity, onBufferOverflow) val newContext = newCoroutineContext(context) val coroutine = ProducerCoroutine(newContext, channel) if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion) diff --git a/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt b/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt index 700f50908c..857a97938f 100644 --- a/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt @@ -4,6 +4,8 @@ package kotlinx.coroutines.channels +import kotlinx.coroutines.internal.* + /** * Rendezvous channel. This channel does not have any buffer at all. An element is transferred from sender * to receiver only when [send] and [receive] invocations meet in time (rendezvous), so [send] suspends @@ -13,7 +15,7 @@ package kotlinx.coroutines.channels * * This implementation is fully lock-free. **/ -internal open class RendezvousChannel : AbstractChannel() { +internal open class RendezvousChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = true protected final override val isBufferEmpty: Boolean get() = true protected final override val isBufferAlwaysFull: Boolean get() = true diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 8fd9ae76a4..7e47e6947a 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -16,7 +16,8 @@ import kotlin.jvm.* import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** - * Creates a flow from the given suspendable [block]. + * Creates a _cold_ flow from the given suspendable [block]. + * The flow being _cold_ means that the [block] is called every time a terminal operator is applied to the resulting flow. * * Example of usage: * @@ -62,7 +63,7 @@ private class SafeFlow(private val block: suspend FlowCollector.() -> Unit } /** - * Creates a flow that produces a single value from the given functional type. + * Creates a _cold_ flow that produces a single value from the given functional type. */ @FlowPreview public fun (() -> T).asFlow(): Flow = flow { @@ -70,8 +71,10 @@ public fun (() -> T).asFlow(): Flow = flow { } /** - * Creates a flow that produces a single value from the given functional type. + * Creates a _cold_ flow that produces a single value from the given functional type. + * * Example of usage: + * * ``` * suspend fun remoteCall(): R = ... * fun remoteCallFlow(): Flow = ::remoteCall.asFlow() @@ -83,7 +86,7 @@ public fun (suspend () -> T).asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given iterable. + * Creates a _cold_ flow that produces values from the given iterable. */ public fun Iterable.asFlow(): Flow = flow { forEach { value -> @@ -92,7 +95,7 @@ public fun Iterable.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given iterator. + * Creates a _cold_ flow that produces values from the given iterator. */ public fun Iterator.asFlow(): Flow = flow { forEach { value -> @@ -101,7 +104,7 @@ public fun Iterator.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given sequence. + * Creates a _cold_ flow that produces values from the given sequence. */ public fun Sequence.asFlow(): Flow = flow { forEach { value -> @@ -113,6 +116,7 @@ public fun Sequence.asFlow(): Flow = flow { * Creates a flow that produces values from the specified `vararg`-arguments. * * Example of usage: + * * ``` * flowOf(1, 2, 3) * ``` @@ -124,7 +128,7 @@ public fun flowOf(vararg elements: T): Flow = flow { } /** - * Creates flow that produces the given [value]. + * Creates a flow that produces the given [value]. */ public fun flowOf(value: T): Flow = flow { /* @@ -144,7 +148,9 @@ private object EmptyFlow : Flow { } /** - * Creates a flow that produces values from the given array. + * Creates a _cold_ flow that produces values from the given array. + * The flow being _cold_ means that the array components are read every time a terminal operator is applied + * to the resulting flow. */ public fun Array.asFlow(): Flow = flow { forEach { value -> @@ -153,7 +159,9 @@ public fun Array.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the array. + * Creates a _cold_ flow that produces values from the array. + * The flow being _cold_ means that the array components are read every time a terminal operator is applied + * to the resulting flow. */ public fun IntArray.asFlow(): Flow = flow { forEach { value -> @@ -162,7 +170,9 @@ public fun IntArray.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the array. + * Creates a _cold_ flow that produces values from the given array. + * The flow being _cold_ means that the array components are read every time a terminal operator is applied + * to the resulting flow. */ public fun LongArray.asFlow(): Flow = flow { forEach { value -> @@ -208,7 +218,7 @@ public fun flowViaChannel( } /** - * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] + * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be * produced by code that is running in a different context or concurrently. * The resulting flow is _cold_, which means that [block] is called every time a terminal operator @@ -256,7 +266,7 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() ChannelFlowBuilder(block) /** - * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] + * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be * produced by code that is running in a different context or concurrently. * @@ -283,11 +293,12 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * Adjacent applications of [callbackFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are * always fused so that only one properly configured channel is used for execution. * - * Example of usage: + * Example of usage that converts a multi-short callback API to a flow. + * For single-shot callbacks use [suspendCancellableCoroutine]. * * ``` * fun flowFrom(api: CallbackBasedApi): Flow = callbackFlow { - * val callback = object : Callback { // implementation of some callback interface + * val callback = object : Callback { // Implementation of some callback interface * override fun onNextValue(value: T) { * // To avoid blocking you can configure channel capacity using * // either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill @@ -311,6 +322,10 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * awaitClose { api.unregister(callback) } * } * ``` + * + * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because + * > `awaitClose` block can be called at any time due to asynchronous nature of cancellation, even + * > concurrently with the call of the callback. */ @ExperimentalCoroutinesApi public fun callbackFlow(@BuilderInference block: suspend ProducerScope.() -> Unit): Flow = CallbackFlowBuilder(block) @@ -319,10 +334,11 @@ public fun callbackFlow(@BuilderInference block: suspend ProducerScope.() private open class ChannelFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowBuilder(block, context, capacity) + capacity: Int = BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowBuilder(block, context, capacity, onBufferOverflow) override suspend fun collectTo(scope: ProducerScope) = block(scope) @@ -334,8 +350,9 @@ private open class ChannelFlowBuilder( private class CallbackFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = BUFFERED -) : ChannelFlowBuilder(block, context, capacity) { + capacity: Int = BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlowBuilder(block, context, capacity, onBufferOverflow) { override suspend fun collectTo(scope: ProducerScope) { super.collectTo(scope) @@ -355,6 +372,6 @@ private class CallbackFlowBuilder( } } - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - CallbackFlowBuilder(block, context, capacity) + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + CallbackFlowBuilder(block, context, capacity, onBufferOverflow) } diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 2d3ef95aa1..762cdcad1b 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -20,6 +20,9 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * the channel afterwards. If you need to iterate over the channel without consuming it, * a regular `for` loop should be used instead. * + * Note, that emitting values from a channel into a flow is not atomic. A value that was received from the + * channel many not reach the flow collector if it was cancelled and will be lost. + * * This function provides a more efficient shorthand for `channel.consumeEach { value -> emit(value) }`. * See [consumeEach][ReceiveChannel.consumeEach]. */ @@ -116,8 +119,9 @@ private class ChannelAsFlow( private val channel: ReceiveChannel, private val consume: Boolean, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL -) : ChannelFlow(context, capacity) { + capacity: Int = Channel.OPTIONAL_CHANNEL, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { private val consumed = atomic(false) private fun markConsumed() { @@ -126,8 +130,11 @@ private class ChannelAsFlow( } } - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelAsFlow(channel, consume, context, capacity) + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelAsFlow(channel, consume, context, capacity, onBufferOverflow) + + override fun dropChannelOperators(): Flow? = + ChannelAsFlow(channel, consume) override suspend fun collectTo(scope: ProducerScope) = SendingCollector(scope).emitAllImpl(channel, consume) // use efficient channel receiving code from emitAll @@ -154,7 +161,7 @@ private class ChannelAsFlow( } } - override fun additionalToStringProps(): String = "channel=$channel, " + override fun additionalToStringProps(): String = "channel=$channel" } /** @@ -181,8 +188,22 @@ public fun BroadcastChannel.asFlow(): Flow = flow { * Use [buffer] operator on the flow before calling `broadcastIn` to specify a value other than * default and to control what happens when data is produced faster than it is consumed, * that is to control backpressure behavior. + * + * ### Deprecated + * + * **This API is deprecated.** The [BroadcastChannel] provides a complex channel-like API for hot flows. + * [SharedFlow] is a easier-to-use and more flow-centric API for the same purposes, so using + * [shareIn] operator is preferred. It is not a direct replacement, so please + * study [shareIn] documentation to see what kind of shared flow fits your use-case. As a rule of thumb: + * + * * Replace `broadcastIn(scope)` and `broadcastIn(scope, CoroutineStart.LAZY)` with `shareIn(scope, 0, SharingStarted.Lazily)`. + * * Replace `broadcastIn(scope, CoroutineStart.DEFAULT)` with `shareIn(scope, 0, SharingStarted.Eagerly)`. */ -@FlowPreview +@Deprecated( + message = "Use shareIn operator and the resulting SharedFlow as a replacement for BroadcastChannel", + replaceWith = ReplaceWith("shareIn(scope, 0, SharingStarted.Lazily)"), + level = DeprecationLevel.WARNING +) public fun Flow.broadcastIn( scope: CoroutineScope, start: CoroutineStart = CoroutineStart.LAZY diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index b7e2518694..19a5b43f31 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -9,8 +9,7 @@ import kotlinx.coroutines.flow.internal.* import kotlin.coroutines.* /** - * A cold asynchronous data stream that sequentially emits values - * and completes normally or with an exception. + * An asynchronous data stream that sequentially emits values and completes normally or with an exception. * * _Intermediate operators_ on the flow such as [map], [filter], [take], [zip], etc are functions that are * applied to the _upstream_ flow or flows and return a _downstream_ flow where further operators can be applied to. @@ -39,11 +38,12 @@ import kotlin.coroutines.* * with an exception for a few operations specifically designed to introduce concurrency into flow * execution such as [buffer] and [flatMapMerge]. See their documentation for details. * - * The `Flow` interface does not carry information whether a flow truly is a cold stream that can be collected repeatedly and - * triggers execution of the same code every time it is collected, or if it is a hot stream that emits different - * values from the same running source on each collection. However, conventionally flows represent cold streams. - * Transitions between hot and cold streams are supported via channels and the corresponding API: - * [channelFlow], [produceIn], [broadcastIn]. + * The `Flow` interface does not carry information whether a flow is a _cold_ stream that can be collected repeatedly and + * triggers execution of the same code every time it is collected, or if it is a _hot_ stream that emits different + * values from the same running source on each collection. Usually flows represent _cold_ streams, but + * there is a [SharedFlow] subtype that represents _hot_ streams. In addition to that, any flow can be turned + * into a _hot_ one by the [stateIn] and [shareIn] operators, or by converting the flow into a hot channel + * via the [produceIn] operator. * * ### Flow builders * @@ -55,6 +55,8 @@ import kotlin.coroutines.* * sequential calls to [emit][FlowCollector.emit] function. * * [channelFlow { ... }][channelFlow] builder function to construct arbitrary flows from * potentially concurrent calls to the [send][kotlinx.coroutines.channels.SendChannel.send] function. + * * [MutableStateFlow] and [MutableSharedFlow] define the corresponding constructor functions to create + * a _hot_ flow that can be directly updated. * * ### Flow constraints * @@ -159,9 +161,9 @@ import kotlin.coroutines.* * * ### Not stable for inheritance * - * **`Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **The `Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use `flow { ... }` builder function to create an implementation. + * Use the `flow { ... }` builder function to create an implementation. */ public interface Flow { /** @@ -201,7 +203,7 @@ public interface Flow { * ``` */ @FlowPreview -public abstract class AbstractFlow : Flow { +public abstract class AbstractFlow : Flow, CancellableFlow { @InternalCoroutinesApi public final override suspend fun collect(collector: FlowCollector) { diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index bb2f584474..59873eba5f 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -9,8 +9,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* -import kotlinx.coroutines.flow.internal.* -import kotlinx.coroutines.flow.internal.unsafeFlow import kotlin.coroutines.* import kotlin.jvm.* @@ -99,7 +97,7 @@ public fun Flow.publishOn(context: CoroutineContext): Flow = noImpl() * Opposed to subscribeOn, it it **possible** to use multiple `flowOn` operators in the one flow * @suppress */ -@Deprecated(message = "Use flowOn instead", level = DeprecationLevel.ERROR) +@Deprecated(message = "Use 'flowOn' instead", level = DeprecationLevel.ERROR) public fun Flow.subscribeOn(context: CoroutineContext): Flow = noImpl() /** @@ -151,7 +149,7 @@ public fun Flow.onErrorResumeNext(fallback: Flow): Flow = noImpl() * @suppress */ @Deprecated( - message = "Use launchIn with onEach, onCompletion and catch operators instead", + message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", level = DeprecationLevel.ERROR ) public fun Flow.subscribe(): Unit = noImpl() @@ -161,7 +159,7 @@ public fun Flow.subscribe(): Unit = noImpl() * @suppress */ @Deprecated( - message = "Use launchIn with onEach, onCompletion and catch operators instead", + message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", level = DeprecationLevel.ERROR )public fun Flow.subscribe(onEach: suspend (T) -> Unit): Unit = noImpl() @@ -170,7 +168,7 @@ public fun Flow.subscribe(): Unit = noImpl() * @suppress */ @Deprecated( - message = "Use launchIn with onEach, onCompletion and catch operators instead", + message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", level = DeprecationLevel.ERROR )public fun Flow.subscribe(onEach: suspend (T) -> Unit, onError: suspend (Throwable) -> Unit): Unit = noImpl() @@ -181,7 +179,7 @@ public fun Flow.subscribe(): Unit = noImpl() */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Flow analogue is named flatMapConcat", + message = "Flow analogue is 'flatMapConcat'", replaceWith = ReplaceWith("flatMapConcat(mapper)") ) public fun Flow.flatMap(mapper: suspend (T) -> Flow): Flow = noImpl() @@ -438,3 +436,50 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F ) @ExperimentalCoroutinesApi public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = runningReduce(operation) + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'publish()' is 'shareIn'. \n" + + "publish().connect() is the default strategy (no extra call is needed), \n" + + "publish().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "publish().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.shareIn(scope, 0)") +) +public fun Flow.publish(): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'publish(bufferSize)' is 'buffer' followed by 'shareIn'. \n" + + "publish().connect() is the default strategy (no extra call is needed), \n" + + "publish().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "publish().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.buffer(bufferSize).shareIn(scope, 0)") +) +public fun Flow.publish(bufferSize: Int): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'replay()' is 'shareIn' with unlimited replay. \n" + + "replay().connect() is the default strategy (no extra call is needed), \n" + + "replay().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "replay().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.shareIn(scope, Int.MAX_VALUE)") +) +public fun Flow.replay(): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'replay(bufferSize)' is 'shareIn' with the specified replay parameter. \n" + + "replay().connect() is the default strategy (no extra call is needed), \n" + + "replay().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "replay().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.shareIn(scope, bufferSize)") +) +public fun Flow.replay(bufferSize: Int): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'cache()' is 'shareIn' with unlimited replay and 'started = SharingStared.Lazily' argument'", + replaceWith = ReplaceWith("this.shareIn(scope, Int.MAX_VALUE, started = SharingStared.Lazily)") +) +public fun Flow.cache(): Flow = noImpl() \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt new file mode 100644 index 0000000000..88dc775842 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -0,0 +1,659 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.native.concurrent.* + +/** + * A _hot_ [Flow] that shares emitted values among all its collectors in a broadcast fashion, so that all collectors + * get all emitted values. A shared flow is called _hot_ because its active instance exists independently of the + * presence of collectors. This is opposed to a regular [Flow], such as defined by the [`flow { ... }`][flow] function, + * which is _cold_ and is started separately for each collector. + * + * **Shared flow never completes**. A call to [Flow.collect] on a shared flow never completes normally, and + * neither does a coroutine started by the [Flow.launchIn] function. An active collector of a shared flow is called a _subscriber_. + * + * A subscriber of a shared flow can be cancelled. This usually happens when the scope in which the coroutine is running + * is cancelled. A subscriber to a shared flow is always [cancellable][Flow.cancellable], and checks for + * cancellation before each emission. Note that most terminal operators like [Flow.toList] would also not complete, + * when applied to a shared flow, but flow-truncating operators like [Flow.take] and [Flow.takeWhile] can be used on a + * shared flow to turn it into a completing one. + * + * A [mutable shared flow][MutableSharedFlow] is created using the [MutableSharedFlow(...)] constructor function. + * Its state can be updated by [emitting][MutableSharedFlow.emit] values to it and performing other operations. + * See the [MutableSharedFlow] documentation for details. + * + * [SharedFlow] is useful for broadcasting events that happen inside an application to subscribers that can come and go. + * For example, the following class encapsulates an event bus that distributes events to all subscribers + * in a _rendezvous_ manner, suspending until all subscribers process each event: + * + * ``` + * class EventBus { + * private val _events = MutableSharedFlow() // private mutable shared flow + * val events = _events.asSharedFlow() // publicly exposed as read-only shared flow + * + * suspend fun produceEvent(event: Event) { + * _events.emit(event) // suspends until all subscribers receive it + * } + * } + * ``` + * + * As an alternative to the above usage with the `MutableSharedFlow(...)` constructor function, + * any _cold_ [Flow] can be converted to a shared flow using the [shareIn] operator. + * + * There is a specialized implementation of shared flow for the case where the most recent state value needs + * to be shared. See [StateFlow] for details. + * + * ### Replay cache and buffer + * + * A shared flow keeps a specific number of the most recent values in its _replay cache_. Every new subscriber first + * gets the values from the replay cache and then gets new emitted values. The maximum size of the replay cache is + * specified when the shared flow is created by the `replay` parameter. A snapshot of the current replay cache + * is available via the [replayCache] property and it can be reset with the [MutableSharedFlow.resetReplayCache] function. + * + * A replay cache also provides buffer for emissions to the shared flow, allowing slow subscribers to + * get values from the buffer without suspending emitters. The buffer space determines how much slow subscribers + * can lag from the fast ones. When creating a shared flow, additional buffer capacity beyond replay can be reserved + * using the `extraBufferCapacity` parameter. + * + * A shared flow with a buffer can be configured to avoid suspension of emitters on buffer overflow using + * the `onBufferOverflow` parameter, which is equal to one of the entries of the [BufferOverflow] enum. When a strategy other + * than [SUSPENDED][BufferOverflow.SUSPEND] is configured, emissions to the shared flow never suspend. + * + * ### SharedFlow vs BroadcastChannel + * + * Conceptually shared flow is similar to [BroadcastChannel][BroadcastChannel] + * and is designed to completely replace `BroadcastChannel` in the future. + * It has the following important differences: + * + * * `SharedFlow` is simpler, because it does not have to implement all the [Channel] APIs, which allows + * for faster and simpler implementation. + * * `SharedFlow` supports configurable replay and buffer overflow strategy. + * * `SharedFlow` has a clear separation into a read-only `SharedFlow` interface and a [MutableSharedFlow]. + * * `SharedFlow` cannot be closed like `BroadcastChannel` and can never represent a failure. + * All errors and completion signals should be explicitly _materialized_ if needed. + * + * To migrate [BroadcastChannel] usage to [SharedFlow], start by replacing usages of the `BroadcastChannel(capacity)` + * constructor with `MutableSharedFlow(0, extraBufferCapacity=capacity)` (broadcast channel does not replay + * values to new subscribers). Replace [send][BroadcastChannel.send] and [offer][BroadcastChannel.offer] calls + * with [emit][MutableStateFlow.emit] and [tryEmit][MutableStateFlow.tryEmit], and convert subscribers' code to flow operators. + * + * ### Concurrency + * + * All methods of shared flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. + * + * ### Operator fusion + * + * Application of [flowOn][Flow.flowOn], [buffer] with [RENDEZVOUS][Channel.RENDEZVOUS] capacity, + * or [cancellable] operators to a shared flow has no effect. + * + * ### Implementation notes + * + * Shared flow implementation uses a lock to ensure thread-safety, but suspending collector and emitter coroutines are + * resumed outside of this lock to avoid dead-locks when using unconfined coroutines. Adding new subscribers + * has `O(1)` amortized cost, but emitting has `O(N)` cost, where `N` is the number of subscribers. + * + * ### Not stable for inheritance + * + * **The `SharedFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * might be added to this interface in the future, but is stable for use. + * Use the `MutableSharedFlow(replay, ...)` constructor function to create an implementation. + */ +@ExperimentalCoroutinesApi +public interface SharedFlow : Flow { + /** + * A snapshot of the replay cache. + */ + public val replayCache: List +} + +/** + * A mutable [SharedFlow] that provides functions to [emit] values to the flow. + * An instance of `MutableSharedFlow` with the given configuration parameters can be created using `MutableSharedFlow(...)` + * constructor function. + * + * See the [SharedFlow] documentation for details on shared flows. + * + * `MutableSharedFlow` is a [SharedFlow] that also provides the abilities to [emit] a value, + * to [tryEmit] without suspension if possible, to track the [subscriptionCount], + * and to [resetReplayCache]. + * + * ### Concurrency + * + * All methods of shared flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. + * + * ### Not stable for inheritance + * + * **The `MutableSharedFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * might be added to this interface in the future, but is stable for use. + * Use the `MutableSharedFlow(...)` constructor function to create an implementation. + */ +@ExperimentalCoroutinesApi +public interface MutableSharedFlow : SharedFlow, FlowCollector { + /** + * Tries to emit a [value] to this shared flow without suspending. It returns `true` if the value was + * emitted successfully. When this function returns `false`, it means that the call to a plain [emit] + * function will suspend until there is a buffer space available. + * + * A shared flow configured with a [BufferOverflow] strategy other than [SUSPEND][BufferOverflow.SUSPEND] + * (either [DROP_OLDEST][BufferOverflow.DROP_OLDEST] or [DROP_LATEST][BufferOverflow.DROP_LATEST]) never + * suspends on [emit], and thus `tryEmit` to such a shared flow always returns `true`. + */ + public fun tryEmit(value: T): Boolean + + /** + * The number of subscribers (active collectors) to this shared flow. + * + * The integer in the resulting [StateFlow] is not negative and starts with zero for a freshly created + * shared flow. + * + * This state can be used to react to changes in the number of subscriptions to this shared flow. + * For example, if you need to call `onActive` when the first subscriber appears and `onInactive` + * when the last one disappears, you can set it up like this: + * + * ``` + * sharedFlow.subscriptionCount + * .map { count -> count > 0 } // map count into active/inactive flag + * .distinctUntilChanged() // only react to true<->false changes + * .onEach { isActive -> // configure an action + * if (isActive) onActive() else onInactive() + * } + * .launchIn(scope) // launch it + * ``` + */ + public val subscriptionCount: StateFlow + + /** + * Resets the [replayCache] of this shared flow to an empty state. + * New subscribers will be receiving only the values that were emitted after this call, + * while old subscribers will still be receiving previously buffered values. + * To reset a shared flow to an initial value, emit the value after this call. + * + * On a [MutableStateFlow], which always contains a single value, this function is not + * supported, and throws an [UnsupportedOperationException]. To reset a [MutableStateFlow] + * to an initial value, just update its [value][MutableStateFlow.value]. + * + * **Note: This is an experimental api.** This function may be removed or renamed in the future. + */ + @ExperimentalCoroutinesApi + public fun resetReplayCache() +} + +/** + * Creates a [MutableSharedFlow] with the given configuration parameters. + * + * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. + * + * @param replay the number of values replayed to new subscribers (cannot be negative, defaults to zero). + * @param extraBufferCapacity the number of values buffered in addition to `replay`. + * [emit][MutableSharedFlow.emit] does not suspend while there is a buffer space remaining (optional, cannot be negative, defaults to zero). + * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to + * [suspending][BufferOverflow.SUSPEND] attempts to [emit][MutableSharedFlow.emit] a value, + * supported only when `replay > 0` or `extraBufferCapacity > 0`). + */ +@Suppress("FunctionName", "UNCHECKED_CAST") +@ExperimentalCoroutinesApi +public fun MutableSharedFlow( + replay: Int = 0, + extraBufferCapacity: Int = 0, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +): MutableSharedFlow { + require(replay >= 0) { "replay cannot be negative, but was $replay" } + require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" } + require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) { + "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow" + } + val bufferCapacity0 = replay + extraBufferCapacity + val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow + return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow) +} + +// ------------------------------------ Implementation ------------------------------------ + +private class SharedFlowSlot : AbstractSharedFlowSlot>() { + @JvmField + var index = -1L // current "to-be-emitted" index, -1 means the slot is free now + + @JvmField + var cont: Continuation? = null // collector waiting for new value + + override fun allocateLocked(flow: SharedFlowImpl<*>): Boolean { + if (index >= 0) return false // not free + index = flow.updateNewCollectorIndexLocked() + return true + } + + override fun freeLocked(flow: SharedFlowImpl<*>): Array?> { + assert { index >= 0 } + val oldIndex = index + index = -1L + cont = null // cleanup continuation reference + return flow.updateCollectorIndexLocked(oldIndex) + } +} + +private class SharedFlowImpl( + private val replay: Int, + private val bufferCapacity: Int, + private val onBufferOverflow: BufferOverflow +) : AbstractSharedFlow(), MutableSharedFlow, CancellableFlow, FusibleFlow { + /* + Logical structure of the buffer + + buffered values + /-----------------------\ + replayCache queued emitters + /----------\/----------------------\ + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + | | 1 | 2 | 3 | 4 | 5 | 6 | E | E | E | E | E | E | | | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + ^ ^ ^ ^ + | | | | + head | head + bufferSize head + totalSize + | | | + index of the slowest | index of the fastest + possible collector | possible collector + | | + | replayIndex == new collector's index + \---------------------- / + range of possible minCollectorIndex + + head == minOf(minCollectorIndex, replayIndex) // by definition + totalSize == bufferSize + queueSize // by definition + + INVARIANTS: + minCollectorIndex = activeSlots.minOf { it.index } ?: (head + bufferSize) + replayIndex <= head + bufferSize + */ + + // Stored state + private var buffer: Array? = null // allocated when needed, allocated size always power of two + private var replayIndex = 0L // minimal index from which new collector gets values + private var minCollectorIndex = 0L // minimal index of active collectors, equal to replayIndex if there are none + private var bufferSize = 0 // number of buffered values + private var queueSize = 0 // number of queued emitters + + // Computed state + private val head: Long get() = minOf(minCollectorIndex, replayIndex) + private val replaySize: Int get() = (head + bufferSize - replayIndex).toInt() + private val totalSize: Int get() = bufferSize + queueSize + private val bufferEndIndex: Long get() = head + bufferSize + private val queueEndIndex: Long get() = head + bufferSize + queueSize + + override val replayCache: List + get() = synchronized(this) { + val replaySize = this.replaySize + if (replaySize == 0) return emptyList() + val result = ArrayList(replaySize) + val buffer = buffer!! // must be allocated, because replaySize > 0 + @Suppress("UNCHECKED_CAST") + for (i in 0 until replaySize) result += buffer.getBufferAt(replayIndex + i) as T + result + } + + @Suppress("UNCHECKED_CAST") + override suspend fun collect(collector: FlowCollector) { + val slot = allocateSlot() + try { + if (collector is SubscribedFlowCollector) collector.onSubscription() + val collectorJob = currentCoroutineContext()[Job] + while (true) { + var newValue: Any? + while (true) { + newValue = tryTakeValue(slot) // attempt no-suspend fast path first + if (newValue !== NO_VALUE) break + awaitValue(slot) // await signal that the new value is available + } + collectorJob?.ensureActive() + collector.emit(newValue as T) + } + } finally { + freeSlot(slot) + } + } + + override fun tryEmit(value: T): Boolean { + var resumes: Array?> = EMPTY_RESUMES + val emitted = synchronized(this) { + if (tryEmitLocked(value)) { + resumes = findSlotsToResumeLocked() + true + } else { + false + } + } + for (cont in resumes) cont?.resume(Unit) + return emitted + } + + override suspend fun emit(value: T) { + if (tryEmit(value)) return // fast-path + emitSuspend(value) + } + + @Suppress("UNCHECKED_CAST") + private fun tryEmitLocked(value: T): Boolean { + // Fast path without collectors -> no buffering + if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true + // With collectors we'll have to buffer + // cannot emit now if buffer is full & blocked by slow collectors + if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) { + when (onBufferOverflow) { + BufferOverflow.SUSPEND -> return false // will suspend + BufferOverflow.DROP_LATEST -> return true // just drop incoming + BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead + } + } + enqueueLocked(value) + bufferSize++ // value was added to buffer + // drop oldest from the buffer if it became more than bufferCapacity + if (bufferSize > bufferCapacity) dropOldestLocked() + // keep replaySize not larger that needed + if (replaySize > replay) { // increment replayIndex by one + updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex) + } + return true + } + + private fun tryEmitNoCollectorsLocked(value: T): Boolean { + assert { nCollectors == 0 } + if (replay == 0) return true // no need to replay, just forget it now + enqueueLocked(value) // enqueue to replayCache + bufferSize++ // value was added to buffer + // drop oldest from the buffer if it became more than replay + if (bufferSize > replay) dropOldestLocked() + minCollectorIndex = head + bufferSize // a default value (max allowed) + return true + } + + private fun dropOldestLocked() { + buffer!!.setBufferAt(head, null) + bufferSize-- + val newHead = head + 1 + if (replayIndex < newHead) replayIndex = newHead + if (minCollectorIndex < newHead) correctCollectorIndexesOnDropOldest(newHead) + assert { head == newHead } // since head = minOf(minCollectorIndex, replayIndex) it should have updated + } + + private fun correctCollectorIndexesOnDropOldest(newHead: Long) { + forEachSlotLocked { slot -> + @Suppress("ConvertTwoComparisonsToRangeCheck") // Bug in JS backend + if (slot.index >= 0 && slot.index < newHead) { + slot.index = newHead // force move it up (this collector was too slow and missed the value at its index) + } + } + minCollectorIndex = newHead + } + + // enqueues item to buffer array, caller shall increment either bufferSize or queueSize + private fun enqueueLocked(item: Any?) { + val curSize = totalSize + val buffer = when (val curBuffer = buffer) { + null -> growBuffer(null, 0, 2) + else -> if (curSize >= curBuffer.size) growBuffer(curBuffer, curSize,curBuffer.size * 2) else curBuffer + } + buffer.setBufferAt(head + curSize, item) + } + + private fun growBuffer(curBuffer: Array?, curSize: Int, newSize: Int): Array { + check(newSize > 0) { "Buffer size overflow" } + val newBuffer = arrayOfNulls(newSize).also { buffer = it } + if (curBuffer == null) return newBuffer + val head = head + for (i in 0 until curSize) { + newBuffer.setBufferAt(head + i, curBuffer.getBufferAt(head + i)) + } + return newBuffer + } + + private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine sc@{ cont -> + var resumes: Array?> = EMPTY_RESUMES + val emitter = synchronized(this) lock@{ + // recheck buffer under lock again (make sure it is really full) + if (tryEmitLocked(value)) { + cont.resume(Unit) + resumes = findSlotsToResumeLocked() + return@lock null + } + // add suspended emitter to the buffer + Emitter(this, head + totalSize, value, cont).also { + enqueueLocked(it) + queueSize++ // added to queue of waiting emitters + // synchronous shared flow might rendezvous with waiting emitter + if (bufferCapacity == 0) resumes = findSlotsToResumeLocked() + } + } + // outside of the lock: register dispose on cancellation + emitter?.let { cont.disposeOnCancellation(it) } + // outside of the lock: resume slots if needed + for (cont in resumes) cont?.resume(Unit) + } + + private fun cancelEmitter(emitter: Emitter) = synchronized(this) { + if (emitter.index < head) return // already skipped past this index + val buffer = buffer!! + if (buffer.getBufferAt(emitter.index) !== emitter) return // already resumed + buffer.setBufferAt(emitter.index, NO_VALUE) + cleanupTailLocked() + } + + internal fun updateNewCollectorIndexLocked(): Long { + val index = replayIndex + if (index < minCollectorIndex) minCollectorIndex = index + return index + } + + // Is called when a collector disappears or changes index, returns a list of continuations to resume after lock + internal fun updateCollectorIndexLocked(oldIndex: Long): Array?> { + assert { oldIndex >= minCollectorIndex } + if (oldIndex > minCollectorIndex) return EMPTY_RESUMES // nothing changes, it was not min + // start computing new minimal index of active collectors + val head = head + var newMinCollectorIndex = head + bufferSize + // take into account a special case of sync shared flow that can go past 1st queued emitter + if (bufferCapacity == 0 && queueSize > 0) newMinCollectorIndex++ + forEachSlotLocked { slot -> + @Suppress("ConvertTwoComparisonsToRangeCheck") // Bug in JS backend + if (slot.index >= 0 && slot.index < newMinCollectorIndex) newMinCollectorIndex = slot.index + } + assert { newMinCollectorIndex >= minCollectorIndex } // can only grow + if (newMinCollectorIndex <= minCollectorIndex) return EMPTY_RESUMES // nothing changes + // Compute new buffer size if we drop items we no longer need and no emitter is resumed: + // We must keep all the items from newMinIndex to the end of buffer + var newBufferEndIndex = bufferEndIndex // var to grow when waiters are resumed + val maxResumeCount = if (nCollectors > 0) { + // If we have collectors we can resume up to maxResumeCount waiting emitters + // a) queueSize -> that's how many waiting emitters we have + // b) bufferCapacity - newBufferSize0 -> that's how many we can afford to resume to add w/o exceeding bufferCapacity + val newBufferSize0 = (newBufferEndIndex - newMinCollectorIndex).toInt() + minOf(queueSize, bufferCapacity - newBufferSize0) + } else { + // If we don't have collectors anymore we must resume all waiting emitters + queueSize // that's how many waiting emitters we have (at most) + } + var resumes: Array?> = EMPTY_RESUMES + val newQueueEndIndex = newBufferEndIndex + queueSize + if (maxResumeCount > 0) { // collect emitters to resume if we have them + resumes = arrayOfNulls(maxResumeCount) + var resumeCount = 0 + val buffer = buffer!! + for (curEmitterIndex in newBufferEndIndex until newQueueEndIndex) { + val emitter = buffer.getBufferAt(curEmitterIndex) + if (emitter !== NO_VALUE) { + emitter as Emitter // must have Emitter class + resumes[resumeCount++] = emitter.cont + buffer.setBufferAt(curEmitterIndex, NO_VALUE) // make as canceled if we moved ahead + buffer.setBufferAt(newBufferEndIndex, emitter.value) + newBufferEndIndex++ + if (resumeCount >= maxResumeCount) break // enough resumed, done + } + } + } + // Compute new buffer size -> how many values we now actually have after resume + val newBufferSize1 = (newBufferEndIndex - head).toInt() + // Compute new replay size -> limit to replay the number of items we need, take into account that it can only grow + var newReplayIndex = maxOf(replayIndex, newBufferEndIndex - minOf(replay, newBufferSize1)) + // adjustment for synchronous case with cancelled emitter (NO_VALUE) + if (bufferCapacity == 0 && newReplayIndex < newQueueEndIndex && buffer!!.getBufferAt(newReplayIndex) == NO_VALUE) { + newBufferEndIndex++ + newReplayIndex++ + } + // Update buffer state + updateBufferLocked(newReplayIndex, newMinCollectorIndex, newBufferEndIndex, newQueueEndIndex) + // just in case we've moved all buffered emitters and have NO_VALUE's at the tail now + cleanupTailLocked() + return resumes + } + + private fun updateBufferLocked( + newReplayIndex: Long, + newMinCollectorIndex: Long, + newBufferEndIndex: Long, + newQueueEndIndex: Long + ) { + // Compute new head value + val newHead = minOf(newMinCollectorIndex, newReplayIndex) + assert { newHead >= head } + // cleanup items we don't have to buffer anymore (because head is about to move) + for (index in head until newHead) buffer!!.setBufferAt(index, null) + // update all state variables to newly computed values + replayIndex = newReplayIndex + minCollectorIndex = newMinCollectorIndex + bufferSize = (newBufferEndIndex - newHead).toInt() + queueSize = (newQueueEndIndex - newBufferEndIndex).toInt() + // check our key invariants (just in case) + assert { bufferSize >= 0 } + assert { queueSize >= 0 } + assert { replayIndex <= this.head + bufferSize } + } + + // Removes all the NO_VALUE items from the end of the queue and reduces its size + private fun cleanupTailLocked() { + // If we have synchronous case, then keep one emitter queued + if (bufferCapacity == 0 && queueSize <= 1) return // return, don't clear it + val buffer = buffer!! + while (queueSize > 0 && buffer.getBufferAt(head + totalSize - 1) === NO_VALUE) { + queueSize-- + buffer.setBufferAt(head + totalSize, null) + } + } + + // returns NO_VALUE if cannot take value without suspension + private fun tryTakeValue(slot: SharedFlowSlot): Any? { + var resumes: Array?> = EMPTY_RESUMES + val value = synchronized(this) { + val index = tryPeekLocked(slot) + if (index < 0) { + NO_VALUE + } else { + val oldIndex = slot.index + val newValue = getPeekedValueLockedAt(index) + slot.index = index + 1 // points to the next index after peeked one + resumes = updateCollectorIndexLocked(oldIndex) + newValue + } + } + for (resume in resumes) resume?.resume(Unit) + return value + } + + // returns -1 if cannot peek value without suspension + private fun tryPeekLocked(slot: SharedFlowSlot): Long { + // return buffered value if possible + val index = slot.index + if (index < bufferEndIndex) return index + if (bufferCapacity > 0) return -1L // if there's a buffer, never try to rendezvous with emitters + // Synchronous shared flow (bufferCapacity == 0) tries to rendezvous + if (index > head) return -1L // ... but only with the first emitter (never look forward) + if (queueSize == 0) return -1L // nothing there to rendezvous with + return index // rendezvous with the first emitter + } + + private fun getPeekedValueLockedAt(index: Long): Any? = + when (val item = buffer!!.getBufferAt(index)) { + is Emitter -> item.value + else -> item + } + + private suspend fun awaitValue(slot: SharedFlowSlot): Unit = suspendCancellableCoroutine { cont -> + synchronized(this) lock@{ + val index = tryPeekLocked(slot) // recheck under this lock + if (index < 0) { + slot.cont = cont // Ok -- suspending + } else { + cont.resume(Unit) // has value, no need to suspend + return@lock + } + slot.cont = cont // suspend, waiting + } + } + + private fun findSlotsToResumeLocked(): Array?> { + var resumes: Array?> = EMPTY_RESUMES + var resumeCount = 0 + forEachSlotLocked loop@{ slot -> + val cont = slot.cont ?: return@loop // only waiting slots + if (tryPeekLocked(slot) < 0) return@loop // only slots that can peek a value + if (resumeCount >= resumes.size) resumes = resumes.copyOf(maxOf(2, 2 * resumes.size)) + resumes[resumeCount++] = cont + slot.cont = null // not waiting anymore + } + return resumes + } + + override fun createSlot() = SharedFlowSlot() + override fun createSlotArray(size: Int): Array = arrayOfNulls(size) + + override fun resetReplayCache() = synchronized(this) { + // Update buffer state + updateBufferLocked( + newReplayIndex = bufferEndIndex, + newMinCollectorIndex = minCollectorIndex, + newBufferEndIndex = bufferEndIndex, + newQueueEndIndex = queueEndIndex + ) + } + + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseSharedFlow(context, capacity, onBufferOverflow) + + private class Emitter( + @JvmField val flow: SharedFlowImpl<*>, + @JvmField var index: Long, + @JvmField val value: Any?, + @JvmField val cont: Continuation + ) : DisposableHandle { + override fun dispose() = flow.cancelEmitter(this) + } +} + +@SharedImmutable +@JvmField +internal val NO_VALUE = Symbol("NO_VALUE") + +private fun Array.getBufferAt(index: Long) = get(index.toInt() and (size - 1)) +private fun Array.setBufferAt(index: Long, item: Any?) = set(index.toInt() and (size - 1), item) + +internal fun SharedFlow.fuseSharedFlow( + context: CoroutineContext, + capacity: Int, + onBufferOverflow: BufferOverflow +): Flow { + // context is irrelevant for shared flow and making additional rendezvous is meaningless + // however, additional non-trivial buffering after shared flow could make sense for very slow subscribers + if ((capacity == Channel.RENDEZVOUS || capacity == Channel.OPTIONAL_CHANNEL) && onBufferOverflow == BufferOverflow.SUSPEND) { + return this + } + // Apply channel flow operator as usual + return ChannelFlowOperatorImpl(this, context, capacity, onBufferOverflow) +} diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt new file mode 100644 index 0000000000..935efdae2b --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* +import kotlin.time.* + +/** + * A command emitted by [SharingStarted] implementations to control the sharing coroutine in + * the [shareIn] and [stateIn] operators. + */ +@ExperimentalCoroutinesApi +public enum class SharingCommand { + /** + * Starts sharing, launching collection of the upstream flow. + * + * Emitting this command again does not do anything. Emit [STOP] and then [START] to restart an + * upstream flow. + */ + START, + + /** + * Stops sharing, cancelling collection of the upstream flow. + */ + STOP, + + /** + * Stops sharing, cancelling collection of the upstream flow, and resets the [SharedFlow.replayCache] + * to its initial state. + * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; + * the [stateIn] operator resets the value to its original `initialValue`. + */ + STOP_AND_RESET_REPLAY_CACHE +} + +/** + * A strategy for starting and stopping the sharing coroutine in [shareIn] and [stateIn] operators. + * + * This interface provides a set of built-in strategies: [Eagerly], [Lazily], [WhileSubscribed], and + * supports custom strategies by implementing this interface's [command] function. + * + * For example, it is possible to define a custom strategy that starts the upstream only when the number + * of subscribers exceeds the given `threshold` and make it an extension on [SharingStarted.Companion] so + * that it looks like a built-in strategy on the use-site: + * + * ``` + * fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = + * object : SharingStarted { + * override fun command(subscriptionCount: StateFlow): Flow = + * subscriptionCount + * .map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } + * } + * ``` + * + * ### Commands + * + * The `SharingStarted` strategy works by emitting [commands][SharingCommand] that control upstream flow from its + * [`command`][command] flow implementation function. Back-to-back emissions of the same command have no effect. + * Only emission of a different command has effect: + * + * * [START][SharingCommand.START] — the upstream flow is stared. + * * [STOP][SharingCommand.STOP] — the upstream flow is stopped. + * * [STOP_AND_RESET_REPLAY_CACHE][SharingCommand.STOP_AND_RESET_REPLAY_CACHE] — + * the upstream flow is stopped and the [SharedFlow.replayCache] is reset to its initial state. + * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; + * the [stateIn] operator resets the value to its original `initialValue`. + * + * Initially, the upstream flow is stopped and is in the initial state, so the emission of additional + * [STOP][SharingCommand.STOP] and [STOP_AND_RESET_REPLAY_CACHE][SharingCommand.STOP_AND_RESET_REPLAY_CACHE] commands will + * have no effect. + * + * The completion of the `command` flow normally has no effect (the upstream flow keeps running if it was running). + * The failure of the `command` flow cancels the sharing coroutine and the upstream flow. + */ +@ExperimentalCoroutinesApi +public interface SharingStarted { + public companion object { + /** + * Sharing is started immediately and never stops. + */ + @ExperimentalCoroutinesApi + public val Eagerly: SharingStarted = StartedEagerly() + + /** + * Sharing is started when the first subscriber appears and never stops. + */ + @ExperimentalCoroutinesApi + public val Lazily: SharingStarted = StartedLazily() + + /** + * Sharing is started when the first subscriber appears, immediately stops when the last + * subscriber disappears (by default), keeping the replay cache forever (by default). + * + * It has the following optional parameters: + * + * * [stopTimeoutMillis] — configures a delay (in milliseconds) between the disappearance of the last + * subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately). + * * [replayExpirationMillis] — configures a delay (in milliseconds) between the stopping of + * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator + * and resets the cached value to the original `initialValue` for the [stateIn] operator). + * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer). + * Use zero value to expire the cache immediately. + * + * This function throws [IllegalArgumentException] when either [stopTimeoutMillis] or [replayExpirationMillis] + * are negative. + */ + @Suppress("FunctionName") + @ExperimentalCoroutinesApi + public fun WhileSubscribed( + stopTimeoutMillis: Long = 0, + replayExpirationMillis: Long = Long.MAX_VALUE + ): SharingStarted = + StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis) + } + + /** + * Transforms the [subscriptionCount][MutableSharedFlow.subscriptionCount] state of the shared flow into the + * flow of [commands][SharingCommand] that control the sharing coroutine. See the [SharingStarted] interface + * documentation for details. + */ + public fun command(subscriptionCount: StateFlow): Flow +} + +/** + * Sharing is started when the first subscriber appears, immediately stops when the last + * subscriber disappears (by default), keeping the replay cache forever (by default). + * + * It has the following optional parameters: + * + * * [stopTimeout] — configures a delay between the disappearance of the last + * subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately). + * * [replayExpiration] — configures a delay between the stopping of + * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator + * and resets the cached value to the original `initialValue` for the [stateIn] operator). + * It defaults to [Duration.INFINITE] (keep replay cache forever, never reset buffer). + * Use [Duration.ZERO] value to expire the cache immediately. + * + * This function throws [IllegalArgumentException] when either [stopTimeout] or [replayExpiration] + * are negative. + */ +@Suppress("FunctionName") +@ExperimentalTime +@ExperimentalCoroutinesApi +public fun SharingStarted.Companion.WhileSubscribed( + stopTimeout: Duration = Duration.ZERO, + replayExpiration: Duration = Duration.INFINITE +): SharingStarted = + StartedWhileSubscribed(stopTimeout.toLongMilliseconds(), replayExpiration.toLongMilliseconds()) + +// -------------------------------- implementation -------------------------------- + +private class StartedEagerly : SharingStarted { + override fun command(subscriptionCount: StateFlow): Flow = + flowOf(SharingCommand.START) + override fun toString(): String = "SharingStarted.Eagerly" +} + +private class StartedLazily : SharingStarted { + override fun command(subscriptionCount: StateFlow): Flow = flow { + var started = false + subscriptionCount.collect { count -> + if (count > 0 && !started) { + started = true + emit(SharingCommand.START) + } + } + } + + override fun toString(): String = "SharingStarted.Lazily" +} + +private class StartedWhileSubscribed( + private val stopTimeout: Long, + private val replayExpiration: Long +) : SharingStarted { + init { + require(stopTimeout >= 0) { "stopTimeout($stopTimeout ms) cannot be negative" } + require(replayExpiration >= 0) { "replayExpiration($replayExpiration ms) cannot be negative" } + } + + override fun command(subscriptionCount: StateFlow): Flow = subscriptionCount + .transformLatest { count -> + if (count > 0) { + emit(SharingCommand.START) + } else { + delay(stopTimeout) + if (replayExpiration > 0) { + emit(SharingCommand.STOP) + delay(replayExpiration) + } + emit(SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + } + } + .dropWhile { it != SharingCommand.START } // don't emit any STOP/RESET_BUFFER to start with, only START + .distinctUntilChanged() // just in case somebody forgets it, don't leak our multiple sending of START + + @OptIn(ExperimentalStdlibApi::class) + override fun toString(): String { + val params = buildList(2) { + if (stopTimeout > 0) add("stopTimeout=${stopTimeout}ms") + if (replayExpiration < Long.MAX_VALUE) add("replayExpiration=${replayExpiration}ms") + } + return "SharingStarted.WhileSubscribed(${params.joinToString()})" + } + + // equals & hashcode to facilitate testing, not documented in public contract + override fun equals(other: Any?): Boolean = + other is StartedWhileSubscribed && + stopTimeout == other.stopTimeout && + replayExpiration == other.replayExpiration + + override fun hashCode(): Int = stopTimeout.hashCode() * 31 + replayExpiration.hashCode() +} diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index b2bbb6d3ae..8587606633 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -13,9 +13,12 @@ import kotlin.coroutines.* import kotlin.native.concurrent.* /** - * A [Flow] that represents a read-only state with a single updatable data [value] that emits updates - * to the value to its collectors. The current value can be retrieved via [value] property. - * The flow of future updates to the value can be observed by collecting values from this flow. + * A [SharedFlow] that represents a read-only state with a single updatable data [value] that emits updates + * to the value to its collectors. A state flow is a _hot_ flow because its active instance exists independently + * of the presence of collectors. Its current value can be retrieved via the [value] property. + * + * **State flow never completes**. A call to [Flow.collect] on a state flow never completes normally, and + * neither does a coroutine started by the [Flow.launchIn] function. An active collector of a state flow is called a _subscriber_. * * A [mutable state flow][MutableStateFlow] is created using `MutableStateFlow(value)` constructor function with * the initial value. The value of mutable state flow can be updated by setting its [value] property. @@ -31,7 +34,7 @@ import kotlin.native.concurrent.* * ``` * class CounterModel { * private val _counter = MutableStateFlow(0) // private mutable state flow - * val counter: StateFlow get() = _counter // publicly exposed as read-only state flow + * val counter = _counter.asStateFlow() // publicly exposed as read-only state flow * * fun inc() { * _counter.value++ @@ -47,6 +50,9 @@ import kotlin.native.concurrent.* * val sumFlow: Flow = aModel.counter.combine(bModel.counter) { a, b -> a + b } * ``` * + * As an alternative to the above usage with the `MutableStateFlow(...)` constructor function, + * any _cold_ [Flow] can be converted to a state flow using the [stateIn] operator. + * * ### Strong equality-based conflation * * Values in state flow are conflated using [Any.equals] comparison in a similar way to @@ -55,12 +61,35 @@ import kotlin.native.concurrent.* * when new value is equal to the previously emitted one. State flow behavior with classes that violate * the contract for [Any.equals] is unspecified. * + * ### State flow is a shared flow + * + * State flow is a special-purpose, high-performance, and efficient implementation of [SharedFlow] for the narrow, + * but widely used case of sharing a state. See the [SharedFlow] documentation for the basic rules, + * constraints, and operators that are applicable to all shared flows. + * + * State flow always has an initial value, replays one most recent value to new subscribers, does not buffer any + * more values, but keeps the last emitted one, and does not support [resetReplayCache][MutableSharedFlow.resetReplayCache]. + * A state flow behaves identically to a shared flow when it is created + * with the following parameters and the [distinctUntilChanged] operator is applied to it: + * + * ``` + * // MutableStateFlow(initialValue) is a shared flow with the following parameters: + * val shared = MutableSharedFlow( + * replay = 1, + * onBufferOverflow = BufferOverflow.DROP_OLDEST + * ) + * shared.tryEmit(initialValue) // emit the initial value + * val state = shared.distinctUntilChanged() // get StateFlow-like behavior + * ``` + * + * Use [SharedFlow] when you need a [StateFlow] with tweaks in its behavior such as extra buffering, replaying more + * values, or omitting the initial value. + * * ### StateFlow vs ConflatedBroadcastChannel * - * Conceptually state flow is similar to - * [ConflatedBroadcastChannel][kotlinx.coroutines.channels.ConflatedBroadcastChannel] + * Conceptually, state flow is similar to [ConflatedBroadcastChannel] * and is designed to completely replace `ConflatedBroadcastChannel` in the future. - * It has the following important difference: + * It has the following important differences: * * * `StateFlow` is simpler, because it does not have to implement all the [Channel] APIs, which allows * for faster, garbage-free implementation, unlike `ConflatedBroadcastChannel` implementation that @@ -70,38 +99,44 @@ import kotlin.native.concurrent.* * * `StateFlow` has a clear separation into a read-only `StateFlow` interface and a [MutableStateFlow]. * * `StateFlow` conflation is based on equality like [distinctUntilChanged] operator, * unlike conflation in `ConflatedBroadcastChannel` that is based on reference identity. - * * `StateFlow` cannot be currently closed like `ConflatedBroadcastChannel` and can never represent a failure. - * This feature might be added in the future if enough compelling use-cases are found. + * * `StateFlow` cannot be closed like `ConflatedBroadcastChannel` and can never represent a failure. + * All errors and completion signals should be explicitly _materialized_ if needed. * * `StateFlow` is designed to better cover typical use-cases of keeping track of state changes in time, taking * more pragmatic design choices for the sake of convenience. * + * To migrate [ConflatedBroadcastChannel] usage to [StateFlow], start by replacing usages of the `ConflatedBroadcastChannel()` + * constructor with `MutableStateFlow(initialValue)`, using `null` as an initial value if you don't have one. + * Replace [send][ConflatedBroadcastChannel.send] and [offer][ConflatedBroadcastChannel.offer] calls + * with updates to the state flow's [MutableStateFlow.value], and convert subscribers' code to flow operators. + * You can use the [filterNotNull] operator to mimic behavior of a `ConflatedBroadcastChannel` without initial value. + * * ### Concurrency * - * All methods of data flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * All methods of state flow are **thread-safe** and can be safely invoked from concurrent coroutines without * external synchronization. * * ### Operator fusion * * Application of [flowOn][Flow.flowOn], [conflate][Flow.conflate], * [buffer] with [CONFLATED][Channel.CONFLATED] or [RENDEZVOUS][Channel.RENDEZVOUS] capacity, - * or a [distinctUntilChanged][Flow.distinctUntilChanged] operator has no effect on the state flow. + * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. * * ### Implementation notes * * State flow implementation is optimized for memory consumption and allocation-freedom. It uses a lock to ensure * thread-safety, but suspending collector coroutines are resumed outside of this lock to avoid dead-locks when - * using unconfined coroutines. Adding new collectors has `O(1)` amortized cost, but updating a [value] has `O(N)` - * cost, where `N` is the number of active collectors. + * using unconfined coroutines. Adding new subscribers has `O(1)` amortized cost, but updating a [value] has `O(N)` + * cost, where `N` is the number of active subscribers. * * ### Not stable for inheritance * - * **`StateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **`The StateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use `MutableStateFlow()` constructor function to create an implementation. + * Use the `MutableStateFlow(value)` constructor function to create an implementation. */ @ExperimentalCoroutinesApi -public interface StateFlow : Flow { +public interface StateFlow : SharedFlow { /** * The current value of this state flow. */ @@ -110,23 +145,35 @@ public interface StateFlow : Flow { /** * A mutable [StateFlow] that provides a setter for [value]. + * An instance of `MutableStateFlow` with the given initial `value` can be created using + * `MutableStateFlow(value)` constructor function. * - * See [StateFlow] documentation for details. + * See the [StateFlow] documentation for details on state flows. * * ### Not stable for inheritance * - * **`MutableStateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **The `MutableStateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use `MutableStateFlow()` constructor function to create an implementation. + * Use the `MutableStateFlow()` constructor function to create an implementation. */ @ExperimentalCoroutinesApi -public interface MutableStateFlow : StateFlow { +public interface MutableStateFlow : StateFlow, MutableSharedFlow { /** * The current value of this state flow. * * Setting a value that is [equal][Any.equals] to the previous one does nothing. */ public override var value: T + + /** + * Atomically compares the current [value] with [expect] and sets it to [update] if it is equal to [expect]. + * The result is `true` if the [value] was set to [update] and `false` otherwise. + * + * This function use a regular comparison using [Any.equals]. If both [expect] and [update] are equal to the + * current [value], this function returns `true`, but it does not actually change the reference that is + * stored in the [value]. + */ + public fun compareAndSet(expect: T, update: T): Boolean } /** @@ -144,14 +191,12 @@ private val NONE = Symbol("NONE") @SharedImmutable private val PENDING = Symbol("PENDING") -private const val INITIAL_SIZE = 2 // optimized for just a few collectors - // StateFlow slots are allocated for its collectors -private class StateFlowSlot { +private class StateFlowSlot : AbstractSharedFlowSlot>() { /** * Each slot can have one of the following states: * - * * `null` -- it is not used right now. Can [allocate] to new collector. + * * `null` -- it is not used right now. Can [allocateLocked] to new collector. * * `NONE` -- used by a collector, but neither suspended nor has pending value. * * `PENDING` -- pending to process new value. * * `CancellableContinuationImpl` -- suspended waiting for new value. @@ -161,15 +206,16 @@ private class StateFlowSlot { */ private val _state = atomic(null) - fun allocate(): Boolean { + override fun allocateLocked(flow: StateFlowImpl<*>): Boolean { // No need for atomic check & update here, since allocated happens under StateFlow lock if (_state.value != null) return false // not free _state.value = NONE // allocated return true } - fun free() { + override fun freeLocked(flow: StateFlowImpl<*>): Array?> { _state.value = null // free now + return EMPTY_RESUMES // nothing more to do } @Suppress("UNCHECKED_CAST") @@ -207,72 +253,97 @@ private class StateFlowSlot { } } -private class StateFlowImpl(initialValue: Any) : SynchronizedObject(), MutableStateFlow, FusibleFlow { - private val _state = atomic(initialValue) // T | NULL +private class StateFlowImpl( + initialState: Any // T | NULL +) : AbstractSharedFlow(), MutableStateFlow, CancellableFlow, FusibleFlow { + private val _state = atomic(initialState) // T | NULL private var sequence = 0 // serializes updates, value update is in process when sequence is odd - private var slots = arrayOfNulls(INITIAL_SIZE) - private var nSlots = 0 // number of allocated (!free) slots - private var nextIndex = 0 // oracle for the next free slot index @Suppress("UNCHECKED_CAST") public override var value: T get() = NULL.unbox(_state.value) - set(value) { - var curSequence = 0 - var curSlots: Array = this.slots // benign race, we will not use it - val newState = value ?: NULL - synchronized(this) { - val oldState = _state.value - if (oldState == newState) return // Don't do anything if value is not changing - _state.value = newState - curSequence = sequence - if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update) - curSequence++ // make it odd - sequence = curSequence - } else { - // update is already in process, notify it, and return - sequence = curSequence + 2 // change sequence to notify, keep it odd - return - } - curSlots = slots // read current reference to collectors under lock + set(value) { updateState(null, value ?: NULL) } + + override fun compareAndSet(expect: T, update: T): Boolean = + updateState(expect ?: NULL, update ?: NULL) + + private fun updateState(expectedState: Any?, newState: Any): Boolean { + var curSequence = 0 + var curSlots: Array? = this.slots // benign race, we will not use it + synchronized(this) { + val oldState = _state.value + if (expectedState != null && oldState != expectedState) return false // CAS support + if (oldState == newState) return true // Don't do anything if value is not changing, but CAS -> true + _state.value = newState + curSequence = sequence + if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update) + curSequence++ // make it odd + sequence = curSequence + } else { + // update is already in process, notify it, and return + sequence = curSequence + 2 // change sequence to notify, keep it odd + return true // updated } - /* - Fire value updates outside of the lock to avoid deadlocks with unconfined coroutines - Loop until we're done firing all the changes. This is sort of simple flat combining that - ensures sequential firing of concurrent updates and avoids the storm of collector resumes - when updates happen concurrently from many threads. - */ - while (true) { - // Benign race on element read from array - for (col in curSlots) { - col?.makePending() - } - // check if the value was updated again while we were updating the old one - synchronized(this) { - if (sequence == curSequence) { // nothing changed, we are done - sequence = curSequence + 1 // make sequence even again - return // done - } - // reread everything for the next loop under the lock - curSequence = sequence - curSlots = slots + curSlots = slots // read current reference to collectors under lock + } + /* + Fire value updates outside of the lock to avoid deadlocks with unconfined coroutines. + Loop until we're done firing all the changes. This is a sort of simple flat combining that + ensures sequential firing of concurrent updates and avoids the storm of collector resumes + when updates happen concurrently from many threads. + */ + while (true) { + // Benign race on element read from array + curSlots?.forEach { + it?.makePending() + } + // check if the value was updated again while we were updating the old one + synchronized(this) { + if (sequence == curSequence) { // nothing changed, we are done + sequence = curSequence + 1 // make sequence even again + return true // done, updated } + // reread everything for the next loop under the lock + curSequence = sequence + curSlots = slots } } + } + + override val replayCache: List + get() = listOf(value) + + override fun tryEmit(value: T): Boolean { + this.value = value + return true + } + + override suspend fun emit(value: T) { + this.value = value + } + + @Suppress("UNCHECKED_CAST") + override fun resetReplayCache() { + throw UnsupportedOperationException("MutableStateFlow.resetReplayCache is not supported") + } override suspend fun collect(collector: FlowCollector) { val slot = allocateSlot() - var prevState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet) try { + if (collector is SubscribedFlowCollector) collector.onSubscription() + val collectorJob = currentCoroutineContext()[Job] + var oldState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet) // The loop is arranged so that it starts delivering current value without waiting first while (true) { // Here the coroutine could have waited for a while to be dispatched, // so we use the most recent state here to ensure the best possible conflation of stale values val newState = _state.value + // always check for cancellation + collectorJob?.ensureActive() // Conflate value emissions using equality - if (prevState == null || newState != prevState) { + if (oldState == null || oldState != newState) { collector.emit(NULL.unbox(newState)) - prevState = newState + oldState = newState } // Note: if awaitPending is cancelled, then it bails out of this loop and calls freeSlot if (!slot.takePending()) { // try fast-path without suspending first @@ -284,33 +355,29 @@ private class StateFlowImpl(initialValue: Any) : SynchronizedObject(), Mutabl } } - private fun allocateSlot(): StateFlowSlot = synchronized(this) { - val size = slots.size - if (nSlots >= size) slots = slots.copyOf(2 * size) - var index = nextIndex - var slot: StateFlowSlot - while (true) { - slot = slots[index] ?: StateFlowSlot().also { slots[index] = it } - index++ - if (index >= slots.size) index = 0 - if (slot.allocate()) break // break when found and allocated free slot - } - nextIndex = index - nSlots++ - slot - } + override fun createSlot() = StateFlowSlot() + override fun createSlotArray(size: Int): Array = arrayOfNulls(size) - private fun freeSlot(slot: StateFlowSlot): Unit = synchronized(this) { - slot.free() - nSlots-- - } + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseStateFlow(context, capacity, onBufferOverflow) +} - override fun fuse(context: CoroutineContext, capacity: Int): FusibleFlow { - // context is irrelevant for state flow and it is always conflated - // so it should not do anything unless buffering is requested - return when (capacity) { - Channel.CONFLATED, Channel.RENDEZVOUS -> this - else -> ChannelFlowOperatorImpl(this, context, capacity) - } +internal fun MutableStateFlow.increment(delta: Int) { + while (true) { // CAS loop + val current = value + if (compareAndSet(current, current + delta)) return } } + +internal fun StateFlow.fuseStateFlow( + context: CoroutineContext, + capacity: Int, + onBufferOverflow: BufferOverflow +): Flow { + // state flow is always conflated so additional conflation does not have any effect + assert { capacity != Channel.CONFLATED } // should be desugared by callers + if ((capacity in 0..1 || capacity == Channel.BUFFERED) && onBufferOverflow == BufferOverflow.DROP_OLDEST) { + return this + } + return fuseSharedFlow(context, capacity, onBufferOverflow) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt new file mode 100644 index 0000000000..ccb5343084 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.native.concurrent.* + +@JvmField +@SharedImmutable +internal val EMPTY_RESUMES = arrayOfNulls?>(0) + +internal abstract class AbstractSharedFlowSlot { + abstract fun allocateLocked(flow: F): Boolean + abstract fun freeLocked(flow: F): Array?> // returns continuations to resume after lock +} + +internal abstract class AbstractSharedFlow> : SynchronizedObject() { + @Suppress("UNCHECKED_CAST") + protected var slots: Array? = null // allocated when needed + private set + protected var nCollectors = 0 // number of allocated (!free) slots + private set + private var nextIndex = 0 // oracle for the next free slot index + private var _subscriptionCount: MutableStateFlow? = null // init on first need + + val subscriptionCount: StateFlow + get() = synchronized(this) { + // allocate under lock in sync with nCollectors variable + _subscriptionCount ?: MutableStateFlow(nCollectors).also { + _subscriptionCount = it + } + } + + protected abstract fun createSlot(): S + + protected abstract fun createSlotArray(size: Int): Array + + @Suppress("UNCHECKED_CAST") + protected fun allocateSlot(): S { + // Actually create slot under lock + var subscriptionCount: MutableStateFlow? = null + val slot = synchronized(this) { + val slots = when (val curSlots = slots) { + null -> createSlotArray(2).also { slots = it } + else -> if (nCollectors >= curSlots.size) { + curSlots.copyOf(2 * curSlots.size).also { slots = it } + } else { + curSlots + } + } + var index = nextIndex + var slot: S + while (true) { + slot = slots[index] ?: createSlot().also { slots[index] = it } + index++ + if (index >= slots.size) index = 0 + if ((slot as AbstractSharedFlowSlot).allocateLocked(this)) break // break when found and allocated free slot + } + nextIndex = index + nCollectors++ + subscriptionCount = _subscriptionCount // retrieve under lock if initialized + slot + } + // increments subscription count + subscriptionCount?.increment(1) + return slot + } + + @Suppress("UNCHECKED_CAST") + protected fun freeSlot(slot: S) { + // Release slot under lock + var subscriptionCount: MutableStateFlow? = null + val resumes = synchronized(this) { + nCollectors-- + subscriptionCount = _subscriptionCount // retrieve under lock if initialized + // Reset next index oracle if we have no more active collectors for more predictable behavior next time + if (nCollectors == 0) nextIndex = 0 + (slot as AbstractSharedFlowSlot).freeLocked(this) + } + /* + Resume suspended coroutines. + This can happens when the subscriber that was freed was a slow one and was holding up buffer. + When this subscriber was freed, previously queued emitted can now wake up and are resumed here. + */ + for (cont in resumes) cont?.resume(Unit) + // decrement subscription count + subscriptionCount?.increment(-1) + } + + protected inline fun forEachSlotLocked(block: (S) -> Unit) { + if (nCollectors == 0) return + slots?.forEach { slot -> + if (slot != null) block(slot) + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt index 994d38074e..e53ef35c45 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -16,7 +16,7 @@ internal fun Flow.asChannelFlow(): ChannelFlow = this as? ChannelFlow ?: ChannelFlowOperatorImpl(this) /** - * Operators that can fuse with [buffer] and [flowOn] operators implement this interface. + * Operators that can fuse with **downstream** [buffer] and [flowOn] operators implement this interface. * * @suppress **This an internal API and should not be used from general code.** */ @@ -24,16 +24,18 @@ internal fun Flow.asChannelFlow(): ChannelFlow = public interface FusibleFlow : Flow { /** * This function is called by [flowOn] (with context) and [buffer] (with capacity) operators - * that are applied to this flow. + * that are applied to this flow. Should not be used with [capacity] of [Channel.CONFLATED] + * (it shall be desugared to `capacity = 0, onBufferOverflow = DROP_OLDEST`). */ public fun fuse( context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL - ): FusibleFlow + capacity: Int = Channel.OPTIONAL_CHANNEL, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND + ): Flow } /** - * Operators that use channels extend this `ChannelFlow` and are always fused with each other. + * Operators that use channels as their "output" extend this `ChannelFlow` and are always fused with each other. * This class servers as a skeleton implementation of [FusibleFlow] and provides other cross-cutting * methods like ability to [produceIn] and [broadcastIn] the corresponding flow, thus making it * possible to directly use the backing channel if it exists (hence the `ChannelFlow` name). @@ -45,8 +47,13 @@ public abstract class ChannelFlow( // upstream context @JvmField public val context: CoroutineContext, // buffer capacity between upstream and downstream context - @JvmField public val capacity: Int + @JvmField public val capacity: Int, + // buffer overflow strategy + @JvmField public val onBufferOverflow: BufferOverflow ) : FusibleFlow { + init { + assert { capacity != Channel.CONFLATED } // CONFLATED must be desugared to 0, DROP_OLDEST by callers + } // shared code to create a suspend lambda from collectTo function in one place internal val collectToFun: suspend (ProducerScope) -> Unit @@ -55,35 +62,62 @@ public abstract class ChannelFlow( private val produceCapacity: Int get() = if (capacity == Channel.OPTIONAL_CHANNEL) Channel.BUFFERED else capacity - public override fun fuse(context: CoroutineContext, capacity: Int): FusibleFlow { + /** + * When this [ChannelFlow] implementation can work without a channel (supports [Channel.OPTIONAL_CHANNEL]), + * then it should return a non-null value from this function, so that a caller can use it without the effect of + * additional [flowOn] and [buffer] operators, by incorporating its + * [context], [capacity], and [onBufferOverflow] into its own implementation. + */ + public open fun dropChannelOperators(): Flow? = null + + public override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): Flow { + assert { capacity != Channel.CONFLATED } // CONFLATED must be desugared to (0, DROP_OLDEST) by callers // note: previous upstream context (specified before) takes precedence val newContext = context + this.context - val newCapacity = when { - this.capacity == Channel.OPTIONAL_CHANNEL -> capacity - capacity == Channel.OPTIONAL_CHANNEL -> this.capacity - this.capacity == Channel.BUFFERED -> capacity - capacity == Channel.BUFFERED -> this.capacity - this.capacity == Channel.CONFLATED -> Channel.CONFLATED - capacity == Channel.CONFLATED -> Channel.CONFLATED - else -> { - // sanity checks - assert { this.capacity >= 0 } - assert { capacity >= 0 } - // combine capacities clamping to UNLIMITED on overflow - val sum = this.capacity + capacity - if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow + val newCapacity: Int + val newOverflow: BufferOverflow + if (onBufferOverflow != BufferOverflow.SUSPEND) { + // this additional buffer never suspends => overwrite preceding buffering configuration + newCapacity = capacity + newOverflow = onBufferOverflow + } else { + // combine capacities, keep previous overflow strategy + newCapacity = when { + this.capacity == Channel.OPTIONAL_CHANNEL -> capacity + capacity == Channel.OPTIONAL_CHANNEL -> this.capacity + this.capacity == Channel.BUFFERED -> capacity + capacity == Channel.BUFFERED -> this.capacity + else -> { + // sanity checks + assert { this.capacity >= 0 } + assert { capacity >= 0 } + // combine capacities clamping to UNLIMITED on overflow + val sum = this.capacity + capacity + if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow + } } + newOverflow = this.onBufferOverflow } - if (newContext == this.context && newCapacity == this.capacity) return this - return create(newContext, newCapacity) + if (newContext == this.context && newCapacity == this.capacity && newOverflow == this.onBufferOverflow) + return this + return create(newContext, newCapacity, newOverflow) } - protected abstract fun create(context: CoroutineContext, capacity: Int): ChannelFlow + protected abstract fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow protected abstract suspend fun collectTo(scope: ProducerScope) - public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel = - scope.broadcast(context, produceCapacity, start, block = collectToFun) + // broadcastImpl is used in broadcastIn operator which is obsolete and replaced by SharedFlow. + // BroadcastChannel does not support onBufferOverflow beyond simple conflation + public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel { + val broadcastCapacity = when (onBufferOverflow) { + BufferOverflow.SUSPEND -> produceCapacity + BufferOverflow.DROP_OLDEST -> Channel.CONFLATED + BufferOverflow.DROP_LATEST -> + throw IllegalArgumentException("Broadcast channel does not support BufferOverflow.DROP_LATEST") + } + return scope.broadcast(context, broadcastCapacity, start, block = collectToFun) + } /** * Here we use ATOMIC start for a reason (#1825). @@ -94,26 +128,33 @@ public abstract class ChannelFlow( * Thus `onCompletion` and `finally` blocks won't be executed and it may lead to a different kinds of memory leaks. */ public open fun produceImpl(scope: CoroutineScope): ReceiveChannel = - scope.produce(context, produceCapacity, start = CoroutineStart.ATOMIC, block = collectToFun) + scope.produce(context, produceCapacity, onBufferOverflow, start = CoroutineStart.ATOMIC, block = collectToFun) override suspend fun collect(collector: FlowCollector): Unit = coroutineScope { collector.emitAll(produceImpl(this)) } - public open fun additionalToStringProps(): String = "" + protected open fun additionalToStringProps(): String? = null // debug toString - override fun toString(): String = - "$classSimpleName[${additionalToStringProps()}context=$context, capacity=$capacity]" + override fun toString(): String { + val props = ArrayList(4) + additionalToStringProps()?.let { props.add(it) } + if (context !== EmptyCoroutineContext) props.add("context=$context") + if (capacity != Channel.OPTIONAL_CHANNEL) props.add("capacity=$capacity") + if (onBufferOverflow != BufferOverflow.SUSPEND) props.add("onBufferOverflow=$onBufferOverflow") + return "$classSimpleName[${props.joinToString(", ")}]" + } } // ChannelFlow implementation that operates on another flow before it internal abstract class ChannelFlowOperator( - @JvmField val flow: Flow, + @JvmField protected val flow: Flow, context: CoroutineContext, - capacity: Int -) : ChannelFlow(context, capacity) { + capacity: Int, + onBufferOverflow: BufferOverflow +) : ChannelFlow(context, capacity, onBufferOverflow) { protected abstract suspend fun flowCollect(collector: FlowCollector) // Changes collecting context upstream to the specified newContext, while collecting in the original context @@ -148,14 +189,19 @@ internal abstract class ChannelFlowOperator( override fun toString(): String = "$flow -> ${super.toString()}" } -// Simple channel flow operator: flowOn, buffer, or their fused combination +/** + * Simple channel flow operator: [flowOn], [buffer], or their fused combination. + */ internal class ChannelFlowOperatorImpl( flow: Flow, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL -) : ChannelFlowOperator(flow, context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowOperatorImpl(flow, context, capacity) + capacity: Int = Channel.OPTIONAL_CHANNEL, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlowOperator(flow, context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowOperatorImpl(flow, context, capacity, onBufferOverflow) + + override fun dropChannelOperators(): Flow? = flow override suspend fun flowCollect(collector: FlowCollector) = flow.collect(collector) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt index 798f38b8bd..530bcc1e5a 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt @@ -14,10 +14,11 @@ internal class ChannelFlowTransformLatest( private val transform: suspend FlowCollector.(value: T) -> Unit, flow: Flow, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlowOperator(flow, context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowTransformLatest(transform, flow, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlowOperator(flow, context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowTransformLatest(transform, flow, context, capacity, onBufferOverflow) override suspend fun flowCollect(collector: FlowCollector) { assert { collector is SendingCollector } // So cancellation behaviour is not leaking into the downstream @@ -41,10 +42,11 @@ internal class ChannelFlowMerge( private val flow: Flow>, private val concurrency: Int, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowMerge(flow, concurrency, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowMerge(flow, concurrency, context, capacity, onBufferOverflow) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { return scope.flowProduce(context, capacity, block = collectToFun) @@ -72,17 +74,17 @@ internal class ChannelFlowMerge( } } - override fun additionalToStringProps(): String = - "concurrency=$concurrency, " + override fun additionalToStringProps(): String = "concurrency=$concurrency" } internal class ChannelLimitedFlowMerge( private val flows: Iterable>, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelLimitedFlowMerge(flows, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelLimitedFlowMerge(flows, context, capacity, onBufferOverflow) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { return scope.flowProduce(context, capacity, block = collectToFun) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 010d781c02..a6d6b76dae 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -60,13 +60,23 @@ import kotlin.jvm.* * Q : -->---------- [2A] -- [2B] -- [2C] -->-- // collect * ``` * - * When operator's code takes time to execute this decreases the total execution time of the flow. + * When the operator's code takes some time to execute, this decreases the total execution time of the flow. * A [channel][Channel] is used between the coroutines to send elements emitted by the coroutine `P` to * the coroutine `Q`. If the code before `buffer` operator (in the coroutine `P`) is faster than the code after * `buffer` operator (in the coroutine `Q`), then this channel will become full at some point and will suspend * the producer coroutine `P` until the consumer coroutine `Q` catches up. * The [capacity] parameter defines the size of this buffer. * + * ### Buffer overflow + * + * By default, the emitter is suspended when the buffer overflows, to let collector catch up. This strategy can be + * overridden with an optional [onBufferOverflow] parameter so that the emitter is never suspended. In this + * case, on buffer overflow either the oldest value in the buffer is dropped with the [DROP_OLDEST][BufferOverflow.DROP_OLDEST] + * strategy and the latest emitted value is added to the buffer, + * or the latest value that is being emitted is dropped with the [DROP_LATEST][BufferOverflow.DROP_LATEST] strategy, + * keeping the buffer intact. + * To implement either of the custom strategies, a buffer of at least one element is used. + * * ### Operator fusion * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are @@ -76,9 +86,12 @@ import kotlin.jvm.* * which effectively requests a buffer of any size. Multiple requests with a specified buffer * size produce a buffer with the sum of the requested buffer sizes. * + * A `buffer` call with a non-default value of the [onBufferOverflow] parameter overrides all immediately preceding + * buffering operators, because it never suspends its upstream, and thus no upstream buffer would ever be used. + * * ### Conceptual implementation * - * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its + * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its basic * implementation is equivalent to the following code that can be written using [produce] * coroutine builder to produce a channel and [consumeEach][ReceiveChannel.consumeEach] extension to consume it: * @@ -96,24 +109,43 @@ import kotlin.jvm.* * * ### Conflation * - * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is provided as a shortcut via - * [conflate] operator. See its documentation for details. + * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is a shortcut to + * `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`, and is available via + * a separate [conflate] operator. See its documentation for details. * * @param capacity type/capacity of the buffer between coroutines. Allowed values are the same as in `Channel(...)` - * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], - * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating - * an explicitly requested size. + * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], + * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating + * an explicitly requested size. + * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to + * [SUSPEND][BufferOverflow.SUSPEND], supported only when `capacity >= 0` or `capacity == Channel.BUFFERED`, + * implicitly creates a channel with at least one buffered element). */ -public fun Flow.buffer(capacity: Int = BUFFERED): Flow { +@Suppress("NAME_SHADOWING") +public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Flow { require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) { "Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity" } + require(capacity != CONFLATED || onBufferOverflow == BufferOverflow.SUSPEND) { + "CONFLATED capacity cannot be used with non-default onBufferOverflow" + } + // desugar CONFLATED capacity to (0, DROP_OLDEST) + var capacity = capacity + var onBufferOverflow = onBufferOverflow + if (capacity == CONFLATED) { + capacity = 0 + onBufferOverflow = BufferOverflow.DROP_OLDEST + } + // create a flow return when (this) { - is FusibleFlow -> fuse(capacity = capacity) - else -> ChannelFlowOperatorImpl(this, capacity = capacity) + is FusibleFlow -> fuse(capacity = capacity, onBufferOverflow = onBufferOverflow) + else -> ChannelFlowOperatorImpl(this, capacity = capacity, onBufferOverflow = onBufferOverflow) } } +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") +public fun Flow.buffer(capacity: Int = BUFFERED): Flow = buffer(capacity) + /** * Conflates flow emissions via conflated channel and runs collector in a separate coroutine. * The effect of this is that emitter is never suspended due to a slow collector, but collector @@ -138,7 +170,9 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow { * assertEquals(listOf(1, 10, 20, 30), result) * ``` * - * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED]. + * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED], + * with is, in turn, a shortcut to a buffer that only keeps the latest element as + * created by `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`. * * ### Operator fusion * @@ -172,13 +206,17 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * * For more explanation of context preservation please refer to [Flow] documentation. * - * This operators retains a _sequential_ nature of flow if changing the context does not call for changing + * This operator retains a _sequential_ nature of flow if changing the context does not call for changing * the [dispatcher][CoroutineDispatcher]. Otherwise, if changing dispatcher is required, it collects * flow emissions in one coroutine that is run using a specified [context] and emits them from another coroutines * with the original collector's context using a channel with a [default][Channel.BUFFERED] buffer size * between two coroutines similarly to [buffer] operator, unless [buffer] operator is explicitly called * before or after `flowOn`, which requests buffering behavior and specifies channel size. * + * Note, that flows operating across different dispatchers might lose some in-flight elements when cancelled. + * In particular, this operator ensures that downstream flow does not resume on cancellation even if the element + * was already emitted by the upstream flow. + * * ### Operator fusion * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are @@ -194,8 +232,8 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * .flowOn(Dispatchers.Default) * ``` * - * Note that an instance of [StateFlow] does not have an execution context by itself, - * so applying `flowOn` to a `StateFlow` has not effect. See [StateFlow] documentation on Operator Fusion. + * Note that an instance of [SharedFlow] does not have an execution context by itself, + * so applying `flowOn` to a `SharedFlow` has not effect. See the [SharedFlow] documentation on Operator Fusion. * * @throws [IllegalArgumentException] if provided context contains [Job] instance. */ @@ -211,17 +249,30 @@ public fun Flow.flowOn(context: CoroutineContext): Flow { /** * Returns a flow which checks cancellation status on each emission and throws * the corresponding cancellation cause if flow collector was cancelled. - * Note that [flow] builder is [cancellable] by default. + * Note that [flow] builder and all implementations of [SharedFlow] are [cancellable] by default. * * This operator provides a shortcut for `.onEach { currentCoroutineContext().ensureActive() }`. * See [ensureActive][CoroutineContext.ensureActive] for details. */ -public fun Flow.cancellable(): Flow { - if (this is AbstractFlow<*>) return this // Fast-path, already cancellable - return unsafeFlow { - collect { +public fun Flow.cancellable(): Flow = + when (this) { + is CancellableFlow<*> -> this // Fast-path, already cancellable + else -> CancellableFlowImpl(this) + } + +/** + * Internal marker for flows that are [cancellable]. + */ +internal interface CancellableFlow : Flow + +/** + * Named implementation class for a flow that is defined by the [cancellable] function. + */ +private class CancellableFlowImpl(private val flow: Flow) : CancellableFlow { + override suspend fun collect(collector: FlowCollector) { + flow.collect { currentCoroutineContext().ensureActive() - emit(it) + collector.emit(it) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index d5d4278a2e..0c40691640 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -11,17 +11,33 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.internal.* import kotlinx.coroutines.selects.* -import kotlin.coroutines.* import kotlin.jvm.* import kotlin.time.* +/* Scaffolding for Knit code examples + + +*/ + /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout][timeoutMillis]. * The latest value is always emitted. * * Example: - * ``` + * + * ```kotlin * flow { * emit(1) * delay(90) @@ -34,7 +50,14 @@ import kotlin.time.* * emit(5) * }.debounce(1000) * ``` - * produces `3, 4, 5`. + * + * + * produces the following emissions + * + * ```text + * 3, 4, 5 + * ``` + * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. @@ -54,7 +77,8 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * A variation of [debounce] that allows specifying the timeout value dynamically. * * Example: - * ``` + * + * ```kotlin * flow { * emit(1) * delay(90) @@ -73,7 +97,14 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * } * } * ``` - * produces `1, 3, 4, 5`. + * + * + * produces the following emissions + * + * ```text + * 1, 3, 4, 5 + * ``` + * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. @@ -92,7 +123,8 @@ public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = * The latest value is always emitted. * * Example: - * ``` + * + * ```kotlin * flow { * emit(1) * delay(90.milliseconds) @@ -105,7 +137,14 @@ public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = * emit(5) * }.debounce(1000.milliseconds) * ``` - * produces `3, 4, 5`. + * + * + * produces the following emissions + * + * ```text + * 3, 4, 5 + * ``` + * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] milliseconds. @@ -122,7 +161,8 @@ public fun Flow.debounceWithDuration(timeout: Duration): Flow = deboun * A variation of [debounceWithDuration] that allows specifying the timeout value dynamically. * * Example: - * ``` + * + * ```kotlin * flow { * emit(1) * delay(90.milliseconds) @@ -141,7 +181,14 @@ public fun Flow.debounceWithDuration(timeout: Duration): Flow = deboun * } * } * ``` - * produces `1, 3, 4, 5`. + * + * + * produces the following emissions + * + * ```text + * 1, 3, 4, 5 + * ``` + * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] milliseconds. @@ -201,16 +248,24 @@ private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : F * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis]. * * Example: - * ``` + * + * ```kotlin * flow { * repeat(10) { * emit(it) - * delay(50) + * delay(110) * } - * }.sample(100) + * }.sample(200) * ``` - * produces `1, 3, 5, 7, 9`. + * + * + * produces the following emissions * + * ```text + * 1, 3, 5, 7, 9 + * ``` + * + * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @FlowPreview @@ -264,15 +319,23 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period]. * * Example: - * ``` + * + * ```kotlin * flow { * repeat(10) { * emit(it) - * delay(50.milliseconds) + * delay(110.milliseconds) * } - * }.sample(100.milliseconds) + * }.sample(200.milliseconds) + * ``` + * + * + * produces the following emissions + * + * ```text + * 1, 3, 5, 7, 9 * ``` - * produces `1, 3, 5, 7, 9`. + * * * Note that the latest element is not emitted if it does not fit into the sampling window. */ diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index 35048484d2..1a34af776f 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -7,9 +7,10 @@ package kotlinx.coroutines.flow +import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* -import kotlinx.coroutines.flow.internal.unsafeFlow as flow +import kotlin.native.concurrent.* /** * Returns flow where all subsequent repetitions of the same value are filtered out. @@ -17,44 +18,69 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * Note that any instance of [StateFlow] already behaves as if `distinctUtilChanged` operator is * applied to it, so applying `distinctUntilChanged` to a `StateFlow` has no effect. * See [StateFlow] documentation on Operator Fusion. + * Also, repeated application of `distinctUntilChanged` operator on any flow has no effect. */ public fun Flow.distinctUntilChanged(): Flow = when (this) { - is StateFlow<*> -> this - else -> distinctUntilChangedBy { it } + is StateFlow<*> -> this // state flows are always distinct + else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent) } /** * Returns flow where all subsequent repetitions of the same value are filtered out, when compared * with each other via the provided [areEquivalent] function. + * + * Note that repeated application of `distinctUntilChanged` operator with the same parameter has no effect. */ +@Suppress("UNCHECKED_CAST") public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> Boolean): Flow = - distinctUntilChangedBy(keySelector = { it }, areEquivalent = areEquivalent) + distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = areEquivalent as (Any?, Any?) -> Boolean) /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * key is extracted with [keySelector] function. + * + * Note that repeated application of `distinctUntilChanged` operator with the same parameter has no effect. */ public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = - distinctUntilChangedBy(keySelector = keySelector, areEquivalent = { old, new -> old == new }) + distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent) + +@SharedImmutable +private val defaultKeySelector: (Any?) -> Any? = { it } + +@SharedImmutable +private val defaultAreEquivalent: (Any?, Any?) -> Boolean = { old, new -> old == new } /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * keys are extracted with [keySelector] function and compared with each other via the * provided [areEquivalent] function. + * + * NOTE: It is non-inline to share a single implementing class. */ -private inline fun Flow.distinctUntilChangedBy( - crossinline keySelector: (T) -> K, - crossinline areEquivalent: (old: K, new: K) -> Boolean -): Flow = - flow { +private fun Flow.distinctUntilChangedBy( + keySelector: (T) -> Any?, + areEquivalent: (old: Any?, new: Any?) -> Boolean +): Flow = when { + this is DistinctFlowImpl<*> && this.keySelector === keySelector && this.areEquivalent === areEquivalent -> this // same + else -> DistinctFlowImpl(this, keySelector, areEquivalent) +} + +private class DistinctFlowImpl( + private val upstream: Flow, + @JvmField val keySelector: (T) -> Any?, + @JvmField val areEquivalent: (old: Any?, new: Any?) -> Boolean +): Flow { + @InternalCoroutinesApi + override suspend fun collect(collector: FlowCollector) { var previousKey: Any? = NULL - collect { value -> + upstream.collect { value -> val key = keySelector(value) @Suppress("UNCHECKED_CAST") - if (previousKey === NULL || !areEquivalent(previousKey as K, key)) { + if (previousKey === NULL || !areEquivalent(previousKey, key)) { previousKey = key - emit(value) + collector.emit(value) } } } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index fb37da3a83..3ffe5fe943 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -55,7 +55,12 @@ internal inline fun Flow.unsafeTransform( } /** - * Invokes the given [action] when this flow starts to be collected. + * Returns a flow that invokes the given [action] **before** this flow starts to be collected. + * + * The [action] is called before the upstream flow is started, so if it is used with a [SharedFlow] + * there is **no guarantee** that emissions from the upstream flow that happen inside or immediately + * after this `onStart` action will be collected + * (see [onSubscription] for an alternative operator on shared flows). * * The receiver of the [action] is [FlowCollector], so `onStart` can emit additional elements. * For example: @@ -80,7 +85,7 @@ public fun Flow.onStart( } /** - * Invokes the given [action] when the given flow is completed or cancelled, passing + * Returns a flow that invokes the given [action] **after** the flow is completed or cancelled, passing * the cancellation exception or failure as cause parameter of [action]. * * Conceptually, `onCompletion` is similar to wrapping the flow collection into a `finally` block, @@ -126,7 +131,7 @@ public fun Flow.onStart( * ``` * * The receiver of the [action] is [FlowCollector] and this operator can be used to emit additional - * elements at the end if it completed successfully. For example: + * elements at the end **if it completed successfully**. For example: * * ``` * flowOf("a", "b", "c") diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index 5500034e9f..7a70fbf7f2 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -2,71 +2,81 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("unused") + package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.coroutines.* /** - * Returns this. - * Applying [flowOn][Flow.flowOn] operator to [StateFlow] has no effect. - * See [StateFlow] documentation on Operator Fusion. + * Applying [cancellable][Flow.cancellable] to a [SharedFlow] has no effect. + * See the [SharedFlow] documentation on Operator Fusion. + */ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Applying 'cancellable' to a SharedFlow has no effect. See the SharedFlow documentation on Operator Fusion.", + replaceWith = ReplaceWith("this") +) +public fun SharedFlow.cancellable(): Flow = noImpl() + +/** + * Applying [flowOn][Flow.flowOn] to [SharedFlow] has no effect. + * See the [SharedFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying flowOn operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", + message = "Applying 'flowOn' to SharedFlow has no effect. See the SharedFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) -public fun StateFlow.flowOn(context: CoroutineContext): Flow = noImpl() +public fun SharedFlow.flowOn(context: CoroutineContext): Flow = noImpl() /** - * Returns this. - * Applying [conflate][Flow.conflate] operator to [StateFlow] has no effect. - * See [StateFlow] documentation on Operator Fusion. + * Applying [conflate][Flow.conflate] to [StateFlow] has no effect. + * See the [StateFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying conflate operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", + message = "Applying 'conflate' to StateFlow has no effect. See the StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) public fun StateFlow.conflate(): Flow = noImpl() /** - * Returns this. - * Applying [distinctUntilChanged][Flow.distinctUntilChanged] operator to [StateFlow] has no effect. - * See [StateFlow] documentation on Operator Fusion. + * Applying [distinctUntilChanged][Flow.distinctUntilChanged] to [StateFlow] has no effect. + * See the [StateFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying distinctUntilChanged operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", + message = "Applying 'distinctUntilChanged' to StateFlow has no effect. See the StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) public fun StateFlow.distinctUntilChanged(): Flow = noImpl() -//@Deprecated( -// message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." + -// "Use currentCoroutineContext().isActive or cancellable() operator instead " + -// "or specify the receiver of isActive explicitly. " + -// "Additionally, flow {} builder emissions are cancellable by default.", -// level = DeprecationLevel.WARNING, // ERROR in 1.4 -// replaceWith = ReplaceWith("currentCoroutineContext().isActive") -//) -//public val FlowCollector<*>.isActive: Boolean -// get() = noImpl() -// -//@Deprecated( -// message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." + -// "Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly", -// level = DeprecationLevel.WARNING, // ERROR in 1.4 -// replaceWith = ReplaceWith("currentCoroutineContext().cancel(cause)") -//) -//public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl() -// -//@Deprecated( -// message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." + -// "Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly", -// level = DeprecationLevel.WARNING, // ERROR in 1.4 -// replaceWith = ReplaceWith("currentCoroutineContext()") -//) -//public val FlowCollector<*>.coroutineContext: CoroutineContext -// get() = noImpl() \ No newline at end of file +@Deprecated( + message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." + + "Use currentCoroutineContext().isActive or cancellable() operator instead " + + "or specify the receiver of isActive explicitly. " + + "Additionally, flow {} builder emissions are cancellable by default.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("currentCoroutineContext().isActive") +) +public val FlowCollector<*>.isActive: Boolean + get() = noImpl() + +@Deprecated( + message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." + + "Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("currentCoroutineContext().cancel(cause)") +) +public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl() + +@Deprecated( + message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." + + "Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("currentCoroutineContext()") +) +public val FlowCollector<*>.coroutineContext: CoroutineContext + get() = noImpl() \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt new file mode 100644 index 0000000000..4dd89ee4bf --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -0,0 +1,412 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* + +// -------------------------------- shareIn -------------------------------- + +/** + * Converts a _cold_ [Flow] into a _hot_ [SharedFlow] that is started in the given coroutine [scope], + * sharing emissions from a single running instance of the upstream flow with multiple downstream subscribers, + * and replaying a specified number of [replay] values to new subscribers. See the [SharedFlow] documentation + * for the general concepts of shared flows. + * + * The starting of the sharing coroutine is controlled by the [started] parameter. The following options + * are supported. + * + * * [Eagerly][SharingStarted.Eagerly] — the upstream flow is started even before the first subscriber appears. Note + * that in this case all values emitted by the upstream beyond the most recent values as specified by + * [replay] parameter **will be immediately discarded**. + * * [Lazily][SharingStarted.Lazily] — starts the upstream flow after the first subscriber appears, which guarantees + * that this first subscriber gets all the emitted values, while subsequent subscribers are only guaranteed to + * get the most recent [replay] values. The upstream flow continues to be active even when all subscribers + * disappear, but only the most recent [replay] values are cached without subscribers. + * * [WhileSubscribed()][SharingStarted.WhileSubscribed] — starts the upstream flow when the first subscriber + * appears, immediately stops when the last subscriber disappears, keeping the replay cache forever. + * It has additional optional configuration parameters as explained in its documentation. + * * A custom strategy can be supplied by implementing the [SharingStarted] interface. + * + * The `shareIn` operator is useful in situations when there is a _cold_ flow that is expensive to create and/or + * to maintain, but there are multiple subscribers that need to collect its values. For example, consider a + * flow of messages coming from a backend over the expensive network connection, taking a lot of + * time to establish. Conceptually, it might be implemented like this: + * + * ``` + * val backendMessages: Flow = flow { + * connectToBackend() // takes a lot of time + * try { + * while (true) { + * emit(receiveMessageFromBackend()) + * } + * } finally { + * disconnectFromBackend() + * } + * } + * ``` + * + * If this flow is directly used in the application, then every time it is collected a fresh connection is + * established, and it will take a while before messages start flowing. However, we can share a single connection + * and establish it eagerly like this: + * + * ``` + * val messages: SharedFlow = backendMessages.shareIn(scope, SharingStarted.Eagerly) + * ``` + * + * Now a single connection is shared between all collectors from `messages`, and there is a chance that the connection + * is already established by the time it is needed. + * + * ### Upstream completion and error handling + * + * **Normal completion of the upstream flow has no effect on subscribers**, and the sharing coroutine continues to run. If a + * a strategy like [SharingStarted.WhileSubscribed] is used, then the upstream can get restarted again. If a special + * action on upstream completion is needed, then an [onCompletion] operator can be used before the + * `shareIn` operator to emit a special value in this case, like this: + * + * ``` + * backendMessages + * .onCompletion { cause -> if (cause == null) emit(UpstreamHasCompletedMessage) } + * .shareIn(scope, SharingStarted.Eagerly) + * ``` + * + * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, + * and will be handled by the [scope] in which the sharing coroutine is launched. Custom exception handling + * can be configured by using the [catch] or [retry] operators before the `shareIn` operator. + * For example, to retry connection on any `IOException` with 1 second delay between attempts, use: + * + * ``` + * val messages = backendMessages + * .retry { e -> + * val shallRetry = e is IOException // other exception are bugs - handle them + * if (shallRetry) delay(1000) + * shallRetry + * } + * .shareIn(scope, SharingStarted.Eagerly) + * ``` + * + * ### Initial value + * + * When a special initial value is needed to signal to subscribers that the upstream is still loading the data, + * use the [onStart] operator on the upstream flow. For example: + * + * ``` + * backendMessages + * .onStart { emit(UpstreamIsStartingMessage) } + * .shareIn(scope, SharingStarted.Eagerly, 1) // replay one most recent message + * ``` + * + * ### Buffering and conflation + * + * The `shareIn` operator runs the upstream flow in a separate coroutine, and buffers emissions from upstream as explained + * in the [buffer] operator's description, using a buffer of [replay] size or the default (whichever is larger). + * This default buffering can be overridden with an explicit buffer configuration by preceding the `shareIn` call + * with [buffer] or [conflate], for example: + * + * * `buffer(0).shareIn(scope, started, 0)` — overrides the default buffer size and creates a [SharedFlow] without a buffer. + * Effectively, it configures sequential processing between the upstream emitter and subscribers, + * as the emitter is suspended until all subscribers process the value. Note, that the value is still immediately + * discarded when there are no subscribers. + * * `buffer(b).shareIn(scope, started, r)` — creates a [SharedFlow] with `replay = r` and `extraBufferCapacity = b`. + * * `conflate().shareIn(scope, started, r)` — creates a [SharedFlow] with `replay = r`, `onBufferOverflow = DROP_OLDEST`, + * and `extraBufferCapacity = 1` when `replay == 0` to support this strategy. + * + * ### Operator fusion + * + * Application of [flowOn][Flow.flowOn], [buffer] with [RENDEZVOUS][Channel.RENDEZVOUS] capacity, + * or [cancellable] operators to the resulting shared flow has no effect. + * + * ### Exceptions + * + * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. + * + * @param scope the coroutine scope in which sharing is started. + * @param started the strategy that controls when sharing is started and stopped. + * @param replay the number of values replayed to new subscribers (cannot be negative, defaults to zero). + */ +@ExperimentalCoroutinesApi +public fun Flow.shareIn( + scope: CoroutineScope, + started: SharingStarted, + replay: Int = 0 +): SharedFlow { + val config = configureSharing(replay) + val shared = MutableSharedFlow( + replay = replay, + extraBufferCapacity = config.extraBufferCapacity, + onBufferOverflow = config.onBufferOverflow + ) + @Suppress("UNCHECKED_CAST") + scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T) + return shared.asSharedFlow() +} + +private class SharingConfig( + @JvmField val upstream: Flow, + @JvmField val extraBufferCapacity: Int, + @JvmField val onBufferOverflow: BufferOverflow, + @JvmField val context: CoroutineContext +) + +// Decomposes upstream flow to fuse with it when possible +private fun Flow.configureSharing(replay: Int): SharingConfig { + assert { replay >= 0 } + val defaultExtraCapacity = replay.coerceAtLeast(Channel.CHANNEL_DEFAULT_CAPACITY) - replay + // Combine with preceding buffer/flowOn and channel-using operators + if (this is ChannelFlow) { + // Check if this ChannelFlow can operate without a channel + val upstream = dropChannelOperators() + if (upstream != null) { // Yes, it can => eliminate the intermediate channel + return SharingConfig( + upstream = upstream, + extraBufferCapacity = when (capacity) { + Channel.OPTIONAL_CHANNEL, Channel.BUFFERED, 0 -> // handle special capacities + when { + onBufferOverflow == BufferOverflow.SUSPEND -> // buffer was configured with suspension + if (capacity == 0) 0 else defaultExtraCapacity // keep explicitly configured 0 or use default + replay == 0 -> 1 // no suspension => need at least buffer of one + else -> 0 // replay > 0 => no need for extra buffer beyond replay because we don't suspend + } + else -> capacity // otherwise just use the specified capacity as extra capacity + }, + onBufferOverflow = onBufferOverflow, + context = context + ) + } + } + // Add sharing operator on top with a default buffer + return SharingConfig( + upstream = this, + extraBufferCapacity = defaultExtraCapacity, + onBufferOverflow = BufferOverflow.SUSPEND, + context = EmptyCoroutineContext + ) +} + +// Launches sharing coroutine +private fun CoroutineScope.launchSharing( + context: CoroutineContext, + upstream: Flow, + shared: MutableSharedFlow, + started: SharingStarted, + initialValue: T +) { + launch(context) { // the single coroutine to rule the sharing + // Optimize common built-in started strategies + when { + started === SharingStarted.Eagerly -> { + // collect immediately & forever + upstream.collect(shared) + } + started === SharingStarted.Lazily -> { + // start collecting on the first subscriber - wait for it first + shared.subscriptionCount.first { it > 0 } + upstream.collect(shared) + } + else -> { + // other & custom strategies + started.command(shared.subscriptionCount) + .distinctUntilChanged() // only changes in command have effect + .collectLatest { // cancels block on new emission + when (it) { + SharingCommand.START -> upstream.collect(shared) // can be cancelled + SharingCommand.STOP -> { /* just cancel and do nothing else */ } + SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> { + if (initialValue === NO_VALUE) { + shared.resetReplayCache() // regular shared flow -> reset cache + } else { + shared.tryEmit(initialValue) // state flow -> reset to initial value + } + } + } + } + } + } + } +} + +// -------------------------------- stateIn -------------------------------- + +/** + * Converts a _cold_ [Flow] into a _hot_ [StateFlow] that is started in the given coroutine [scope], + * sharing the most recently emitted value from a single running instance of the upstream flow with multiple + * downstream subscribers. See the [StateFlow] documentation for the general concepts of state flows. + * + * The starting of the sharing coroutine is controlled by the [started] parameter, as explained in the + * documentation for [shareIn] operator. + * + * The `stateIn` operator is useful in situations when there is a _cold_ flow that provides updates to the + * value of some state and is expensive to create and/or to maintain, but there are multiple subscribers + * that need to collect the most recent state value. For example, consider a + * flow of state updates coming from a backend over the expensive network connection, taking a lot of + * time to establish. Conceptually it might be implemented like this: + * + * ``` + * val backendState: Flow = flow { + * connectToBackend() // takes a lot of time + * try { + * while (true) { + * emit(receiveStateUpdateFromBackend()) + * } + * } finally { + * disconnectFromBackend() + * } + * } + * ``` + * + * If this flow is directly used in the application, then every time it is collected a fresh connection is + * established, and it will take a while before state updates start flowing. However, we can share a single connection + * and establish it eagerly like this: + * + * ``` + * val state: StateFlow = backendMessages.stateIn(scope, SharingStarted.Eagerly, State.LOADING) + * ``` + * + * Now, a single connection is shared between all collectors from `state`, and there is a chance that the connection + * is already established by the time it is needed. + * + * ### Upstream completion and error handling + * + * **Normal completion of the upstream flow has no effect on subscribers**, and the sharing coroutine continues to run. If a + * a strategy like [SharingStarted.WhileSubscribed] is used, then the upstream can get restarted again. If a special + * action on upstream completion is needed, then an [onCompletion] operator can be used before + * the `stateIn` operator to emit a special value in this case. See the [shareIn] operator's documentation for an example. + * + * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, + * and will be handled by the [scope] in which the sharing coroutine is launched. Custom exception handling + * can be configured by using the [catch] or [retry] operators before the `stateIn` operator, similarly to + * the [shareIn] operator. + * + * ### Operator fusion + * + * Application of [flowOn][Flow.flowOn], [conflate][Flow.conflate], + * [buffer] with [CONFLATED][Channel.CONFLATED] or [RENDEZVOUS][Channel.RENDEZVOUS] capacity, + * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. + * + * @param scope the coroutine scope in which sharing is started. + * @param started the strategy that controls when sharing is started and stopped. + * @param initialValue the initial value of the state flow. + * This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy + * with the `replayExpirationMillis` parameter. + */ +@ExperimentalCoroutinesApi +public fun Flow.stateIn( + scope: CoroutineScope, + started: SharingStarted, + initialValue: T +): StateFlow { + val config = configureSharing(1) + val state = MutableStateFlow(initialValue) + scope.launchSharing(config.context, config.upstream, state, started, initialValue) + return state.asStateFlow() +} + +/** + * Starts the upstream flow in a given [scope], suspends until the first value is emitted, and returns a _hot_ + * [StateFlow] of future emissions, sharing the most recently emitted value from this running instance of the upstream flow + * with multiple downstream subscribers. See the [StateFlow] documentation for the general concepts of state flows. + * + * @param scope the coroutine scope in which sharing is started. + */ +@ExperimentalCoroutinesApi +public suspend fun Flow.stateIn(scope: CoroutineScope): StateFlow { + val config = configureSharing(1) + val result = CompletableDeferred>() + scope.launchSharingDeferred(config.context, config.upstream, result) + return result.await() +} + +private fun CoroutineScope.launchSharingDeferred( + context: CoroutineContext, + upstream: Flow, + result: CompletableDeferred> +) { + launch(context) { + var state: MutableStateFlow? = null + upstream.collect { value -> + state?.let { it.value = value } ?: run { + state = MutableStateFlow(value).also { + result.complete(it.asStateFlow()) + } + } + } + } +} + +// -------------------------------- asSharedFlow/asStateFlow -------------------------------- + +/** + * Represents this mutable shared flow as a read-only shared flow. + */ +@ExperimentalCoroutinesApi +public fun MutableSharedFlow.asSharedFlow(): SharedFlow = + ReadonlySharedFlow(this) + +/** + * Represents this mutable state flow as a read-only state flow. + */ +@ExperimentalCoroutinesApi +public fun MutableStateFlow.asStateFlow(): StateFlow = + ReadonlyStateFlow(this) + +private class ReadonlySharedFlow( + flow: SharedFlow +) : SharedFlow by flow, CancellableFlow, FusibleFlow { + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseSharedFlow(context, capacity, onBufferOverflow) +} + +private class ReadonlyStateFlow( + flow: StateFlow +) : StateFlow by flow, CancellableFlow, FusibleFlow { + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseStateFlow(context, capacity, onBufferOverflow) +} + +// -------------------------------- onSubscription -------------------------------- + +/** + * Returns a flow that invokes the given [action] **after** this shared flow starts to be collected + * (after the subscription is registered). + * + * The [action] is called before any value is emitted from the upstream + * flow to this subscription but after the subscription is established. It is guaranteed that all emissions to + * the upstream flow that happen inside or immediately after this `onSubscription` action will be + * collected by this subscription. + * + * The receiver of the [action] is [FlowCollector], so `onSubscription` can emit additional elements. + */ +@ExperimentalCoroutinesApi +public fun SharedFlow.onSubscription(action: suspend FlowCollector.() -> Unit): SharedFlow = + SubscribedSharedFlow(this, action) + +private class SubscribedSharedFlow( + private val sharedFlow: SharedFlow, + private val action: suspend FlowCollector.() -> Unit +) : SharedFlow by sharedFlow { + override suspend fun collect(collector: FlowCollector) = + sharedFlow.collect(SubscribedFlowCollector(collector, action)) +} + +internal class SubscribedFlowCollector( + private val collector: FlowCollector, + private val action: suspend FlowCollector.() -> Unit +) : FlowCollector by collector { + suspend fun onSubscription() { + val safeCollector = SafeCollector(collector, currentCoroutineContext()) + try { + safeCollector.action() + } finally { + safeCollector.releaseIntercepted() + } + if (collector is SubscribedFlowCollector) collector.onSubscription() + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index 520311ee5d..e3552d2893 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -67,7 +67,7 @@ public fun Flow.withIndex(): Flow> = flow { } /** - * Returns a flow which performs the given [action] on each value of the original flow. + * Returns a flow that invokes the given [action] **before** each value of the upstream flow is emitted downstream. */ public fun Flow.onEach(action: suspend (T) -> Unit): Flow = transform { value -> action(value) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index d36e1bbf7b..83f5498e4d 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -9,6 +9,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.Symbol import kotlin.jvm.* /** @@ -47,33 +48,39 @@ public suspend inline fun Flow.fold( } /** - * The terminal operator, that awaits for one and only one value to be published. + * The terminal operator that awaits for one and only one value to be emitted. * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow * that contains more than one element. */ public suspend fun Flow.single(): T { var result: Any? = NULL collect { value -> - if (result !== NULL) error("Expected only one element") + require(result === NULL) { "Flow has more than one element" } result = value } - if (result === NULL) throw NoSuchElementException("Expected at least one element") - @Suppress("UNCHECKED_CAST") + if (result === NULL) throw NoSuchElementException("Flow is empty") return result as T } /** - * The terminal operator, that awaits for one and only one value to be published. - * Throws [IllegalStateException] for flow that contains more than one element. + * The terminal operator that awaits for one and only one value to be emitted. + * Returns the single value or `null`, if the flow was empty or emitted more than one value. */ -public suspend fun Flow.singleOrNull(): T? { - var result: T? = null - collect { value -> - if (result != null) error("Expected only one element") - result = value +public suspend fun Flow.singleOrNull(): T? { + var result: Any? = NULL + collectWhile { + // No values yet, update result + if (result === NULL) { + result = it + true + } else { + // Second value, reset result and bail out + result = NULL + false + } } - return result + return if (result === NULL) null else result as T } /** @@ -112,7 +119,7 @@ public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { * The terminal operator that returns the first element emitted by the flow and then cancels flow's collection. * Returns `null` if the flow was empty. */ -public suspend fun Flow.firstOrNull(): T? { +public suspend fun Flow.firstOrNull(): T? { var result: T? = null collectWhile { result = it @@ -122,10 +129,10 @@ public suspend fun Flow.firstOrNull(): T? { } /** - * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. + * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. * Returns `null` if the flow did not contain an element matching the [predicate]. */ -public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { +public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { var result: T? = null collectWhile { if (predicate(it)) { diff --git a/kotlinx-coroutines-core/common/src/internal/Atomic.kt b/kotlinx-coroutines-core/common/src/internal/Atomic.kt index 94f6ab9cf2..a27d5491d1 100644 --- a/kotlinx-coroutines-core/common/src/internal/Atomic.kt +++ b/kotlinx-coroutines-core/common/src/internal/Atomic.kt @@ -39,7 +39,8 @@ public abstract class OpDescriptor { } @SharedImmutable -private val NO_DECISION: Any = Symbol("NO_DECISION") +@JvmField +internal val NO_DECISION: Any = Symbol("NO_DECISION") /** * Descriptor for multi-word atomic operation. @@ -52,9 +53,13 @@ private val NO_DECISION: Any = Symbol("NO_DECISION") * * @suppress **This is unstable API and it is subject to change.** */ +@InternalCoroutinesApi public abstract class AtomicOp : OpDescriptor() { private val _consensus = atomic(NO_DECISION) + // Returns NO_DECISION when there is not decision yet + val consensus: Any? get() = _consensus.value + val isDecided: Boolean get() = _consensus.value !== NO_DECISION /** diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index cf31fcf07d..b7b2954f6a 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -2,10 +2,10 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines +package kotlinx.coroutines.internal import kotlinx.atomicfu.* -import kotlinx.coroutines.internal.* +import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.jvm.* import kotlin.native.concurrent.* @@ -19,7 +19,7 @@ internal val REUSABLE_CLAIMED = Symbol("REUSABLE_CLAIMED") internal class DispatchedContinuation( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation -) : DispatchedTask(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation by continuation { +) : DispatchedTask(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation by continuation { @JvmField @Suppress("PropertyName") internal var _state: Any? = UNDEFINED @@ -37,19 +37,19 @@ internal class DispatchedContinuation( * 3) [REUSABLE_CLAIMED]. CC is currently being reused and its owner executes `suspend` block: * ``` * // state == null | CC - * suspendAtomicCancellableCoroutineReusable { cont -> + * suspendCancellableCoroutineReusable { cont -> * // state == REUSABLE_CLAIMED * block(cont) * } * // state == CC * ``` - * 4) [Throwable] continuation was cancelled with this cause while being in [suspendAtomicCancellableCoroutineReusable], + * 4) [Throwable] continuation was cancelled with this cause while being in [suspendCancellableCoroutineReusable], * [CancellableContinuationImpl.getResult] will check for cancellation later. * * [REUSABLE_CLAIMED] state is required to prevent the lost resume in the channel. * AbstractChannel.receive method relies on the fact that the following pattern * ``` - * suspendAtomicCancellableCoroutineReusable { cont -> + * suspendCancellableCoroutineReusable { cont -> * val result = pollFastPath() * if (result != null) cont.resume(result) * } @@ -67,12 +67,12 @@ internal class DispatchedContinuation( /* * Reusability control: * `null` -> no reusability at all, false - * If current state is not CCI, then we are within `suspendAtomicCancellableCoroutineReusable`, true + * If current state is not CCI, then we are within `suspendCancellableCoroutineReusable`, true * Else, if result is CCI === requester. * Identity check my fail for the following pattern: * ``` * loop: - * suspendAtomicCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle + * suspendCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle * suspendCancellableCoroutine { } // **Not reusable**, handle should be disposed after {}, otherwise * it will leak because it won't be freed by `releaseInterceptedContinuation` * ``` @@ -83,7 +83,7 @@ internal class DispatchedContinuation( } /** - * Claims the continuation for [suspendAtomicCancellableCoroutineReusable] block, + * Claims the continuation for [suspendCancellableCoroutineReusable] block, * so all cancellations will be postponed. */ @Suppress("UNCHECKED_CAST") @@ -119,7 +119,7 @@ internal class DispatchedContinuation( * If continuation was cancelled, it becomes non-reusable. * * ``` - * suspendAtomicCancellableCoroutineReusable { // <- claimed + * suspendCancellableCoroutineReusable { // <- claimed * // Any asynchronous cancellation is "postponed" while this block * // is being executed * } // postponed cancellation is checked here in `getResult` @@ -180,10 +180,10 @@ internal class DispatchedContinuation( val state = result.toState() if (dispatcher.isDispatchNeeded(context)) { _state = state - resumeMode = MODE_ATOMIC_DEFAULT + resumeMode = MODE_ATOMIC dispatcher.dispatch(context, this) } else { - executeUnconfined(state, MODE_ATOMIC_DEFAULT) { + executeUnconfined(state, MODE_ATOMIC) { withCoroutineContext(this.context, countOrElement) { continuation.resumeWith(result) } @@ -194,29 +194,42 @@ internal class DispatchedContinuation( // We inline it to save an entry on the stack in cases where it shows (unconfined dispatcher) // It is used only in Continuation.resumeCancellableWith @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancellableWith(result: Result) { - val state = result.toState() + inline fun resumeCancellableWith( + result: Result, + noinline onCancellation: ((cause: Throwable) -> Unit)? + ) { + val state = result.toState(onCancellation) if (dispatcher.isDispatchNeeded(context)) { _state = state resumeMode = MODE_CANCELLABLE dispatcher.dispatch(context, this) } else { executeUnconfined(state, MODE_CANCELLABLE) { - if (!resumeCancelled()) { + if (!resumeCancelled(state)) { resumeUndispatchedWith(result) } } } } + // takeState had already cleared the state so we cancel takenState here + override fun cancelCompletedResult(takenState: Any?, cause: Throwable) { + // It is Ok to call onCancellation here without try/catch around it, since this function only faces + // a "bound" cancellation handler that performs the safe call to the user-specified code. + if (takenState is CompletedWithCancellation) { + takenState.onCancellation(cause) + } + } + @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancelled(): Boolean { + inline fun resumeCancelled(state: Any?): Boolean { val job = context[Job] if (job != null && !job.isActive) { - resumeWithException(job.getCancellationException()) + val cause = job.getCancellationException() + cancelCompletedResult(state, cause) + resumeWithException(cause) return true } - return false } @@ -245,8 +258,11 @@ internal class DispatchedContinuation( * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public fun Continuation.resumeCancellableWith(result: Result): Unit = when (this) { - is DispatchedContinuation -> resumeCancellableWith(result) +public fun Continuation.resumeCancellableWith( + result: Result, + onCancellation: ((cause: Throwable) -> Unit)? = null +): Unit = when (this) { + is DispatchedContinuation -> resumeCancellableWith(result, onCancellation) else -> resumeWith(result) } @@ -265,6 +281,7 @@ private inline fun DispatchedContinuation<*>.executeUnconfined( contState: Any?, mode: Int, doYield: Boolean = false, block: () -> Unit ): Boolean { + assert { mode != MODE_UNINITIALIZED } // invalid execution mode val eventLoop = ThreadLocalEventLoop.eventLoop // If we are yielding and unconfined queue is empty, we can bail out as part of fast path if (doYield && eventLoop.isUnconfinedQueueEmpty) return false diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 32258ba101..1f4942a358 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -8,12 +8,44 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -@PublishedApi internal const val MODE_ATOMIC_DEFAULT = 0 // schedule non-cancellable dispatch for suspendCoroutine -@PublishedApi internal const val MODE_CANCELLABLE = 1 // schedule cancellable dispatch for suspendCancellableCoroutine -@PublishedApi internal const val MODE_UNDISPATCHED = 2 // when the thread is right, but need to mark it with current coroutine +/** + * Non-cancellable dispatch mode. + * + * **DO NOT CHANGE THE CONSTANT VALUE**. It might be inlined into legacy user code that was calling + * inline `suspendAtomicCancellableCoroutine` function and did not support reuse. + */ +internal const val MODE_ATOMIC = 0 + +/** + * Cancellable dispatch mode. It is used by user-facing [suspendCancellableCoroutine]. + * Note, that implementation of cancellability checks mode via [Int.isCancellableMode] extension. + * + * **DO NOT CHANGE THE CONSTANT VALUE**. It is being into the user code from [suspendCancellableCoroutine]. + */ +@PublishedApi +internal const val MODE_CANCELLABLE = 1 + +/** + * Cancellable dispatch mode for [suspendCancellableCoroutineReusable]. + * Note, that implementation of cancellability checks mode via [Int.isCancellableMode] extension; + * implementation of reuse checks mode via [Int.isReusableMode] extension. + */ +internal const val MODE_CANCELLABLE_REUSABLE = 2 + +/** + * Undispatched mode for [CancellableContinuation.resumeUndispatched]. + * It is used when the thread is right, but it needs to be marked with the current coroutine. + */ +internal const val MODE_UNDISPATCHED = 4 -internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE -internal val Int.isDispatchedMode get() = this == MODE_ATOMIC_DEFAULT || this == MODE_CANCELLABLE +/** + * Initial mode for [DispatchedContinuation] implementation, should never be used for dispatch, because it is always + * overwritten when continuation is resumed with the actual resume mode. + */ +internal const val MODE_UNINITIALIZED = -1 + +internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE || this == MODE_CANCELLABLE_REUSABLE +internal val Int.isReusableMode get() = this == MODE_CANCELLABLE_REUSABLE internal abstract class DispatchedTask( @JvmField public var resumeMode: Int @@ -22,16 +54,32 @@ internal abstract class DispatchedTask( internal abstract fun takeState(): Any? - internal open fun cancelResult(state: Any?, cause: Throwable) {} + /** + * Called when this task was cancelled while it was being dispatched. + */ + internal open fun cancelCompletedResult(takenState: Any?, cause: Throwable) {} + /** + * There are two implementations of `DispatchedTask`: + * * [DispatchedContinuation] keeps only simple values as successfully results. + * * [CancellableContinuationImpl] keeps additional data with values and overrides this method to unwrap it. + */ @Suppress("UNCHECKED_CAST") internal open fun getSuccessfulResult(state: Any?): T = state as T - internal fun getExceptionalResult(state: Any?): Throwable? = + /** + * There are two implementations of `DispatchedTask`: + * * [DispatchedContinuation] is just an intermediate storage that stores the exception that has its stack-trace + * properly recovered and is ready to pass to the [delegate] continuation directly. + * * [CancellableContinuationImpl] stores raw cause of the failure in its state; when it needs to be dispatched + * its stack-trace has to be recovered, so it overrides this method for that purpose. + */ + internal open fun getExceptionalResult(state: Any?): Throwable? = (state as? CompletedExceptionally)?.cause public final override fun run() { + assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching val taskContext = this.taskContext var fatalException: Throwable? = null try { @@ -41,19 +89,22 @@ internal abstract class DispatchedTask( val state = takeState() // NOTE: Must take state in any case, even if cancelled withCoroutineContext(context, delegate.countOrElement) { val exception = getExceptionalResult(state) - val job = if (resumeMode.isCancellableMode) context[Job] else null /* * Check whether continuation was originally resumed with an exception. * If so, it dominates cancellation, otherwise the original exception * will be silently lost. */ - if (exception == null && job != null && !job.isActive) { + val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null + if (job != null && !job.isActive) { val cause = job.getCancellationException() - cancelResult(state, cause) + cancelCompletedResult(state, cause) continuation.resumeWithStackTrace(cause) } else { - if (exception != null) continuation.resumeWithException(exception) - else continuation.resume(getSuccessfulResult(state)) + if (exception != null) { + continuation.resumeWithException(exception) + } else { + continuation.resume(getSuccessfulResult(state)) + } } } } catch (e: Throwable) { @@ -97,8 +148,10 @@ internal abstract class DispatchedTask( } internal fun DispatchedTask.dispatch(mode: Int) { + assert { mode != MODE_UNINITIALIZED } // invalid mode value for this method val delegate = this.delegate - if (mode.isDispatchedMode && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { + val undispatched = mode == MODE_UNDISPATCHED + if (!undispatched && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { // dispatch directly using this instance's Runnable implementation val dispatcher = delegate.dispatcher val context = delegate.context @@ -108,21 +161,21 @@ internal fun DispatchedTask.dispatch(mode: Int) { resumeUnconfined() } } else { - resume(delegate, mode) + // delegate is coming from 3rd-party interceptor implementation (and does not support cancellation) + // or undispatched mode was requested + resume(delegate, undispatched) } } @Suppress("UNCHECKED_CAST") -internal fun DispatchedTask.resume(delegate: Continuation, useMode: Int) { - // slow-path - use delegate +internal fun DispatchedTask.resume(delegate: Continuation, undispatched: Boolean) { + // This resume is never cancellable. The result is always delivered to delegate continuation. val state = takeState() - val exception = getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } + val exception = getExceptionalResult(state) val result = if (exception != null) Result.failure(exception) else Result.success(getSuccessfulResult(state)) - when (useMode) { - MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) - MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) - MODE_UNDISPATCHED -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) - else -> error("Invalid mode $useMode") + when { + undispatched -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) + else -> delegate.resumeWith(result) } } @@ -134,7 +187,7 @@ private fun DispatchedTask<*>.resumeUnconfined() { } else { // Was not active -- run event loop until all unconfined tasks are executed runUnconfinedEventLoop(eventLoop) { - resume(delegate, MODE_UNDISPATCHED) + resume(delegate, undispatched = true) } } } diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index f1663c3ddc..8508e39239 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -73,6 +73,7 @@ public expect abstract class AbstractAtomicDesc : AtomicDesc { protected open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure public open fun onPrepare(prepareOp: PrepareOp): Any? // non-null on failure + public open fun onRemoved(affected: LockFreeLinkedListNode) // non-null on failure protected abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } diff --git a/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt b/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt new file mode 100644 index 0000000000..1744359e93 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* + +internal typealias OnUndeliveredElement = (E) -> Unit + +internal fun OnUndeliveredElement.callUndeliveredElementCatchingException( + element: E, + undeliveredElementException: UndeliveredElementException? = null +): UndeliveredElementException? { + try { + invoke(element) + } catch (ex: Throwable) { + // undeliveredElementException.cause !== ex is an optimization in case the same exception is thrown + // over and over again by on OnUndeliveredElement + if (undeliveredElementException != null && undeliveredElementException.cause !== ex) { + undeliveredElementException.addSuppressedThrowable(ex) + } else { + return UndeliveredElementException("Exception in undelivered element handler for $element", ex) + } + } + return undeliveredElementException +} + +internal fun OnUndeliveredElement.callUndeliveredElement(element: E, context: CoroutineContext) { + callUndeliveredElementCatchingException(element, null)?.let { ex -> + handleCoroutineException(context, ex) + } +} + +internal fun OnUndeliveredElement.bindCancellationFun(element: E, context: CoroutineContext): (Throwable) -> Unit = + { _: Throwable -> callUndeliveredElement(element, context) } + +/** + * Internal exception that is thrown when [OnUndeliveredElement] handler in + * a [kotlinx.coroutines.channels.Channel] throws an exception. + */ +internal class UndeliveredElementException(message: String, cause: Throwable) : RuntimeException(message, cause) diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt index 1b1c389dc4..f814b152b2 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.intrinsics import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -21,9 +22,12 @@ public fun (suspend () -> T).startCoroutineCancellable(completion: Continuat * Use this function to start coroutine in a cancellable way, so that it can be cancelled * while waiting to be dispatched. */ -internal fun (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation) = +internal fun (suspend (R) -> T).startCoroutineCancellable( + receiver: R, completion: Continuation, + onCancellation: ((cause: Throwable) -> Unit)? = null +) = runSafely(completion) { - createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit)) + createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation) } /** diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index e744a0c724..99c54f8417 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -189,14 +189,8 @@ public interface SelectInstance { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * Atomicity of cancellation depends on the clause: [onSend][SendChannel.onSend], [onReceive][ReceiveChannel.onReceive], - * [onReceiveOrNull][ReceiveChannel.onReceiveOrNull], and [onLock][Mutex.onLock] clauses are - * *atomically cancellable*. When select throws [CancellationException] it means that those clauses had not performed - * their respective operations. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this select operation - * was already resumed on atomically cancellable clause and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -655,7 +649,7 @@ internal class SelectBuilderImpl( if (trySelect()) block.startCoroutineCancellable(completion) // shall be cancellable while waits for dispatch } - disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action)) + disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action, context)) } private class DisposeNode( diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 61e046c77a..73aaab5fbf 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -45,12 +45,10 @@ public interface Mutex { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended lock invocation is atomic* -- when this function - * throws [CancellationException] it means that the mutex was not locked. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this lock operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * This function releases the lock if it was already acquired by this function before the [CancellationException] + * was thrown. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -124,8 +122,6 @@ public suspend inline fun Mutex.withLock(owner: Any? = null, action: () -> T @SharedImmutable private val LOCK_FAIL = Symbol("LOCK_FAIL") @SharedImmutable -private val ENQUEUE_FAIL = Symbol("ENQUEUE_FAIL") -@SharedImmutable private val UNLOCK_FAIL = Symbol("UNLOCK_FAIL") @SharedImmutable private val SELECT_SUCCESS = Symbol("SELECT_SUCCESS") @@ -194,7 +190,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { return lockSuspend(owner) } - private suspend fun lockSuspend(owner: Any?) = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable sc@ { cont -> val waiter = LockCont(owner, cont) _state.loop { state -> when (state) { @@ -254,7 +250,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { } is LockedQueue -> { check(state.owner !== owner) { "Already locked by $owner" } - val node = LockSelect(owner, this, select, block) + val node = LockSelect(owner, select, block) if (state.addLastIf(node) { _state.value === state }) { // successfully enqueued select.disposeOnSelect(node) @@ -352,7 +348,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { override fun toString(): String = "LockedQueue[$owner]" } - private abstract class LockWaiter( + private abstract inner class LockWaiter( @JvmField val owner: Any? ) : LockFreeLinkedListNode(), DisposableHandle { final override fun dispose() { remove() } @@ -360,27 +356,32 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { abstract fun completeResumeLockWaiter(token: Any) } - private class LockCont( + private inner class LockCont( owner: Any?, @JvmField val cont: CancellableContinuation ) : LockWaiter(owner) { - override fun tryResumeLockWaiter() = cont.tryResume(Unit) + override fun tryResumeLockWaiter() = cont.tryResume(Unit, idempotent = null) { + // if this continuation gets cancelled during dispatch to the caller, then release the lock + unlock(owner) + } override fun completeResumeLockWaiter(token: Any) = cont.completeResume(token) - override fun toString(): String = "LockCont[$owner, $cont]" + override fun toString(): String = "LockCont[$owner, $cont] for ${this@MutexImpl}" } - private class LockSelect( + private inner class LockSelect( owner: Any?, - @JvmField val mutex: Mutex, @JvmField val select: SelectInstance, @JvmField val block: suspend (Mutex) -> R ) : LockWaiter(owner) { override fun tryResumeLockWaiter(): Any? = if (select.trySelect()) SELECT_SUCCESS else null override fun completeResumeLockWaiter(token: Any) { assert { token === SELECT_SUCCESS } - block.startCoroutine(receiver = mutex, completion = select.completion) + block.startCoroutineCancellable(receiver = this@MutexImpl, completion = select.completion) { + // if this continuation gets cancelled during dispatch to the caller, then release the lock + unlock(owner) + } } - override fun toString(): String = "LockSelect[$owner, $mutex, $select]" + override fun toString(): String = "LockSelect[$owner, $select] for ${this@MutexImpl}" } // atomic unlock operation that checks that waiters queue is empty diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index 27c976ce3f..84b7f4f8a2 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -33,9 +33,10 @@ public interface Semaphore { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended semaphore acquisition is atomic* -- when this function - * throws [CancellationException] it means that the semaphore was not acquired. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * This function releases the semaphore if it was already acquired by this function before the [CancellationException] + * was thrown. * * Note, that this function does not check for cancellation when it does not suspend. * Use [CoroutineScope.isActive] or [CoroutineScope.ensureActive] to periodically @@ -148,6 +149,8 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se private val _availablePermits = atomic(permits - acquiredPermits) override val availablePermits: Int get() = max(_availablePermits.value, 0) + private val onCancellationRelease = { _: Throwable -> release() } + override fun tryAcquire(): Boolean { _availablePermits.loop { p -> if (p <= 0) return false @@ -164,7 +167,7 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se acquireSlowPath() } - private suspend fun acquireSlowPath() = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + private suspend fun acquireSlowPath() = suspendCancellableCoroutineReusable sc@ { cont -> while (true) { if (addAcquireToQueue(cont)) return@sc val p = _availablePermits.getAndDecrement() @@ -203,6 +206,8 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se // On CAS failure -- the cell must be either PERMIT or BROKEN // If the cell already has PERMIT from tryResumeNextFromQueue, try to grab it if (segment.cas(i, PERMIT, TAKEN)) { // took permit thus eliminating acquire/release pair + // The following resume must always succeed, since continuation was not published yet and we don't have + // to pass onCancellationRelease handle, since the coroutine did not suspend yet and cannot be cancelled cont.resume(Unit) return true } @@ -232,15 +237,15 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se return !segment.cas(i, PERMIT, BROKEN) } cellState === CANCELLED -> return false // the acquire was already cancelled - else -> return (cellState as CancellableContinuation).tryResume() + else -> return (cellState as CancellableContinuation).tryResumeAcquire() } } -} -private fun CancellableContinuation.tryResume(): Boolean { - val token = tryResume(Unit) ?: return false - completeResume(token) - return true + private fun CancellableContinuation.tryResumeAcquire(): Boolean { + val token = tryResume(Unit, null, onCancellationRelease) ?: return false + completeResume(token) + return true + } } private class CancelSemaphoreAcquisitionHandler( diff --git a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt index a9f58dd6ee..c763faf225 100644 --- a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt +++ b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt @@ -87,23 +87,23 @@ class AtomicCancellationCommonTest : TestBase() { } @Test - fun testLockAtomicCancel() = runTest { + fun testLockCancellable() = runTest { expect(1) val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) mutex.lock() // suspends - expect(4) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) mutex.unlock() // unlock mutex first job.cancel() // cancel the job next yield() // now yield - finish(5) + finish(4) } @Test - fun testSelectLockAtomicCancel() = runTest { + fun testSelectLockCancellable() = runTest { expect(1) val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -114,13 +114,12 @@ class AtomicCancellationCommonTest : TestBase() { "OK" } } - assertEquals("OK", result) - expect(5) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) mutex.unlock() // unlock mutex first job.cancel() // cancel the job next yield() // now yield - finish(6) + finish(4) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt b/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt new file mode 100644 index 0000000000..2fe0c914e6 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class AwaitCancellationTest : TestBase() { + + @Test + fun testCancellation() = runTest(expected = { it is CancellationException }) { + expect(1) + coroutineScope { + val deferred: Deferred = async { + expect(2) + awaitCancellation() + } + yield() + expect(3) + require(deferred.isActive) + deferred.cancel() + finish(4) + deferred.await() + } + } +} diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt index 00f719e632..3c11182e00 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt @@ -23,10 +23,23 @@ class CancellableContinuationHandlersTest : TestBase() { fun testDoubleSubscriptionAfterCompletion() = runTest { suspendCancellableCoroutine { c -> c.resume(Unit) - // Nothing happened - c.invokeOnCancellation { expectUnreached() } - // Cannot validate after completion + // First invokeOnCancellation is Ok c.invokeOnCancellation { expectUnreached() } + // Second invokeOnCancellation is not allowed + assertFailsWith { c.invokeOnCancellation { expectUnreached() } } + } + } + + @Test + fun testDoubleSubscriptionAfterCompletionWithException() = runTest { + assertFailsWith { + suspendCancellableCoroutine { c -> + c.resumeWithException(TestException()) + // First invokeOnCancellation is Ok + c.invokeOnCancellation { expectUnreached() } + // Second invokeOnCancellation is not allowed + assertFailsWith { c.invokeOnCancellation { expectUnreached() } } + } } } @@ -46,6 +59,59 @@ class CancellableContinuationHandlersTest : TestBase() { } } + @Test + fun testSecondSubscriptionAfterCancellation() = runTest { + try { + suspendCancellableCoroutine { c -> + // Set IOC first + c.invokeOnCancellation { + assertNull(it) + expect(2) + } + expect(1) + // then cancel (it gets called) + c.cancel() + // then try to install another one + assertFailsWith { c.invokeOnCancellation { expectUnreached() } } + } + } catch (e: CancellationException) { + finish(3) + } + } + + @Test + fun testSecondSubscriptionAfterResumeCancelAndDispatch() = runTest { + var cont: CancellableContinuation? = null + val job = launch(start = CoroutineStart.UNDISPATCHED) { + // will be cancelled during dispatch + assertFailsWith { + suspendCancellableCoroutine { c -> + cont = c + // Set IOC first -- not called (completed) + c.invokeOnCancellation { + assertTrue(it is CancellationException) + expect(4) + } + expect(1) + } + } + expect(5) + } + expect(2) + // then resume it + cont!!.resume(Unit) // schedule cancelled continuation for dispatch + // then cancel the job during dispatch + job.cancel() + expect(3) + yield() // finish dispatching (will call IOC handler here!) + expect(6) + // then try to install another one after we've done dispatching it + assertFailsWith { + cont!!.invokeOnCancellation { expectUnreached() } + } + finish(7) + } + @Test fun testDoubleSubscriptionAfterCancellationWithCause() = runTest { try { diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt index 38fc9ff281..f9f610ceb5 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt @@ -116,4 +116,26 @@ class CancellableContinuationTest : TestBase() { continuation!!.resume(Unit) // Should not fail finish(4) } + + @Test + fun testCompleteJobWhileSuspended() = runTest { + expect(1) + val completableJob = Job() + val coroutineBlock = suspend { + assertFailsWith { + suspendCancellableCoroutine { cont -> + expect(2) + assertSame(completableJob, cont.context[Job]) + completableJob.complete() + } + expectUnreached() + } + expect(3) + } + coroutineBlock.startCoroutine(Continuation(completableJob) { + assertEquals(Unit, it.getOrNull()) + expect(4) + }) + finish(5) + } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt index 39176a9a94..fbfa082555 100644 --- a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 @@ -44,6 +44,33 @@ class CancellableResumeTest : TestBase() { expectUnreached() } + @Test + fun testResumeImmediateAfterCancelWithHandlerFailure() = runTest( + expected = { it is TestException }, + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { + expect(1) + suspendCancellableCoroutine { cont -> + expect(2) + cont.invokeOnCancellation { + expect(3) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } + cont.cancel(TestException("FAIL")) + expect(4) + cont.resume("OK") { cause -> + expect(5) + assertTrue(cause is TestException) + throw TestException3("FAIL") // onCancellation block fails with exception + } + finish(6) + } + expectUnreached() + } + @Test fun testResumeImmediateAfterIndirectCancel() = runTest( expected = { it is CancellationException } @@ -63,6 +90,33 @@ class CancellableResumeTest : TestBase() { expectUnreached() } + @Test + fun testResumeImmediateAfterIndirectCancelWithHandlerFailure() = runTest( + expected = { it is CancellationException }, + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { + expect(1) + val ctx = coroutineContext + suspendCancellableCoroutine { cont -> + expect(2) + cont.invokeOnCancellation { + expect(3) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } + ctx.cancel() + expect(4) + cont.resume("OK") { cause -> + expect(5) + throw TestException3("FAIL") // onCancellation block fails with exception + } + finish(6) + } + expectUnreached() + } + @Test fun testResumeLaterNormally() = runTest { expect(1) @@ -110,7 +164,12 @@ class CancellableResumeTest : TestBase() { } @Test - fun testResumeCancelWhileDispatched() = runTest { + fun testResumeLaterAfterCancelWithHandlerFailure() = runTest( + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { expect(1) lateinit var cc: CancellableContinuation val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -118,36 +177,117 @@ class CancellableResumeTest : TestBase() { try { suspendCancellableCoroutine { cont -> expect(3) - // resumed first, then cancelled, so no invokeOnCancellation call - cont.invokeOnCancellation { expectUnreached() } + cont.invokeOnCancellation { + expect(5) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } cc = cont } expectUnreached() } catch (e: CancellationException) { - expect(8) + finish(9) } } expect(4) + job.cancel(TestCancellationException()) + expect(6) cc.resume("OK") { cause -> expect(7) assertTrue(cause is TestCancellationException) + throw TestException3("FAIL") // onCancellation block fails with exception + } + expect(8) + } + + @Test + fun testResumeCancelWhileDispatched() = runTest { + expect(1) + lateinit var cc: CancellableContinuation + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + try { + suspendCancellableCoroutine { cont -> + expect(3) + // resumed first, dispatched, then cancelled, but still got invokeOnCancellation call + cont.invokeOnCancellation { cause -> + // Note: invokeOnCancellation is called before cc.resume(value) { ... } handler + expect(7) + assertTrue(cause is TestCancellationException) + } + cc = cont + } + expectUnreached() + } catch (e: CancellationException) { + expect(9) + } + } + expect(4) + cc.resume("OK") { cause -> + // Note: this handler is called after invokeOnCancellation handler + expect(8) + assertTrue(cause is TestCancellationException) } expect(5) job.cancel(TestCancellationException()) // cancel while execution is dispatched expect(6) yield() // to coroutine -- throws cancellation exception - finish(9) + finish(10) } + @Test + fun testResumeCancelWhileDispatchedWithHandlerFailure() = runTest( + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { + expect(1) + lateinit var cc: CancellableContinuation + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + try { + suspendCancellableCoroutine { cont -> + expect(3) + // resumed first, dispatched, then cancelled, but still got invokeOnCancellation call + cont.invokeOnCancellation { cause -> + // Note: invokeOnCancellation is called before cc.resume(value) { ... } handler + expect(7) + assertTrue(cause is TestCancellationException) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } + cc = cont + } + expectUnreached() + } catch (e: CancellationException) { + expect(9) + } + } + expect(4) + cc.resume("OK") { cause -> + // Note: this handler is called after invokeOnCancellation handler + expect(8) + assertTrue(cause is TestCancellationException) + throw TestException3("FAIL") // onCancellation block fails with exception + } + expect(5) + job.cancel(TestCancellationException()) // cancel while execution is dispatched + expect(6) + yield() // to coroutine -- throws cancellation exception + finish(10) + } @Test fun testResumeUnconfined() = runTest { val outerScope = this withContext(Dispatchers.Unconfined) { val result = suspendCancellableCoroutine { - outerScope.launch { it.resume("OK", {}) } + outerScope.launch { + it.resume("OK") { + expectUnreached() + } + } } assertEquals("OK", result) } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt b/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt new file mode 100644 index 0000000000..b69eb22e17 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.coroutines.* +import kotlin.test.* + +/** + * When using [suspendCoroutine] from the standard library the continuation must be dispatched atomically, + * without checking for cancellation at any point in time. + */ +class DispatchedContinuationTest : TestBase() { + private lateinit var cont: Continuation + + @Test + fun testCancelThenResume() = runTest { + expect(1) + launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + coroutineContext[Job]!!.cancel() + // a regular suspendCoroutine will still suspend despite the fact that coroutine was cancelled + val value = suspendCoroutine { + expect(3) + cont = it + } + expect(6) + assertEquals("OK", value) + } + expect(4) + cont.resume("OK") + expect(5) + yield() // to the launched job + finish(7) + } + + @Test + fun testCancelThenResumeUnconfined() = runTest { + expect(1) + launch(Dispatchers.Unconfined) { + expect(2) + coroutineContext[Job]!!.cancel() + // a regular suspendCoroutine will still suspend despite the fact that coroutine was cancelled + val value = suspendCoroutine { + expect(3) + cont = it + } + expect(5) + assertEquals("OK", value) + } + expect(4) + cont.resume("OK") // immediately resumes -- because unconfined + finish(6) + } + + @Test + fun testResumeThenCancel() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + val value = suspendCoroutine { + expect(3) + cont = it + } + expect(7) + assertEquals("OK", value) + } + expect(4) + cont.resume("OK") + expect(5) + // now cancel the job, which the coroutine is waiting to be dispatched + job.cancel() + expect(6) + yield() // to the launched job + finish(8) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt index a6ddd81185..91d941b32c 100644 --- a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt @@ -42,7 +42,7 @@ class BasicOperationsTest : TestBase() { @Test fun testInvokeOnClose() = TestChannelKind.values().forEach { kind -> reset() - val channel = kind.create() + val channel = kind.create() channel.invokeOnClose { if (it is AssertionError) { expect(3) @@ -59,7 +59,7 @@ class BasicOperationsTest : TestBase() { fun testInvokeOnClosed() = TestChannelKind.values().forEach { kind -> reset() expect(1) - val channel = kind.create() + val channel = kind.create() channel.close() channel.invokeOnClose { expect(2) } assertFailsWith { channel.invokeOnClose { expect(3) } } @@ -69,7 +69,7 @@ class BasicOperationsTest : TestBase() { @Test fun testMultipleInvokeOnClose() = TestChannelKind.values().forEach { kind -> reset() - val channel = kind.create() + val channel = kind.create() channel.invokeOnClose { expect(3) } expect(1) assertFailsWith { channel.invokeOnClose { expect(4) } } @@ -81,7 +81,7 @@ class BasicOperationsTest : TestBase() { @Test fun testIterator() = runTest { TestChannelKind.values().forEach { kind -> - val channel = kind.create() + val channel = kind.create() val iterator = channel.iterator() assertFailsWith { iterator.next() } channel.close() @@ -91,7 +91,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testReceiveOrNull(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async(NonCancellable) { channel.receive() } @@ -108,7 +108,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testReceiveOrNullException(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async(NonCancellable) { channel.receive() } @@ -132,7 +132,7 @@ class BasicOperationsTest : TestBase() { @Suppress("ReplaceAssertBooleanWithAssertEquality") private suspend fun testReceiveOrClosed(kind: TestChannelKind) = coroutineScope { reset() - val channel = kind.create() + val channel = kind.create() launch { expect(2) channel.send(1) @@ -159,7 +159,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testOffer(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async { channel.send(42) } yield() channel.close() @@ -184,7 +184,7 @@ class BasicOperationsTest : TestBase() { private suspend fun testSendAfterClose(kind: TestChannelKind) { assertFailsWith { coroutineScope { - val channel = kind.create() + val channel = kind.create() channel.close() launch { @@ -195,7 +195,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testSendReceive(kind: TestChannelKind, iterations: Int) = coroutineScope { - val channel = kind.create() + val channel = kind.create() launch { repeat(iterations) { channel.send(it) } channel.close() diff --git a/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt b/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt index bb3142e54c..ab1a85d697 100644 --- a/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt @@ -63,7 +63,7 @@ class BroadcastTest : TestBase() { val a = produce { expect(3) send("MSG") - expect(5) + expectUnreached() // is not executed, because send is cancelled } expect(2) yield() // to produce @@ -72,7 +72,7 @@ class BroadcastTest : TestBase() { expect(4) yield() // to abort produce assertTrue(a.isClosedForReceive) // the source channel was consumed - finish(6) + finish(5) } @Test diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt new file mode 100644 index 0000000000..41f60479f2 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* +import kotlin.test.* + +class ChannelBufferOverflowTest : TestBase() { + @Test + fun testDropLatest() = runTest { + val c = Channel(2, BufferOverflow.DROP_LATEST) + assertTrue(c.offer(1)) + assertTrue(c.offer(2)) + assertTrue(c.offer(3)) // overflows, dropped + c.send(4) // overflows dropped + assertEquals(1, c.receive()) + assertTrue(c.offer(5)) + assertTrue(c.offer(6)) // overflows, dropped + assertEquals(2, c.receive()) + assertEquals(5, c.receive()) + assertEquals(null, c.poll()) + } + + @Test + fun testDropOldest() = runTest { + val c = Channel(2, BufferOverflow.DROP_OLDEST) + assertTrue(c.offer(1)) + assertTrue(c.offer(2)) + assertTrue(c.offer(3)) // overflows, keeps 2, 3 + c.send(4) // overflows, keeps 3, 4 + assertEquals(3, c.receive()) + assertTrue(c.offer(5)) + assertTrue(c.offer(6)) // overflows, keeps 5, 6 + assertEquals(5, c.receive()) + assertEquals(6, c.receive()) + assertEquals(null, c.poll()) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt index 72ba315450..413c91f5a7 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -9,7 +9,6 @@ import kotlin.test.* class ChannelFactoryTest : TestBase() { - @Test fun testRendezvousChannel() { assertTrue(Channel() is RendezvousChannel) @@ -19,21 +18,31 @@ class ChannelFactoryTest : TestBase() { @Test fun testLinkedListChannel() { assertTrue(Channel(Channel.UNLIMITED) is LinkedListChannel) + assertTrue(Channel(Channel.UNLIMITED, BufferOverflow.DROP_OLDEST) is LinkedListChannel) + assertTrue(Channel(Channel.UNLIMITED, BufferOverflow.DROP_LATEST) is LinkedListChannel) } @Test fun testConflatedChannel() { assertTrue(Channel(Channel.CONFLATED) is ConflatedChannel) + assertTrue(Channel(1, BufferOverflow.DROP_OLDEST) is ConflatedChannel) } @Test fun testArrayChannel() { assertTrue(Channel(1) is ArrayChannel) + assertTrue(Channel(1, BufferOverflow.DROP_LATEST) is ArrayChannel) assertTrue(Channel(10) is ArrayChannel) } @Test - fun testInvalidCapacityNotSupported() = runTest({ it is IllegalArgumentException }) { - Channel(-3) + fun testInvalidCapacityNotSupported() { + assertFailsWith { Channel(-3) } + } + + @Test + fun testUnsupportedBufferOverflow() { + assertFailsWith { Channel(Channel.CONFLATED, BufferOverflow.DROP_OLDEST) } + assertFailsWith { Channel(Channel.CONFLATED, BufferOverflow.DROP_LATEST) } } } diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt new file mode 100644 index 0000000000..d2ef3d2691 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.selects.* +import kotlin.test.* + +/** + * Tests for failures inside `onUndeliveredElement` handler in [Channel]. + */ +class ChannelUndeliveredElementFailureTest : TestBase() { + private val item = "LOST" + private val onCancelFail: (String) -> Unit = { throw TestException(it) } + private val shouldBeUnhandled: List<(Throwable) -> Boolean> = listOf({ it.isElementCancelException() }) + + private fun Throwable.isElementCancelException() = + this is UndeliveredElementException && cause is TestException && cause!!.message == item + + @Test + fun testSendCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.send(item) + expectUnreached() + } + job.cancel() + } + + @Test + fun testSendSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onSend(item) { + expectUnreached() + } + } + } + job.cancel() + } + + @Test + fun testReceiveCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.receive() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onReceive { + expectUnreached() + } + } + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrNullCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.receiveOrNull() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrNullSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onReceiveOrNull { + expectUnreached() + } + } + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrClosedCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.receiveOrClosed() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrClosedSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onReceiveOrClosed { + expectUnreached() + } + } + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testHasNextCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.iterator().hasNext() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testChannelCancelledFail() = runTest(expected = { it.isElementCancelException()}) { + val channel = Channel(1, onUndeliveredElement = onCancelFail) + channel.send(item) + channel.cancel() + expectUnreached() + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt new file mode 100644 index 0000000000..0391e00033 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt @@ -0,0 +1,104 @@ +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlin.test.* + +class ChannelUndeliveredElementTest : TestBase() { + @Test + fun testSendSuccessfully() = runAllKindsTest { kind -> + val channel = kind.create { it.cancel() } + val res = Resource("OK") + launch { + channel.send(res) + } + val ok = channel.receive() + assertEquals("OK", ok.value) + assertFalse(res.isCancelled) // was not cancelled + channel.close() + assertFalse(res.isCancelled) // still was not cancelled + } + + @Test + fun testRendezvousSendCancelled() = runTest { + val channel = Channel { it.cancel() } + val res = Resource("OK") + val sender = launch(start = CoroutineStart.UNDISPATCHED) { + assertFailsWith { + channel.send(res) // suspends & get cancelled + } + } + sender.cancelAndJoin() + assertTrue(res.isCancelled) + } + + @Test + fun testBufferedSendCancelled() = runTest { + val channel = Channel(1) { it.cancel() } + val resA = Resource("A") + val resB = Resource("B") + val sender = launch(start = CoroutineStart.UNDISPATCHED) { + channel.send(resA) // goes to buffer + assertFailsWith { + channel.send(resB) // suspends & get cancelled + } + } + sender.cancelAndJoin() + assertFalse(resA.isCancelled) // it is in buffer, not cancelled + assertTrue(resB.isCancelled) // send was cancelled + channel.cancel() // now cancel the channel + assertTrue(resA.isCancelled) // now cancelled in buffer + } + + @Test + fun testConflatedResourceCancelled() = runTest { + val channel = Channel(Channel.CONFLATED) { it.cancel() } + val resA = Resource("A") + val resB = Resource("B") + channel.send(resA) + assertFalse(resA.isCancelled) + assertFalse(resB.isCancelled) + channel.send(resB) + assertTrue(resA.isCancelled) // it was conflated (lost) and thus cancelled + assertFalse(resB.isCancelled) + channel.close() + assertFalse(resB.isCancelled) // not cancelled yet, can be still read by receiver + channel.cancel() + assertTrue(resB.isCancelled) // now it is cancelled + } + + @Test + fun testSendToClosedChannel() = runAllKindsTest { kind -> + val channel = kind.create { it.cancel() } + channel.close() // immediately close channel + val res = Resource("OK") + assertFailsWith { + channel.send(res) // send fails to closed channel, resource was not delivered + } + assertTrue(res.isCancelled) + } + + private fun runAllKindsTest(test: suspend CoroutineScope.(TestChannelKind) -> Unit) { + for (kind in TestChannelKind.values()) { + if (kind.viaBroadcast) continue // does not support onUndeliveredElement + try { + runTest { + test(kind) + } + } catch(e: Throwable) { + error("$kind: $e", e) + } + } + } + + private class Resource(val value: String) { + private val _cancelled = atomic(false) + + val isCancelled: Boolean + get() = _cancelled.value + + fun cancel() { + check(!_cancelled.getAndSet(true)) { "Already cancelled" } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt new file mode 100644 index 0000000000..e80309be89 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +// Test that ArrayChannel(1, DROP_OLDEST) works just like ConflatedChannel() +class ConflatedChannelArrayModelTest : ConflatedChannelTest() { + override fun createConflatedChannel(): Channel = + ArrayChannel(1, BufferOverflow.DROP_OLDEST, null) +} diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 4deb3858f0..18f2843868 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -7,10 +7,13 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlin.test.* -class ConflatedChannelTest : TestBase() { +open class ConflatedChannelTest : TestBase() { + protected open fun createConflatedChannel() = + Channel(Channel.CONFLATED) + @Test fun testBasicConflationOfferPoll() { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() assertNull(q.poll()) assertTrue(q.offer(1)) assertTrue(q.offer(2)) @@ -21,7 +24,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflatedSend() = runTest { - val q = ConflatedChannel() + val q = createConflatedChannel() q.send(1) q.send(2) // shall conflated previously sent assertEquals(2, q.receiveOrNull()) @@ -29,7 +32,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflatedClose() = runTest { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() q.send(1) q.close() // shall become closed but do not conflate last sent item yet assertTrue(q.isClosedForSend) @@ -43,7 +46,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflationSendReceive() = runTest { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() expect(1) launch { // receiver coroutine expect(4) @@ -71,7 +74,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConsumeAll() = runTest { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() expect(1) for (i in 1..10) { q.send(i) // stores as last @@ -85,7 +88,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testCancelWithCause() = runTest({ it is TestCancellationException }) { - val channel = Channel(Channel.CONFLATED) + val channel = createConflatedChannel() channel.cancel(TestCancellationException()) channel.receiveOrNull() } diff --git a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt index 69d8fd03e3..993be78e17 100644 --- a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt +++ b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt @@ -7,9 +7,10 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.selects.* -enum class TestChannelKind(val capacity: Int, - private val description: String, - private val viaBroadcast: Boolean = false +enum class TestChannelKind( + val capacity: Int, + private val description: String, + val viaBroadcast: Boolean = false ) { RENDEZVOUS(0, "RendezvousChannel"), ARRAY_1(1, "ArrayChannel(1)"), @@ -22,8 +23,11 @@ enum class TestChannelKind(val capacity: Int, CONFLATED_BROADCAST(Channel.CONFLATED, "ConflatedBroadcastChannel", viaBroadcast = true) ; - fun create(): Channel = if (viaBroadcast) ChannelViaBroadcast(BroadcastChannel(capacity)) - else Channel(capacity) + fun create(onUndeliveredElement: ((T) -> Unit)? = null): Channel = when { + viaBroadcast && onUndeliveredElement != null -> error("Broadcast channels to do not support onUndeliveredElement") + viaBroadcast -> ChannelViaBroadcast(BroadcastChannel(capacity)) + else -> Channel(capacity, onUndeliveredElement = onUndeliveredElement) + } val isConflated get() = capacity == Channel.CONFLATED override fun toString(): String = description diff --git a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt index 9b257d933e..ddb1d88ae2 100644 --- a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt +++ b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -7,11 +7,12 @@ package kotlinx.coroutines import kotlin.coroutines.* import kotlin.jvm.* -private class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineDispatcher(), Delay { - +internal class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineDispatcher(), Delay { private val originalDispatcher = enclosingScope.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher private val heap = ArrayList() // TODO use MPP heap/ordered set implementation (commonize ThreadSafeHeap) - private var currentTime = 0L + + var currentTime = 0L + private set init { /* @@ -50,17 +51,20 @@ private class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineD @ExperimentalCoroutinesApi override fun isDispatchNeeded(context: CoroutineContext): Boolean = originalDispatcher.isDispatchNeeded(context) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - val task = TimedTask(block, currentTime + timeMillis) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + val task = TimedTask(block, deadline(timeMillis)) heap += task return task } override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - val task = TimedTask(Runnable { with(continuation) { resumeUndispatched(Unit) } }, currentTime + timeMillis) + val task = TimedTask(Runnable { with(continuation) { resumeUndispatched(Unit) } }, deadline(timeMillis)) heap += task continuation.invokeOnCancellation { task.dispose() } } + + private fun deadline(timeMillis: Long) = + if (timeMillis == Long.MAX_VALUE) Long.MAX_VALUE else currentTime + timeMillis } /** diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt new file mode 100644 index 0000000000..7b66977226 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +/** + * A _behavioral_ test for conflation options that can be configured by the [buffer] operator to test that it is + * implemented properly and that adjacent [buffer] calls are fused properly. +*/ +class BufferConflationTest : TestBase() { + private val n = 100 // number of elements to emit for test + + private fun checkConflate( + capacity: Int, + onBufferOverflow: BufferOverflow = BufferOverflow.DROP_OLDEST, + op: suspend Flow.() -> Flow + ) = runTest { + expect(1) + // emit all and conflate, then collect first & last + val expectedList = when (onBufferOverflow) { + BufferOverflow.DROP_OLDEST -> listOf(0) + (n - capacity until n).toList() // first item & capacity last ones + BufferOverflow.DROP_LATEST -> (0..capacity).toList() // first & capacity following ones + else -> error("cannot happen") + } + flow { + repeat(n) { i -> + expect(i + 2) + emit(i) + } + } + .op() + .collect { i -> + val j = expectedList.indexOf(i) + expect(n + 2 + j) + } + finish(n + 2 + expectedList.size) + } + + @Test + fun testConflate() = + checkConflate(1) { + conflate() + } + + @Test + fun testBufferConflated() = + checkConflate(1) { + buffer(Channel.CONFLATED) + } + + @Test + fun testBufferDropOldest() = + checkConflate(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testBuffer0DropOldest() = + checkConflate(1) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testBuffer1DropOldest() = + checkConflate(1) { + buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testBuffer10DropOldest() = + checkConflate(10) { + buffer(10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testConflateOverridesBuffer() = + checkConflate(1) { + buffer(42).conflate() + } + + @Test // conflate().conflate() should work like a single conflate + fun testDoubleConflate() = + checkConflate(1) { + conflate().conflate() + } + + @Test + fun testConflateBuffer10Combine() = + checkConflate(10) { + conflate().buffer(10) + } + + @Test + fun testBufferDropLatest() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test + fun testBuffer0DropLatest() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test + fun testBuffer1DropLatest() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(1, onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test // overrides previous buffer + fun testBufferDropLatestOverrideBuffer() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(42).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test // overrides previous conflate + fun testBufferDropLatestOverrideConflate() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + conflate().buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test + fun testBufferDropLatestBuffer7Combine() = + checkConflate(7, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).buffer(7) + } + + @Test + fun testConflateOverrideBufferDropLatest() = + checkConflate(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).conflate() + } + + @Test + fun testBuffer3DropOldestOverrideBuffer8DropLatest() = + checkConflate(3, BufferOverflow.DROP_OLDEST) { + buffer(8, onBufferOverflow = BufferOverflow.DROP_LATEST) + .buffer(3, BufferOverflow.DROP_OLDEST) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt index b68e115637..6352aacf41 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.flow @@ -9,13 +9,21 @@ import kotlinx.coroutines.channels.* import kotlin.math.* import kotlin.test.* +/** + * A _behavioral_ test for buffering that is introduced by the [buffer] operator to test that it is + * implemented properly and that adjacent [buffer] calls are fused properly. + */ class BufferTest : TestBase() { - private val n = 50 // number of elements to emit for test + private val n = 200 // number of elements to emit for test private val defaultBufferSize = 64 // expected default buffer size (per docs) // Use capacity == -1 to check case of "no buffer" private fun checkBuffer(capacity: Int, op: suspend Flow.() -> Flow) = runTest { expect(1) + /* + Channels perform full rendezvous. Sender does not suspend when there is a suspended receiver and vice-versa. + Thus, perceived batch size is +2 from capacity. + */ val batchSize = capacity + 2 flow { repeat(n) { i -> @@ -163,27 +171,6 @@ class BufferTest : TestBase() { .flowOn(wrapperDispatcher()).buffer(5) } - @Test - fun testConflate() = runTest { - expect(1) - // emit all and conflate / then collect first & last - flow { - repeat(n) { i -> - expect(i + 2) - emit(i) - } - } - .buffer(Channel.CONFLATED) - .collect { i -> - when (i) { - 0 -> expect(n + 2) // first value - n - 1 -> expect(n + 3) // last value - else -> error("Unexpected $i") - } - } - finish(n + 4) - } - @Test fun testCancellation() = runTest { val result = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt index 802ba1ef2f..eedfac2ea3 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt @@ -134,15 +134,14 @@ class CatchTest : TestBase() { // flowOn with a different dispatcher introduces asynchrony so that all exceptions in the // upstream flows are handled before they go downstream .onEach { value -> - expect(8) - assertEquals("OK", value) + expectUnreached() // already cancelled } .catch { e -> - expect(9) + expect(8) assertTrue(e is TestException) assertSame(d0, kotlin.coroutines.coroutineContext[ContinuationInterceptor] as CoroutineContext) } .collect() - finish(10) + finish(9) } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt index a619355b68..2893321998 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.flow @@ -238,6 +238,22 @@ abstract class CombineTestBase : TestBase() { assertFailsWith(flow) finish(7) } + + @Test + fun testCancelledCombine() = runTest( + expected = { it is CancellationException } + ) { + coroutineScope { + val flow = flow { + emit(Unit) // emit + } + cancel() // cancel the scope + flow.combineLatest(flow) { u, _ -> u }.collect { + // should not be reached, because cancelled before it runs + expectUnreached() + } + } + } } class CombineTest : CombineTestBase() { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt index fc03d367c6..68e7f66b9d 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt @@ -94,4 +94,33 @@ class DistinctUntilChangedTest : TestBase() { val flow = flowOf(null, 1, null, null).distinctUntilChanged() assertEquals(listOf(null, 1, null), flow.toList()) } + + @Test + fun testRepeatedDistinctFusionDefault() = testRepeatedDistinctFusion { + distinctUntilChanged() + } + + // A separate variable is needed for K/N that does not optimize non-captured lambdas (yet) + private val areEquivalentTestFun: (old: Int, new: Int) -> Boolean = { old, new -> old == new } + + @Test + fun testRepeatedDistinctFusionAreEquivalent() = testRepeatedDistinctFusion { + distinctUntilChanged(areEquivalentTestFun) + } + + // A separate variable is needed for K/N that does not optimize non-captured lambdas (yet) + private val keySelectorTestFun: (Int) -> Int = { it % 2 } + + @Test + fun testRepeatedDistinctFusionByKey() = testRepeatedDistinctFusion { + distinctUntilChangedBy(keySelectorTestFun) + } + + private fun testRepeatedDistinctFusion(op: Flow.() -> Flow) = runTest { + val flow = (1..10).asFlow() + val d1 = flow.op() + assertNotSame(flow, d1) + val d2 = d1.op() + assertSame(d1, d2) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt index f8350ff584..68653281cc 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -341,4 +341,20 @@ class FlowOnTest : TestBase() { assertEquals(expected, value) } } + + @Test + fun testCancelledFlowOn() = runTest { + assertFailsWith { + coroutineScope { + val scope = this + flow { + emit(Unit) // emit to buffer + scope.cancel() // now cancel outer scope + }.flowOn(wrapperDispatcher()).collect { + // should not be reached, because cancelled before it runs + expectUnreached() + } + } + } + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt index 7f0c548ca6..f55e8beeb2 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt @@ -231,7 +231,7 @@ class OnCompletionTest : TestBase() { @Test fun testSingle() = runTest { - assertFailsWith { + assertFailsWith { flowOf(239).onCompletion { assertNull(it) expect(1) @@ -240,7 +240,7 @@ class OnCompletionTest : TestBase() { expectUnreached() } catch (e: Throwable) { // Second emit -- failure - assertTrue { e is IllegalStateException } + assertTrue { e is IllegalArgumentException } throw e } }.single() diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index b28320c391..5f2b5a74cd 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -67,14 +67,12 @@ class ZipTest : TestBase() { val f1 = flow { emit("1") emit("2") - hang { - expect(1) - } + expectUnreached() // the above emit will get cancelled because f2 ends } val f2 = flowOf("a", "b") assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) - finish(2) + finish(1) } @Test @@ -92,25 +90,6 @@ class ZipTest : TestBase() { finish(2) } - @Test - fun testCancelWhenFlowIsDone2() = runTest { - val f1 = flow { - emit("1") - emit("2") - try { - emit("3") - expectUnreached() - } finally { - expect(1) - } - - } - - val f2 = flowOf("a", "b") - assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) - finish(2) - } - @Test fun testContextIsIsolatedReversed() = runTest { val f1 = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt new file mode 100644 index 0000000000..9c6aed211a --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.math.* +import kotlin.test.* + +/** + * Similar to [BufferTest], but tests [shareIn] buffering and its fusion with [buffer] operators. + */ +class ShareInBufferTest : TestBase() { + private val n = 200 // number of elements to emit for test + private val defaultBufferSize = 64 // expected default buffer size (per docs) + + // Use capacity == -1 to check case of "no buffer" + private fun checkBuffer(capacity: Int, op: suspend Flow.(CoroutineScope) -> Flow) = runTest { + expect(1) + /* + Shared flows do not perform full rendezvous. On buffer overflow emitter always suspends until all + subscribers get the value and then resumes. Thus, perceived batch size is +1 from buffer capacity. + */ + val batchSize = capacity + 1 + val upstream = flow { + repeat(n) { i -> + val batchNo = i / batchSize + val batchIdx = i % batchSize + expect(batchNo * batchSize * 2 + batchIdx + 2) + emit(i) + } + emit(-1) // done + } + coroutineScope { + upstream + .op(this) + .takeWhile { i -> i >= 0 } // until done + .collect { i -> + val batchNo = i / batchSize + val batchIdx = i % batchSize + // last batch might have smaller size + val k = min((batchNo + 1) * batchSize, n) - batchNo * batchSize + expect(batchNo * batchSize * 2 + k + batchIdx + 2) + } + coroutineContext.cancelChildren() // cancels sharing + } + finish(2 * n + 2) + } + + @Test + fun testReplay0DefaultBuffer() = + checkBuffer(defaultBufferSize) { + shareIn(it, SharingStarted.Eagerly) + } + + @Test + fun testReplay1DefaultBuffer() = + checkBuffer(defaultBufferSize) { + shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test // buffer is padded to default size as needed + fun testReplay10DefaultBuffer() = + checkBuffer(maxOf(10, defaultBufferSize)) { + shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test // buffer is padded to default size as needed + fun testReplay100DefaultBuffer() = + checkBuffer( maxOf(100, defaultBufferSize)) { + shareIn(it, SharingStarted.Eagerly, 100) + } + + @Test + fun testDefaultBufferKeepsDefault() = + checkBuffer(defaultBufferSize) { + buffer().shareIn(it, SharingStarted.Eagerly) + } + + @Test + fun testOverrideDefaultBuffer0() = + checkBuffer(0) { + buffer(0).shareIn(it, SharingStarted.Eagerly) + } + + @Test + fun testOverrideDefaultBuffer10() = + checkBuffer(10) { + buffer(10).shareIn(it, SharingStarted.Eagerly) + } + + @Test // buffer and replay sizes add up + fun testBufferReplaySum() = + checkBuffer(41) { + buffer(10).buffer(20).shareIn(it, SharingStarted.Eagerly, 11) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt new file mode 100644 index 0000000000..0528e97e7d --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +/** + * Similar to [ShareInBufferTest] and [BufferConflationTest], + * but tests [shareIn] and its fusion with [conflate] operator. + */ +class ShareInConflationTest : TestBase() { + private val n = 100 + + private fun checkConflation( + bufferCapacity: Int, + onBufferOverflow: BufferOverflow = BufferOverflow.DROP_OLDEST, + op: suspend Flow.(CoroutineScope) -> Flow + ) = runTest { + expect(1) + // emit all and conflate, then should collect bufferCapacity latest ones + val done = Job() + flow { + repeat(n) { i -> + expect(i + 2) + emit(i) + } + done.join() // wait until done collection + emit(-1) // signal flow completion + } + .op(this) + .takeWhile { i -> i >= 0 } + .collect { i -> + val first = if (onBufferOverflow == BufferOverflow.DROP_LATEST) 0 else n - bufferCapacity + val last = first + bufferCapacity - 1 + if (i in first..last) { + expect(n + i - first + 2) + if (i == last) done.complete() // received the last one + } else { + error("Unexpected $i") + } + } + finish(n + bufferCapacity + 2) + } + + @Test + fun testConflateReplay1() = + checkConflation(1) { + conflate().shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test // still looks like conflating the last value for the first subscriber (will not replay to others though) + fun testConflateReplay0() = + checkConflation(1) { + conflate().shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testConflateReplay5() = + checkConflation(5) { + conflate().shareIn(it, SharingStarted.Eagerly, 5) + } + + @Test + fun testBufferDropOldestReplay1() = + checkConflation(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test + fun testBufferDropOldestReplay0() = + checkConflation(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBufferDropOldestReplay10() = + checkConflation(10) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test + fun testBuffer20DropOldestReplay0() = + checkConflation(20) { + buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBuffer7DropOldestReplay11() = + checkConflation(18) { + buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 11) + } + + @Test // a preceding buffer() gets overridden by conflate() + fun testBufferConflateOverride() = + checkConflation(1) { + buffer(23).conflate().shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) + fun testBufferDropOldestOverride() = + checkConflation(1) { + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test + fun testBufferDropLatestReplay0() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBufferDropLatestReplay1() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test + fun testBufferDropLatestReplay10() = + checkConflation(10, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test + fun testBuffer0DropLatestReplay0() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBuffer0DropLatestReplay1() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test + fun testBuffer0DropLatestReplay10() = + checkConflation(10, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test + fun testBuffer5DropLatestReplay0() = + checkConflation(5, BufferOverflow.DROP_LATEST) { + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBuffer5DropLatestReplay10() = + checkConflation(15, BufferOverflow.DROP_LATEST) { + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + fun testBufferDropLatestOverride() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt new file mode 100644 index 0000000000..371d01472e --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class ShareInFusionTest : TestBase() { + /** + * Test perfect fusion for operators **after** [shareIn]. + */ + @Test + fun testOperatorFusion() = runTest { + val sh = emptyFlow().shareIn(this, SharingStarted.Eagerly) + assertTrue(sh !is MutableSharedFlow<*>) // cannot be cast to mutable shared flow!!! + assertSame(sh, (sh as Flow<*>).cancellable()) + assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) + coroutineContext.cancelChildren() + } + + @Test + fun testFlowOnContextFusion() = runTest { + val flow = flow { + assertEquals("FlowCtx", currentCoroutineContext()[CoroutineName]?.name) + emit("OK") + }.flowOn(CoroutineName("FlowCtx")) + assertEquals("OK", flow.shareIn(this, SharingStarted.Eagerly, 1).first()) + coroutineContext.cancelChildren() + } + + /** + * Tests that `channelFlow { ... }.buffer(x)` works according to the [channelFlow] docs, and subsequent + * application of [shareIn] does not leak upstream. + */ + @Test + fun testChannelFlowBufferShareIn() = runTest { + expect(1) + val flow = channelFlow { + // send a batch of 10 elements using [offer] + for (i in 1..10) { + assertTrue(offer(i)) // offer must succeed, because buffer + } + send(0) // done + }.buffer(10) // request a buffer of 10 + // ^^^^^^^^^ buffer stays here + val shared = flow.shareIn(this, SharingStarted.Eagerly) + shared + .takeWhile { it > 0 } + .collect { i -> expect(i + 1) } + finish(12) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt new file mode 100644 index 0000000000..9020f5f311 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class ShareInTest : TestBase() { + @Test + fun testReplay0Eager() = runTest { + expect(1) + val flow = flowOf("OK") + val shared = flow.shareIn(this, SharingStarted.Eagerly) + yield() // actually start sharing + // all subscribers miss "OK" + val jobs = List(10) { + shared.onEach { expectUnreached() }.launchIn(this) + } + yield() // ensure nothing is collected + jobs.forEach { it.cancel() } + finish(2) + } + + @Test + fun testReplay0Lazy() = testReplayZeroOrOne(0) + + @Test + fun testReplay1Lazy() = testReplayZeroOrOne(1) + + private fun testReplayZeroOrOne(replay: Int) = runTest { + expect(1) + val doneBarrier = Job() + val flow = flow { + expect(2) + emit("OK") + doneBarrier.join() + emit("DONE") + } + val sharingJob = Job() + val shared = flow.shareIn(this + sharingJob, started = SharingStarted.Lazily, replay = replay) + yield() // should not start sharing + // first subscriber gets "OK", other subscribers miss "OK" + val n = 10 + val replayOfs = replay * (n - 1) + val subscriberJobs = List(n) { index -> + val subscribedBarrier = Job() + val job = shared + .onSubscription { + subscribedBarrier.complete() + } + .onEach { value -> + when (value) { + "OK" -> { + expect(3 + index) + if (replay == 0) { // only the first subscriber collects "OK" without replay + assertEquals(0, index) + } + } + "DONE" -> { + expect(4 + index + replayOfs) + } + else -> expectUnreached() + } + } + .takeWhile { it != "DONE" } + .launchIn(this) + subscribedBarrier.join() // wait until the launched job subscribed before launching the next one + job + } + doneBarrier.complete() + subscriberJobs.joinAll() + expect(4 + n + replayOfs) + sharingJob.cancel() + finish(5 + n + replayOfs) + } + + @Test + fun testUpstreamCompleted() = + testUpstreamCompletedOrFailed(failed = false) + + @Test + fun testUpstreamFailed() = + testUpstreamCompletedOrFailed(failed = true) + + private fun testUpstreamCompletedOrFailed(failed: Boolean) = runTest { + val emitted = Job() + val terminate = Job() + val sharingJob = CompletableDeferred() + val upstream = flow { + emit("OK") + emitted.complete() + terminate.join() + if (failed) throw TestException() + } + val shared = upstream.shareIn(this + sharingJob, SharingStarted.Eagerly, 1) + assertEquals(emptyList(), shared.replayCache) + emitted.join() // should start sharing, emit & cache + assertEquals(listOf("OK"), shared.replayCache) + terminate.complete() + sharingJob.complete(Unit) + sharingJob.join() // should complete sharing + assertEquals(listOf("OK"), shared.replayCache) // cache is still there + if (failed) { + assertTrue(sharingJob.getCompletionExceptionOrNull() is TestException) + } else { + assertNull(sharingJob.getCompletionExceptionOrNull()) + } + } + + @Test + fun testWhileSubscribedBasic() = + testWhileSubscribed(1, SharingStarted.WhileSubscribed()) + + @Test + fun testWhileSubscribedCustomAtLeast1() = + testWhileSubscribed(1, SharingStarted.WhileSubscribedAtLeast(1)) + + @Test + fun testWhileSubscribedCustomAtLeast2() = + testWhileSubscribed(2, SharingStarted.WhileSubscribedAtLeast(2)) + + @OptIn(ExperimentalStdlibApi::class) + private fun testWhileSubscribed(threshold: Int, started: SharingStarted) = runTest { + expect(1) + val flowState = FlowState() + val n = 3 // max number of subscribers + val log = Channel(2 * n) + + suspend fun checkStartTransition(subscribers: Int) { + when (subscribers) { + in 0 until threshold -> assertFalse(flowState.started) + threshold -> { + flowState.awaitStart() // must eventually start the flow + for (i in 1..threshold) { + assertEquals("sub$i: OK", log.receive()) // threshold subs must receive the values + } + } + in threshold + 1..n -> assertTrue(flowState.started) + } + } + + suspend fun checkStopTransition(subscribers: Int) { + when (subscribers) { + in threshold + 1..n -> assertTrue(flowState.started) + threshold - 1 -> flowState.awaitStop() // upstream flow must be eventually stopped + in 0..threshold - 2 -> assertFalse(flowState.started) // should have stopped already + } + } + + val flow = flow { + flowState.track { + emit("OK") + delay(Long.MAX_VALUE) // await forever, will get cancelled + } + } + + val shared = flow.shareIn(this, started) + repeat(5) { // repeat scenario a few times + yield() + assertFalse(flowState.started) // flow is not running even if we yield + // start 3 subscribers + val subs = ArrayList() + for (i in 1..n) { + subs += shared + .onEach { value -> // only the first threshold subscribers get the value + when (i) { + in 1..threshold -> log.offer("sub$i: $value") + else -> expectUnreached() + } + } + .onCompletion { log.offer("sub$i: completion") } + .launchIn(this) + checkStartTransition(i) + } + // now cancel all subscribers + for (i in 1..n) { + subs.removeFirst().cancel() // cancel subscriber + assertEquals("sub$i: completion", log.receive()) // subscriber shall shutdown + checkStopTransition(n - i) + } + } + coroutineContext.cancelChildren() // cancel sharing job + finish(2) + } + + @Suppress("TestFunctionName") + private fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = + object : SharingStarted { + override fun command(subscriptionCount: StateFlow): Flow = + subscriptionCount + .map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } + } + + private class FlowState { + private val timeLimit = 10000L + private val _started = MutableStateFlow(false) + val started: Boolean get() = _started.value + fun start() = check(_started.compareAndSet(expect = false, update = true)) + fun stop() = check(_started.compareAndSet(expect = true, update = false)) + suspend fun awaitStart() = withTimeout(timeLimit) { _started.first { it } } + suspend fun awaitStop() = withTimeout(timeLimit) { _started.first { !it } } + } + + private suspend fun FlowState.track(block: suspend () -> Unit) { + start() + try { + block() + } finally { + stop() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt new file mode 100644 index 0000000000..f716389fb7 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt @@ -0,0 +1,331 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.coroutines.* +import kotlin.test.* + +/** + * This test suit for [SharedFlow] has a dense framework that allows to test complex + * suspend/resume scenarios while keeping the code readable. Each test here is for + * one specific [SharedFlow] configuration, testing all the various corner cases in its + * behavior. + */ +class SharedFlowScenarioTest : TestBase() { + @Test + fun testReplay1Extra2() = + testSharedFlow(MutableSharedFlow(1, 2)) { + // total buffer size == 3 + expectReplayOf() + emitRightNow(1); expectReplayOf(1) + emitRightNow(2); expectReplayOf(2) + emitRightNow(3); expectReplayOf(3) + emitRightNow(4); expectReplayOf(4) // no prob - no subscribers + val a = subscribe("a"); collect(a, 4) + emitRightNow(5); expectReplayOf(5) + emitRightNow(6); expectReplayOf(6) + emitRightNow(7); expectReplayOf(7) + // suspend/collect sequentially + val e8 = emitSuspends(8); collect(a, 5); emitResumes(e8); expectReplayOf(8) + val e9 = emitSuspends(9); collect(a, 6); emitResumes(e9); expectReplayOf(9) + // buffer full, but parallel emitters can still suspend (queue up) + val e10 = emitSuspends(10) + val e11 = emitSuspends(11) + val e12 = emitSuspends(12) + collect(a, 7); emitResumes(e10); expectReplayOf(10) // buffer 8, 9 | 10 + collect(a, 8); emitResumes(e11); expectReplayOf(11) // buffer 9, 10 | 11 + sharedFlow.resetReplayCache(); expectReplayOf() // 9, 10 11 | no replay + collect(a, 9); emitResumes(e12); expectReplayOf(12) + collect(a, 10, 11, 12); expectReplayOf(12) // buffer empty | 12 + emitRightNow(13); expectReplayOf(13) + emitRightNow(14); expectReplayOf(14) + emitRightNow(15); expectReplayOf(15) // buffer 13, 14 | 15 + val e16 = emitSuspends(16) + val e17 = emitSuspends(17) + val e18 = emitSuspends(18) + cancel(e17); expectReplayOf(15) // cancel in the middle of three emits; buffer 13, 14 | 15 + collect(a, 13); emitResumes(e16); expectReplayOf(16) // buffer 14, 15, | 16 + collect(a, 14); emitResumes(e18); expectReplayOf(18) // buffer 15, 16 | 18 + val e19 = emitSuspends(19) + val e20 = emitSuspends(20) + val e21 = emitSuspends(21) + cancel(e21); expectReplayOf(18) // cancel last emit; buffer 15, 16, 18 + collect(a, 15); emitResumes(e19); expectReplayOf(19) // buffer 16, 18 | 19 + collect(a, 16); emitResumes(e20); expectReplayOf(20) // buffer 18, 19 | 20 + collect(a, 18, 19, 20); expectReplayOf(20) // buffer empty | 20 + emitRightNow(22); expectReplayOf(22) + emitRightNow(23); expectReplayOf(23) + emitRightNow(24); expectReplayOf(24) // buffer 22, 23 | 24 + val e25 = emitSuspends(25) + val e26 = emitSuspends(26) + val e27 = emitSuspends(27) + cancel(e25); expectReplayOf(24) // cancel first emit, buffer 22, 23 | 24 + sharedFlow.resetReplayCache(); expectReplayOf() // buffer 22, 23, 24 | no replay + val b = subscribe("b") // new subscriber + collect(a, 22); emitResumes(e26); expectReplayOf(26) // buffer 23, 24 | 26 + collect(b, 26) + collect(a, 23); emitResumes(e27); expectReplayOf(27) // buffer 24, 26 | 27 + collect(a, 24, 26, 27) // buffer empty | 27 + emitRightNow(28); expectReplayOf(28) + emitRightNow(29); expectReplayOf(29) // buffer 27, 28 | 29 + collect(a, 28, 29) // but b is slow + val e30 = emitSuspends(30) + val e31 = emitSuspends(31) + val e32 = emitSuspends(32) + val e33 = emitSuspends(33) + val e34 = emitSuspends(34) + val e35 = emitSuspends(35) + val e36 = emitSuspends(36) + val e37 = emitSuspends(37) + val e38 = emitSuspends(38) + val e39 = emitSuspends(39) + cancel(e31) // cancel emitter in queue + cancel(b) // cancel slow subscriber -> 3 emitters resume + emitResumes(e30); emitResumes(e32); emitResumes(e33); expectReplayOf(33) // buffer 30, 32 | 33 + val c = subscribe("c"); collect(c, 33) // replays + cancel(e34) + collect(a, 30); emitResumes(e35); expectReplayOf(35) // buffer 32, 33 | 35 + cancel(e37) + cancel(a); emitResumes(e36); emitResumes(e38); expectReplayOf(38) // buffer 35, 36 | 38 + collect(c, 35); emitResumes(e39); expectReplayOf(39) // buffer 36, 38 | 39 + collect(c, 36, 38, 39); expectReplayOf(39) + cancel(c); expectReplayOf(39) // replay stays + } + + @Test + fun testReplay1() = + testSharedFlow(MutableSharedFlow(1)) { + emitRightNow(0); expectReplayOf(0) + emitRightNow(1); expectReplayOf(1) + emitRightNow(2); expectReplayOf(2) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() + emitRightNow(3); expectReplayOf(3) + emitRightNow(4); expectReplayOf(4) + val a = subscribe("a"); collect(a, 4) + emitRightNow(5); expectReplayOf(5); collect(a, 5) + emitRightNow(6) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() + val e7 = emitSuspends(7) + val e8 = emitSuspends(8) + val e9 = emitSuspends(9) + collect(a, 6); emitResumes(e7); expectReplayOf(7) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() // buffer 7 | -- no replay, but still buffered + val b = subscribe("b") + collect(a, 7); emitResumes(e8); expectReplayOf(8) + collect(b, 8) // buffer | 8 -- a is slow + val e10 = emitSuspends(10) + val e11 = emitSuspends(11) + val e12 = emitSuspends(12) + cancel(e9) + collect(a, 8); emitResumes(e10); expectReplayOf(10) + collect(a, 10) // now b's slow + cancel(e11) + collect(b, 10); emitResumes(e12); expectReplayOf(12) + collect(a, 12) + collect(b, 12) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() // nothing is buffered -- both collectors up to date + emitRightNow(13); expectReplayOf(13) + collect(b, 13) // a is slow + val e14 = emitSuspends(14) + val e15 = emitSuspends(15) + val e16 = emitSuspends(16) + cancel(e14) + cancel(a); emitResumes(e15); expectReplayOf(15) // cancelling slow subscriber + collect(b, 15); emitResumes(e16); expectReplayOf(16) + collect(b, 16) + } + + @Test + fun testReplay2Extra2DropOldest() = + testSharedFlow(MutableSharedFlow(2, 2, BufferOverflow.DROP_OLDEST)) { + emitRightNow(0); expectReplayOf(0) + emitRightNow(1); expectReplayOf(0, 1) + emitRightNow(2); expectReplayOf(1, 2) + emitRightNow(3); expectReplayOf(2, 3) + emitRightNow(4); expectReplayOf(3, 4) + val a = subscribe("a") + collect(a, 3) + emitRightNow(5); expectReplayOf(4, 5) + emitRightNow(6); expectReplayOf(5, 6) + emitRightNow(7); expectReplayOf(6, 7) // buffer 4, 5 | 6, 7 + emitRightNow(8); expectReplayOf(7, 8) // buffer 5, 6 | 7, 8 + emitRightNow(9); expectReplayOf(8, 9) // buffer 6, 7 | 8, 9 + collect(a, 6, 7) + val b = subscribe("b") + collect(b, 8, 9) // buffer | 8, 9 + emitRightNow(10); expectReplayOf(9, 10) // buffer 8 | 9, 10 + collect(a, 8, 9, 10) // buffer | 9, 10, note "b" had not collected 10 yet + emitRightNow(11); expectReplayOf(10, 11) // buffer | 10, 11 + emitRightNow(12); expectReplayOf(11, 12) // buffer 10 | 11, 12 + emitRightNow(13); expectReplayOf(12, 13) // buffer 10, 11 | 12, 13 + emitRightNow(14); expectReplayOf(13, 14) // buffer 11, 12 | 13, 14, "b" missed 10 + collect(b, 11, 12, 13, 14) + sharedFlow.resetReplayCache(); expectReplayOf() // buffer 11, 12, 13, 14 | + sharedFlow.resetReplayCache(); expectReplayOf() + collect(a, 11, 12, 13, 14) + emitRightNow(15); expectReplayOf(15) + collect(a, 15) + collect(b, 15) + } + + private fun testSharedFlow( + sharedFlow: MutableSharedFlow, + scenario: suspend ScenarioDsl.() -> Unit + ) = runTest { + var dsl: ScenarioDsl? = null + try { + coroutineScope { + dsl = ScenarioDsl(sharedFlow, coroutineContext) + dsl!!.scenario() + dsl!!.stop() + } + } catch (e: Throwable) { + dsl?.printLog() + throw e + } + } + + private data class TestJob(val job: Job, val name: String) { + override fun toString(): String = name + } + + private open class Action + private data class EmitResumes(val job: TestJob) : Action() + private data class Collected(val job: TestJob, val value: Any?) : Action() + private data class ResumeCollecting(val job: TestJob) : Action() + private data class Cancelled(val job: TestJob) : Action() + + @OptIn(ExperimentalStdlibApi::class) + private class ScenarioDsl( + val sharedFlow: MutableSharedFlow, + coroutineContext: CoroutineContext + ) { + private val log = ArrayList() + private val timeout = 10000L + private val scope = CoroutineScope(coroutineContext + Job()) + private val actions = HashSet() + private val actionWaiters = ArrayDeque>() + private var expectedReplay = emptyList() + + private fun checkReplay() { + assertEquals(expectedReplay, sharedFlow.replayCache) + } + + private fun wakeupWaiters() { + repeat(actionWaiters.size) { + actionWaiters.removeFirst().resume(Unit) + } + } + + private fun addAction(action: Action) { + actions.add(action) + wakeupWaiters() + } + + private suspend fun awaitAction(action: Action) { + withTimeoutOrNull(timeout) { + while (!actions.remove(action)) { + suspendCancellableCoroutine { actionWaiters.add(it) } + } + } ?: error("Timed out waiting for action: $action") + wakeupWaiters() + } + + private fun launchEmit(a: T): TestJob { + val name = "emit($a)" + val job = scope.launch(start = CoroutineStart.UNDISPATCHED) { + val job = TestJob(coroutineContext[Job]!!, name) + try { + log(name) + sharedFlow.emit(a) + log("$name resumes") + addAction(EmitResumes(job)) + } catch(e: CancellationException) { + log("$name cancelled") + addAction(Cancelled(job)) + } + } + return TestJob(job, name) + } + + fun expectReplayOf(vararg a: T) { + expectedReplay = a.toList() + checkReplay() + } + + fun emitRightNow(a: T) { + val job = launchEmit(a) + assertTrue(actions.remove(EmitResumes(job))) + } + + fun emitSuspends(a: T): TestJob { + val job = launchEmit(a) + assertFalse(EmitResumes(job) in actions) + checkReplay() + return job + } + + suspend fun emitResumes(job: TestJob) { + awaitAction(EmitResumes(job)) + } + + suspend fun cancel(job: TestJob) { + log("cancel(${job.name})") + job.job.cancel() + awaitAction(Cancelled(job)) + } + + fun subscribe(id: String): TestJob { + val name = "collect($id)" + val job = scope.launch(start = CoroutineStart.UNDISPATCHED) { + val job = TestJob(coroutineContext[Job]!!, name) + try { + awaitAction(ResumeCollecting(job)) + log("$name start") + sharedFlow.collect { value -> + log("$name -> $value") + addAction(Collected(job, value)) + awaitAction(ResumeCollecting(job)) + log("$name -> $value resumes") + } + error("$name completed") + } catch(e: CancellationException) { + log("$name cancelled") + addAction(Cancelled(job)) + } + } + return TestJob(job, name) + } + + suspend fun collect(job: TestJob, vararg a: T) { + for (value in a) { + checkReplay() // should not have changed + addAction(ResumeCollecting(job)) + awaitAction(Collected(job, value)) + } + } + + fun stop() { + log("--- stop") + scope.cancel() + } + + private fun log(text: String) { + log.add(text) + } + + fun printLog() { + println("--- The most recent log entries ---") + log.takeLast(30).forEach(::println) + println("--- That's it ---") + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt new file mode 100644 index 0000000000..32d88f3c99 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt @@ -0,0 +1,798 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.random.* +import kotlin.test.* + +/** + * This test suite contains some basic tests for [SharedFlow]. There are some scenarios here written + * using [expect] and they are not very readable. See [SharedFlowScenarioTest] for a better + * behavioral test-suit. + */ +class SharedFlowTest : TestBase() { + @Test + fun testRendezvousSharedFlowBasic() = runTest { + expect(1) + val sh = MutableSharedFlow() + assertTrue(sh.replayCache.isEmpty()) + assertEquals(0, sh.subscriptionCount.value) + sh.emit(1) // no suspend + assertTrue(sh.replayCache.isEmpty()) + assertEquals(0, sh.subscriptionCount.value) + expect(2) + // one collector + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(3) + sh.collect { + when(it) { + 4 -> expect(5) + 6 -> expect(7) + 10 -> expect(11) + 13 -> expect(14) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(4) + assertEquals(1, sh.subscriptionCount.value) + sh.emit(4) + assertTrue(sh.replayCache.isEmpty()) + expect(6) + sh.emit(6) + expect(8) + // one more collector + val job2 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(9) + sh.collect { + when(it) { + 10 -> expect(12) + 13 -> expect(15) + 17 -> expect(18) + null -> expect(20) + 21 -> expect(22) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(10) + assertEquals(2, sh.subscriptionCount.value) + sh.emit(10) // to both collectors now! + assertTrue(sh.replayCache.isEmpty()) + expect(13) + sh.emit(13) + expect(16) + job1.cancel() // cancel the first collector + yield() + assertEquals(1, sh.subscriptionCount.value) + expect(17) + sh.emit(17) // only to second collector + expect(19) + sh.emit(null) // emit null to the second collector + expect(21) + sh.emit(21) // non-null again + expect(23) + job2.cancel() // cancel the second collector + yield() + assertEquals(0, sh.subscriptionCount.value) + expect(24) + sh.emit(24) // does not go anywhere + assertEquals(0, sh.subscriptionCount.value) + assertTrue(sh.replayCache.isEmpty()) + finish(25) + } + + @Test + fun testRendezvousSharedFlowReset() = runTest { + expect(1) + val sh = MutableSharedFlow() + val barrier = Channel(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.collect { + when (it) { + 3 -> { + expect(4) + barrier.receive() // hold on before collecting next one + } + 6 -> expect(10) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(3) + sh.emit(3) // rendezvous + expect(5) + assertFalse(sh.tryEmit(5)) // collector is not ready now + launch(start = CoroutineStart.UNDISPATCHED) { + expect(6) + sh.emit(6) // suspends + expect(12) + } + expect(7) + yield() // no wakeup -> all suspended + expect(8) + // now reset cache -> nothing happens, there is no cache + sh.resetReplayCache() + yield() + expect(9) + // now resume collector + barrier.send(Unit) + yield() // to collector + expect(11) + yield() // to emitter + expect(13) + assertFalse(sh.tryEmit(13)) // rendezvous does not work this way + job.cancel() + finish(14) + } + + @Test + fun testReplay1SharedFlowBasic() = runTest { + expect(1) + val sh = MutableSharedFlow(1) + assertTrue(sh.replayCache.isEmpty()) + assertEquals(0, sh.subscriptionCount.value) + sh.emit(1) // no suspend + assertEquals(listOf(1), sh.replayCache) + assertEquals(0, sh.subscriptionCount.value) + expect(2) + sh.emit(2) // no suspend + assertEquals(listOf(2), sh.replayCache) + expect(3) + // one collector + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(4) + sh.collect { + when(it) { + 2 -> expect(5) // got it immediately from replay cache + 6 -> expect(8) + null -> expect(14) + 17 -> expect(18) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(6) + assertEquals(1, sh.subscriptionCount.value) + sh.emit(6) // does not suspend, but buffers + assertEquals(listOf(6), sh.replayCache) + expect(7) + yield() + expect(9) + // one more collector + val job2 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(10) + sh.collect { + when(it) { + 6 -> expect(11) // from replay cache + null -> expect(15) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(12) + assertEquals(2, sh.subscriptionCount.value) + sh.emit(null) + expect(13) + assertEquals(listOf(null), sh.replayCache) + yield() + assertEquals(listOf(null), sh.replayCache) + expect(16) + job2.cancel() + yield() + assertEquals(1, sh.subscriptionCount.value) + expect(17) + sh.emit(17) + assertEquals(listOf(17), sh.replayCache) + yield() + expect(19) + job1.cancel() + yield() + assertEquals(0, sh.subscriptionCount.value) + assertEquals(listOf(17), sh.replayCache) + finish(20) + } + + @Test + fun testReplay1() = runTest { + expect(1) + val sh = MutableSharedFlow(1) + assertEquals(listOf(), sh.replayCache) + val barrier = Channel(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.collect { + when (it) { + 3 -> { + expect(4) + barrier.receive() // collector waits + } + 5 -> expect(10) + 6 -> expect(11) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(3) + assertTrue(sh.tryEmit(3)) // buffered + assertEquals(listOf(3), sh.replayCache) + yield() // to collector + expect(5) + assertTrue(sh.tryEmit(5)) // buffered + assertEquals(listOf(5), sh.replayCache) + launch(start = CoroutineStart.UNDISPATCHED) { + expect(6) + sh.emit(6) // buffer full, suspended + expect(13) + } + expect(7) + assertEquals(listOf(5), sh.replayCache) + sh.resetReplayCache() // clear cache + assertEquals(listOf(), sh.replayCache) + expect(8) + yield() // emitter still suspended + expect(9) + assertEquals(listOf(), sh.replayCache) + assertFalse(sh.tryEmit(10)) // still no buffer space + assertEquals(listOf(), sh.replayCache) + barrier.send(Unit) // resume collector + yield() // to collector + expect(12) + yield() // to emitter, that should have resumed + expect(14) + job.cancel() + assertEquals(listOf(6), sh.replayCache) + finish(15) + } + + @Test + fun testReplay2Extra1() = runTest { + expect(1) + val sh = MutableSharedFlow( + replay = 2, + extraBufferCapacity = 1 + ) + assertEquals(listOf(), sh.replayCache) + assertTrue(sh.tryEmit(0)) + assertEquals(listOf(0), sh.replayCache) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + var cnt = 0 + sh.collect { + when (it) { + 0 -> when (cnt++) { + 0 -> expect(3) + 1 -> expect(14) + else -> expectUnreached() + } + 1 -> expect(6) + 2 -> expect(7) + 3 -> expect(8) + 4 -> expect(12) + 5 -> expect(13) + 16 -> expect(17) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(4) + assertTrue(sh.tryEmit(1)) // buffered + assertEquals(listOf(0, 1), sh.replayCache) + assertTrue(sh.tryEmit(2)) // buffered + assertEquals(listOf(1, 2), sh.replayCache) + assertTrue(sh.tryEmit(3)) // buffered (buffer size is 3) + assertEquals(listOf(2, 3), sh.replayCache) + expect(5) + yield() // to collector + expect(9) + assertEquals(listOf(2, 3), sh.replayCache) + assertTrue(sh.tryEmit(4)) // can buffer now + assertEquals(listOf(3, 4), sh.replayCache) + assertTrue(sh.tryEmit(5)) // can buffer now + assertEquals(listOf(4, 5), sh.replayCache) + assertTrue(sh.tryEmit(0)) // can buffer one more, let it be zero again + assertEquals(listOf(5, 0), sh.replayCache) + expect(10) + assertFalse(sh.tryEmit(10)) // cannot buffer anymore! + sh.resetReplayCache() // replay cache + assertEquals(listOf(), sh.replayCache) // empty + assertFalse(sh.tryEmit(0)) // still cannot buffer anymore (reset does not help) + assertEquals(listOf(), sh.replayCache) // empty + expect(11) + yield() // resume collector, will get next values + expect(15) + sh.resetReplayCache() // reset again, nothing happens + assertEquals(listOf(), sh.replayCache) // empty + yield() // collector gets nothing -- no change + expect(16) + assertTrue(sh.tryEmit(16)) + assertEquals(listOf(16), sh.replayCache) + yield() // gets it + expect(18) + job.cancel() + finish(19) + } + + @Test + fun testBufferNoReplayCancelWhileBuffering() = runTest { + val n = 123 + val sh = MutableSharedFlow(replay = 0, extraBufferCapacity = n) + repeat(3) { + val m = n / 2 // collect half, then suspend + val barrier = Channel(1) + val collectorJob = sh + .onSubscription { + barrier.send(1) + } + .onEach { value -> + if (value == m) { + barrier.send(2) + delay(Long.MAX_VALUE) + } + } + .launchIn(this) + assertEquals(1, barrier.receive()) // make sure it subscribes + launch(start = CoroutineStart.UNDISPATCHED) { + for (i in 0 until n + m) sh.emit(i) // these emits should go Ok + barrier.send(3) + sh.emit(n + 4) // this emit will suspend on buffer overflow + barrier.send(4) + } + assertEquals(2, barrier.receive()) // wait until m collected + assertEquals(3, barrier.receive()) // wait until all are emitted + collectorJob.cancel() // cancelling collector job must clear buffer and resume emitter + assertEquals(4, barrier.receive()) // verify that emitter resumes + } + } + + @Test + fun testRepeatedResetWithReplay() = runTest { + val n = 10 + val sh = MutableSharedFlow(n) + var i = 0 + repeat(3) { + // collector is slow + val collector = sh.onEach { delay(Long.MAX_VALUE) }.launchIn(this) + val emitter = launch { + repeat(3 * n) { sh.emit(i); i++ } + } + repeat(3) { yield() } // enough to run it to suspension + assertEquals((i - n until i).toList(), sh.replayCache) + sh.resetReplayCache() + assertEquals(emptyList(), sh.replayCache) + repeat(3) { yield() } // enough to run it to suspension + assertEquals(emptyList(), sh.replayCache) // still blocked + collector.cancel() + emitter.cancel() + repeat(3) { yield() } // enough to run it to suspension + } + } + + @Test + fun testSynchronousSharedFlowEmitterCancel() = runTest { + expect(1) + val sh = MutableSharedFlow() + val barrier1 = Job() + val barrier2 = Job() + val barrier3 = Job() + val collector1 = sh.onEach { + when (it) { + 1 -> expect(3) + 2 -> { + expect(6) + barrier2.complete() + } + 3 -> { + expect(9) + barrier3.complete() + } + else -> expectUnreached() + } + }.launchIn(this) + val collector2 = sh.onEach { + when (it) { + 1 -> { + expect(4) + barrier1.complete() + delay(Long.MAX_VALUE) + } + else -> expectUnreached() + } + }.launchIn(this) + repeat(2) { yield() } // launch both subscribers + val emitter = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.emit(1) + barrier1.join() + expect(5) + sh.emit(2) // suspends because of slow collector2 + expectUnreached() // will be cancelled + } + barrier2.join() // wait + expect(7) + // Now cancel the emitter! + emitter.cancel() + yield() + // Cancel slow collector + collector2.cancel() + yield() + // emit to fast collector1 + expect(8) + sh.emit(3) + barrier3.join() + expect(10) + // cancel it, too + collector1.cancel() + finish(11) + } + + @Test + fun testDifferentBufferedFlowCapacities() { + for (replay in 0..10) { + for (extraBufferCapacity in 0..5) { + if (replay == 0 && extraBufferCapacity == 0) continue // test only buffered shared flows + try { + val sh = MutableSharedFlow(replay, extraBufferCapacity) + // repeat the whole test a few times to make sure it works correctly when slots are reused + repeat(3) { + testBufferedFlow(sh, replay) + } + } catch (e: Throwable) { + error("Failed for replay=$replay, extraBufferCapacity=$extraBufferCapacity", e) + } + } + } + } + + private fun testBufferedFlow(sh: MutableSharedFlow, replay: Int) = runTest { + reset() + expect(1) + val n = 100 // initially emitted to fill buffer + for (i in 1..n) assertTrue(sh.tryEmit(i)) + // initial expected replayCache + val rcStart = n - replay + 1 + val rcRange = rcStart..n + val rcSize = n - rcStart + 1 + assertEquals(rcRange.toList(), sh.replayCache) + // create collectors + val m = 10 // collectors created + var ofs = 0 + val k = 42 // emissions to collectors + val ecRange = n + 1..n + k + val jobs = List(m) { jobIndex -> + launch(start = CoroutineStart.UNDISPATCHED) { + sh.collect { i -> + when (i) { + in rcRange -> expect(2 + i - rcStart + jobIndex * rcSize) + in ecRange -> expect(2 + ofs + jobIndex) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + } + ofs = rcSize * m + 2 + expect(ofs) + // emit to all k times + for (p in ecRange) { + sh.emit(p) + expect(1 + ofs) // buffered, no suspend + yield() + ofs += 2 + m + expect(ofs) + } + assertEquals(ecRange.toList().takeLast(replay), sh.replayCache) + // cancel all collectors + jobs.forEach { it.cancel() } + yield() + // replay cache is still there + assertEquals(ecRange.toList().takeLast(replay), sh.replayCache) + finish(1 + ofs) + } + + @Test + fun testDropLatest() = testDropLatestOrOldest(BufferOverflow.DROP_LATEST) + + @Test + fun testDropOldest() = testDropLatestOrOldest(BufferOverflow.DROP_OLDEST) + + private fun testDropLatestOrOldest(bufferOverflow: BufferOverflow) = runTest { + reset() + expect(1) + val sh = MutableSharedFlow(1, onBufferOverflow = bufferOverflow) + sh.emit(1) + sh.emit(2) + // always keeps last w/o collectors + assertEquals(listOf(2), sh.replayCache) + assertEquals(0, sh.subscriptionCount.value) + // one collector + val valueAfterOverflow = when (bufferOverflow) { + BufferOverflow.DROP_OLDEST -> 5 + BufferOverflow.DROP_LATEST -> 4 + else -> error("not supported in this test: $bufferOverflow") + } + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.collect { + when(it) { + 2 -> { // replayed + expect(3) + yield() // and suspends, busy waiting + } + valueAfterOverflow -> expect(7) + 8 -> expect(9) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(4) + assertEquals(1, sh.subscriptionCount.value) + assertEquals(listOf(2), sh.replayCache) + sh.emit(4) // buffering, collector is busy + assertEquals(listOf(4), sh.replayCache) + expect(5) + sh.emit(5) // Buffer overflow here, will not suspend + assertEquals(listOf(valueAfterOverflow), sh.replayCache) + expect(6) + yield() // to the job + expect(8) + sh.emit(8) // not busy now + assertEquals(listOf(8), sh.replayCache) // buffered + yield() // to process + expect(10) + job.cancel() // cancel the job + yield() + assertEquals(0, sh.subscriptionCount.value) + finish(11) + } + + @Test + public fun testOnSubscription() = runTest { + expect(1) + val sh = MutableSharedFlow() + fun share(s: String) { launch(start = CoroutineStart.UNDISPATCHED) { sh.emit(s) } } + sh + .onSubscription { + emit("collector->A") + share("share->A") + } + .onSubscription { + emit("collector->B") + share("share->B") + } + .onStart { + emit("collector->C") + share("share->C") // get's lost, no subscribers yet + } + .onStart { + emit("collector->D") + share("share->D") // get's lost, no subscribers yet + } + .onEach { + when (it) { + "collector->D" -> expect(2) + "collector->C" -> expect(3) + "collector->A" -> expect(4) + "collector->B" -> expect(5) + "share->A" -> expect(6) + "share->B" -> { + expect(7) + currentCoroutineContext().cancel() + } + else -> expectUnreached() + } + } + .launchIn(this) + .join() + finish(8) + } + + @Test + fun onSubscriptionThrows() = runTest { + expect(1) + val sh = MutableSharedFlow(1) + sh.tryEmit("OK") // buffer a string + assertEquals(listOf("OK"), sh.replayCache) + sh + .onSubscription { + expect(2) + throw TestException() + } + .catch { e -> + assertTrue(e is TestException) + expect(3) + } + .collect { + // onSubscription throw before replay is emitted, so no value is collected if it throws + expectUnreached() + } + assertEquals(0, sh.subscriptionCount.value) + finish(4) + } + + @Test + fun testBigReplayManySubscribers() = testManySubscribers(true) + + @Test + fun testBigBufferManySubscribers() = testManySubscribers(false) + + private fun testManySubscribers(replay: Boolean) = runTest { + val n = 100 + val rnd = Random(replay.hashCode()) + val sh = MutableSharedFlow( + replay = if (replay) n else 0, + extraBufferCapacity = if (replay) 0 else n + ) + val subs = ArrayList() + for (i in 1..n) { + sh.emit(i) + val subBarrier = Channel() + val subJob = SubJob() + subs += subJob + // will receive all starting from replay or from new emissions only + subJob.lastReceived = if (replay) 0 else i + subJob.job = sh + .onSubscription { + subBarrier.send(Unit) // signal subscribed + } + .onEach { value -> + assertEquals(subJob.lastReceived + 1, value) + subJob.lastReceived = value + } + .launchIn(this) + subBarrier.receive() // wait until subscribed + // must have also receive all from the replay buffer directly after being subscribed + assertEquals(subJob.lastReceived, i) + // 50% of time cancel one subscriber + if (i % 2 == 0) { + val victim = subs.removeAt(rnd.nextInt(subs.size)) + yield() // make sure victim processed all emissions + assertEquals(victim.lastReceived, i) + victim.job.cancel() + } + } + yield() // make sure the last emission is processed + for (subJob in subs) { + assertEquals(subJob.lastReceived, n) + subJob.job.cancel() + } + } + + private class SubJob { + lateinit var job: Job + var lastReceived = 0 + } + + @Test + fun testStateFlowModel() = runTest { + val stateFlow = MutableStateFlow(null) + val expect = modelLog(stateFlow) + val sharedFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + sharedFlow.tryEmit(null) // initial value + val actual = modelLog(sharedFlow) { distinctUntilChanged() } + for (i in 0 until minOf(expect.size, actual.size)) { + if (actual[i] != expect[i]) { + for (j in maxOf(0, i - 10)..i) println("Actual log item #$j: ${actual[j]}") + assertEquals(expect[i], actual[i], "Log item #$i") + } + } + assertEquals(expect.size, actual.size) + } + + private suspend fun modelLog( + sh: MutableSharedFlow, + op: Flow.() -> Flow = { this } + ): List = coroutineScope { + val rnd = Random(1) + val result = ArrayList() + val job = launch { + sh.op().collect { value -> + result.add("Collect: $value") + repeat(rnd.nextInt(0..2)) { + result.add("Collect: yield") + yield() + } + } + } + repeat(1000) { + val value = if (rnd.nextBoolean()) null else rnd.nextData() + if (rnd.nextInt(20) == 0) { + result.add("resetReplayCache & emit: $value") + if (sh !is StateFlow<*>) sh.resetReplayCache() + assertTrue(sh.tryEmit(value)) + } else { + result.add("Emit: $value") + sh.emit(value) + } + repeat(rnd.nextInt(0..2)) { + result.add("Emit: yield") + yield() + } + } + result.add("main: cancel") + job.cancel() + result.add("main: yield") + yield() + result.add("main: join") + job.join() + result + } + + data class Data(val x: Int) + private val dataCache = (1..5).associateWith { Data(it) } + + // Note that we test proper null support here, too + private fun Random.nextData(): Data? { + val x = nextInt(0..5) + if (x == 0) return null + // randomly reuse ref or create a new instance + return if(nextBoolean()) dataCache[x] else Data(x) + } + + @Test + fun testOperatorFusion() { + val sh = MutableSharedFlow() + assertSame(sh, (sh as Flow<*>).cancellable()) + assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) + } + + @Test + fun testIllegalArgumentException() { + assertFailsWith { MutableSharedFlow(-1) } + assertFailsWith { MutableSharedFlow(0, extraBufferCapacity = -1) } + assertFailsWith { MutableSharedFlow(0, onBufferOverflow = BufferOverflow.DROP_LATEST) } + assertFailsWith { MutableSharedFlow(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) } + } + + @Test + public fun testReplayCancellability() = testCancellability(fromReplay = true) + + @Test + public fun testEmitCancellability() = testCancellability(fromReplay = false) + + private fun testCancellability(fromReplay: Boolean) = runTest { + expect(1) + val sh = MutableSharedFlow(5) + fun emitTestData() { + for (i in 1..5) assertTrue(sh.tryEmit(i)) + } + if (fromReplay) emitTestData() // fill in replay first + var subscribed = true + val job = sh + .onSubscription { subscribed = true } + .onEach { i -> + when (i) { + 1 -> expect(2) + 2 -> expect(3) + 3 -> { + expect(4) + currentCoroutineContext().cancel() + } + else -> expectUnreached() // shall check for cancellation + } + } + .launchIn(this) + yield() + assertTrue(subscribed) // yielding in enough + if (!fromReplay) emitTestData() // emit after subscription + job.join() + finish(5) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt new file mode 100644 index 0000000000..496fb7f8ff --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.test.* + +/** + * Functional tests for [SharingStarted] using [withVirtualTime] and a DSL to describe + * testing scenarios and expected behavior for different implementations. + */ +class SharingStartedTest : TestBase() { + @Test + fun testEagerly() = + testSharingStarted(SharingStarted.Eagerly, SharingCommand.START) { + subscriptions(1) + rampUpAndDown() + subscriptions(0) + delay(100) + } + + @Test + fun testLazily() = + testSharingStarted(SharingStarted.Lazily) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + } + + @Test + fun testWhileSubscribed() = + testSharingStarted(SharingStarted.WhileSubscribed()) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP) + delay(100) + } + + @Test + fun testWhileSubscribedExpireImmediately() = + testSharingStarted(SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + delay(100) + } + + @Test + fun testWhileSubscribedWithTimeout() = + testSharingStarted(SharingStarted.WhileSubscribed(stopTimeoutMillis = 100)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + delay(50) // don't give it time to stop + subscriptions(1) // resubscribe again + rampUpAndDown() + subscriptions(0) + afterTime(100, SharingCommand.STOP) + delay(100) + } + + @Test + fun testWhileSubscribedExpiration() = + testSharingStarted(SharingStarted.WhileSubscribed(replayExpirationMillis = 200)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP) + delay(150) // don't give it time to reset cache + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP) + afterTime(200, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + } + + @Test + fun testWhileSubscribedStopAndExpiration() = + testSharingStarted(SharingStarted.WhileSubscribed(stopTimeoutMillis = 400, replayExpirationMillis = 300)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + delay(350) // don't give it time to stop + subscriptions(1) + rampUpAndDown() + subscriptions(0) + afterTime(400, SharingCommand.STOP) + delay(250) // don't give it time to reset cache + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + afterTime(400, SharingCommand.STOP) + afterTime(300, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + delay(100) + } + + private suspend fun SharingStartedDsl.rampUpAndDown() { + for (i in 2..10) { + delay(100) + subscriptions(i) + } + delay(1000) + for (i in 9 downTo 1) { + subscriptions(i) + delay(100) + } + } + + private fun testSharingStarted( + started: SharingStarted, + initialCommand: SharingCommand? = null, + scenario: suspend SharingStartedDsl.() -> Unit + ) = withVirtualTime { + expect(1) + val dsl = SharingStartedDsl(started, initialCommand, coroutineContext) + dsl.launch() + // repeat every scenario 3 times + repeat(3) { + dsl.scenario() + delay(1000) + } + dsl.stop() + finish(2) + } + + private class SharingStartedDsl( + val started: SharingStarted, + initialCommand: SharingCommand?, + coroutineContext: CoroutineContext + ) { + val subscriptionCount = MutableStateFlow(0) + var previousCommand: SharingCommand? = null + var expectedCommand: SharingCommand? = initialCommand + var expectedTime = 0L + + val dispatcher = coroutineContext[ContinuationInterceptor] as VirtualTimeDispatcher + val scope = CoroutineScope(coroutineContext + Job()) + + suspend fun launch() { + started + .command(subscriptionCount.asStateFlow()) + .onEach { checkCommand(it) } + .launchIn(scope) + letItRun() + } + + fun checkCommand(command: SharingCommand) { + assertTrue(command != previousCommand) + previousCommand = command + assertEquals(expectedCommand, command) + assertEquals(expectedTime, dispatcher.currentTime) + } + + suspend fun subscriptions(count: Int, command: SharingCommand? = null) { + expectedTime = dispatcher.currentTime + subscriptionCount.value = count + if (command != null) { + afterTime(0, command) + } else { + letItRun() + } + } + + suspend fun afterTime(time: Long = 0, command: SharingCommand) { + expectedCommand = command + val remaining = (time - 1).coerceAtLeast(0) // previous letItRun delayed 1ms + expectedTime += remaining + delay(remaining) + letItRun() + } + + private suspend fun letItRun() { + delay(1) + assertEquals(expectedCommand, previousCommand) // make sure expected command was emitted + expectedTime++ // make one more time tick we've delayed + } + + fun stop() { + scope.cancel() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt new file mode 100644 index 0000000000..bcf626e3e3 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.test.* +import kotlin.time.* + +class SharingStartedWhileSubscribedTest : TestBase() { + @Test // make sure equals works properly, or otherwise other tests don't make sense + fun testEqualsAndHashcode() { + val params = listOf(0L, 1L, 10L, 100L, 213L, Long.MAX_VALUE) + // HashMap will simultaneously test equals, hashcode and their consistency + val map = HashMap>() + for (i in params) { + for (j in params) { + map[SharingStarted.WhileSubscribed(i, j)] = i to j + } + } + for (i in params) { + for (j in params) { + assertEquals(i to j, map[SharingStarted.WhileSubscribed(i, j)]) + } + } + } + + @OptIn(ExperimentalTime::class) + @Test + fun testDurationParams() { + assertEquals(SharingStarted.WhileSubscribed(0), SharingStarted.WhileSubscribed(Duration.ZERO)) + assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(10.milliseconds)) + assertEquals(SharingStarted.WhileSubscribed(1000), SharingStarted.WhileSubscribed(1.seconds)) + assertEquals(SharingStarted.WhileSubscribed(Long.MAX_VALUE), SharingStarted.WhileSubscribed(Duration.INFINITE)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 0), SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed(replayExpiration = 3.milliseconds)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), SharingStarted.WhileSubscribed(replayExpiration = 7.seconds)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = Long.MAX_VALUE), SharingStarted.WhileSubscribed(replayExpiration = Duration.INFINITE)) + } +} + diff --git a/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt similarity index 52% rename from kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt rename to kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt index a6be97eb97..0a2c0458c4 100644 --- a/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* import kotlin.test.* class StateFlowTest : TestBase() { @@ -56,7 +57,7 @@ class StateFlowTest : TestBase() { expect(2) assertFailsWith { state.collect { value -> - when(value.i) { + when (value.i) { 0 -> expect(3) // initial value 2 -> expect(5) 4 -> expect(7) @@ -103,6 +104,7 @@ class StateFlowTest : TestBase() { class CounterModel { // private data flow private val _counter = MutableStateFlow(0) + // publicly exposed as a flow val counter: StateFlow get() = _counter @@ -110,4 +112,85 @@ class StateFlowTest : TestBase() { _counter.value++ } } + + @Test + public fun testOnSubscriptionWithException() = runTest { + expect(1) + val state = MutableStateFlow("A") + state + .onSubscription { + emit("collector->A") + state.value = "A" + } + .onSubscription { + emit("collector->B") + state.value = "B" + throw TestException() + } + .onStart { + emit("collector->C") + state.value = "C" + } + .onStart { + emit("collector->D") + state.value = "D" + } + .onEach { + when (it) { + "collector->D" -> expect(2) + "collector->C" -> expect(3) + "collector->A" -> expect(4) + "collector->B" -> expect(5) + else -> expectUnreached() + } + } + .catch { e -> + assertTrue(e is TestException) + expect(6) + } + .launchIn(this) + .join() + assertEquals(0, state.subscriptionCount.value) + finish(7) + } + + @Test + fun testOperatorFusion() { + val state = MutableStateFlow(String) + assertSame(state, (state as Flow<*>).cancellable()) + assertSame(state, (state as Flow<*>).distinctUntilChanged()) + assertSame(state, (state as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(state, (state as Flow<*>).conflate()) + assertSame(state, state.buffer(Channel.CONFLATED)) + assertSame(state, state.buffer(Channel.RENDEZVOUS)) + } + + @Test + fun testResetUnsupported() { + val state = MutableStateFlow(42) + assertFailsWith { state.resetReplayCache() } + assertEquals(42, state.value) + assertEquals(listOf(42), state.replayCache) + } + + @Test + fun testReferenceUpdatesAndCAS() { + val d0 = Data(0) + val d0_1 = Data(0) + val d1 = Data(1) + val d1_1 = Data(1) + val d1_2 = Data(1) + val state = MutableStateFlow(d0) + assertSame(d0, state.value) + state.value = d0_1 // equal, nothing changes + assertSame(d0, state.value) + state.value = d1 // updates + assertSame(d1, state.value) + assertFalse(state.compareAndSet(d0, d0)) // wrong value + assertSame(d1, state.value) + assertTrue(state.compareAndSet(d1_1, d1_2)) // "updates", but ref stays + assertSame(d1, state.value) + assertTrue(state.compareAndSet(d1_1, d0)) // updates, reference changes + assertSame(d0, state.value) + } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt new file mode 100644 index 0000000000..2a613afaf7 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +/** + * It is mostly covered by [ShareInTest], this just add state-specific checks. + */ +class StateInTest : TestBase() { + @Test + fun testOperatorFusion() = runTest { + val state = flowOf("OK").stateIn(this) + assertTrue(state !is MutableStateFlow<*>) // cannot be cast to mutable state flow + assertSame(state, (state as Flow<*>).cancellable()) + assertSame(state, (state as Flow<*>).distinctUntilChanged()) + assertSame(state, (state as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(state, (state as Flow<*>).conflate()) + assertSame(state, state.buffer(Channel.CONFLATED)) + assertSame(state, state.buffer(Channel.RENDEZVOUS)) + assertSame(state, state.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)) + assertSame(state, state.buffer(0, onBufferOverflow = BufferOverflow.DROP_OLDEST)) + assertSame(state, state.buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST)) + coroutineContext.cancelChildren() + } + + @Test + fun testUpstreamCompletedNoInitialValue() = + testUpstreamCompletedOrFailedReset(failed = false, iv = false) + + @Test + fun testUpstreamFailedNoInitialValue() = + testUpstreamCompletedOrFailedReset(failed = true, iv = false) + + @Test + fun testUpstreamCompletedWithInitialValue() = + testUpstreamCompletedOrFailedReset(failed = false, iv = true) + + @Test + fun testUpstreamFailedWithInitialValue() = + testUpstreamCompletedOrFailedReset(failed = true, iv = true) + + private fun testUpstreamCompletedOrFailedReset(failed: Boolean, iv: Boolean) = runTest { + val emitted = Job() + val terminate = Job() + val sharingJob = CompletableDeferred() + val upstream = flow { + emit("OK") + emitted.complete() + terminate.join() + if (failed) throw TestException() + } + val scope = this + sharingJob + val shared: StateFlow + if (iv) { + shared = upstream.stateIn(scope, SharingStarted.Eagerly, null) + assertEquals(null, shared.value) + } else { + shared = upstream.stateIn(scope) + assertEquals("OK", shared.value) // waited until upstream emitted + } + emitted.join() // should start sharing, emit & cache + assertEquals("OK", shared.value) + terminate.complete() + sharingJob.complete(Unit) + sharingJob.join() // should complete sharing + assertEquals("OK", shared.value) // value is still there + if (failed) { + assertTrue(sharingJob.getCompletionExceptionOrNull() is TestException) + } else { + assertNull(sharingJob.getCompletionExceptionOrNull()) + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt index edb9f00fa6..fa7fc9cb6c 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt @@ -128,6 +128,12 @@ class FirstTest : TestBase() { assertNull(emptyFlow().firstOrNull { true }) } + @Test + fun testFirstOrNullWithNullElement() = runTest { + assertNull(flowOf(null).firstOrNull()) + assertNull(flowOf(null).firstOrNull { true }) + } + @Test fun testFirstOrNullWhenErrorCancelsUpstream() = runTest { val latch = Channel() diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt index 4e89b93bd7..2c1277b1e1 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt @@ -7,7 +7,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.test.* -class SingleTest : TestBase() { +class SingleTest : TestBase() { @Test fun testSingle() = runTest { @@ -25,8 +25,8 @@ class SingleTest : TestBase() { emit(239L) emit(240L) } - assertFailsWith { flow.single() } - assertFailsWith { flow.singleOrNull() } + assertFailsWith { flow.single() } + assertNull(flow.singleOrNull()) } @Test @@ -61,6 +61,10 @@ class SingleTest : TestBase() { assertEquals(1, flowOf(1).single()) assertNull(flowOf(null).single()) assertFailsWith { flowOf().single() } + + assertEquals(1, flowOf(1).singleOrNull()) + assertNull(flowOf(null).singleOrNull()) + assertNull(flowOf().singleOrNull()) } @Test @@ -69,5 +73,22 @@ class SingleTest : TestBase() { val flow = flowOf(instance) assertSame(instance, flow.single()) assertSame(instance, flow.singleOrNull()) + + val flow2 = flow { + emit(BadClass()) + emit(BadClass()) + } + assertFailsWith { flow2.single() } + } + + @Test + fun testSingleNoWait() = runTest { + val flow = flow { + emit(1) + emit(2) + awaitCancellation() + } + + assertNull(flow.singleOrNull()) } } diff --git a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt index 5af68f6be5..e31ccfc16d 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt @@ -24,19 +24,20 @@ class SelectLoopTest : TestBase() { expect(3) throw TestException() } - var isDone = false - while (!isDone) { - select { - channel.onReceiveOrNull { - expect(4) - assertEquals(Unit, it) - } - job.onJoin { - expect(5) - isDone = true + try { + while (true) { + select { + channel.onReceiveOrNull { + expectUnreached() + } + job.onJoin { + expectUnreached() + } } } + } catch (e: CancellationException) { + // select will get cancelled because of the failure of job + finish(4) } - finish(6) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 033b39c7e0..06b938d41a 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -7,24 +7,19 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - - public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default) - + public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default, false) public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined } -private class JsMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { - - override val immediate: MainCoroutineDispatcher - get() = throw UnsupportedOperationException("Immediate dispatching is not supported on JS") - +private class JsMainDispatcher( + val delegate: CoroutineDispatcher, + private val invokeImmediately: Boolean +) : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher = + if (invokeImmediately) this else JsMainDispatcher(delegate, true) + override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index a0dfcba2b7..e1b3dcd7a9 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -35,7 +35,7 @@ internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { messageQueue.enqueue(block) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) return ClearTimeout(handle) } @@ -81,7 +81,7 @@ internal class WindowDispatcher(private val window: Window) : CoroutineDispatche window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis)) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-core/js/src/Promise.kt b/kotlinx-coroutines-core/js/src/Promise.kt index 6c3de76426..ab2003236a 100644 --- a/kotlinx-coroutines-core/js/src/Promise.kt +++ b/kotlinx-coroutines-core/js/src/Promise.kt @@ -62,6 +62,8 @@ public fun Promise.asDeferred(): Deferred { * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * stops waiting for the promise and immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> this@await.then( diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index 342b11c69a..b69850576e 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -124,6 +124,8 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { return null } + actual open fun onRemoved(affected: Node) {} + actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode val failure = failure(affected) diff --git a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt b/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt new file mode 100644 index 0000000000..7ca6a242b2 --- /dev/null +++ b/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class ImmediateDispatcherTest : TestBase() { + + @Test + fun testImmediate() = runTest { + expect(1) + val job = launch { expect(3) } + withContext(Dispatchers.Main.immediate) { + expect(2) + } + job.join() + finish(4) + } + + @Test + fun testMain() = runTest { + expect(1) + val job = launch { expect(2) } + withContext(Dispatchers.Main) { + expect(3) + } + job.join() + finish(4) + } +} diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin index 8a7160fbad39f25ba582998c532c6ff65c788d5f..76ee41159dbc84c6de5231dbec11bebf0d37b008 100644 GIT binary patch delta 72 zcmX@adw_RCA)}aRaz<)$wqJgUUujNGKw?p1ZfZ$t(dJG@b0%&UW(FW&WME1v+kBdN Z2NS=RCxbSFPBa6vCxa%CIe8MB0RS(N6Kenf delta 74 zcmX@Wdx&>KA)|zNaz<)$c0giLVs2_lYLRELUw(;SX->}Oc1Ck1ZUIIHrlc}v1|Zmc bl6eOczosXH7K3&)1G6Uskf}3y0-FH; } +# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when +# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used. +-dontwarn java.lang.instrument.ClassFileTransformer +-dontwarn sun.misc.SignalHandler +-dontwarn java.lang.instrument.Instrumentation +-dontwarn sun.misc.Signal diff --git a/kotlinx-coroutines-core/jvm/src/CommonPool.kt b/kotlinx-coroutines-core/jvm/src/CommonPool.kt index 60f30cfe14..2203313120 100644 --- a/kotlinx-coroutines-core/jvm/src/CommonPool.kt +++ b/kotlinx-coroutines-core/jvm/src/CommonPool.kt @@ -103,6 +103,8 @@ internal object CommonPool : ExecutorCoroutineDispatcher() { (pool ?: getOrCreatePoolSync()).execute(wrapTask(block)) } catch (e: RejectedExecutionException) { unTrackTask() + // CommonPool only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.enqueue(block) } } diff --git a/kotlinx-coroutines-core/jvm/src/DebugStrings.kt b/kotlinx-coroutines-core/jvm/src/DebugStrings.kt index 184fb655e3..2ccfebc6d3 100644 --- a/kotlinx-coroutines-core/jvm/src/DebugStrings.kt +++ b/kotlinx-coroutines-core/jvm/src/DebugStrings.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlin.coroutines.* // internal debugging tools for string representation diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index ed84f55e74..787cbf9c44 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import java.util.concurrent.* +import kotlin.coroutines.* internal actual val DefaultDelay: Delay = DefaultExecutor @@ -54,7 +55,7 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { * Livelock is possible only if `runBlocking` is called on internal default executed (which is used by default [delay]), * but it's not exposed as public API. */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) override fun run() { diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt index 8cd3bb1bae..8033fb38e5 100644 --- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt @@ -97,7 +97,7 @@ public actual object Dispatchers { * The [CoroutineDispatcher] that is designed for offloading blocking IO tasks to a shared pool of threads. * * Additional threads in this pool are created and are shutdown on demand. - * The number of threads used by this dispatcher is limited by the value of + * The number of threads used by tasks in this dispatcher is limited by the value of * "`kotlinx.coroutines.io.parallelism`" ([IO_PARALLELISM_PROPERTY_NAME]) system property. * It defaults to the limit of 64 threads or the number of cores (whichever is larger). * @@ -106,9 +106,13 @@ public actual object Dispatchers { * If you need a higher number of parallel threads, * you should use a custom dispatcher backed by your own thread pool. * + * ### Implementation note + * * This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using * `withContext(Dispatchers.IO) { ... }` does not lead to an actual switching to another thread — * typically execution continues in the same thread. + * As a result of thread sharing, more than 64 (default parallelism) threads can be created (but not used) + * during operations over IO dispatcher. */ @JvmStatic public val IO: CoroutineDispatcher = DefaultScheduler.IO diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index a4d6b46c43..8ffc22d8bb 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* * Instances of [ExecutorCoroutineDispatcher] should be closed by the owner of the dispatcher. * * This class is generally used as a bridge between coroutine-based API and - * asynchronous API which requires instance of the [Executor]. + * asynchronous API that requires an instance of the [Executor]. */ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable { /** @suppress */ @@ -38,6 +38,12 @@ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closea /** * Converts an instance of [ExecutorService] to an implementation of [ExecutorCoroutineDispatcher]. + * + * If the underlying executor throws [RejectedExecutionException] on + * attempt to submit a continuation task (it happens when [closing][ExecutorCoroutineDispatcher.close] the + * resulting dispatcher, on underlying executor [shutdown][ExecutorService.shutdown], or when it uses limited queues), + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. */ @JvmName("from") // this is for a nice Java API, see issue #255 public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher = @@ -45,6 +51,12 @@ public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher /** * Converts an instance of [Executor] to an implementation of [CoroutineDispatcher]. + * + * If the underlying executor throws [RejectedExecutionException] on + * attempt to submit a continuation task (it happens when [closing][ExecutorCoroutineDispatcher.close] the + * resulting dispatcher, on underlying executor [shutdown][ExecutorService.shutdown], or when it uses limited queues), + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. */ @JvmName("from") // this is for a nice Java API, see issue #255 public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher = @@ -82,7 +94,8 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa executor.execute(wrapTask(block)) } catch (e: RejectedExecutionException) { unTrackTask() - DefaultExecutor.enqueue(block) + cancelJobOnRejection(context, e) + Dispatchers.IO.dispatch(context, block) } } @@ -93,7 +106,7 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { val future = if (removesFutureOnCancellation) { - scheduleBlock(ResumeUndispatchedRunnable(this, continuation), timeMillis, TimeUnit.MILLISECONDS) + scheduleBlock(ResumeUndispatchedRunnable(this, continuation), continuation.context, timeMillis) } else { null } @@ -106,24 +119,31 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa DefaultExecutor.scheduleResumeAfterDelay(timeMillis, continuation) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val future = if (removesFutureOnCancellation) { - scheduleBlock(block, timeMillis, TimeUnit.MILLISECONDS) + scheduleBlock(block, context, timeMillis) } else { null } - - return if (future != null ) DisposableFutureHandle(future) else DefaultExecutor.invokeOnTimeout(timeMillis, block) + return when { + future != null -> DisposableFutureHandle(future) + else -> DefaultExecutor.invokeOnTimeout(timeMillis, block, context) + } } - private fun scheduleBlock(block: Runnable, time: Long, unit: TimeUnit): ScheduledFuture<*>? { + private fun scheduleBlock(block: Runnable, context: CoroutineContext, timeMillis: Long): ScheduledFuture<*>? { return try { - (executor as? ScheduledExecutorService)?.schedule(block, time, unit) + (executor as? ScheduledExecutorService)?.schedule(block, timeMillis, TimeUnit.MILLISECONDS) } catch (e: RejectedExecutionException) { + cancelJobOnRejection(context, e) null } } + private fun cancelJobOnRejection(context: CoroutineContext, exception: RejectedExecutionException) { + context.cancel(CancellationException("The task was rejected", exception)) + } + override fun close() { (executor as? ExecutorService)?.shutdown() } diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt index a0e1ffa2c7..aa18cd38d6 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt @@ -14,6 +14,11 @@ import kotlin.coroutines.* * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its thread). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * + * If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and + * attempt to submit a continuation task is made, + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. + * * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due @@ -35,6 +40,11 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its threads). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * + * If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and + * attempt to submit a continuation task is made, + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. + * * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index 29f37dac28..97f9978139 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -416,20 +416,26 @@ public actual open class LockFreeLinkedListNode { val next = this.next val removed = next.removed() if (affected._next.compareAndSet(this, removed)) { + // The element was actually removed + desc.onRemoved(affected) // Complete removal operation here. It bails out if next node is also removed and it becomes // responsibility of the next's removes to call correctPrev which would help fix all the links. next.correctPrev(null) } return REMOVE_PREPARED } - val isDecided = if (decision != null) { + // We need to ensure progress even if it operation result consensus was already decided + val consensus = if (decision != null) { // some other logic failure, including RETRY_ATOMIC -- reach consensus on decision fail reason ASAP atomicOp.decide(decision) - true // atomicOp.isDecided will be true as a result } else { - atomicOp.isDecided // consult with current decision status like in Harris DCSS + atomicOp.consensus // consult with current decision status like in Harris DCSS + } + val update: Any = when { + consensus === NO_DECISION -> atomicOp // desc.onPrepare returned null -> start doing atomic op + consensus == null -> desc.updatedNext(affected, next) // move forward if consensus on success + else -> next // roll back if consensus if failure } - val update: Any = if (isDecided) next else atomicOp // restore if decision was already reached affected._next.compareAndSet(this, update) return null } @@ -445,9 +451,10 @@ public actual open class LockFreeLinkedListNode { protected open fun takeAffectedNode(op: OpDescriptor): Node? = affectedNode!! // null for RETRY_ATOMIC protected open fun failure(affected: Node): Any? = null // next: Node | Removed protected open fun retry(affected: Node, next: Any): Boolean = false // next: Node | Removed - protected abstract fun updatedNext(affected: Node, next: Node): Any protected abstract fun finishOnSuccess(affected: Node, next: Node) + public abstract fun updatedNext(affected: Node, next: Node): Any + public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure @@ -456,6 +463,8 @@ public actual open class LockFreeLinkedListNode { return null } + public open fun onRemoved(affected: Node) {} // called once when node was prepared & later removed + @Suppress("UNCHECKED_CAST") final override fun prepare(op: AtomicOp<*>): Any? { while (true) { // lock free loop on next diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index ddfcdbb142..5b2b9ff68c 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -87,17 +87,14 @@ private class MissingMainCoroutineDispatcher( override val immediate: MainCoroutineDispatcher get() = this - override fun isDispatchNeeded(context: CoroutineContext): Boolean { + override fun isDispatchNeeded(context: CoroutineContext): Boolean = missing() - } - override suspend fun delay(time: Long) { + override suspend fun delay(time: Long) = missing() - } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = missing() - } override fun dispatch(context: CoroutineContext, block: Runnable) = missing() diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt index e0890eff66..202c6e1d06 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt @@ -65,6 +65,8 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block) } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.dispatch(context, block) } @@ -72,6 +74,8 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block, tailDispatch = true) } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.dispatchYield(context, block) } @@ -110,7 +114,9 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block, context, tailDispatch) } catch (e: RejectedExecutionException) { - // Context shouldn't be lost here to properly invoke before/after task + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. + // TaskContext shouldn't be lost here to properly invoke before/after task DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context)) } } diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt index e7c8b6b671..649c95375d 100644 --- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt @@ -230,7 +230,7 @@ public class TestCoroutineContext(private val name: String? = null) : CoroutineC }, timeMillis) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val node = postDelayed(block, timeMillis) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt b/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt index 8a7dce01ee..2612b84153 100644 --- a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt @@ -9,25 +9,24 @@ import kotlinx.coroutines.selects.* import kotlin.test.* class AtomicCancellationTest : TestBase() { - @Test - fun testSendAtomicCancel() = runBlocking { + fun testSendCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) channel.send(42) // suspends - expect(4) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) assertEquals(42, channel.receive()) // will schedule sender for further execution job.cancel() // cancel the job next yield() // now yield - finish(5) + finish(4) } @Test - fun testSelectSendAtomicCancel() = runBlocking { + fun testSelectSendCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -38,34 +37,33 @@ class AtomicCancellationTest : TestBase() { "OK" } } - assertEquals("OK", result) - expect(5) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) assertEquals(42, channel.receive()) // will schedule sender for further execution job.cancel() // cancel the job next yield() // now yield - finish(6) + finish(4) } @Test - fun testReceiveAtomicCancel() = runBlocking { + fun testReceiveCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) assertEquals(42, channel.receive()) // suspends - expect(4) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) channel.send(42) // will schedule receiver for further execution job.cancel() // cancel the job next yield() // now yield - finish(5) + finish(4) } @Test - fun testSelectReceiveAtomicCancel() = runBlocking { + fun testSelectReceiveCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -77,14 +75,13 @@ class AtomicCancellationTest : TestBase() { "OK" } } - assertEquals("OK", result) - expect(5) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) channel.send(42) // will schedule receiver for further execution job.cancel() // cancel the job next yield() // now yield - finish(6) + finish(4) } @Test diff --git a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt index 033b9b7bc9..ebf08a03d0 100644 --- a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -29,6 +29,8 @@ class ExecutorsTest : TestBase() { val context = newFixedThreadPoolContext(2, "TestPool") runBlocking(context) { checkThreadName("TestPool") + delay(10) + checkThreadName("TestPool") // should dispatch on the right thread } context.close() } @@ -38,6 +40,8 @@ class ExecutorsTest : TestBase() { val executor = Executors.newSingleThreadExecutor { r -> Thread(r, "TestExecutor") } runBlocking(executor.asCoroutineDispatcher()) { checkThreadName("TestExecutor") + delay(10) + checkThreadName("TestExecutor") // should dispatch on the right thread } executor.shutdown() } diff --git a/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt b/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt index 9bf8ffad85..c9f722a5b8 100644 --- a/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -15,8 +15,21 @@ import kotlin.test.* @RunWith(Parameterized::class) class FailingCoroutinesMachineryTest( private val element: CoroutineContext.Element, - private val dispatcher: CoroutineDispatcher + private val dispatcher: TestDispatcher ) : TestBase() { + class TestDispatcher(val name: String, val block: () -> CoroutineDispatcher) { + private var _value: CoroutineDispatcher? = null + + val value: CoroutineDispatcher + get() = _value ?: block().also { _value = it } + + override fun toString(): String = name + + fun reset() { + runCatching { (_value as? ExecutorCoroutineDispatcher)?.close() } + _value = null + } + } private var caught: Throwable? = null private val latch = CountDownLatch(1) @@ -75,7 +88,7 @@ class FailingCoroutinesMachineryTest( @After fun tearDown() { - runCatching { (dispatcher as? ExecutorCoroutineDispatcher)?.close() } + dispatcher.reset() if (lazyOuterDispatcher.isInitialized()) lazyOuterDispatcher.value.close() } @@ -84,14 +97,14 @@ class FailingCoroutinesMachineryTest( @Parameterized.Parameters(name = "Element: {0}, dispatcher: {1}") fun dispatchers(): List> { val elements = listOf(FailingRestore, FailingUpdate) - val dispatchers = listOf( - Dispatchers.Unconfined, - Dispatchers.Default, - Executors.newFixedThreadPool(1).asCoroutineDispatcher(), - Executors.newScheduledThreadPool(1).asCoroutineDispatcher(), - ThrowingDispatcher, ThrowingDispatcher2 + val dispatchers = listOf( + TestDispatcher("Dispatchers.Unconfined") { Dispatchers.Unconfined }, + TestDispatcher("Dispatchers.Default") { Dispatchers.Default }, + TestDispatcher("Executors.newFixedThreadPool(1)") { Executors.newFixedThreadPool(1).asCoroutineDispatcher() }, + TestDispatcher("Executors.newScheduledThreadPool(1)") { Executors.newScheduledThreadPool(1).asCoroutineDispatcher() }, + TestDispatcher("ThrowingDispatcher") { ThrowingDispatcher }, + TestDispatcher("ThrowingDispatcher2") { ThrowingDispatcher2 } ) - return elements.flatMap { element -> dispatchers.map { dispatcher -> arrayOf(element, dispatcher) @@ -102,13 +115,13 @@ class FailingCoroutinesMachineryTest( @Test fun testElement() = runTest { - launch(NonCancellable + dispatcher + exceptionHandler + element) {} + launch(NonCancellable + dispatcher.value + exceptionHandler + element) {} checkException() } @Test fun testNestedElement() = runTest { - launch(NonCancellable + dispatcher + exceptionHandler) { + launch(NonCancellable + dispatcher.value + exceptionHandler) { launch(element) { } } checkException() @@ -117,7 +130,7 @@ class FailingCoroutinesMachineryTest( @Test fun testNestedDispatcherAndElement() = runTest { launch(lazyOuterDispatcher.value + NonCancellable + exceptionHandler) { - launch(element + dispatcher) { } + launch(element + dispatcher.value) { } } checkException() } diff --git a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt b/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt index ec3635ca36..50d86f32be 100644 --- a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import org.junit.* +import kotlin.coroutines.* /** * Test a race between job failure and join. @@ -12,22 +13,52 @@ import org.junit.* * See [#1123](https://github.com/Kotlin/kotlinx.coroutines/issues/1123). */ class JobStructuredJoinStressTest : TestBase() { - private val nRepeats = 1_000 * stressTestMultiplier + private val nRepeats = 10_000 * stressTestMultiplier @Test - fun testStress() { - repeat(nRepeats) { + fun testStressRegularJoin() { + stress(Job::join) + } + + @Test + fun testStressSuspendCancellable() { + stress { job -> + suspendCancellableCoroutine { cont -> + job.invokeOnCompletion { cont.resume(Unit) } + } + } + } + + @Test + fun testStressSuspendCancellableReusable() { + stress { job -> + suspendCancellableCoroutineReusable { cont -> + job.invokeOnCompletion { cont.resume(Unit) } + } + } + } + + private fun stress(join: suspend (Job) -> Unit) { + expect(1) + repeat(nRepeats) { index -> assertFailsWith { runBlocking { // launch in background val job = launch(Dispatchers.Default) { throw TestException("OK") // crash } - assertFailsWith { - job.join() + try { + join(job) + error("Should not complete successfully") + } catch (e: CancellationException) { + // must always crash with cancellation exception + expect(2 + index) + } catch (e: Throwable) { + error("Unexpected exception", e) } } } } + finish(2 + nRepeats) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt new file mode 100644 index 0000000000..a6f4dd6b18 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.scheduling.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class RejectedExecutionTest : TestBase() { + private val threadName = "RejectedExecutionTest" + private val executor = RejectingExecutor() + + @After + fun tearDown() { + executor.shutdown() + executor.awaitTermination(10, TimeUnit.SECONDS) + } + + @Test + fun testRejectOnLaunch() = runTest { + expect(1) + val job = launch(executor.asCoroutineDispatcher()) { + expectUnreached() + } + assertEquals(1, executor.submittedTasks) + assertTrue(job.isCancelled) + finish(2) + } + + @Test + fun testRejectOnLaunchAtomic() = runTest { + expect(1) + val job = launch(executor.asCoroutineDispatcher(), start = CoroutineStart.ATOMIC) { + expect(2) + assertEquals(true, coroutineContext[Job]?.isCancelled) + assertIoThread() // was rejected on start, but start was atomic + } + assertEquals(1, executor.submittedTasks) + job.join() + finish(3) + } + + @Test + fun testRejectOnWithContext() = runTest { + expect(1) + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expectUnreached() + } + } + assertEquals(1, executor.submittedTasks) + finish(2) + } + + @Test + fun testRejectOnResumeInContext() = runTest { + expect(1) + executor.acceptTasks = 1 // accept one task + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expect(2) + assertExecutorThread() + try { + withContext(Dispatchers.Default) { + expect(3) + assertDefaultDispatcherThread() + // We have to wait until caller executor thread had already suspended (if not running task), + // so that we resume back to it a new task is posted + executor.awaitNotRunningTask() + expect(4) + assertDefaultDispatcherThread() + } + // cancelled on resume back + } finally { + expect(5) + assertIoThread() + } + expectUnreached() + } + } + assertEquals(2, executor.submittedTasks) + finish(6) + } + + @Test + fun testRejectOnDelay() = runTest { + expect(1) + executor.acceptTasks = 1 // accept one task + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expect(2) + assertExecutorThread() + try { + delay(10) // cancelled + } finally { + // Since it was cancelled on attempt to delay, it still stays on the same thread + assertExecutorThread() + } + expectUnreached() + } + } + assertEquals(2, executor.submittedTasks) + finish(3) + } + + @Test + fun testRejectWithTimeout() = runTest { + expect(1) + executor.acceptTasks = 1 // accept one task + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expect(2) + assertExecutorThread() + withTimeout(1000) { + expect(3) // atomic entry into the block (legacy behavior, it seem to be Ok with way) + assertEquals(true, coroutineContext[Job]?.isCancelled) // but the job is already cancelled + } + expectUnreached() + } + } + assertEquals(2, executor.submittedTasks) + finish(4) + } + + private inner class RejectingExecutor : ScheduledThreadPoolExecutor(1, { r -> Thread(r, threadName) }) { + var acceptTasks = 0 + var submittedTasks = 0 + val runningTask = MutableStateFlow(false) + + override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> { + submittedTasks++ + if (submittedTasks > acceptTasks) throw RejectedExecutionException() + val wrapper = Runnable { + runningTask.value = true + try { + command.run() + } finally { + runningTask.value = false + } + } + return super.schedule(wrapper, delay, unit) + } + + suspend fun awaitNotRunningTask() = runningTask.first { !it } + } + + private fun assertExecutorThread() { + val thread = Thread.currentThread() + if (!thread.name.startsWith(threadName)) error("Not an executor thread: $thread") + } + + private fun assertDefaultDispatcherThread() { + val thread = Thread.currentThread() + if (thread !is CoroutineScheduler.Worker) error("Not a thread from Dispatchers.Default: $thread") + assertEquals(CoroutineScheduler.WorkerState.CPU_ACQUIRED, thread.state) + } + + private fun assertIoThread() { + val thread = Thread.currentThread() + if (thread !is CoroutineScheduler.Worker) error("Not a thread from Dispatchers.IO: $thread") + assertEquals(CoroutineScheduler.WorkerState.BLOCKING, thread.state) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt index 892a2a62d4..56f1e28313 100644 --- a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt @@ -11,15 +11,14 @@ import kotlin.coroutines.* import kotlin.test.* class ReusableCancellableContinuationTest : TestBase() { - @Test fun testReusable() = runTest { - testContinuationsCount(10, 1, ::suspendAtomicCancellableCoroutineReusable) + testContinuationsCount(10, 1, ::suspendCancellableCoroutineReusable) } @Test fun testRegular() = runTest { - testContinuationsCount(10, 10, ::suspendAtomicCancellableCoroutine) + testContinuationsCount(10, 10, ::suspendCancellableCoroutine) } private suspend inline fun CoroutineScope.testContinuationsCount( @@ -51,7 +50,7 @@ class ReusableCancellableContinuationTest : TestBase() { fun testCancelledOnClaimedCancel() = runTest { expect(1) try { - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { it.cancel() } expectUnreached() @@ -65,7 +64,7 @@ class ReusableCancellableContinuationTest : TestBase() { expect(1) // Bind child at first var continuation: Continuation<*>? = null - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { expect(2) continuation = it launch { // Attach to the parent, avoid fast path @@ -77,13 +76,16 @@ class ReusableCancellableContinuationTest : TestBase() { ensureActive() // Verify child was bound FieldWalker.assertReachableCount(1, coroutineContext[Job]) { it === continuation } - suspendAtomicCancellableCoroutineReusable { - expect(5) - coroutineContext[Job]!!.cancel() - it.resume(Unit) + try { + suspendCancellableCoroutineReusable { + expect(5) + coroutineContext[Job]!!.cancel() + it.resume(Unit) // will not dispatch, will get CancellationException + } + } catch (e: CancellationException) { + assertFalse(isActive) + finish(6) } - assertFalse(isActive) - finish(6) } @Test @@ -93,7 +95,7 @@ class ReusableCancellableContinuationTest : TestBase() { launch { cont!!.resumeWith(Result.success(Unit)) } - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { cont = it } ensureActive() @@ -108,7 +110,7 @@ class ReusableCancellableContinuationTest : TestBase() { launch { // Attach to the parent, avoid fast path cont!!.resumeWith(Result.success(Unit)) } - suspendAtomicCancellableCoroutine { + suspendCancellableCoroutine { cont = it } ensureActive() @@ -121,7 +123,7 @@ class ReusableCancellableContinuationTest : TestBase() { expect(1) var cont: Continuation<*>? = null try { - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { cont = it it.cancel() } @@ -137,7 +139,7 @@ class ReusableCancellableContinuationTest : TestBase() { val currentJob = coroutineContext[Job]!! expect(1) // Bind child at first - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { expect(2) // Attach to the parent, avoid fast path launch { @@ -153,15 +155,23 @@ class ReusableCancellableContinuationTest : TestBase() { assertFalse(isActive) // Child detached FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } - suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } - FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - + expect(5) + try { + // Resume is non-atomic, so it throws cancellation exception + suspendCancellableCoroutineReusable { + expect(6) // but the code inside the block is executed + it.resume(Unit) + } + } catch (e: CancellationException) { + FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } + expect(7) + } try { - suspendAtomicCancellableCoroutineReusable {} + // No resume -- still cancellation exception + suspendCancellableCoroutineReusable {} } catch (e: CancellationException) { FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - finish(5) + finish(8) } } diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index bf462cc78f..17238e873c 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -69,6 +69,8 @@ public actual open class TestBase actual constructor() { throw makeError(message, cause) } + public fun hasError() = error.get() != null + private fun makeError(message: Any, cause: Throwable? = null): IllegalStateException = IllegalStateException(message.toString(), cause).also { setError(it) @@ -107,7 +109,7 @@ public actual open class TestBase actual constructor() { * Asserts that this line is never executed. */ public actual fun expectUnreached() { - error("Should not be reached") + error("Should not be reached, current action index is ${actionIndex.get()}") } /** diff --git a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt index 54ba7b639f..2e73b2432a 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt @@ -48,8 +48,9 @@ class BroadcastChannelMultiReceiveStressTest( launch(pool + CoroutineName("Sender")) { var i = 0L while (isActive) { - broadcast.send(++i) - sentTotal.set(i) // set sentTotal only if `send` was not cancelled + i++ + broadcast.send(i) // could be cancelled + sentTotal.set(i) // only was for it if it was not cancelled } } val receivers = mutableListOf() @@ -88,10 +89,8 @@ class BroadcastChannelMultiReceiveStressTest( try { withTimeout(5000) { receivers.forEachIndexed { index, receiver -> - if (lastReceived[index].get() == total) - receiver.cancel() - else - receiver.join() + if (lastReceived[index].get() >= total) receiver.cancel() + receiver.join() } } } catch (e: Exception) { @@ -112,7 +111,7 @@ class BroadcastChannelMultiReceiveStressTest( check(i == last + 1) { "Last was $last, got $i" } receivedTotal.incrementAndGet() lastReceived[receiverIndex].set(i) - return i == stopOnReceive.get() + return i >= stopOnReceive.get() } private suspend fun doReceive(channel: ReceiveChannel, receiverIndex: Int) { diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt deleted file mode 100644 index 6556888a0f..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.channels - -import kotlinx.coroutines.* -import kotlinx.coroutines.selects.* -import org.junit.After -import org.junit.Test -import org.junit.runner.* -import org.junit.runners.* -import kotlin.random.Random -import java.util.concurrent.atomic.* -import kotlin.test.* - -/** - * Tests cancel atomicity for channel send & receive operations, including their select versions. - */ -@RunWith(Parameterized::class) -class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBase() { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun params(): Collection> = TestChannelKind.values().map { arrayOf(it) } - } - - private val TEST_DURATION = 1000L * stressTestMultiplier - - private val dispatcher = newFixedThreadPoolContext(2, "ChannelAtomicCancelStressTest") - private val scope = CoroutineScope(dispatcher) - - private val channel = kind.create() - private val senderDone = Channel(1) - private val receiverDone = Channel(1) - - private var lastSent = 0 - private var lastReceived = 0 - - private var stoppedSender = 0 - private var stoppedReceiver = 0 - - private var missedCnt = 0 - private var dupCnt = 0 - - val failed = AtomicReference() - - lateinit var sender: Job - lateinit var receiver: Job - - @After - fun tearDown() { - dispatcher.close() - } - - fun fail(e: Throwable) = failed.compareAndSet(null, e) - - private inline fun cancellable(done: Channel, block: () -> Unit) { - try { - block() - } finally { - if (!done.offer(true)) - fail(IllegalStateException("failed to offer to done channel")) - } - } - - @Test - fun testAtomicCancelStress() = runBlocking { - println("--- ChannelAtomicCancelStressTest $kind") - val deadline = System.currentTimeMillis() + TEST_DURATION - launchSender() - launchReceiver() - while (System.currentTimeMillis() < deadline && failed.get() == null) { - when (Random.nextInt(3)) { - 0 -> { // cancel & restart sender - stopSender() - launchSender() - } - 1 -> { // cancel & restart receiver - stopReceiver() - launchReceiver() - } - 2 -> yield() // just yield (burn a little time) - } - } - stopSender() - stopReceiver() - println(" Sent $lastSent ints to channel") - println(" Received $lastReceived ints from channel") - println(" Stopped sender $stoppedSender times") - println("Stopped receiver $stoppedReceiver times") - println(" Missed $missedCnt ints") - println(" Duplicated $dupCnt ints") - failed.get()?.let { throw it } - assertEquals(0, dupCnt) - if (!kind.isConflated) { - assertEquals(0, missedCnt) - assertEquals(lastSent, lastReceived) - } - } - - private fun launchSender() { - sender = scope.launch(start = CoroutineStart.ATOMIC) { - cancellable(senderDone) { - var counter = 0 - while (true) { - val trySend = lastSent + 1 - when (Random.nextInt(2)) { - 0 -> channel.send(trySend) - 1 -> select { channel.onSend(trySend) {} } - else -> error("cannot happen") - } - lastSent = trySend // update on success - when { - // must artificially slow down LINKED_LIST sender to avoid overwhelming receiver and going OOM - kind == TestChannelKind.LINKED_LIST -> while (lastSent > lastReceived + 100) yield() - // yield periodically to check cancellation on conflated channels - kind.isConflated -> if (counter++ % 100 == 0) yield() - } - } - } - } - } - - private suspend fun stopSender() { - stoppedSender++ - sender.cancel() - senderDone.receive() - } - - private fun launchReceiver() { - receiver = scope.launch(start = CoroutineStart.ATOMIC) { - cancellable(receiverDone) { - while (true) { - val received = when (Random.nextInt(2)) { - 0 -> channel.receive() - 1 -> select { channel.onReceive { it } } - else -> error("cannot happen") - } - val expected = lastReceived + 1 - if (received > expected) - missedCnt++ - if (received < expected) - dupCnt++ - lastReceived = received - } - } - } - } - - private suspend fun stopReceiver() { - stoppedReceiver++ - receiver.cancel() - receiverDone.receive() - } -} diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt new file mode 100644 index 0000000000..76713aa173 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import java.util.concurrent.atomic.* +import kotlin.random.* +import kotlin.test.* + +class ChannelCancelUndeliveredElementStressTest : TestBase() { + private val repeatTimes = 10_000 * stressTestMultiplier + + // total counters + private var sendCnt = 0 + private var offerFailedCnt = 0 + private var receivedCnt = 0 + private var undeliveredCnt = 0 + + // last operation + private var lastReceived = 0 + private var dSendCnt = 0 + private var dSendExceptionCnt = 0 + private var dOfferFailedCnt = 0 + private var dReceivedCnt = 0 + private val dUndeliveredCnt = AtomicInteger() + + @Test + fun testStress() = runTest { + repeat(repeatTimes) { + val channel = Channel(1) { dUndeliveredCnt.incrementAndGet() } + val j1 = launch(Dispatchers.Default) { + sendOne(channel) // send first + sendOne(channel) // send second + } + val j2 = launch(Dispatchers.Default) { + receiveOne(channel) // receive one element from the channel + channel.cancel() // cancel the channel + } + + joinAll(j1, j2) + + // All elements must be either received or undelivered (IN every run) + if (dSendCnt - dOfferFailedCnt != dReceivedCnt + dUndeliveredCnt.get()) { + println(" Send: $dSendCnt") + println("Send Exception: $dSendExceptionCnt") + println(" Offer failed: $dOfferFailedCnt") + println(" Received: $dReceivedCnt") + println(" Undelivered: ${dUndeliveredCnt.get()}") + error("Failed") + } + offerFailedCnt += dOfferFailedCnt + receivedCnt += dReceivedCnt + undeliveredCnt += dUndeliveredCnt.get() + // clear for next run + dSendCnt = 0 + dSendExceptionCnt = 0 + dOfferFailedCnt = 0 + dReceivedCnt = 0 + dUndeliveredCnt.set(0) + } + // Stats + println(" Send: $sendCnt") + println(" Offer failed: $offerFailedCnt") + println(" Received: $receivedCnt") + println(" Undelivered: $undeliveredCnt") + assertEquals(sendCnt - offerFailedCnt, receivedCnt + undeliveredCnt) + } + + private suspend fun sendOne(channel: Channel) { + dSendCnt++ + val i = ++sendCnt + try { + when (Random.nextInt(2)) { + 0 -> channel.send(i) + 1 -> if (!channel.offer(i)) { + dOfferFailedCnt++ + } + } + } catch(e: Throwable) { + assertTrue(e is CancellationException) // the only exception possible in this test + dSendExceptionCnt++ + throw e + } + } + + private suspend fun receiveOne(channel: Channel) { + val received = when (Random.nextInt(3)) { + 0 -> channel.receive() + 1 -> channel.receiveOrNull() ?: error("Cannot be closed yet") + 2 -> select { + channel.onReceive { it } + } + else -> error("Cannot happen") + } + assertTrue(received > lastReceived) + dReceivedCnt++ + lastReceived = received + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt index 00c5a6090f..f414c33338 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt @@ -35,7 +35,7 @@ class ChannelSendReceiveStressTest( private val maxBuffer = 10_000 // artificial limit for LinkedListChannel - val channel = kind.create() + val channel = kind.create() private val sendersCompleted = AtomicInteger() private val receiversCompleted = AtomicInteger() private val dupes = AtomicInteger() diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt new file mode 100644 index 0000000000..1188329a4c --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import org.junit.After +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import kotlin.random.Random +import kotlin.test.* + +/** + * Tests resource transfer via channel send & receive operations, including their select versions, + * using `onUndeliveredElement` to detect lost resources and close them properly. + */ +@RunWith(Parameterized::class) +class ChannelUndeliveredElementStressTest(private val kind: TestChannelKind) : TestBase() { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun params(): Collection> = + TestChannelKind.values() + .filter { !it.viaBroadcast } + .map { arrayOf(it) } + } + + private val iterationDurationMs = 100L + private val testIterations = 20 * stressTestMultiplier // 2 sec + + private val dispatcher = newFixedThreadPoolContext(2, "ChannelAtomicCancelStressTest") + private val scope = CoroutineScope(dispatcher) + + private val channel = kind.create { it.failedToDeliver() } + private val senderDone = Channel(1) + private val receiverDone = Channel(1) + + @Volatile + private var lastReceived = -1L + + private var stoppedSender = 0L + private var stoppedReceiver = 0L + + private var sentCnt = 0L // total number of send attempts + private var receivedCnt = 0L // actually received successfully + private var dupCnt = 0L // duplicates (should never happen) + private val failedToDeliverCnt = atomic(0L) // out of sent + + private val modulo = 1 shl 25 + private val mask = (modulo - 1).toLong() + private val sentStatus = ItemStatus() // 1 - send norm, 2 - send select, +2 - did not throw exception + private val receivedStatus = ItemStatus() // 1-6 received + private val failedStatus = ItemStatus() // 1 - failed + + lateinit var sender: Job + lateinit var receiver: Job + + @After + fun tearDown() { + dispatcher.close() + } + + private inline fun cancellable(done: Channel, block: () -> Unit) { + try { + block() + } finally { + if (!done.offer(true)) + error(IllegalStateException("failed to offer to done channel")) + } + } + + @Test + fun testAtomicCancelStress() = runBlocking { + println("=== ChannelAtomicCancelStressTest $kind") + var nextIterationTime = System.currentTimeMillis() + iterationDurationMs + var iteration = 0 + launchSender() + launchReceiver() + while (!hasError()) { + if (System.currentTimeMillis() >= nextIterationTime) { + nextIterationTime += iterationDurationMs + iteration++ + verify(iteration) + if (iteration % 10 == 0) printProgressSummary(iteration) + if (iteration >= testIterations) break + launchSender() + launchReceiver() + } + when (Random.nextInt(3)) { + 0 -> { // cancel & restart sender + stopSender() + launchSender() + } + 1 -> { // cancel & restart receiver + stopReceiver() + launchReceiver() + } + 2 -> yield() // just yield (burn a little time) + } + } + } + + private suspend fun verify(iteration: Int) { + stopSender() + drainReceiver() + stopReceiver() + try { + assertEquals(0, dupCnt) + assertEquals(sentCnt - failedToDeliverCnt.value, receivedCnt) + } catch (e: Throwable) { + printProgressSummary(iteration) + printErrorDetails() + throw e + } + sentStatus.clear() + receivedStatus.clear() + failedStatus.clear() + } + + private fun printProgressSummary(iteration: Int) { + println("--- ChannelAtomicCancelStressTest $kind -- $iteration of $testIterations") + println(" Sent $sentCnt times to channel") + println(" Received $receivedCnt times from channel") + println(" Failed to deliver ${failedToDeliverCnt.value} times") + println(" Stopped sender $stoppedSender times") + println(" Stopped receiver $stoppedReceiver times") + println(" Duplicated $dupCnt deliveries") + } + + private fun printErrorDetails() { + val min = minOf(sentStatus.min, receivedStatus.min, failedStatus.min) + val max = maxOf(sentStatus.max, receivedStatus.max, failedStatus.max) + for (x in min..max) { + val sentCnt = if (sentStatus[x] != 0) 1 else 0 + val receivedCnt = if (receivedStatus[x] != 0) 1 else 0 + val failedToDeliverCnt = failedStatus[x] + if (sentCnt - failedToDeliverCnt != receivedCnt) { + println("!!! Error for value $x: " + + "sentStatus=${sentStatus[x]}, " + + "receivedStatus=${receivedStatus[x]}, " + + "failedStatus=${failedStatus[x]}" + ) + } + } + } + + + private fun launchSender() { + sender = scope.launch(start = CoroutineStart.ATOMIC) { + cancellable(senderDone) { + var counter = 0 + while (true) { + val trySendData = Data(sentCnt++) + val sendMode = Random.nextInt(2) + 1 + sentStatus[trySendData.x] = sendMode + when (sendMode) { + 1 -> channel.send(trySendData) + 2 -> select { channel.onSend(trySendData) {} } + else -> error("cannot happen") + } + sentStatus[trySendData.x] = sendMode + 2 + when { + // must artificially slow down LINKED_LIST sender to avoid overwhelming receiver and going OOM + kind == TestChannelKind.LINKED_LIST -> while (sentCnt > lastReceived + 100) yield() + // yield periodically to check cancellation on conflated channels + kind.isConflated -> if (counter++ % 100 == 0) yield() + } + } + } + } + } + + private suspend fun stopSender() { + stoppedSender++ + sender.cancel() + senderDone.receive() + } + + private fun launchReceiver() { + receiver = scope.launch(start = CoroutineStart.ATOMIC) { + cancellable(receiverDone) { + while (true) { + val receiveMode = Random.nextInt(6) + 1 + val receivedData = when (receiveMode) { + 1 -> channel.receive() + 2 -> select { channel.onReceive { it } } + 3 -> channel.receiveOrNull() ?: error("Should not be closed") + 4 -> select { channel.onReceiveOrNull { it ?: error("Should not be closed") } } + 5 -> channel.receiveOrClosed().value + 6 -> { + val iterator = channel.iterator() + check(iterator.hasNext()) { "Should not be closed" } + iterator.next() + } + else -> error("cannot happen") + } + receivedCnt++ + val received = receivedData.x + if (received <= lastReceived) + dupCnt++ + lastReceived = received + receivedStatus[received] = receiveMode + } + } + } + } + + private suspend fun drainReceiver() { + while (!channel.isEmpty) yield() // burn time until receiver gets it all + } + + private suspend fun stopReceiver() { + stoppedReceiver++ + receiver.cancel() + receiverDone.receive() + } + + private inner class Data(val x: Long) { + private val failedToDeliver = atomic(false) + + fun failedToDeliver() { + check(failedToDeliver.compareAndSet(false, true)) { "onUndeliveredElement notified twice" } + failedToDeliverCnt.incrementAndGet() + failedStatus[x] = 1 + } + } + + inner class ItemStatus { + private val a = ByteArray(modulo) + private val _min = atomic(Long.MAX_VALUE) + private val _max = atomic(-1L) + + val min: Long get() = _min.value + val max: Long get() = _max.value + + operator fun set(x: Long, value: Int) { + a[(x and mask).toInt()] = value.toByte() + _min.update { y -> minOf(x, y) } + _max.update { y -> maxOf(x, y) } + } + + operator fun get(x: Long): Int = a[(x and mask).toInt()].toInt() + + fun clear() { + if (_max.value < 0) return + for (x in _min.value.._max.value) a[(x and mask).toInt()] = 0 + _min.value = Long.MAX_VALUE + _max.value = -1L + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt index 864a0b4c2e..888522c63c 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt @@ -39,7 +39,7 @@ class InvokeOnCloseStressTest : TestBase(), CoroutineScope { private suspend fun runStressTest(kind: TestChannelKind) { repeat(iterations) { val counter = AtomicInteger(0) - val channel = kind.create() + val channel = kind.create() val latch = CountDownLatch(1) val j1 = async { diff --git a/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt b/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt index 07c431bb4d..eeddfb5f49 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt @@ -28,7 +28,7 @@ class SimpleSendReceiveJvmTest( } } - val channel = kind.create() + val channel = kind.create() @Test fun testSimpleSendReceive() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt new file mode 100644 index 0000000000..d2a5d536b2 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelay01 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + emit(1) + delay(90) + emit(2) + delay(90) + emit(3) + delay(1010) + emit(4) + delay(1010) + emit(5) +}.debounce(1000) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt new file mode 100644 index 0000000000..1b6b12f041 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelay02 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + repeat(10) { + emit(it) + delay(110) + } +}.sample(200) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt new file mode 100644 index 0000000000..a19e6cb181 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt @@ -0,0 +1,26 @@ +@file:OptIn(ExperimentalTime::class) +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelayDuration01 + +import kotlin.time.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + emit(1) + delay(90.milliseconds) + emit(2) + delay(90.milliseconds) + emit(3) + delay(1010.milliseconds) + emit(4) + delay(1010.milliseconds) + emit(5) +}.debounce(1000.milliseconds) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt new file mode 100644 index 0000000000..e43dfd1e05 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt @@ -0,0 +1,21 @@ +@file:OptIn(ExperimentalTime::class) +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelayDuration02 + +import kotlin.time.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + repeat(10) { + emit(it) + delay(110.milliseconds) + } +}.sample(200.milliseconds) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt new file mode 100644 index 0000000000..226d31cc00 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.test + +import kotlinx.coroutines.knit.* +import org.junit.Test + +class FlowDelayTest { + @Test + fun testExampleDelay01() { + test("ExampleDelay01") { kotlinx.coroutines.examples.exampleDelay01.main() }.verifyLines( + "3, 4, 5" + ) + } + + @Test + fun testExampleDelayDuration01() { + test("ExampleDelayDuration01") { kotlinx.coroutines.examples.exampleDelayDuration01.main() }.verifyLines( + "3, 4, 5" + ) + } + + @Test + fun testExampleDelay02() { + test("ExampleDelay02") { kotlinx.coroutines.examples.exampleDelay02.main() }.verifyLines( + "1, 3, 5, 7, 9" + ) + } + + @Test + fun testExampleDelayDuration02() { + test("ExampleDelayDuration02") { kotlinx.coroutines.examples.exampleDelayDuration02.main() }.verifyLines( + "1, 3, 5, 7, 9" + ) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt new file mode 100644 index 0000000000..7d346bdc33 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import java.util.* +import java.util.concurrent.atomic.* +import kotlin.random.* +import kotlin.test.* +import kotlin.time.* +import kotlin.time.TimeSource + +@OptIn(ExperimentalTime::class) +class SharingStressTest : TestBase() { + private val testDuration = 1000L * stressTestMultiplier + private val nSubscribers = 5 + private val testStarted = TimeSource.Monotonic.markNow() + + @get:Rule + val emitterDispatcher = ExecutorRule(1) + + @get:Rule + val subscriberDispatcher = ExecutorRule(nSubscribers) + + @Test + public fun testNoReplayLazy() = + testStress(0, started = SharingStarted.Lazily) + + @Test + public fun testNoReplayWhileSubscribed() = + testStress(0, started = SharingStarted.WhileSubscribed()) + + @Test + public fun testNoReplayWhileSubscribedTimeout() = + testStress(0, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 50L)) + + @Test + public fun testReplay100WhileSubscribed() = + testStress(100, started = SharingStarted.WhileSubscribed()) + + @Test + public fun testReplay100WhileSubscribedReset() = + testStress(100, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0L)) + + @Test + public fun testReplay100WhileSubscribedTimeout() = + testStress(100, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 50L)) + + @Test + public fun testStateLazy() = + testStress(1, started = SharingStarted.Lazily) + + @Test + public fun testStateWhileSubscribed() = + testStress(1, started = SharingStarted.WhileSubscribed()) + + @Test + public fun testStateWhileSubscribedReset() = + testStress(1, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0L)) + + private fun testStress(replay: Int, started: SharingStarted) = runTest { + log("-- Stress with replay=$replay, started=$started") + val random = Random(1) + val emitIndex = AtomicLong() + val cancelledEmits = HashSet() + val missingCollects = Collections.synchronizedSet(LinkedHashSet()) + // at most one copy of upstream can be running at any time + val isRunning = AtomicInteger(0) + val upstream = flow { + assertEquals(0, isRunning.getAndIncrement()) + try { + while (true) { + val value = emitIndex.getAndIncrement() + try { + emit(value) + } catch (e: CancellationException) { + // emission was cancelled -> could be missing + cancelledEmits.add(value) + throw e + } + } + } finally { + assertEquals(1, isRunning.getAndDecrement()) + } + } + val subCount = MutableStateFlow(0) + val sharingJob = Job() + val sharingScope = this + emitterDispatcher + sharingJob + val usingStateFlow = replay == 1 + val sharedFlow = if (usingStateFlow) + upstream.stateIn(sharingScope, started, 0L) + else + upstream.shareIn(sharingScope, started, replay) + try { + val subscribers = ArrayList() + withTimeoutOrNull(testDuration) { + // start and stop subscribers + while (true) { + log("Staring $nSubscribers subscribers") + repeat(nSubscribers) { + subscribers += launchSubscriber(sharedFlow, usingStateFlow, subCount, missingCollects) + } + // wait until they all subscribed + subCount.first { it == nSubscribers } + // let them work a bit more & make sure emitter did not hang + val fromEmitIndex = emitIndex.get() + val waitEmitIndex = fromEmitIndex + 100 // wait until 100 emitted + withTimeout(10000) { // wait for at most 10s for something to be emitted + do { + delay(random.nextLong(50L..100L)) + } while (emitIndex.get() < waitEmitIndex) // Ok, enough was emitted, wait more if not + } + // Stop all subscribers and ensure they collected something + log("Stopping subscribers (emitted = ${emitIndex.get() - fromEmitIndex})") + subscribers.forEach { + it.job.cancelAndJoin() + assertTrue { it.count > 0 } // something must be collected too + } + subscribers.clear() + log("Intermission") + delay(random.nextLong(10L..100L)) // wait a bit before starting them again + } + } + if (!subscribers.isEmpty()) { + log("Stopping subscribers") + subscribers.forEach { it.job.cancelAndJoin() } + } + } finally { + log("--- Finally: Cancelling sharing job") + sharingJob.cancel() + } + sharingJob.join() // make sure sharing job did not hang + log("Emitter was cancelled ${cancelledEmits.size} times") + log("Collectors missed ${missingCollects.size} values") + for (value in missingCollects) { + assertTrue(value in cancelledEmits, "Value $value is missing for no apparent reason") + } + } + + private fun CoroutineScope.launchSubscriber( + sharedFlow: SharedFlow, + usingStateFlow: Boolean, + subCount: MutableStateFlow, + missingCollects: MutableSet + ): SubJob { + val subJob = SubJob() + subJob.job = launch(subscriberDispatcher) { + var last = -1L + sharedFlow + .onSubscription { + subCount.increment(1) + } + .onCompletion { + subCount.increment(-1) + } + .collect { j -> + subJob.count++ + // last must grow sequentially, no jumping or losses + if (last == -1L) { + last = j + } else { + val expected = last + 1 + if (usingStateFlow) + assertTrue(expected <= j) + else { + if (expected != j) { + if (j == expected + 1) { + // if missing just one -- could be race with cancelled emit + missingCollects.add(expected) + } else { + // broken otherwise + assertEquals(expected, j) + } + } + } + last = j + } + } + } + return subJob + } + + private class SubJob { + lateinit var job: Job + var count = 0L + } + + private fun log(msg: String) = println("${testStarted.elapsedNow().toLongMilliseconds()} ms: $msg") +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt b/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt new file mode 100644 index 0000000000..fc4996c7c0 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import java.util.concurrent.* +import kotlin.test.* + +@Suppress("BlockingMethodInNonBlockingContext") +class StateFlowCancellabilityTest : TestBase() { + @Test + fun testCancellabilityNoConflation() = runTest { + expect(1) + val state = MutableStateFlow(0) + var subscribed = true + var lastReceived = -1 + val barrier = CyclicBarrier(2) + val job = state + .onSubscription { + subscribed = true + barrier.await() + } + .onEach { i -> + when (i) { + 0 -> expect(2) // initial value + 1 -> expect(3) + 2 -> { + expect(4) + currentCoroutineContext().cancel() + } + else -> expectUnreached() // shall check for cancellation + } + lastReceived = i + barrier.await() + barrier.await() + } + .launchIn(this + Dispatchers.Default) + barrier.await() + assertTrue(subscribed) // should have subscribed in the first barrier + barrier.await() + assertEquals(0, lastReceived) // should get initial value, too + for (i in 1..3) { // emit after subscription + state.value = i + barrier.await() // let it go + if (i < 3) { + barrier.await() // wait for receive + assertEquals(i, lastReceived) // shall receive it + } + } + job.join() + finish(5) + } +} + diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt new file mode 100644 index 0000000000..e7def132ae --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleCancel08 + +import kotlinx.coroutines.* + +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + val resource = withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + Resource() // Acquire a resource and return it from withTimeout block + } + resource.close() // Release the resource + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt new file mode 100644 index 0000000000..95424f5108 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleCancel09 + +import kotlinx.coroutines.* + +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + var resource: Resource? = null // Not acquired yet + try { + withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + resource = Resource() // Store a resource to the variable if acquired + } + // We can do something else with the resource here + } finally { + resource?.close() // Release the resource if it was acquired + } + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt index 7fc57c2ee3..ea5003b0c7 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from basics.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class BasicsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt index a2e91de82d..0cff63a834 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class CancellationGuideTest { @@ -87,4 +88,11 @@ class CancellationGuideTest { "Result is null" ) } + + @Test + fun testExampleCancel09() { + test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines( + "0" + ) + } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt index 209d439663..d15a550adb 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from channels.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class ChannelsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt index 50c3fd7e62..1f9692d56b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from composing-suspending-functions.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class ComposingGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt index c0c32410d5..d6f1c21dc0 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from coroutine-context-and-dispatchers.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class DispatcherGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt index c42ba31d3a..f34fd3f83b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from exception-handling.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class ExceptionsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index 7fa9cc84dd..c7d4a72082 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from flow.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class FlowGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt index e3f47b9648..55650d4c6a 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from select-expression.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class SelectGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt index 8d534a09ea..3162b24cbc 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from shared-mutable-state-and-concurrency.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class SharedStateGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt index ae4b5fce30..e4fa5e9bfe 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt @@ -12,8 +12,8 @@ import org.junit.* class ConcurrentWeakMapTest : TestBase() { @Test fun testSimple() { - val expect = (1..1000).associateWith { it.toString() } - val m = ConcurrentWeakMap() + val expect = (1..1000).associate { it.toString().let { it to it } } + val m = ConcurrentWeakMap() // repeat adding/removing a few times repeat(5) { assertEquals(0, m.size) @@ -27,7 +27,7 @@ class ConcurrentWeakMapTest : TestBase() { assertEquals(expect.keys, m.keys) assertEquals(expect.entries, m.entries) for ((k, v) in expect) { - assertEquals(v, m.get(k)) + assertEquals(v, m[k]) } assertEquals(expect.size, m.size) if (it % 2 == 0) { @@ -38,9 +38,9 @@ class ConcurrentWeakMapTest : TestBase() { m.clear() } assertEquals(0, m.size) - for ((k, v) in expect) { - assertNull(m.get(k)) + for ((k, _) in expect) { + assertNull(m[k]) } } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt similarity index 98% rename from kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt rename to kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt index fb1c85bce1..7eda9043db 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt +++ b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt @@ -1,8 +1,8 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines.guide.test +package kotlinx.coroutines.knit import kotlinx.coroutines.* import kotlinx.coroutines.internal.* diff --git a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt index 8ecb8fd741..bb713b258d 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.sync import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* import kotlin.test.* class MutexStressTest : TestBase() { @@ -26,4 +27,67 @@ class MutexStressTest : TestBase() { jobs.forEach { it.join() } assertEquals(n * k, shared) } + + @Test + fun stressUnlockCancelRace() = runTest { + val n = 10_000 * stressTestMultiplier + val mutex = Mutex(true) // create a locked mutex + newSingleThreadContext("SemaphoreStressTest").use { pool -> + repeat (n) { + // Initially, we hold the lock and no one else can `lock`, + // otherwise it's a bug. + assertTrue(mutex.isLocked) + var job1EnteredCriticalSection = false + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + mutex.lock() + job1EnteredCriticalSection = true + mutex.unlock() + } + // check that `job1` didn't finish the call to `acquire()` + assertEquals(false, job1EnteredCriticalSection) + val job2 = launch(pool) { + mutex.unlock() + } + // Because `job2` executes in a separate thread, this + // cancellation races with the call to `unlock()`. + job1.cancelAndJoin() + job2.join() + assertFalse(mutex.isLocked) + mutex.lock() + } + } + } + + @Test + fun stressUnlockCancelRaceWithSelect() = runTest { + val n = 10_000 * stressTestMultiplier + val mutex = Mutex(true) // create a locked mutex + newSingleThreadContext("SemaphoreStressTest").use { pool -> + repeat (n) { + // Initially, we hold the lock and no one else can `lock`, + // otherwise it's a bug. + assertTrue(mutex.isLocked) + var job1EnteredCriticalSection = false + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + select { + mutex.onLock { + job1EnteredCriticalSection = true + mutex.unlock() + } + } + } + // check that `job1` didn't finish the call to `acquire()` + assertEquals(false, job1EnteredCriticalSection) + val job2 = launch(pool) { + mutex.unlock() + } + // Because `job2` executes in a separate thread, this + // cancellation races with the call to `unlock()`. + job1.cancelAndJoin() + job2.join() + assertFalse(mutex.isLocked) + mutex.lock() + } + } + } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt index 9c77990862..374a1e3d7c 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt @@ -5,7 +5,6 @@ import org.junit.Test import kotlin.test.assertEquals class SemaphoreStressTest : TestBase() { - @Test fun stressTestAsMutex() = runBlocking(Dispatchers.Default) { val n = 10_000 * stressTestMultiplier @@ -71,14 +70,14 @@ class SemaphoreStressTest : TestBase() { // Initially, we hold the permit and no one else can `acquire`, // otherwise it's a bug. assertEquals(0, semaphore.availablePermits) - var job1_entered_critical_section = false + var job1EnteredCriticalSection = false val job1 = launch(start = CoroutineStart.UNDISPATCHED) { semaphore.acquire() - job1_entered_critical_section = true + job1EnteredCriticalSection = true semaphore.release() } // check that `job1` didn't finish the call to `acquire()` - assertEquals(false, job1_entered_critical_section) + assertEquals(false, job1EnteredCriticalSection) val job2 = launch(pool) { semaphore.release() } @@ -91,5 +90,4 @@ class SemaphoreStressTest : TestBase() { } } } - } diff --git a/kotlinx-coroutines-core/knit.properties b/kotlinx-coroutines-core/knit.properties new file mode 100644 index 0000000000..93ce87db8f --- /dev/null +++ b/kotlinx-coroutines-core/knit.properties @@ -0,0 +1,10 @@ +# +# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +knit.package=kotlinx.coroutines.examples +knit.dir=jvm/test/examples/ + +test.package=kotlinx.coroutines.examples.test +test.dir=jvm/test/examples/test/ + diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt index bcc7f48963..4ec1289ee7 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt @@ -16,8 +16,8 @@ internal actual object DefaultExecutor : CoroutineDispatcher(), Delay { takeEventLoop().dispatch(context, block) override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = - takeEventLoop().invokeOnTimeout(timeMillis, block) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + takeEventLoop().invokeOnTimeout(timeMillis, block, context) actual fun enqueue(task: Runnable): Unit = loopWasShutDown() } diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt index aca1cc0693..c06b7c2f0a 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt @@ -7,24 +7,16 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default) - public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing } private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { - override val immediate: MainCoroutineDispatcher get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native") - override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt index d6c6525504..b397d6f182 100644 --- a/kotlinx-coroutines-core/native/src/EventLoop.kt +++ b/kotlinx-coroutines-core/native/src/EventLoop.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlin.coroutines.* import kotlin.system.* internal actual abstract class EventLoopImplPlatform: EventLoop() { @@ -13,7 +14,7 @@ internal actual abstract class EventLoopImplPlatform: EventLoop() { } internal class EventLoopImpl: EventLoopImplBase() { - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) } diff --git a/kotlinx-coroutines-core/native/src/WorkerMain.native.kt b/kotlinx-coroutines-core/native/src/WorkerMain.native.kt new file mode 100644 index 0000000000..84cc9f42b9 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/WorkerMain.native.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +// It is used in the main sources of native-mt branch +internal expect inline fun workerMain(block: () -> Unit) diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt index 9657830e35..99ab042f3c 100644 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt @@ -124,6 +124,8 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { return null } + actual open fun onRemoved(affected: Node) {} + actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode val failure = failure(affected) diff --git a/kotlinx-coroutines-core/native/test/WorkerTest.kt b/kotlinx-coroutines-core/native/test/WorkerTest.kt index 84acedac94..d6b5fad182 100644 --- a/kotlinx-coroutines-core/native/test/WorkerTest.kt +++ b/kotlinx-coroutines-core/native/test/WorkerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -19,6 +19,7 @@ class WorkerTest : TestBase() { delay(1) } }.result + worker.requestTermination() } @Test @@ -31,5 +32,6 @@ class WorkerTest : TestBase() { }.join() } }.result + worker.requestTermination() } } diff --git a/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt new file mode 100644 index 0000000000..3445cb9897 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.cinterop.* + +internal actual inline fun workerMain(block: () -> Unit) { + autoreleasepool { + block() + } +} diff --git a/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt new file mode 100644 index 0000000000..78ed765967 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import platform.CoreFoundation.* +import kotlin.native.concurrent.* +import kotlin.native.internal.test.* +import kotlin.system.* + +// This is a separate entry point for tests in background +fun mainBackground(args: Array) { + val worker = Worker.start(name = "main-background") + worker.execute(TransferMode.SAFE, { args.freeze() }) { + val result = testLauncherEntryPoint(it) + exitProcess(result) + } + CFRunLoopRun() + error("CFRunLoopRun should never return") +} + +// This is a separate entry point for tests with leak checker +fun mainNoExit(args: Array) { + workerMain { // autoreleasepool to make sure interop objects are properly freed + testLauncherEntryPoint(args) + } +} \ No newline at end of file diff --git a/js/example-frontend-js/src/main/web/main.js b/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt similarity index 51% rename from js/example-frontend-js/src/main/web/main.js rename to kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt index d2440ffaef..cac0530e4e 100644 --- a/js/example-frontend-js/src/main/web/main.js +++ b/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt @@ -2,7 +2,6 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -// ------ Main bundle for example application ------ +package kotlinx.coroutines -require("example-frontend"); -require("style.css"); +internal actual inline fun workerMain(block: () -> Unit) = block() diff --git a/kotlinx-coroutines-core/nativeOther/test/Launcher.kt b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt new file mode 100644 index 0000000000..feddd4c097 --- /dev/null +++ b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.native.concurrent.* +import kotlin.native.internal.test.* +import kotlin.system.* + +// This is a separate entry point for tests in background +fun mainBackground(args: Array) { + val worker = Worker.start(name = "main-background") + worker.execute(TransferMode.SAFE, { args.freeze() }) { + val result = testLauncherEntryPoint(it) + exitProcess(result) + }.result // block main thread +} + +// This is a separate entry point for tests with leak checker +fun mainNoExit(args: Array) { + testLauncherEntryPoint(args) +} \ No newline at end of file diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 81e62e772a..5518e00ef3 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -23,7 +23,7 @@ https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.8' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.0-M1' } ``` @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.8.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.0-M1.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. @@ -170,6 +170,98 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana at kotlinx.coroutines.debug.DebugProbes.install(DebugProbes.kt:49) --> +#### Build failures due to duplicate resource files + +Building an Android project that depends on `kotlinx-coroutines-debug` (usually introduced by being a transitive +dependency of `kotlinx-coroutines-test`) may fail with `DuplicateRelativeFileException` for `META-INF/AL2.0`, +`META-INF/LGPL2.1`, or `win32-x86/attach_hotspot_windows.dll` when trying to merge the Android resource. + +The problem is that Android merges the resources of all its dependencies into a single directory and complains about +conflicts, but: +* `kotlinx-coroutines-debug` transitively depends on JNA and JNA-platform, both of which include license files in their + META-INF directories. Trying to merge these files leads to conflicts, which means that any Android project that + depends on JNA and JNA-platform will experience build failures. +* Additionally, `kotlinx-coroutines-debug` embeds `byte-buddy-agent` and `byte-buddy`, along with their resource files. + Then, if the project separately depends on `byte-buddy`, merging the resources of `kotlinx-coroutines-debug` with ones + from `byte-buddy` and `byte-buddy-agent` will lead to conflicts as the resource files are duplicated. + +One possible workaround for these issues is to add the following to the `android` block in your gradle file for the +application subproject: +```groovy + packagingOptions { + // for JNA and JNA-platform + exclude "META-INF/AL2.0" + exclude "META-INF/LGPL2.1" + // for byte-buddy + exclude "META-INF/licenses/ASM" + pickFirst "win32-x86-64/attach_hotspot_windows.dll" + pickFirst "win32-x86/attach_hotspot_windows.dll" + } +``` +This will cause the resource merge algorithm to exclude the problematic license files altogether and only leave a single +copy of the files needed for `byte-buddy-agent` to work. + +Alternatively, avoid depending on `kotlinx-coroutines-debug`. In particular, if the only reason why this library a +dependency of your project is that `kotlinx-coroutines-test` in turn depends on it, you may change your dependency on +`kotlinx.coroutines.test` to exclude `kotlinx-coroutines-debug`. For example, you could replace +```kotlin +androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") +``` +with +```groovy +androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" +} +``` + [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index 8507721e30..fd0279123f 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -115,16 +115,22 @@ class CoroutinesDumpTest : DebugTestBase() { coroutineThread!!.interrupt() val expected = - ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" + - "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" + - "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" + - "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + - "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)").trimStackTrace() - assertTrue(result.startsWith(expected)) + "kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable\$default(Cancellable.kt)\n" + + "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt)\n" + + "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)" + if (!result.startsWith(expected)) { + println("=== Actual result") + println(result) + error("Does not start with expected lines") + } + } @Test diff --git a/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt b/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt deleted file mode 100644 index bf34917b77..0000000000 --- a/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -import kotlinx.coroutines.* -import kotlinx.coroutines.debug.* -import org.junit.* - -/** - * This stress tests ensure that no actual [OutOfMemoryError] occurs when lots of coroutines are created and - * leaked in various ways under debugger. A faster but more fragile version of this test is in [DebugLeaksTest]. - */ -class DebugLeaksStressTest : DebugTestBase() { - private val nRepeat = 100_000 * stressTestMultiplier - private val nBytes = 100_000 - - @Test - fun testIteratorLeak() { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - iterator { yield(bytes) } - } - } - - @Test - fun testLazyGlobalCoroutineLeak() { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - GlobalScope.launch(start = CoroutineStart.LAZY) { println(bytes) } - } - } - - @Test - fun testLazyCancelledChildCoroutineLeak() = runTest { - coroutineScope { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - val child = launch(start = CoroutineStart.LAZY) { println(bytes) } - child.cancel() - } - } - } - - @Test - fun testAbandonedGlobalCoroutineLeak() { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - GlobalScope.launch { - suspendForever() - println(bytes) - } - } - } - - private suspend fun suspendForever() = suspendCancellableCoroutine { } -} diff --git a/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/kotlinx-coroutines-debug/test/DebugProbesTest.kt index 24050e563c..3b32db3a5a 100644 --- a/kotlinx-coroutines-debug/test/DebugProbesTest.kt +++ b/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -40,24 +40,25 @@ class DebugProbesTest : DebugTestBase() { val deferred = createDeferred() val traces = listOf( "java.util.concurrent.ExecutionException\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" + "\t(Coroutine boundary)\n" + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt)\n" + "\t(Coroutine creation stacktrace)\n" + - "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + - "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + - "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable\$default(Cancellable.kt)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt)\n" + "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)\n" + - "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:138)\n" + - "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:19)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:38)", + "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt)", "Caused by: java.util.concurrent.ExecutionException\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt)\n") nestedMethod(deferred, traces) deferred.join() } diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 97a1178f3c..b82fe8577e 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0-M1' } ``` diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index e3b1f73e4f..c99ec5cbf1 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -25,7 +25,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun getCurrentTime ()J - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun pauseDispatcher ()V public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun resumeDispatcher ()V diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt index 4706d627ef..cad2636f97 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt @@ -65,7 +65,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val node = postDelayed(block, timeMillis) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt index c18e4108bb..baa1aa5fd2 100644 --- a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt +++ b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt @@ -46,8 +46,8 @@ internal class TestMainDispatcher(private val mainFactory: MainDispatcherFactory delay.delay(time) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - return delay.invokeOnTimeout(timeMillis, block) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + return delay.invokeOnTimeout(timeMillis, block, context) } public fun setDispatcher(dispatcher: CoroutineDispatcher) { diff --git a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt index 0013a654a6..e21c82b95c 100644 --- a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt @@ -54,11 +54,11 @@ class TestRunBlockingOrderTest : TestBase() { } @Test - fun testInfiniteDelay() = runBlockingTest { + fun testVeryLongDelay() = runBlockingTest { expect(1) delay(100) // move time forward a bit some that naive time + delay gives an overflow launch { - delay(Long.MAX_VALUE) // infinite delay + delay(Long.MAX_VALUE / 2) // very long delay finish(4) } launch { diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index d4bc1698ef..1f5bdec7d0 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -15,6 +15,8 @@ public final class kotlinx/coroutines/jdk9/PublishKt { public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; + public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; + public static synthetic fun asPublisher$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; public static final fun collect (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle b/reactive/kotlinx-coroutines-jdk9/build.gradle deleted file mode 100644 index 8737e8ed6d..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -targetCompatibility = 9 - -dependencies { - compile project(":kotlinx-coroutines-reactive") - compile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" -} - -compileTestKotlin { - kotlinOptions.jvmTarget = "9" -} - -compileKotlin { - kotlinOptions.jvmTarget = "9" -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle.kts b/reactive/kotlinx-coroutines-jdk9/build.gradle.kts new file mode 100644 index 0000000000..c721746f3b --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + compile(project(":kotlinx-coroutines-reactive")) + compile("org.reactivestreams:reactive-streams-flow-adapters:${version("reactive_streams")}") +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "9" + } + + compileTestKotlin { + kotlinOptions.jvmTarget = "9" + } +} + +externalDocumentationLink( + url = "https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html" +) diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index 89caf82c54..5d546dffd3 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -4,12 +4,14 @@ package kotlinx.coroutines.jdk9 +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asPublisher import kotlinx.coroutines.reactive.collect +import org.reactivestreams.* +import kotlin.coroutines.* import java.util.concurrent.Flow as JFlow -import org.reactivestreams.FlowAdapters /** * Transforms the given reactive [Publisher] into [Flow]. @@ -25,9 +27,15 @@ public fun JFlow.Publisher.asFlow(): Flow = /** * Transforms the given flow to a reactive specification compliant [Publisher]. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -public fun Flow.asPublisher(): JFlow.Publisher { - val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher() +@JvmOverloads // binary compatibility +public fun Flow.asPublisher(context: CoroutineContext = EmptyCoroutineContext): JFlow.Publisher { + val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher(context) return FlowAdapters.toFlowPublisher(reactivePublisher) } diff --git a/reactive/kotlinx-coroutines-reactive/README.md b/reactive/kotlinx-coroutines-reactive/README.md index 0a59b2c251..aed262263d 100644 --- a/reactive/kotlinx-coroutines-reactive/README.md +++ b/reactive/kotlinx-coroutines-reactive/README.md @@ -6,7 +6,7 @@ Coroutine builders: | **Name** | **Result** | **Scope** | **Description** | --------------- | ----------------------------- | ---------------- | --------------- -| [publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe +| [kotlinx.coroutines.reactive.publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe Integration with [Flow]: @@ -37,7 +37,7 @@ Suspending extension functions and suspending iteration: [ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html -[publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html +[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html [Publisher.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/as-flow.html [Flow.asPublisher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/kotlinx.coroutines.flow.-flow/as-publisher.html [org.reactivestreams.Publisher.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-first.html diff --git a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api index bed065d582..961fdbe238 100644 --- a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api +++ b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api @@ -5,6 +5,9 @@ public final class kotlinx/coroutines/reactive/AwaitKt { public static final fun awaitFirstOrNull (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitLast (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitSingle (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingleOrDefault (Lorg/reactivestreams/Publisher;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingleOrElse (Lorg/reactivestreams/Publisher;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingleOrNull (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class kotlinx/coroutines/reactive/ChannelKt { @@ -32,7 +35,7 @@ public final class kotlinx/coroutines/reactive/FlowKt { public final class kotlinx/coroutines/reactive/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, org/reactivestreams/Subscription { public final field flow Lkotlinx/coroutines/flow/Flow; public final field subscriber Lorg/reactivestreams/Subscriber; - public fun (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;)V + public fun (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;Lkotlin/coroutines/CoroutineContext;)V public fun cancel ()V public fun request (J)V } @@ -65,5 +68,7 @@ public final class kotlinx/coroutines/reactive/PublisherCoroutine : kotlinx/coro public final class kotlinx/coroutines/reactive/ReactiveFlowKt { public static final fun asFlow (Lorg/reactivestreams/Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Lorg/reactivestreams/Publisher; + public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher; + public static synthetic fun asPublisher$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; } diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts index c69148fecf..2ace4f9fcc 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -2,10 +2,6 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink -import org.jetbrains.dokka.gradle.DokkaTask -import java.net.URL - val reactiveStreamsVersion = property("reactive_streams_version") dependencies { @@ -13,31 +9,28 @@ dependencies { testCompile("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion") } -tasks { - val testNG = register("testNG") { - useTestNG() - reports.html.destination = file("$buildDir/reports/testng") - include("**/*ReactiveStreamTckTest.*") - // Skip testNG when tests are filtered with --tests, otherwise it simply fails - onlyIf { - filter.includePatterns.isEmpty() - } - doFirst { - // Classic gradle, nothing works without doFirst - println("TestNG tests: ($includes)") - } +val testNG by tasks.registering(Test::class) { + useTestNG() + reports.html.destination = file("$buildDir/reports/testng") + include("**/*ReactiveStreamTckTest.*") + // Skip testNG when tests are filtered with --tests, otherwise it simply fails + onlyIf { + filter.includePatterns.isEmpty() } - - named("test") { - reports.html.destination = file("$buildDir/reports/junit") - - dependsOn(testNG) + doFirst { + // Classic gradle, nothing works without doFirst + println("TestNG tests: ($includes)") } +} - withType().configureEach { - externalDocumentationLink(delegateClosureOf { - url = URL("https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - }) - } +tasks.test { + reports.html.destination = file("$buildDir/reports/junit") +} + +tasks.check { + dependsOn(testNG) } + +externalDocumentationLink( + url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/" +) diff --git a/reactive/kotlinx-coroutines-reactive/src/Await.kt b/reactive/kotlinx-coroutines-reactive/src/Await.kt index 9ea2e3c50e..7956c26010 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Await.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Await.kt @@ -80,13 +80,53 @@ public suspend fun Publisher.awaitLast(): T = awaitOne(Mode.LAST) */ public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) +/** + * Awaits for the single value from the given publisher or the [default] value if none is emitted without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingleOrDefault(default: T): T = awaitOne(Mode.SINGLE_OR_DEFAULT, default) + +/** + * Awaits for the single value from the given publisher or `null` value if none is emitted without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingleOrNull(): T = awaitOne(Mode.SINGLE_OR_DEFAULT) + +/** + * Awaits for the single value from the given publisher or call [defaultValue] to get a value if none is emitted without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingleOrElse(defaultValue: () -> T): T = awaitOne(Mode.SINGLE_OR_DEFAULT) ?: defaultValue() + // ------------------------ private ------------------------ private enum class Mode(val s: String) { FIRST("awaitFirst"), FIRST_OR_DEFAULT("awaitFirstOrDefault"), LAST("awaitLast"), - SINGLE("awaitSingle"); + SINGLE("awaitSingle"), + SINGLE_OR_DEFAULT("awaitSingleOrDefault"); override fun toString(): String = s } @@ -114,8 +154,8 @@ private suspend fun Publisher.awaitOne( cont.resume(t) } } - Mode.LAST, Mode.SINGLE -> { - if (mode == Mode.SINGLE && seenValue) { + Mode.LAST, Mode.SINGLE, Mode.SINGLE_OR_DEFAULT -> { + if ((mode == Mode.SINGLE || mode == Mode.SINGLE_OR_DEFAULT) && seenValue) { subscription.cancel() if (cont.isActive) cont.resumeWithException(IllegalArgumentException("More than one onNext value for $mode")) @@ -134,7 +174,7 @@ private suspend fun Publisher.awaitOne( return } when { - mode == Mode.FIRST_OR_DEFAULT -> { + (mode == Mode.FIRST_OR_DEFAULT || mode == Mode.SINGLE_OR_DEFAULT) -> { cont.resume(default as T) } cont.isActive -> { diff --git a/reactive/kotlinx-coroutines-reactive/src/Channel.kt b/reactive/kotlinx-coroutines-reactive/src/Channel.kt index 379fc4ed53..26f14ec63d 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Channel.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Channel.kt @@ -48,7 +48,7 @@ public suspend inline fun Publisher.collect(action: (T) -> Unit): Unit = @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") private class SubscriptionChannel( private val request: Int -) : LinkedListChannel(), Subscriber { +) : LinkedListChannel(null), Subscriber { init { require(request >= 0) { "Invalid request size: $request" } } diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index efa9c9c9f1..5834220c40 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -34,16 +34,24 @@ public fun Publisher.asFlow(): Flow = * * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, * see its documentation for additional details. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) +@JvmOverloads // binary compatibility +public fun Flow.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher = + FlowAsPublisher(this, Dispatchers.Unconfined + context) private class PublisherAsFlow( private val publisher: Publisher, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - PublisherAsFlow(publisher, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + PublisherAsFlow(publisher, context, capacity, onBufferOverflow) /* * Suppress for Channel.CHANNEL_DEFAULT_CAPACITY. @@ -52,13 +60,15 @@ private class PublisherAsFlow( */ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") private val requestSize: Long - get() = when (capacity) { - Channel.CONFLATED -> Long.MAX_VALUE // request all and conflate incoming - Channel.RENDEZVOUS -> 1L // need to request at least one anyway - Channel.UNLIMITED -> Long.MAX_VALUE // reactive streams way to say "give all" must be Long.MAX_VALUE - Channel.BUFFERED -> Channel.CHANNEL_DEFAULT_CAPACITY.toLong() - else -> capacity.toLong().also { check(it >= 1) } - } + get() = + if (onBufferOverflow != BufferOverflow.SUSPEND) { + Long.MAX_VALUE // request all, since buffering strategy is to never suspend + } else when (capacity) { + Channel.RENDEZVOUS -> 1L // need to request at least one anyway + Channel.UNLIMITED -> Long.MAX_VALUE // reactive streams way to say "give all", must be Long.MAX_VALUE + Channel.BUFFERED -> Channel.CHANNEL_DEFAULT_CAPACITY.toLong() + else -> capacity.toLong().also { check(it >= 1) } + } override suspend fun collect(collector: FlowCollector) { val collectContext = coroutineContext @@ -78,7 +88,7 @@ private class PublisherAsFlow( } private suspend fun collectImpl(injectContext: CoroutineContext, collector: FlowCollector) { - val subscriber = ReactiveSubscriber(capacity, requestSize) + val subscriber = ReactiveSubscriber(capacity, onBufferOverflow, requestSize) // inject subscribe context into publisher publisher.injectCoroutineContext(injectContext).subscribe(subscriber) try { @@ -105,10 +115,14 @@ private class PublisherAsFlow( @Suppress("SubscriberImplementation") private class ReactiveSubscriber( capacity: Int, + onBufferOverflow: BufferOverflow, private val requestSize: Long ) : Subscriber { private lateinit var subscription: Subscription - private val channel = Channel(capacity) + + // This implementation of ReactiveSubscriber always uses "offer" in its onNext implementation and it cannot + // be reliable with rendezvous channel, so a rendezvous channel is replaced with buffer=1 channel + private val channel = Channel(if (capacity == Channel.RENDEZVOUS) 1 else capacity, onBufferOverflow) suspend fun takeNextOrNull(): T? = channel.receiveOrNull() @@ -153,11 +167,14 @@ internal fun Publisher.injectCoroutineContext(coroutineContext: Coroutine * Adapter that transforms [Flow] into TCK-complaint [Publisher]. * [cancel] invocation cancels the original flow. */ -@Suppress("PublisherImplementation") -private class FlowAsPublisher(private val flow: Flow) : Publisher { +@Suppress("ReactiveStreamsPublisherImplementation") +private class FlowAsPublisher( + private val flow: Flow, + private val context: CoroutineContext +) : Publisher { override fun subscribe(subscriber: Subscriber?) { if (subscriber == null) throw NullPointerException() - subscriber.onSubscribe(FlowSubscription(flow, subscriber)) + subscriber.onSubscribe(FlowSubscription(flow, subscriber, context)) } } @@ -165,8 +182,9 @@ private class FlowAsPublisher(private val flow: Flow) : Publisher @InternalCoroutinesApi public class FlowSubscription( @JvmField public val flow: Flow, - @JvmField public val subscriber: Subscriber -) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, true) { + @JvmField public val subscriber: Subscriber, + context: CoroutineContext +) : Subscription, AbstractCoroutine(context, true) { private val requested = atomic(0L) private val producer = atomic?>(createInitialContinuation()) diff --git a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt index c044d92725..e7b8cb17ae 100644 --- a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt @@ -8,10 +8,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test import org.reactivestreams.* +import java.util.concurrent.* import kotlin.test.* class FlowAsPublisherTest : TestBase() { - @Test fun testErrorOnCancellationIsReported() { expect(1) @@ -75,4 +75,78 @@ class FlowAsPublisherTest : TestBase() { }) finish(4) } + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asPublisher().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsPublisherTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asPublisher(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } } diff --git a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt index 6f7d98480b..18cd012d16 100644 --- a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt @@ -48,6 +48,9 @@ class IntegrationTest( assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) assertFailsWith { pub.awaitLast() } assertFailsWith { pub.awaitSingle() } + assertEquals("OK", pub.awaitSingleOrDefault("OK")) + assertNull(pub.awaitSingleOrNull()) + assertEquals("ELSE", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { cnt++ } assertEquals(0, cnt) @@ -65,6 +68,9 @@ class IntegrationTest( assertEquals("OK", pub.awaitFirstOrElse { "ELSE" }) assertEquals("OK", pub.awaitLast()) assertEquals("OK", pub.awaitSingle()) + assertEquals("OK", pub.awaitSingleOrDefault("!")) + assertEquals("OK", pub.awaitSingleOrNull()) + assertEquals("OK", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { assertEquals("OK", it) @@ -84,10 +90,13 @@ class IntegrationTest( } assertEquals(1, pub.awaitFirst()) assertEquals(1, pub.awaitFirstOrDefault(0)) - assertEquals(n, pub.awaitLast()) assertEquals(1, pub.awaitFirstOrNull()) assertEquals(1, pub.awaitFirstOrElse { 0 }) + assertEquals(n, pub.awaitLast()) assertFailsWith { pub.awaitSingle() } + assertFailsWith { pub.awaitSingleOrDefault(0) } + assertFailsWith { pub.awaitSingleOrNull() } + assertFailsWith { pub.awaitSingleOrElse { 0 } } checkNumbers(n, pub) val channel = pub.openSubscription() checkNumbers(n, channel.asPublisher(ctx(coroutineContext))) diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt index 61f88f6af3..04833e9814 100644 --- a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.reactive import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* +import org.reactivestreams.* import kotlin.test.* class PublisherAsFlowTest : TestBase() { @@ -181,4 +182,85 @@ class PublisherAsFlowTest : TestBase() { } finish(6) } + + @Test + fun testRequestRendezvous() = + testRequestSizeWithBuffer(Channel.RENDEZVOUS, BufferOverflow.SUSPEND, 1) + + @Test + fun testRequestBuffer1() = + testRequestSizeWithBuffer(1, BufferOverflow.SUSPEND, 1) + + @Test + fun testRequestBuffer10() = + testRequestSizeWithBuffer(10, BufferOverflow.SUSPEND, 10) + + @Test + fun testRequestBufferUnlimited() = + testRequestSizeWithBuffer(Channel.UNLIMITED, BufferOverflow.SUSPEND, Long.MAX_VALUE) + + @Test + fun testRequestBufferOverflowSuspend() = + testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.SUSPEND, 64) + + @Test + fun testRequestBufferOverflowDropOldest() = + testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.DROP_OLDEST, Long.MAX_VALUE) + + @Test + fun testRequestBufferOverflowDropLatest() = + testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.DROP_LATEST, Long.MAX_VALUE) + + @Test + fun testRequestBuffer10OverflowDropOldest() = + testRequestSizeWithBuffer(10, BufferOverflow.DROP_OLDEST, Long.MAX_VALUE) + + @Test + fun testRequestBuffer10OverflowDropLatest() = + testRequestSizeWithBuffer(10, BufferOverflow.DROP_LATEST, Long.MAX_VALUE) + + /** + * Tests `publisher.asFlow.buffer(...)` chain, verifying expected requests size and that only expected + * values are delivered. + */ + private fun testRequestSizeWithBuffer( + capacity: Int, + onBufferOverflow: BufferOverflow, + expectedRequestSize: Long + ) = runTest { + val m = 50 + // publishers numbers from 1 to m + val publisher = Publisher { s -> + s.onSubscribe(object : Subscription { + var lastSent = 0 + var remaining = 0L + override fun request(n: Long) { + assertEquals(expectedRequestSize, n) + remaining += n + check(remaining >= 0) + while (lastSent < m && remaining > 0) { + s.onNext(++lastSent) + remaining-- + } + if (lastSent == m) s.onComplete() + } + + override fun cancel() {} + }) + } + val flow = publisher + .asFlow() + .buffer(capacity, onBufferOverflow) + val list = flow.toList() + val runSize = if (capacity == Channel.BUFFERED) 1 else capacity + val expected = when (onBufferOverflow) { + // Everything is expected to be delivered + BufferOverflow.SUSPEND -> (1..m).toList() + // Only the last one (by default) or the last "capacity" items delivered + BufferOverflow.DROP_OLDEST -> (m - runSize + 1..m).toList() + // Only the first one (by default) or the first "capacity" items delivered + BufferOverflow.DROP_LATEST -> (1..runSize).toList() + } + assertEquals(expected, list) + } } diff --git a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api index 422f36b1ea..b46fe338e5 100644 --- a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api +++ b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api @@ -38,6 +38,8 @@ public final class kotlinx/coroutines/reactor/ReactorContextKt { public final class kotlinx/coroutines/reactor/ReactorFlowKt { public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;)Lreactor/core/publisher/Flux; + public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lreactor/core/publisher/Flux; + public static synthetic fun asFlux$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lreactor/core/publisher/Flux; } public final class kotlinx/coroutines/reactor/SchedulerCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay { @@ -47,7 +49,7 @@ public final class kotlinx/coroutines/reactor/SchedulerCoroutineDispatcher : kot public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lreactor/core/scheduler/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle b/reactive/kotlinx-coroutines-reactor/build.gradle deleted file mode 100644 index 3b640bd5cc..0000000000 --- a/reactive/kotlinx-coroutines-reactor/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile "io.projectreactor:reactor-core:$reactor_version" - compile project(':kotlinx-coroutines-reactive') -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://projectreactor.io/docs/core/$reactor_version/api/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} - -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" -} - -compileKotlin { - kotlinOptions.jvmTarget = "1.8" -} diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle.kts b/reactive/kotlinx-coroutines-reactor/build.gradle.kts new file mode 100644 index 0000000000..d5fd208a27 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactor/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +val reactorVersion = version("reactor") + +dependencies { + compile("io.projectreactor:reactor-core:$reactorVersion") + compile(project(":kotlinx-coroutines-reactive")) +} + + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "1.8" + } + + compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" + } +} + +externalDocumentationLink( + url = "https://projectreactor.io/docs/core/$reactorVersion/api/" +) diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt index d665c88d35..a478ab1ef8 100644 --- a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt +++ b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt @@ -4,25 +4,38 @@ package kotlinx.coroutines.reactor +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.reactive.FlowSubscription +import org.reactivestreams.* import reactor.core.CoreSubscriber import reactor.core.publisher.Flux +import kotlin.coroutines.* /** * Converts the given flow to a cold flux. * The original flow is cancelled when the flux subscriber is disposed. * * This function is integrated with [ReactorContext], see its documentation for additional details. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -public fun Flow.asFlux(): Flux = FlowAsFlux(this) +@JvmOverloads // binary compatibility +public fun Flow.asFlux(context: CoroutineContext = EmptyCoroutineContext): Flux = + FlowAsFlux(this, Dispatchers.Unconfined + context) -private class FlowAsFlux(private val flow: Flow) : Flux() { +private class FlowAsFlux( + private val flow: Flow, + private val context: CoroutineContext +) : Flux() { override fun subscribe(subscriber: CoreSubscriber?) { if (subscriber == null) throw NullPointerException() val hasContext = !subscriber.currentContext().isEmpty val source = if (hasContext) flow.flowOn(subscriber.currentContext().asCoroutineContext()) else flow - subscriber.onSubscribe(FlowSubscription(source, subscriber)) + subscriber.onSubscribe(FlowSubscription(source, subscriber, context)) } } diff --git a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt index e176c07bb9..4fb5514322 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt @@ -39,7 +39,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = scheduler.schedule(block, timeMillis, TimeUnit.MILLISECONDS).asDisposableHandle() /** @suppress */ diff --git a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt index e4bd8b315b..cecc89592e 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt @@ -4,10 +4,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* import org.junit.Test +import org.reactivestreams.* import reactor.core.publisher.* import reactor.util.context.Context +import java.util.concurrent.* import kotlin.test.* +@Suppress("ReactiveStreamsSubscriberImplementation") class FlowAsFluxTest : TestBase() { @Test fun testFlowAsFluxContextPropagation() { @@ -68,4 +71,78 @@ class FlowAsFluxTest : TestBase() { } finish(4) } -} \ No newline at end of file + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asFlux().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsFluxTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asFlux(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt index 3879c62c71..cc336ba6b5 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt @@ -68,6 +68,72 @@ class FluxSingleTest : TestBase() { } } + @Test + fun testAwaitSingleOrDefault() { + val flux = flux { + send(Flux.empty().awaitSingleOrDefault("O") + "K") + } + + checkSingleValue(flux) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitSingleOrDefaultException() { + val flux = flux { + send(Flux.just("O", "#").awaitSingleOrDefault("!") + "K") + } + + checkErroneous(flux) { + assert(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitSingleOrNull() { + val flux = flux { + send(Flux.empty().awaitSingleOrNull() ?: "OK") + } + + checkSingleValue(flux) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitSingleOrNullException() { + val flux = flux { + send((Flux.just("O", "#").awaitSingleOrNull() ?: "!") + "K") + } + + checkErroneous(flux) { + assert(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitSingleOrElse() { + val flux = flux { + send(Flux.empty().awaitSingleOrElse { "O" } + "K") + } + + checkSingleValue(flux) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitSingleOrElseException() { + val flux = flux { + send(Flux.just("O", "#").awaitSingleOrElse { "!" } + "K") + } + + checkErroneous(flux) { + assert(it is IllegalArgumentException) + } + } + @Test fun testAwaitFirst() { val flux = flux { diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api index 22f40384f0..4370325f58 100644 --- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api +++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api @@ -30,11 +30,19 @@ public final class kotlinx/coroutines/rx2/RxCompletableKt { public final class kotlinx/coroutines/rx2/RxConvertKt { public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Completable; public static final fun asFlow (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlowable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; + public static synthetic fun asFlowable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Maybe; public static final fun asObservable (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static final fun asObservable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static synthetic fun asObservable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Single; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; } public final class kotlinx/coroutines/rx2/RxFlowableKt { @@ -76,7 +84,7 @@ public final class kotlinx/coroutines/rx2/SchedulerCoroutineDispatcher : kotlinx public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lio/reactivex/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt index e253161db0..633693e756 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt @@ -64,7 +64,7 @@ public suspend inline fun ObservableSource.collect(action: (T) -> Unit): @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private class SubscriptionChannel : - LinkedListChannel(), Observer, MaybeObserver + LinkedListChannel(null), Observer, MaybeObserver { private val _subscription = atomic(null) diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index 0be606ffc2..cf73ef2ea8 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -10,12 +10,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* +import org.reactivestreams.* import java.util.concurrent.atomic.* import kotlin.coroutines.* /** * Converts this job to the hot reactive completable that signals - * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. + * with [onCompleted][CompletableObserver.onComplete] when the corresponding job completes. * * Every subscriber gets the signal at the same time. * Unsubscribing from the resulting completable **does not** affect the original job in any way. @@ -49,7 +50,7 @@ public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMay /** * Converts this deferred value to the hot reactive single that signals either - * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. + * [onSuccess][SingleObserver.onSuccess] or [onError][SingleObserver.onError]. * * Every subscriber gets the same completion value. * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. @@ -64,21 +65,6 @@ public fun Deferred.asSingle(context: CoroutineContext): Single this@asSingle.await() } -/** - * Converts a stream of elements received from the channel to the hot reactive observable. - * - * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers, - * they'll receive values in round-robin way. - */ -@Deprecated( - message = "Deprecated in the favour of Flow", - level = DeprecationLevel.WARNING, replaceWith = ReplaceWith("this.consumeAsFlow().asObservable()") -) -public fun ReceiveChannel.asObservable(context: CoroutineContext): Observable = rxObservable(context) { - for (t in this@asObservable) - send(t) -} - /** * Transforms given cold [ObservableSource] into cold [Flow]. * @@ -106,15 +92,19 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { /** * Converts the given flow to a cold observable. * The original flow is cancelled when the observable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Observer] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -@JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asObservable() : Observable = Observable.create { emitter -> +public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ - val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { + val job = GlobalScope.launch(Dispatchers.Unconfined + context, start = CoroutineStart.ATOMIC) { try { collect { value -> emitter.onNext(value) } emitter.onComplete() @@ -135,7 +125,35 @@ public fun Flow.asObservable() : Observable = Observable.create { /** * Converts the given flow to a cold flowable. * The original flow is cancelled when the flowable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -@JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) +public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + Flowable.fromPublisher(asPublisher(context)) + +@Deprecated( + message = "Deprecated in the favour of Flow", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("this.consumeAsFlow().asObservable(context)", "kotlinx.coroutines.flow.consumeAsFlow") +) // Deprecated since 1.4.0 +public fun ReceiveChannel.asObservable(context: CoroutineContext): Observable = rxObservable(context) { + for (t in this@asObservable) + send(t) +} + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + asFlowable(context) + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = asObservable(context) diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt index 3ddb67649e..9952eb91a0 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt @@ -38,7 +38,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) return DisposableHandle { disposable.dispose() } } diff --git a/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt b/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt index a43366555e..cfc3240741 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* import org.junit.Assert import org.junit.Test import kotlin.test.* @@ -126,7 +127,7 @@ class ConvertTest : TestBase() { delay(50) send("K") } - val observable = c.asObservable(Dispatchers.Unconfined) + val observable = c.consumeAsFlow().asObservable(Dispatchers.Unconfined) checkSingleValue(observable.reduce { t1, t2 -> t1 + t2 }.toSingle()) { assertEquals("OK", it) } @@ -140,7 +141,7 @@ class ConvertTest : TestBase() { delay(50) throw TestException("K") } - val observable = c.asObservable(Dispatchers.Unconfined) + val observable = c.consumeAsFlow().asObservable(Dispatchers.Unconfined) val single = rxSingle(Dispatchers.Unconfined) { var result = "" try { @@ -155,4 +156,4 @@ class ConvertTest : TestBase() { assertEquals("OK", it) } } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt new file mode 100644 index 0000000000..1cbded6dc3 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx2 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import org.reactivestreams.* +import java.util.concurrent.* +import kotlin.test.* + +@Suppress("ReactiveStreamsSubscriberImplementation") +class FlowAsFlowableTest : TestBase() { + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asFlowable().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsFlowableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asFlowable(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt index 0908b34cf2..3cde182260 100644 --- a/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt @@ -4,9 +4,12 @@ package kotlinx.coroutines.rx2 +import io.reactivex.* +import io.reactivex.disposables.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test +import java.util.concurrent.* import kotlin.test.* class FlowAsObservableTest : TestBase() { @@ -139,4 +142,70 @@ class FlowAsObservableTest : TestBase() { observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) } + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asObservable().subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsObservableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asObservable(dispatcher).subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(e: Throwable) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } } diff --git a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt index 22e0e72191..540fa76b7e 100644 --- a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import org.junit.Test import org.junit.runner.* import org.junit.runners.* @@ -92,7 +93,7 @@ class IntegrationTest( assertFailsWith { observable.awaitSingle() } checkNumbers(n, observable) val channel = observable.openSubscription() - checkNumbers(n, channel.asObservable(ctx(coroutineContext))) + checkNumbers(n, channel.consumeAsFlow().asObservable(ctx(coroutineContext))) channel.cancel() } @@ -131,4 +132,4 @@ class IntegrationTest( assertEquals(n, last) } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api index 27c3d3dfa0..6d2dd63d2c 100644 --- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -26,10 +26,18 @@ public final class kotlinx/coroutines/rx3/RxCompletableKt { public final class kotlinx/coroutines/rx3/RxConvertKt { public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Completable; public static final fun asFlow (Lio/reactivex/rxjava3/core/ObservableSource;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlowable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; + public static synthetic fun asFlowable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Maybe; + public static final fun asObservable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; + public static synthetic fun asObservable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Single; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; } public final class kotlinx/coroutines/rx3/RxFlowableKt { @@ -63,7 +71,7 @@ public final class kotlinx/coroutines/rx3/SchedulerCoroutineDispatcher : kotlinx public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lio/reactivex/rxjava3/core/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt index acb907b765..737cf6710d 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt @@ -54,7 +54,7 @@ public suspend inline fun ObservableSource.collect(action: (T) -> Unit): @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private class SubscriptionChannel : - LinkedListChannel(), Observer, MaybeObserver + LinkedListChannel(null), Observer, MaybeObserver { private val _subscription = atomic(null) diff --git a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt index f9e2e2158f..9bb38c088f 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt @@ -10,12 +10,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* +import org.reactivestreams.* import java.util.concurrent.atomic.* import kotlin.coroutines.* /** * Converts this job to the hot reactive completable that signals - * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. + * with [onCompleted][CompletableObserver.onComplete] when the corresponding job completes. * * Every subscriber gets the signal at the same time. * Unsubscribing from the resulting completable **does not** affect the original job in any way. @@ -49,7 +50,7 @@ public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMay /** * Converts this deferred value to the hot reactive single that signals either - * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. + * [onSuccess][SingleObserver.onSuccess] or [onError][SingleObserver.onError]. * * Every subscriber gets the same completion value. * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. @@ -91,15 +92,19 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { /** * Converts the given flow to a cold observable. * The original flow is cancelled when the observable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Observer] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -@JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asObservable() : Observable = Observable.create { emitter -> +public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ - val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { + val job = GlobalScope.launch(Dispatchers.Unconfined + context, start = CoroutineStart.ATOMIC) { try { collect { value -> emitter.onNext(value) } emitter.onComplete() @@ -120,7 +125,25 @@ public fun Flow.asObservable() : Observable = Observable.create { /** * Converts the given flow to a cold flowable. * The original flow is cancelled when the flowable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -@JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) +public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + Flowable.fromPublisher(asPublisher(context)) + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + asFlowable(context) + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = asObservable(context) diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt index 6e91aeea85..a426aea6ba 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt @@ -38,7 +38,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) return DisposableHandle { disposable.dispose() } } diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt new file mode 100644 index 0000000000..a73fee469e --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import org.reactivestreams.* +import java.util.concurrent.* +import kotlin.test.* + +@Suppress("ReactiveStreamsSubscriberImplementation") +class FlowAsFlowableTest : TestBase() { + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asFlowable().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsFlowableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asFlowable(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt index 50c4ae7dad..5759f9f426 100644 --- a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt @@ -4,9 +4,12 @@ package kotlinx.coroutines.rx3 +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test +import java.util.concurrent.* import kotlin.test.* class FlowAsObservableTest : TestBase() { @@ -139,4 +142,70 @@ class FlowAsObservableTest : TestBase() { observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) } + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asObservable().subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsObservableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asObservable(dispatcher).subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(e: Throwable) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } } diff --git a/site/build.gradle.kts b/site/build.gradle.kts index a18062a371..eba7b1a1bc 100644 --- a/site/build.gradle.kts +++ b/site/build.gradle.kts @@ -7,7 +7,7 @@ import groovy.lang.* val buildDocsDir = "$buildDir/docs" val jekyllDockerImage = "jekyll/jekyll:${version("jekyll")}" -val copyDocs = tasks.register("copyDocs") { +val copyDocs by tasks.registering(Copy::class) { val dokkaTasks = rootProject.getTasksByName("dokka", true) from(dokkaTasks.map { "${it.project.buildDir}/dokka" }) { @@ -21,12 +21,12 @@ val copyDocs = tasks.register("copyDocs") { dependsOn(dokkaTasks) } -val copyExampleFrontendJs = tasks.register("copyExampleFrontendJs") { +val copyExampleFrontendJs by tasks.registering(Copy::class) { val srcBuildDir = project(":example-frontend-js").buildDir from("$srcBuildDir/dist") into("$buildDocsDir/example-frontend-js") - dependsOn(":example-frontend-js:bundle") + dependsOn(":example-frontend-js:browserDistribution") } tasks.register("site") { diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 071f794cb2..5df8d7f0ed 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your @@ -310,7 +310,7 @@ processing the previous one. The [actor] coroutine builder accepts an optional controls the implementation of the channel that this actor is using for its mailbox. The description of all the available choices is given in documentation of the [`Channel()`][Channel] factory function. -Let us change the code to use `ConflatedChannel` by passing [Channel.CONFLATED] capacity value. The +Let us change the code to use a conflated channel by passing [Channel.CONFLATED] capacity value. The change is only to the line that creates an actor: ```kotlin diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt index 9cf813bc3a..c677d9911a 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt @@ -21,7 +21,7 @@ public class TestComponent { fun launchDelayed() { scope.launch { - delay(Long.MAX_VALUE) + delay(Long.MAX_VALUE / 2) delayedLaunchCompleted = true } } diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 0be3b9c1cb..cd34eb1285 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -20,8 +20,8 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.3.71 -coroutines_version=1.3.8 +kotlin_version=1.4.0 +coroutines_version=1.4.0-M1 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api b/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api index b97d8462c3..090c14e09c 100644 --- a/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api +++ b/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api @@ -1,7 +1,7 @@ public abstract class kotlinx/coroutines/android/HandlerDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getImmediate ()Lkotlinx/coroutines/android/HandlerDispatcher; - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; } public final class kotlinx/coroutines/android/HandlerDispatcherKt { diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts index 4be32fc5c6..4f24788359 100644 --- a/ui/kotlinx-coroutines-android/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/build.gradle.kts @@ -6,10 +6,6 @@ import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink import org.jetbrains.dokka.gradle.DokkaTask import java.net.URL -repositories { - google() -} - configurations { create("r8") } @@ -25,59 +21,20 @@ dependencies { "r8"("com.android.tools.build:builder:4.0.0-alpha06") // Contains r8-2.0.4-dev } -open class RunR8Task : JavaExec() { - - @OutputDirectory - lateinit var outputDex: File - - @InputFile - lateinit var inputConfig: File - - @InputFile - val inputConfigCommon: File = File("testdata/r8-test-common.pro") - - @InputFiles - val jarFile: File = project.tasks.named("jar").get().archivePath - - init { - classpath = project.configurations["r8"] - main = "com.android.tools.r8.R8" - } - - override fun exec() { - // Resolve classpath only during execution - val arguments = mutableListOf( - "--release", - "--no-desugaring", - "--output", outputDex.absolutePath, - "--pg-conf", inputConfig.absolutePath - ) - arguments.addAll(project.configurations.runtimeClasspath.files.map { it.absolutePath }) - arguments.add(jarFile.absolutePath) - - args = arguments - - project.delete(outputDex) - outputDex.mkdirs() - - super.exec() - } -} - val optimizedDexDir = File(buildDir, "dex-optim/") val unOptimizedDexDir = File(buildDir, "dex-unoptim/") val optimizedDexFile = File(optimizedDexDir, "classes.dex") val unOptimizedDexFile = File(unOptimizedDexDir, "classes.dex") -val runR8 = tasks.register("runR8") { +val runR8 by tasks.registering(RunR8::class) { outputDex = optimizedDexDir inputConfig = file("testdata/r8-test-rules.pro") dependsOn("jar") } -val runR8NoOptim = tasks.register("runR8NoOptim") { +val runR8NoOptim by tasks.registering(RunR8::class) { outputDex = unOptimizedDexDir inputConfig = file("testdata/r8-test-rules-no-optim.pro") @@ -100,9 +57,6 @@ tasks.test { } } -tasks.withType().configureEach { - externalDocumentationLink(delegateClosureOf { - url = URL("https://developer.android.com/reference/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - }) -} +externalDocumentationLink( + url = "https://developer.android.com/reference/" +) diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 0be3b9c1cb..cd34eb1285 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -20,8 +20,8 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.3.71 -coroutines_version=1.3.8 +kotlin_version=1.4.0 +coroutines_version=1.4.0-M1 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro index fd25b215c3..0d04990ad9 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro @@ -9,6 +9,8 @@ boolean ANDROID_DETECTED return true; } +-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} + # Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher -assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt { boolean SUPPORT_MISSING return false; diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index 1693409875..af79da7c97 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -140,7 +140,7 @@ internal class HandlerContext private constructor( continuation.invokeOnCancellation { handler.removeCallbacks(block) } } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY)) return object : DisposableHandle { override fun dispose() { diff --git a/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api b/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api index 620e904612..e2c3b8f326 100644 --- a/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api +++ b/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api @@ -5,7 +5,7 @@ public final class kotlinx/coroutines/javafx/JavaFxConvertKt { public abstract class kotlinx/coroutines/javafx/JavaFxDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } diff --git a/ui/kotlinx-coroutines-javafx/build.gradle b/ui/kotlinx-coroutines-javafx/build.gradle deleted file mode 100644 index 77f1b09650..0000000000 --- a/ui/kotlinx-coroutines-javafx/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -plugins { - id 'org.openjfx.javafxplugin' -} - -javafx { - version = javafx_version - modules = ['javafx.controls'] - configuration = 'compile' -} - -task checkJdk8() { - // only fail w/o JDK_18 when actually trying to test, not during project setup phase - doLast { - if (!System.env.JDK_18) { - throw new GradleException("JDK_18 environment variable is not defined. " + - "Can't run JDK 8 compatibility tests. " + - "Please ensure JDK 8 is installed and that JDK_18 points to it.") - } - } -} - -task jdk8Test(type: Test, dependsOn: [compileTestKotlin, checkJdk8]) { - classpath = files { test.classpath } - testClassesDirs = files { test.testClassesDirs } - executable = "$System.env.JDK_18/bin/java" -} - -// Run these tests only during nightly stress test -jdk8Test.onlyIf { project.properties['stressTest'] != null } -build.dependsOn jdk8Test diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts new file mode 100644 index 0000000000..112441e0ed --- /dev/null +++ b/ui/kotlinx-coroutines-javafx/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("org.openjfx.javafxplugin") +} + +javafx { + version = version("javafx") + modules = listOf("javafx.controls") + configuration = "compile" +} + +val JDK_18: String? by lazy { + System.getenv("JDK_18") +} + +val checkJdk8 by tasks.registering { + // only fail w/o JDK_18 when actually trying to test, not during project setup phase + doLast { + if (JDK_18 == null) { + throw GradleException( + """ + JDK_18 environment variable is not defined. + Can't run JDK 8 compatibility tests. + Please ensure JDK 8 is installed and that JDK_18 points to it. + """.trimIndent() + ) + } + } +} + +val jdk8Test by tasks.registering(Test::class) { + // Run these tests only during nightly stress test + onlyIf { project.properties["stressTest"] != null } + + val test = tasks.test.get() + + classpath = test.classpath + testClassesDirs = test.testClassesDirs + executable = "$JDK_18/bin/java" + + dependsOn("compileTestKotlin") + dependsOn(checkJdk8) +} + +tasks.build { + dependsOn(jdk8Test) +} diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index a13a68368e..c3069d636f 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -42,7 +42,7 @@ public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay { } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler { block.run() }) diff --git a/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api b/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api index 09556e807f..d33191fd96 100644 --- a/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api +++ b/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api @@ -1,7 +1,7 @@ public abstract class kotlinx/coroutines/swing/SwingDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt index 77f109df91..054ed1f60e 100644 --- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt +++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt @@ -36,7 +36,7 @@ public sealed class SwingDispatcher : MainCoroutineDispatcher(), Delay { } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val timer = schedule(timeMillis, TimeUnit.MILLISECONDS, ActionListener { block.run() })