From 4a4cea49c4fc578bcda937de19be2d3516ef1a04 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 14 Nov 2025 13:59:48 -0500 Subject: [PATCH] Wip --- build.gradle.kts | 2 + buildSrc/build.gradle.kts | 1 + gradle/libs.versions.toml | 4 +- .../okhttp3/coroutines/Bug9191ReproTest.kt | 177 +++++++++++++++++- settings.gradle.kts | 7 + 5 files changed, 184 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9514641eb..515b91898 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ buildscript { repositories { mavenCentral() + mavenLocal() gradlePluginPortal() google() } @@ -55,6 +56,7 @@ allprojects { repositories { mavenCentral() + mavenLocal() google() } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 66da41144..f978a0b30 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -20,6 +20,7 @@ plugins { repositories { mavenCentral() + mavenLocal() gradlePluginPortal() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 503d0a018..7ab49b4aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -agp = "8.13.0" +agp = "8.12.0" biz-aQute-bnd = "7.1.0" checkStyle = "12.1.1" com-squareup-moshi = "1.15.2" -com-squareup-okio = "3.16.2" +com-squareup-okio = "3.17.0-SNAPSHOT" de-mannodermaus-junit5 = "1.9.0" graalvm = "25.0.1" #noinspection UnusedVersionCatalogEntry diff --git a/okhttp-coroutines/src/test/kotlin/okhttp3/coroutines/Bug9191ReproTest.kt b/okhttp-coroutines/src/test/kotlin/okhttp3/coroutines/Bug9191ReproTest.kt index 23c9e5d0c..1a7eeccfc 100644 --- a/okhttp-coroutines/src/test/kotlin/okhttp3/coroutines/Bug9191ReproTest.kt +++ b/okhttp-coroutines/src/test/kotlin/okhttp3/coroutines/Bug9191ReproTest.kt @@ -11,12 +11,15 @@ import java.net.ServerSocket import java.time.Duration import java.time.Instant import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.test.assertFailsWith import kotlinx.coroutines.runBlocking import okhttp3.Call import okhttp3.Dns import okhttp3.EventListener import okhttp3.OkHttpClient import okhttp3.Request +import okio.AsyncTimeout import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -29,9 +32,9 @@ class ReproduceOkHttpIssueTest { val proxyHost = "unresponsive-proxy-host" // 2. Set Timeouts - val callTimeout = Duration.ofSeconds(3) - val connectTimeout = Duration.ofSeconds(1) - val readTimeout = Duration.ofSeconds(2) + val callTimeout = Duration.ofSeconds(3 * 2) + val connectTimeout = Duration.ofSeconds(1 * 2) + val readTimeout = Duration.ofSeconds(2 * 2) // 3. Build the Client with the Custom Dns var client = OkHttpClient.Builder() @@ -63,8 +66,13 @@ class ReproduceOkHttpIssueTest { @AfterEach fun close() { - executorService.shutdownNow() - stallingServer.stop() + Thread.interrupted() + try { + executorService.shutdownNow() + stallingServer.stop() + } catch (e: Exception) { + e.printStackTrace() + } } @Test @@ -102,6 +110,165 @@ class ReproduceOkHttpIssueTest { } } + class TestTimeout( + val name: String, + val thread: Thread = Thread.currentThread(), + ) : AsyncTimeout() { + override fun timedOut() { + thread.interrupt() + } + + override fun toString(): String { + return name + } + } + + var recordingStart = 0L + val testStart = System.nanoTime() + + fun sleepUntil(time: Long) { + val currentElapsed = (System.nanoTime() - testStart) + val targetElapsed = time - recordingStart + + val nanos = targetElapsed - currentElapsed + val ms = nanos / 1_000_000L + val ns = nanos - (ms * 1_000_000L) + if (ms > 0L || nanos > 0) { + Thread.sleep(ms, ns.toInt()) + } + } + + /** Annoyingly this does what we want. */ + @Test + @Throws(InterruptedException::class) + fun perfectTest() { + val T1 = TestTimeout("T1") + T1.timeout(3000000000, TimeUnit.NANOSECONDS) + + val T2 = TestTimeout("T2") + T2.timeout(10000000000, TimeUnit.NANOSECONDS) + + val T3 = TestTimeout("T3") + T3.timeout(2000000000, TimeUnit.NANOSECONDS) + + val T4 = TestTimeout("T4") + T4.timeout(10000000000, TimeUnit.NANOSECONDS) + + val T5 = TestTimeout("T5") + T5.timeout(2000000000, TimeUnit.NANOSECONDS) + +// sleepUntil(0) + T1.enter() + sleepUntil(21_021_375) + T2.enter() + sleepUntil(43_177_042) + T2.exit() + sleepUntil(46_650_750) + T2.enter() + sleepUntil(50_056_459) + T2.exit() + sleepUntil(53_838_167) + T3.enter() + sleepUntil(81_087_334) + T3.exit() + sleepUntil(2_000_000_000) + T4.enter() + sleepUntil(2_169_520_000) + T4.exit() + sleepUntil(2_175_734_375) + T4.enter() + sleepUntil(2_182_283_250) + T4.exit() + sleepUntil(2_188_262_042) + T5.enter() + sleepUntil(2_195_482_917) + T5.exit() + assertFailsWith { + sleepUntil(3_500_000_000) + } + T1.exit() + } + + /** Annoyingly this does what we want. */ + @Test + @Throws(InterruptedException::class) + fun failingTest() { + val T1 = TestTimeout("T1") + T1.timeout(3000000000, TimeUnit.NANOSECONDS) + + val T2 = TestTimeout("T2") + T2.timeout(10000000000, TimeUnit.NANOSECONDS) + + val T3 = TestTimeout("T3") + T3.timeout(2000000000, TimeUnit.NANOSECONDS) + + val T4 = TestTimeout("T4") + T4.timeout(10000000000, TimeUnit.NANOSECONDS) + + val T5 = TestTimeout("T5") + T5.timeout(2000000000, TimeUnit.NANOSECONDS) + + recordingStart = 48037384402166 + sleepUntil(48037384402166) + T1.enter() + sleepUntil(48037410711791) + T2.enter() + sleepUntil(48037429544208) + T2.exit() + sleepUntil(48037432485375) + T2.enter() + sleepUntil(48037435469416) + T2.exit() + sleepUntil(48037438890625) + T3.enter() +//calling timedOut 48037482781291 on null + sleepUntil(48037484109708) + T3.exit() + sleepUntil(48039554055708) + T4.enter() + sleepUntil(48039558740333) + T4.exit() + sleepUntil(48039562572583) + T4.enter() + sleepUntil(48039566864625) + T4.exit() + sleepUntil(48039570599375) + T5.enter() +//calling timedOut 48039574546166 on null + sleepUntil(48039575464541) + T5.exit() +//calling timedOut 48040398250541 on null +//calling timedOut 48040401757166 on T1 // took too long? + assertFailsWith { + sleepUntil(1_000_000_000 + 48040415361958) // this should time out, but too late? + } + T1.exit() + + } + + @Test + @Throws(InterruptedException::class) + fun tff() { + executorService.submit { stallingServer.start(8080) } + Thread.sleep(2000) + + val startTime = Instant.now() + + try { + runBlocking { client.newCall(request).executeAsync() }.use { response -> + println("Call Succeeded unexpectedly.") + } + } catch (e: Exception) { + val totalTime = Duration.between(startTime, Instant.now()) + println("--- TEST RESULT ---") + println("Exception: " + e.javaClass.getName() + ": " + e.message) + println("Total Time: $totalTime") + println("Expected Time (Call Timeout): $callTimeout") + println("Observed Time (2 x Read Timeout): " + readTimeout.multipliedBy(2)) + assertThat(totalTime).isLessThan(readTimeout.multipliedBy(2)) + } + } + class StallingServer { private lateinit var serverSocket: ServerSocket diff --git a/settings.gradle.kts b/settings.gradle.kts index f78afa259..83886203d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,3 +70,10 @@ if (androidHome != null || sdkDir != null) { } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +//includeBuild("/Users/jwilson/Development/okio") { +// dependencySubstitution { +// substitute(module("com.squareup.okio:okio")) +// .using(project(":okio")) +// } +//}