Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {

api("com.google.code.gson:gson:2.10")
api("org.apache.httpcomponents:httpclient:4.5.13")
api("org.apache.httpcomponents.client5:httpclient5:5.5")
api("io.prometheus:prometheus-metrics-core:1.1.0")

testImplementation(kotlin("test"))
Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/com/ecwid/apiclient/v3/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.ecwid.apiclient.v3.dto.variation.result.*
import com.ecwid.apiclient.v3.httptransport.HttpTransport
import com.ecwid.apiclient.v3.impl.*
import com.ecwid.apiclient.v3.jsontransformer.JsonTransformerProvider
import java.io.Closeable
import kotlin.reflect.KClass

open class ApiClient private constructor(
Expand Down Expand Up @@ -98,7 +99,8 @@ open class ApiClient private constructor(
SlugInfoApiClient by slugInfoApiClient,
ProductReviewsApiClient by productReviewsApiClient,
StoreExtrafieldsApiClient by storeExtrafieldsApiClient,
SwatchesApiClient by swatchesApiClient {
SwatchesApiClient by swatchesApiClient,
Closeable {

constructor(apiClientHelper: ApiClientHelper) : this(
apiClientHelper = apiClientHelper,
Expand Down Expand Up @@ -126,6 +128,10 @@ open class ApiClient private constructor(
swatchesApiClient = SwatchesApiClientImpl(apiClientHelper),
)

override fun close() {
apiClientHelper.httpTransport.close()
}

companion object {

fun create(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ecwid.apiclient.v3.httptransport

interface HttpTransport {
import java.io.Closeable

interface HttpTransport : Closeable {
fun makeHttpRequest(httpRequest: HttpRequest): HttpResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.apache.http.client.HttpClient
import org.apache.http.client.config.RequestConfig
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager
import java.io.Closeable
import java.io.IOException

private const val DEFAULT_CONNECTION_TIMEOUT = 10_000 // 10 sec
Expand All @@ -33,6 +34,7 @@ internal const val MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 60L
val EMPTY_WAITING_REACTION: (Long) -> Unit = { }
val EMPTY_BEFORE_REQUEST_ACTION: () -> Unit = { }

@Deprecated("Use ApacheCommonsHttpClientTransport from client5 package")
open class ApacheCommonsHttpClientTransport(
private val httpClient: HttpClient = buildHttpClient(),
private val rateLimitRetryStrategy: RateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(),
Expand Down Expand Up @@ -106,6 +108,12 @@ open class ApacheCommonsHttpClientTransport(
}
}

override fun close() {
if (httpClient is Closeable) {
httpClient.close()
}
}

companion object {

private fun buildHttpClient(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.ecwid.apiclient.v3.httptransport.impl.client5

import com.ecwid.apiclient.v3.httptransport.HttpRequest
import com.ecwid.apiclient.v3.httptransport.HttpResponse
import com.ecwid.apiclient.v3.httptransport.HttpTransport
import org.apache.hc.client5.http.classic.HttpClient
import org.apache.hc.client5.http.config.ConnectionConfig
import org.apache.hc.client5.http.config.RequestConfig
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder
import org.apache.hc.core5.http.Header
import java.io.Closeable
import java.io.IOException
import java.util.concurrent.TimeUnit

private const val DEFAULT_CONNECTION_TIMEOUT = 10_000L // 10 sec
private const val DEFAULT_READ_TIMEOUT = 60_000 // 1 min

private const val DEFAULT_MAX_CONNECTIONS = 10

/**
* Number of attempts to retry request if server responded with 429
*/
internal const val DEFAULT_RATE_LIMIT_ATTEMPTS = 2

/**
* Number of seconds to wait until next attempt, if server didn't send Retry-After header
*/
internal const val DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 10L

/**
* Maximal delay in seconds before next attempt
*/
internal const val MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 60L

val EMPTY_WAITING_REACTION: (Long) -> Unit = { }
val EMPTY_BEFORE_REQUEST_ACTION: () -> Unit = { }

open class ApacheCommonsHttpClient5Transport(
private val httpClient: HttpClient = buildHttpClient(),
private val rateLimitRetryStrategy: RateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(),
) : HttpTransport {

constructor(
httpClient: HttpClient,
defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS,
defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION,
beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION,
) : this(
httpClient,
rateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(
defaultRateLimitAttempts = defaultRateLimitAttempts,
defaultRateLimitRetryInterval = defaultRateLimitRetryInterval,
maxRateLimitRetryInterval = maxRateLimitRetryInterval,
onEverySecondOfWaiting = onEverySecondOfWaiting,
beforeEachRequestAttempt = beforeEachRequestAttempt
)
)

constructor(
defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT,
defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT,
defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS,
defaultHeaders: List<Header> = emptyList(),
rateLimitRetryStrategy: RateLimitRetryStrategy
) : this(
httpClient = buildHttpClient(
defaultConnectionTimeout = defaultConnectionTimeout,
defaultReadTimeout = defaultReadTimeout,
defaultMaxConnections = defaultMaxConnections,
defaultHeaders = defaultHeaders
),
rateLimitRetryStrategy = rateLimitRetryStrategy
)

constructor(
defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT,
defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT,
defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS,
defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS,
defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
defaultHeaders: List<Header> = emptyList(),
onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION,
beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION,
) : this(
httpClient = buildHttpClient(
defaultConnectionTimeout = defaultConnectionTimeout,
defaultReadTimeout = defaultReadTimeout,
defaultMaxConnections = defaultMaxConnections,
defaultHeaders = defaultHeaders
),
rateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(
defaultRateLimitAttempts = defaultRateLimitAttempts,
defaultRateLimitRetryInterval = defaultRateLimitRetryInterval,
maxRateLimitRetryInterval = maxRateLimitRetryInterval,
onEverySecondOfWaiting = onEverySecondOfWaiting,
beforeEachRequestAttempt = beforeEachRequestAttempt,
)
)

override fun makeHttpRequest(httpRequest: HttpRequest): HttpResponse {
return try {
rateLimitRetryStrategy.makeHttpRequest(httpClient, httpRequest)
} catch (e: IOException) {
HttpResponse.TransportError(e)
}
}

override fun close() {
if (httpClient is Closeable) {
httpClient.close()
}
}

companion object {

private fun buildHttpClient(
defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT,
defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT,
defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS,
defaultHeaders: List<Header> = emptyList(),
): HttpClient {
val connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(defaultConnectionTimeout, TimeUnit.SECONDS)
.setSocketTimeout(defaultReadTimeout, TimeUnit.SECONDS)
.build()

val connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnTotal(defaultMaxConnections)
.setMaxConnPerRoute(defaultMaxConnections)
.setDefaultConnectionConfig(connectionConfig)
.build()

val requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(defaultConnectionTimeout, TimeUnit.SECONDS)
.build()

val httpClientBuilder = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.setRedirectStrategy(RemoveDisallowedHeadersRedirectStrategy())
// TODO .setRetryHandler()
// TODO .setServiceUnavailableRetryStrategy()
if (defaultHeaders.isNotEmpty()) {
httpClientBuilder.setDefaultHeaders(defaultHeaders)
}
return httpClientBuilder.build()
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.ecwid.apiclient.v3.httptransport.impl.client5

import com.ecwid.apiclient.v3.httptransport.HttpRequest
import com.ecwid.apiclient.v3.httptransport.HttpResponse
import com.ecwid.apiclient.v3.httptransport.TransportHttpBody
import org.apache.hc.core5.http.ClassicHttpRequest
import org.apache.hc.core5.http.ClassicHttpResponse
import org.apache.hc.core5.http.ContentType
import org.apache.hc.core5.http.HttpEntity
import org.apache.hc.core5.http.HttpStatus
import org.apache.hc.core5.http.io.entity.*
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder

internal fun HttpRequest.toHttpUriRequest(): ClassicHttpRequest {
val requestBuilder = when (this) {
is HttpRequest.HttpGetRequest -> {
ClassicRequestBuilder.get(uri)
}
is HttpRequest.HttpPostRequest -> {
ClassicRequestBuilder
.post(uri)
.setEntity(transportHttpBody.toEntity())
}
is HttpRequest.HttpPutRequest -> {
ClassicRequestBuilder
.put(uri)
.setEntity(transportHttpBody.toEntity())
}
is HttpRequest.HttpDeleteRequest -> {
ClassicRequestBuilder.delete(uri)
}
}

return requestBuilder
.apply updated@{
this@updated.charset = Charsets.UTF_8
this@toHttpUriRequest.params.forEach(this@updated::addParameter)
this@toHttpUriRequest.headers.forEach(this@updated::addHeader)
}
.build()
}

internal fun ClassicHttpResponse.toApiResponse(): HttpResponse {
val responseBytes = EntityUtils.toByteArray(entity)
return if (code == HttpStatus.SC_OK) {
HttpResponse.Success(responseBytes)
} else {
HttpResponse.Error(code, reasonPhrase, responseBytes)
}
}

private fun TransportHttpBody.toEntity(): HttpEntity? = when (this) {
is TransportHttpBody.EmptyBody ->
null
is TransportHttpBody.InputStreamBody ->
BufferedHttpEntity(InputStreamEntity(stream, mimeType.toContentType()))
is TransportHttpBody.ByteArrayBody ->
ByteArrayEntity(byteArray, mimeType.toContentType())
is TransportHttpBody.LocalFileBody ->
FileEntity(file, mimeType.toContentType())
}

private fun String.toContentType(): ContentType = ContentType.create(this, Charsets.UTF_8)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ecwid.apiclient.v3.httptransport.impl.client5

import com.ecwid.apiclient.v3.httptransport.HttpRequest
import com.ecwid.apiclient.v3.httptransport.HttpResponse
import org.apache.hc.client5.http.classic.HttpClient

class NoRetryRateLimitRetryStrategy : RateLimitRetryStrategy {
override fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse {
return httpClient.execute(httpRequest.toHttpUriRequest()) { response ->
response.toApiResponse()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ecwid.apiclient.v3.httptransport.impl.client5

import com.ecwid.apiclient.v3.httptransport.HttpRequest
import com.ecwid.apiclient.v3.httptransport.HttpResponse
import org.apache.hc.client5.http.classic.HttpClient

interface RateLimitRetryStrategy {
fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse
}

Loading