From 99fa2cd96492579c503270896c57f06e0a5b2783 Mon Sep 17 00:00:00 2001 From: "Md. Touhidur Rahman" <46617994+touhidurrr@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:01:52 +0600 Subject: [PATCH] Update to Ktor 3 and more (#13782) * upgrade ktor version to `3.2.3` * Update EndpointImplementations.kt * update `androidx.core:core-ktx` to `1.16.0` * fix `Principal` deprecated message --- android/build.gradle.kts | 6 ++-- buildSrc/src/main/kotlin/BuildConfig.kt | 2 +- .../unciv/logic/multiplayer/apiv2/ApiV2.kt | 22 +++++++------- .../com/unciv/logic/multiplayer/apiv2/Conf.kt | 9 +++--- .../apiv2/EndpointImplementations.kt | 29 +++++++------------ .../logic/multiplayer/chat/ChatWebSocket.kt | 27 ++++++++--------- .../src/com/unciv/app/server/UncivServer.kt | 10 +++---- 7 files changed, 50 insertions(+), 55 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 44dd3032c7..da11eb58fb 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -147,11 +147,11 @@ tasks.register("run") { } dependencies { - implementation("androidx.core:core-ktx:1.15.0") - implementation("androidx.work:work-runtime-ktx:2.10.0") + implementation("androidx.core:core-ktx:1.16.0") + implementation("androidx.work:work-runtime-ktx:2.10.3") // Needed to convert e.g. Android 26 API calls to Android 21 // If you remove this run `./gradlew :android:lintDebug` to ensure everything's okay. // If you want to upgrade this, check it's working by building an apk, // or by running `./gradlew :android:assembleRelease` which does that - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") } diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index f02a00eccd..7a3601df18 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -8,7 +8,7 @@ object BuildConfig { const val appVersion = "4.17.12" const val gdxVersion = "1.13.1" - const val ktorVersion = "2.3.13" + const val ktorVersion = "3.2.3" const val coroutinesVersion = "1.8.1" const val jnaVersion = "5.17.0" diff --git a/core/src/com/unciv/logic/multiplayer/apiv2/ApiV2.kt b/core/src/com/unciv/logic/multiplayer/apiv2/ApiV2.kt index a0bc535803..bfd53e2f9f 100644 --- a/core/src/com/unciv/logic/multiplayer/apiv2/ApiV2.kt +++ b/core/src/com/unciv/logic/multiplayer/apiv2/ApiV2.kt @@ -11,14 +11,11 @@ import com.unciv.logic.multiplayer.storage.ApiV2FileStorageWrapper import com.unciv.logic.multiplayer.storage.MultiplayerFileNotFoundException import com.unciv.utils.Concurrency import com.unciv.utils.Log -import io.ktor.client.call.body -import io.ktor.client.plugins.websocket.ClientWebSocketSession -import io.ktor.client.request.get -import io.ktor.http.isSuccess -import io.ktor.websocket.Frame -import io.ktor.websocket.FrameType -import io.ktor.websocket.readText -import io.ktor.websocket.close +import io.ktor.client.call.* +import io.ktor.client.plugins.websocket.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.websocket.* import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -35,7 +32,7 @@ import java.time.Instant import java.util.Random import java.util.UUID import java.util.concurrent.atomic.AtomicReference -import kotlin.collections.set +import kotlin.time.Duration /** * Main class to interact with multiplayer servers implementing [ApiVersion.ApiV2] @@ -339,7 +336,7 @@ class ApiV2(private val baseUrl: String) : ApiV2Wrapper(baseUrl), Disposable { * [delay] internally to quit waiting for the result of the operation. * This function may also throw arbitrary exceptions for network failures. */ - suspend fun awaitPing(size: Int = 2, timeout: Long? = null): Double? { + suspend fun awaitPing(size: Int = 2, timeout: Duration? = null): Double? { require(size < 2) { "Size too small to identify ping responses uniquely" } val body = ByteArray(size) Random().nextBytes(body) @@ -501,7 +498,10 @@ class ApiV2(private val baseUrl: String) : ApiV2Wrapper(baseUrl), Disposable { * Note that this callback might not get called if no new WS connection was created. * It returns the measured round trip time in milliseconds if everything was fine. */ - suspend fun ensureConnectedWebSocket(timeout: Long = DEFAULT_WEBSOCKET_PING_TIMEOUT, jobCallback: ((Job) -> Unit)? = null): Double? { + suspend fun ensureConnectedWebSocket( + timeout: Duration = DEFAULT_WEBSOCKET_PING_TIMEOUT, + jobCallback: ((Job) -> Unit)? = null + ): Double? { val pingMeasurement = try { awaitPing(timeout = timeout) } catch (e: Exception) { diff --git a/core/src/com/unciv/logic/multiplayer/apiv2/Conf.kt b/core/src/com/unciv/logic/multiplayer/apiv2/Conf.kt index ddcab84794..e3cce9661c 100644 --- a/core/src/com/unciv/logic/multiplayer/apiv2/Conf.kt +++ b/core/src/com/unciv/logic/multiplayer/apiv2/Conf.kt @@ -1,6 +1,7 @@ package com.unciv.logic.multiplayer.apiv2 import java.time.Duration +import kotlin.time.Duration.Companion.seconds /** Name of the session cookie returned and expected by the server */ internal const val SESSION_COOKIE_NAME = "id" @@ -9,13 +10,13 @@ internal const val SESSION_COOKIE_NAME = "id" internal const val DEFAULT_LOBBY_MAX_PLAYERS = 32 /** Default ping frequency for outgoing WebSocket connection in seconds */ -internal const val DEFAULT_WEBSOCKET_PING_FREQUENCY = 15_000L +internal val DEFAULT_WEBSOCKET_PING_FREQUENCY = 15.seconds /** Default session timeout expected from multiplayer servers (unreliable) */ -internal val DEFAULT_SESSION_TIMEOUT = Duration.ofSeconds(15 * 60) +internal val DEFAULT_SESSION_TIMEOUT = Duration.ofMinutes(15) /** Default cache expiry timeout to indicate that certain data needs to be re-fetched */ -internal val DEFAULT_CACHE_EXPIRY = Duration.ofSeconds(30 * 60) +internal val DEFAULT_CACHE_EXPIRY = Duration.ofMinutes(30) /** Default timeout for a single request (miliseconds) */ internal const val DEFAULT_REQUEST_TIMEOUT = 10_000L @@ -24,4 +25,4 @@ internal const val DEFAULT_REQUEST_TIMEOUT = 10_000L internal const val DEFAULT_CONNECT_TIMEOUT = 5_000L /** Default timeout for a single WebSocket PING-PONG roundtrip */ -internal const val DEFAULT_WEBSOCKET_PING_TIMEOUT = 10_000L +internal val DEFAULT_WEBSOCKET_PING_TIMEOUT = 10.seconds diff --git a/core/src/com/unciv/logic/multiplayer/apiv2/EndpointImplementations.kt b/core/src/com/unciv/logic/multiplayer/apiv2/EndpointImplementations.kt index be2cdd5866..f7b7cc9fc4 100644 --- a/core/src/com/unciv/logic/multiplayer/apiv2/EndpointImplementations.kt +++ b/core/src/com/unciv/logic/multiplayer/apiv2/EndpointImplementations.kt @@ -7,24 +7,17 @@ package com.unciv.logic.multiplayer.apiv2 import com.unciv.utils.Log -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.plugins.cookies.get -import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.cookies.* +import io.ktor.client.request.* import io.ktor.client.request.request -import io.ktor.client.request.setBody -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.* import io.ktor.client.statement.request -import io.ktor.http.ContentType -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.http.isSuccess -import io.ktor.http.path -import io.ktor.http.setCookie -import io.ktor.util.network.UnresolvedAddressException +import io.ktor.http.* +import io.ktor.util.network.* import java.io.IOException +import java.time.Duration import java.time.Instant import java.util.UUID @@ -46,7 +39,7 @@ private const val DEFAULT_RANDOM_PASSWORD_LENGTH = 32 /** * Max age of a cached entry before it will be re-queried */ -private const val MAX_CACHE_AGE_SECONDS = 60L +private val MAX_CACHE_AGE = Duration.ofSeconds(60) /** * Perform a HTTP request via [method] to [endpoint] @@ -218,7 +211,7 @@ private object Cache { } /** - * Wrapper around [request] to cache responses to GET queries up to [MAX_CACHE_AGE_SECONDS] + * Wrapper around [request] to cache responses to GET queries up to [MAX_CACHE_AGE] */ suspend fun get( endpoint: String, @@ -230,7 +223,7 @@ private object Cache { retry: (suspend () -> Boolean)? = null ): HttpResponse? { val result = responseCache[endpoint] - if (cache && result != null && result.first.plusSeconds(MAX_CACHE_AGE_SECONDS).isAfter(Instant.now())) { + if (cache && result != null && (result.first + MAX_CACHE_AGE).isAfter(Instant.now())) { return result.second } val response = request(HttpMethod.Get, endpoint, client, authHelper, refine, suppress, retry) diff --git a/core/src/com/unciv/logic/multiplayer/chat/ChatWebSocket.kt b/core/src/com/unciv/logic/multiplayer/chat/ChatWebSocket.kt index d4d86ef75f..123f6058f4 100644 --- a/core/src/com/unciv/logic/multiplayer/chat/ChatWebSocket.kt +++ b/core/src/com/unciv/logic/multiplayer/chat/ChatWebSocket.kt @@ -26,6 +26,8 @@ import kotlinx.serialization.json.ClassDiscriminatorMode import kotlinx.serialization.json.Json import kotlin.random.Random import kotlin.time.Clock +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit import kotlin.time.ExperimentalTime @@ -77,17 +79,17 @@ class ChatRestartException : CancellationException("Chat restart requested") class ChatStopException : CancellationException("Chat stop requested") object ChatWebSocket { + private const val MAX_RECONNECTION_ATTEMPTS = 100 + private val INITIAL_RECONNECT_TIME = 1.seconds + private val MAX_RECONNECT_TIME = 64.seconds + private val INITIAL_SESSION_WAIT_FOR_TIME = 5.seconds + private var isStarted = false @OptIn(ExperimentalTime::class) private var lastRetry = Clock.System.now() private var reconnectionAttempts = 0 - private var reconnectTimeSeconds = INITIAL_RECONNECT_TIME_SECONDS - - private const val INITIAL_RECONNECT_TIME_SECONDS = 1 - private const val MAX_RECONNECTION_ATTEMPTS = 100 - private const val MAX_RECONNECT_TIME_SECONDS = 64 - private const val INITIAL_SESSION_WAIT_FOR_MS = 5_000L + private var reconnectTime = INITIAL_RECONNECT_TIME private var job: Job? = null private var session: DefaultClientWebSocketSession? = null @@ -95,7 +97,7 @@ object ChatWebSocket { @OptIn(ExperimentalSerializationApi::class) private val client = HttpClient(CIO) { install(WebSockets) { - pingInterval = 30_000 + pingInterval = 30.seconds contentConverter = KotlinxWebsocketSerializationConverter(Json { classDiscriminator = "type" // DO NOT OMIT @@ -110,7 +112,7 @@ object ChatWebSocket { lastRetry = Clock.System.now() reconnectionAttempts = 0 - reconnectionAttempts = INITIAL_RECONNECT_TIME_SECONDS + reconnectTime = INITIAL_RECONNECT_TIME } private fun getChatUrl(): Url = URLBuilder( @@ -132,9 +134,9 @@ object ChatWebSocket { fun requestMessageSend(message: Message) { start() Concurrency.run("MultiplayerChatSendMessage") { - withTimeoutOrNull(INITIAL_SESSION_WAIT_FOR_MS) { + withTimeoutOrNull(INITIAL_SESSION_WAIT_FOR_TIME) { while (session == null) { - delay(100) + delay(100.milliseconds) } } session?.runCatching { @@ -241,9 +243,8 @@ object ChatWebSocket { GlobalScope.launch { if (!force) { // exponential backoff same as described here: https://cloud.google.com/memorystore/docs/redis/exponential-backoff - delay(Random.nextLong(1000) + 1000L * reconnectTimeSeconds) - reconnectTimeSeconds = - (reconnectTimeSeconds * 2).coerceAtMost(MAX_RECONNECT_TIME_SECONDS) + delay(Random.nextLong(1000).milliseconds + reconnectTime) + reconnectTime = (reconnectTime * 2).coerceAtMost(MAX_RECONNECT_TIME) if (job?.isActive == true) return@launch } diff --git a/server/src/com/unciv/app/server/UncivServer.kt b/server/src/com/unciv/app/server/UncivServer.kt index cd96955116..aeb4f4dd63 100644 --- a/server/src/com/unciv/app/server/UncivServer.kt +++ b/server/src/com/unciv/app/server/UncivServer.kt @@ -33,6 +33,7 @@ import java.io.File import java.util.Collections.synchronizedMap import java.util.Collections.synchronizedSet import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -140,7 +141,7 @@ private class WebSocketSessionManager { data class BasicAuthInfo( val userId: Uuid, val password: String, -) : Principal +) /** * Checks if a [String] is a valid UUID @@ -264,8 +265,8 @@ private class UncivServerRunner : CliktCommand() { } if (chatV1Enabled) install(WebSockets) { - pingPeriodMillis = 30_000 - timeoutMillis = 60_000 + pingPeriod = 30.seconds + timeout = 60.seconds maxFrameSize = Long.MAX_VALUE @OptIn(ExperimentalSerializationApi::class) contentConverter = KotlinxWebsocketSerializationConverter(Json { @@ -386,8 +387,7 @@ private class UncivServerRunner : CliktCommand() { try { while (isActive) { - val message = receiveDeserialized() - when (message) { + when (val message = receiveDeserialized()) { is Message.Chat -> { val gameId = message.gameId.toUuidOrNull() if (gameId == null) {