Skip to content

Commit 81b68b0

Browse files
feat: use coroutines for async services
feat: use coroutines for async services
1 parent f5b75ed commit 81b68b0

7 files changed

Lines changed: 42 additions & 43 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The API documentation can be found [here](https://docs.lithic.com).
1717
#### Gradle
1818

1919
```kotlin
20-
implementation("com.lithic.api:lithic-java:0.0.4")
20+
implementation("com.lithic.api:lithic-java:0.2.0")
2121
```
2222

2323
#### Maven
@@ -26,7 +26,7 @@ implementation("com.lithic.api:lithic-java:0.0.4")
2626
<dependency>
2727
<groupId>com.lithic.api</groupId>
2828
<artifactId>lithic-java</artifactId>
29-
<version>0.0.4</version>
29+
<version>0.2.0</version>
3030
</dependency>
3131
```
3232

lithic-java-client-okhttp/src/main/kotlin/com/lithic/api/client/okhttp/OkHttpClient.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
3434
): HttpResponse {
3535
val call = okHttpClient.newCall(request.toRequest())
3636

37-
try {
38-
val response = call.execute()
39-
return response.toResponse()
37+
return try {
38+
call.execute().toResponse()
4039
} catch (e: IOException) {
4140
throw LithicIoException("Request failed", e)
4241
} finally {

lithic-java-core/src/main/kotlin/com/lithic/api/core/ClientOptions.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private constructor(
3232

3333
private var httpClient: HttpClient? = null
3434
private var jsonMapper: JsonMapper? = null
35-
private var clock: Clock? = null
35+
private var clock: Clock = Clock.systemUTC()
3636
private var headers: MutableMap<String, MutableList<String>> = mutableMapOf()
3737
private var responseValidation: Boolean = false
3838
private var maxRetries: Int = 2
@@ -96,10 +96,11 @@ private constructor(
9696
return ClientOptions(
9797
RetryingHttpClient.builder()
9898
.httpClient(httpClient!!)
99+
.clock(clock)
99100
.maxRetries(maxRetries)
100101
.build(),
101102
jsonMapper ?: jsonMapper(),
102-
clock ?: Clock.systemUTC(),
103+
clock,
103104
headers.toUnmodifiable(),
104105
responseValidation,
105106
webhookSecret,

lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ package com.lithic.api.core.http
55
import com.google.common.util.concurrent.MoreExecutors
66
import com.lithic.api.errors.LithicIoException
77
import java.io.IOException
8-
import java.time.ZonedDateTime
8+
import java.time.Clock
9+
import java.time.OffsetDateTime
910
import java.time.format.DateTimeFormatter
1011
import java.time.format.DateTimeParseException
1112
import java.time.temporal.ChronoUnit
13+
import java.util.Timer
14+
import java.util.TimerTask
1215
import java.util.UUID
1316
import java.util.concurrent.CompletableFuture
1417
import java.util.concurrent.ThreadLocalRandom
@@ -20,6 +23,7 @@ import kotlin.math.pow
2023
class RetryingHttpClient
2124
private constructor(
2225
private val httpClient: HttpClient,
26+
private val clock: Clock,
2327
private val maxRetries: Int,
2428
private val idempotencyHeader: String?,
2529
) : HttpClient {
@@ -52,8 +56,7 @@ private constructor(
5256
null
5357
}
5458

55-
val backoffMillis = getRetryBackoffMillis(retries, response)
56-
Thread.sleep(backoffMillis)
59+
Thread.sleep(getRetryBackoffMillis(retries, response))
5760
}
5861
}
5962

@@ -87,8 +90,7 @@ private constructor(
8790
}
8891
}
8992

90-
val backoffMillis = getRetryBackoffMillis(retries, response)
91-
return sleepAsync(backoffMillis).thenCompose {
93+
return sleepAsync(getRetryBackoffMillis(retries, response)).thenCompose {
9294
wrap(httpClient.executeAsync(request))
9395
}
9496
},
@@ -149,38 +151,27 @@ private constructor(
149151
private fun getRetryBackoffMillis(retries: Int, response: HttpResponse?): Long {
150152
// About the Retry-After header:
151153
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
152-
val retryAfter: Long =
153-
when (val header = response?.headers()?.get("retry-after")?.getOrNull(0)) {
154-
null -> -1L
155-
else ->
156-
// When given as delay-seconds
157-
// - parse as Double in case the value has a floating point
158-
// - then convert to Long, the loss of precision is acceptable, Retry-After is
159-
// expected to be used with
160-
// integers
161-
header.toDoubleOrNull()?.toLong()
162-
163-
// When given as http-date
154+
val retryAfter =
155+
response?.headers()?.get("Retry-After")?.getOrNull(0)?.let { retryAfter ->
156+
retryAfter.toLongOrNull()
164157
?: try {
165-
ChronoUnit.SECONDS.between(
166-
ZonedDateTime.now(),
167-
ZonedDateTime.from(
168-
DateTimeFormatter.RFC_1123_DATE_TIME.parse(header)
169-
)
170-
)
171-
} catch (e: DateTimeParseException) {
172-
-1L
173-
}
158+
ChronoUnit.SECONDS.between(
159+
OffsetDateTime.now(clock),
160+
OffsetDateTime.parse(retryAfter, DateTimeFormatter.RFC_1123_DATE_TIME)
161+
)
162+
} catch (e: DateTimeParseException) {
163+
null
164+
}
174165
}
175166

176167
// If the API asks us to wait a certain amount of time (and it's a reasonable amount), just
177168
// do what it says.
178-
if (retryAfter in 1..60) {
169+
if (retryAfter != null && retryAfter in 1..60) {
179170
return TimeUnit.SECONDS.toMillis(retryAfter)
180171
}
181172

182173
// Apply exponential backoff, but not more than the max.
183-
val backoffSeconds = min(INITIAL_RETRY_DELAY * 2.0.pow(retries - 1), MAX_RETRY_DELAY)
174+
val backoffSeconds = min(0.5 * 2.0.pow(retries - 1), 2.0)
184175

185176
// Apply some jitter
186177
val jitter = ThreadLocalRandom.current().nextDouble()
@@ -189,32 +180,44 @@ private constructor(
189180
}
190181

191182
private fun sleepAsync(millis: Long): CompletableFuture<Void> {
192-
return CompletableFuture.runAsync { Thread.sleep(millis) }
183+
val future = CompletableFuture<Void>()
184+
TIMER.schedule(
185+
object : TimerTask() {
186+
override fun run() {
187+
future.complete(null)
188+
}
189+
},
190+
millis
191+
)
192+
return future
193193
}
194194

195195
companion object {
196196

197-
private const val INITIAL_RETRY_DELAY: Double = 0.5
198-
private const val MAX_RETRY_DELAY: Double = 2.0
197+
private val TIMER = Timer("RetryingHttpClient", true)
199198

200199
@JvmStatic fun builder() = Builder()
201200
}
202201

203202
class Builder {
204203

205204
private var httpClient: HttpClient? = null
205+
private var clock: Clock = Clock.systemUTC()
206206
private var maxRetries: Int = 2
207207
private var idempotencyHeader: String? = null
208208

209209
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
210210

211+
fun clock(clock: Clock) = apply { this.clock = clock }
212+
211213
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
212214

213215
fun idempotencyHeader(header: String) = apply { this.idempotencyHeader = header }
214216

215217
fun build(): HttpClient =
216218
RetryingHttpClient(
217219
checkNotNull(httpClient) { "`httpClient` is required but was not set" },
220+
clock,
218221
maxRetries,
219222
idempotencyHeader,
220223
)

lithic-java-core/src/test/kotlin/com/lithic/api/core/http/HttpRequestTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import org.assertj.core.api.Assertions.assertThat
44
import org.junit.jupiter.api.Test
55

66
internal class HttpRequestTest {
7+
78
@Test
8-
@Throws(Exception::class)
99
fun caseInsensitiveHeadersAccessors() {
1010
val request =
1111
HttpRequest.builder()

lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ internal class RetryingHttpClientTest {
2121
}
2222

2323
@Test
24-
@Throws(Exception::class)
2524
fun byDefaultShouldNotAddIdempotencyHeaderToRequest() {
2625
val request =
2726
HttpRequest.builder().method(HttpMethod.POST).addPathSegment("something").build()
@@ -33,7 +32,6 @@ internal class RetryingHttpClientTest {
3332
}
3433

3534
@Test
36-
@Throws(Exception::class)
3735
fun whenProvidedShouldAddIdempotencyHeaderToRequest() {
3836
val request =
3937
HttpRequest.builder().method(HttpMethod.POST).addPathSegment("something").build()
@@ -53,7 +51,6 @@ internal class RetryingHttpClientTest {
5351
}
5452

5553
@Test
56-
@Throws(Exception::class)
5754
fun retryAfterHeader() {
5855
val request =
5956
HttpRequest.builder().method(HttpMethod.POST).addPathSegment("something").build()

lithic-java-core/src/test/kotlin/com/lithic/api/core/http/SerializerTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ internal class SerializerTest {
9898
}
9999

100100
@Test
101-
@Throws(Exception::class)
102101
fun serializeBooleanPrefixedWithIs() {
103102
val value = ClassWithBooleanFieldPrefixedWithIs.builder().isActive(true).build()
104103
assertThat(jsonMapper().writeValueAsString(value)).isEqualTo("{\"is_active\":true}")

0 commit comments

Comments
 (0)