commonMain.com.algolia.client.transport.internal.KtorRequester.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of algoliasearch-client-kotlin-jvm Show documentation
Show all versions of algoliasearch-client-kotlin-jvm Show documentation
"Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations. Algolia API Client for Kotlin lets you easily use the Algolia Search REST API from your JVM project, such as Android or backend implementations."
package com.algolia.client.transport.internal
import com.algolia.client.configuration.CallType
import com.algolia.client.configuration.Host
import com.algolia.client.configuration.internal.HEADER_APIKEY
import com.algolia.client.exception.AlgoliaRetryException
import com.algolia.client.exception.internal.asApiException
import com.algolia.client.exception.internal.asClientException
import com.algolia.client.transport.RequestConfig
import com.algolia.client.transport.RequestMethod
import com.algolia.client.transport.RequestOptions
import com.algolia.client.transport.Requester
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.network.sockets.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.utils.*
import io.ktor.http.*
import io.ktor.util.*
import io.ktor.util.reflect.*
import io.ktor.utils.io.errors.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.JsonObject
import kotlin.time.Duration
/** Default implementation of [Requester] using Ktor's [HttpClient]. */
public class KtorRequester(
public val httpClient: HttpClient,
private val connectTimeout: Duration,
private val readTimeout: Duration,
private val writeTimeout: Duration,
hosts: List,
) : Requester {
private val hostStatusExpirationDelayMS: Long = 1000L * 60L * 5L
private val mutex: Mutex = Mutex()
private val retryableHosts = hosts.map { RetryableHost(it) }
public override fun setClientApiKey(apiKey: String) {
headers {
if (contains(HEADER_APIKEY)) {
remove(HEADER_APIKEY)
}
append(HEADER_APIKEY, apiKey)
}
}
override suspend fun execute(
requestConfig: RequestConfig,
requestOptions: RequestOptions?,
returnType: TypeInfo,
): T {
val callType = callTypeOf(requestConfig)
val hosts = callableHosts(callType)
val errors by lazy(LazyThreadSafetyMode.NONE) { mutableListOf() }
val requestBuilder = httpRequestBuilderOf(requestConfig, requestOptions)
for (host in hosts) {
requestBuilder.url.protocol = URLProtocol.createOrDefault(host.protocol)
requestBuilder.url.host = host.url
if (host.port != null) {
requestBuilder.url.port = host.port!!
}
requestBuilder.setTimeout(requestOptions, callType, host)
try {
val response = httpClient.request(requestBuilder)
val body = response.body(returnType)
mutex.withLock { host.reset() }
return body
} catch (exception: Throwable) {
host.onError(exception)
errors += exception.asClientException()
}
}
throw AlgoliaRetryException(errors)
}
private fun callTypeOf(requestConfig: RequestConfig): CallType {
return if (requestConfig.isRead || requestConfig.method == RequestMethod.GET) {
CallType.Read
} else {
CallType.Write
}
}
/** Get list of [RetryableHost] for a given [CallType]. */
private suspend fun callableHosts(callType: CallType): List {
return mutex.withLock {
retryableHosts.expireHostsOlderThan(hostStatusExpirationDelayMS)
val hostsCallType = retryableHosts.filterCallType(callType)
val hostsCallTypeAreUp = hostsCallType.filter { it.isUp }
hostsCallTypeAreUp.ifEmpty { hostsCallType.onEach { it.reset() } }
}
}
/** Handle API request exceptions. */
private suspend fun RetryableHost.onError(throwable: Throwable) {
when (throwable) {
is CancellationException -> throw throwable // propagate coroutine cancellation
is ClientRequestException -> throw throwable.asApiException()
is HttpRequestTimeoutException,
is SocketTimeoutException,
is ConnectTimeoutException -> mutex.withLock { hasTimedOut() }
is IOException,
is ResponseException -> mutex.withLock { hasFailed() }
else -> throw throwable.asClientException()
}
}
/** Set socket read/write timeout. */
private fun HttpRequestBuilder.setTimeout(
requestOptions: RequestOptions?,
callType: CallType,
host: RetryableHost,
) {
timeout {
val timeout =
when (callType) {
CallType.Read -> requestOptions?.readTimeout ?: readTimeout
CallType.Write -> requestOptions?.writeTimeout ?: writeTimeout
}
connectTimeoutMillis = connectTimeout.inWholeMilliseconds
socketTimeoutMillis = timeout.inWholeMilliseconds * (host.retryCount + 1)
}
}
private fun List.expireHostsOlderThan(hostStatusExpirationDelayMS: Long) {
forEach {
val timeDelayExpired = currentTimeMillis() - it.lastUpdated
if (timeDelayExpired > hostStatusExpirationDelayMS) {
it.reset()
}
}
}
private fun List.filterCallType(callType: CallType): List {
return filter { it.callType == callType || it.callType == null }
}
private fun httpRequestBuilderOf(
requestConfig: RequestConfig,
requestOptions: RequestOptions? = null,
): HttpRequestBuilder {
return HttpRequestBuilder().apply {
url {
pathSegments = requestConfig.path
}
method = requestConfig.method.ktorHttpMethod
contentType(ContentType.Application.Json)
requestConfig.run {
requestHeaders(headers)
queryParameter(query)
when {
body != null -> setBody(body.body, body.bodyType)
requiresBody(requestConfig) -> setBody(EmptyObject)
else -> setBody(EmptyContent)
}
}
requestOptions?.run {
requestHeaders(headers)
queryParameter(urlParameters)
body?.let { setBody(it) }
}
}
}
private fun requiresBody(requestConfig: RequestConfig) =
requestConfig.method == RequestMethod.POST || requestConfig.method == RequestMethod.PUT
private fun HttpRequestBuilder.requestHeaders(headerOptions: Map) {
headers.replaceAll(headerOptions)
}
private fun HttpRequestBuilder.queryParameter(parameters: Map) {
url.encodedParameters.replaceAllEncoded(parameters)
}
private fun StringValuesBuilder.replaceAllEncoded(input: Map) {
input.onEach { (key, value) ->
if (contains(key)) remove(key)
when (value) {
is Iterable<*> -> append(
key.encodeURLParameter(),
value.joinToString(",") {
it.toString()
}.encodeURLParameter(),
)
else -> append(key.encodeURLParameter(), value.toString().encodeURLParameter())
}
}
}
private fun StringValuesBuilder.replaceAll(input: Map) {
input.onEach { (key, value) ->
if (contains(key)) remove(key)
when (value) {
is Iterable<*> -> appendAll(key, value.map { it.toString() })
else -> append(key, value.toString())
}
}
}
private val RequestMethod.ktorHttpMethod: HttpMethod
get() =
when (this) {
RequestMethod.DELETE -> HttpMethod.Delete
RequestMethod.GET -> HttpMethod.Get
RequestMethod.HEAD -> HttpMethod.Head
RequestMethod.PATCH -> HttpMethod.Patch
RequestMethod.PUT -> HttpMethod.Put
RequestMethod.POST -> HttpMethod.Post
RequestMethod.OPTIONS -> HttpMethod.Options
}
public companion object {
/** Represents an empty Json object */
private val EmptyObject = JsonObject(emptyMap())
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy