From d24a35c176fa39e37fd34e02426ecdeaf39cf1e5 Mon Sep 17 00:00:00 2001 From: Alex Riedler Date: Wed, 4 Dec 2024 08:29:47 -0500 Subject: [PATCH 1/2] fix(jvm): make job cancellation hashcode not fail after deserialization --- kotlinx-coroutines-core/jvm/src/Exceptions.kt | 4 +-- .../JobCancellationExceptionSerializerTest.kt | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index 6175595713..989a2c47ea 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -61,6 +61,6 @@ internal actual class JobCancellationException public actual constructor( override fun equals(other: Any?): Boolean = other === this || other is JobCancellationException && other.message == message && other.job == job && other.cause == cause - override fun hashCode(): Int = - (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) + override fun hashCode(): Int = /* since job is transient it is indeed nullable after deserialization */ + (message!!.hashCode() * 31 + (job?.hashCode() ?: 0)) * 31 + (cause?.hashCode() ?: 0) } diff --git a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt index c063e9457e..3352c64d6f 100644 --- a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt @@ -36,4 +36,31 @@ class JobCancellationExceptionSerializerTest : TestBase() { finish(4) } } + + @Test + fun testHashCodeAfterDeserialization() = runTest { + try { + coroutineScope { + val job = launch { + hang {} + } + throw JobCancellationException( + message = "Job Cancelled", + job = job, + cause = null, + ) + } + } catch (e: Throwable) { + val outputStream = ByteArrayOutputStream() + ObjectOutputStream(outputStream).use { + it.writeObject(e) + } + val deserializedException = + ObjectInputStream(outputStream.toByteArray().inputStream()).use { + it.readObject() as JobCancellationException + } + // verify hashCode does not fail even though Job is transient + assert(deserializedException.hashCode() != 0) + } + } } From 773f874aaf8835f6db8cde8066166478b7aba6fe Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 4 Dec 2024 19:01:55 +0100 Subject: [PATCH 2/2] ~more robust test --- kotlinx-coroutines-core/jvm/src/Exceptions.kt | 8 ++++++-- .../jvm/test/JobCancellationExceptionSerializerTest.kt | 7 +++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index 989a2c47ea..a7519862e0 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -61,6 +61,10 @@ internal actual class JobCancellationException public actual constructor( override fun equals(other: Any?): Boolean = other === this || other is JobCancellationException && other.message == message && other.job == job && other.cause == cause - override fun hashCode(): Int = /* since job is transient it is indeed nullable after deserialization */ - (message!!.hashCode() * 31 + (job?.hashCode() ?: 0)) * 31 + (cause?.hashCode() ?: 0) + + override fun hashCode(): Int { + // since job is transient it is indeed nullable after deserialization + @Suppress("UNNECESSARY_SAFE_CALL") + return (message!!.hashCode() * 31 + (job?.hashCode() ?: 0)) * 31 + (cause?.hashCode() ?: 0) + } } diff --git a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt index 3352c64d6f..18c3d29db5 100644 --- a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt @@ -41,16 +41,15 @@ class JobCancellationExceptionSerializerTest : TestBase() { fun testHashCodeAfterDeserialization() = runTest { try { coroutineScope { - val job = launch { - hang {} - } + expect(1) throw JobCancellationException( message = "Job Cancelled", - job = job, + job = Job(), cause = null, ) } } catch (e: Throwable) { + finish(2) val outputStream = ByteArrayOutputStream() ObjectOutputStream(outputStream).use { it.writeObject(e)