Skip to content

Commit d07aa2d

Browse files
feat(client): expose sleeper option
fix(client): ensure single timer is created per client
1 parent 6ba57dd commit d07aa2d

File tree

8 files changed

+119
-22
lines changed

8 files changed

+119
-22
lines changed

inty-kotlin-client-okhttp/src/main/kotlin/com/inty/api/client/okhttp/IntyOkHttpClient.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.inty.api.client.IntyClient
77
import com.inty.api.client.IntyClientImpl
88
import com.inty.api.core.ClientOptions
9+
import com.inty.api.core.Sleeper
910
import com.inty.api.core.Timeout
1011
import com.inty.api.core.http.Headers
1112
import com.inty.api.core.http.HttpClient
@@ -103,6 +104,17 @@ class IntyOkHttpClient private constructor() {
103104
*/
104105
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
105106

107+
/**
108+
* The interface to use for delaying execution, like during retries.
109+
*
110+
* This is primarily useful for using fake delays in tests.
111+
*
112+
* Defaults to real execution delays.
113+
*
114+
* This class takes ownership of the sleeper and closes it when closed.
115+
*/
116+
fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
117+
106118
/**
107119
* The clock to use for operations that require timing, like retries.
108120
*

inty-kotlin-client-okhttp/src/main/kotlin/com/inty/api/client/okhttp/IntyOkHttpClientAsync.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.inty.api.client.IntyClientAsync
77
import com.inty.api.client.IntyClientAsyncImpl
88
import com.inty.api.core.ClientOptions
9+
import com.inty.api.core.Sleeper
910
import com.inty.api.core.Timeout
1011
import com.inty.api.core.http.Headers
1112
import com.inty.api.core.http.HttpClient
@@ -103,6 +104,17 @@ class IntyOkHttpClientAsync private constructor() {
103104
*/
104105
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
105106

107+
/**
108+
* The interface to use for delaying execution, like during retries.
109+
*
110+
* This is primarily useful for using fake delays in tests.
111+
*
112+
* Defaults to real execution delays.
113+
*
114+
* This class takes ownership of the sleeper and closes it when closed.
115+
*/
116+
fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
117+
106118
/**
107119
* The clock to use for operations that require timing, like retries.
108120
*

inty-kotlin-core/src/main/kotlin/com/inty/api/core/ClientOptions.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ private constructor(
3838
* needs to be overridden.
3939
*/
4040
val jsonMapper: JsonMapper,
41+
/**
42+
* The interface to use for delaying execution, like during retries.
43+
*
44+
* This is primarily useful for using fake delays in tests.
45+
*
46+
* Defaults to real execution delays.
47+
*
48+
* This class takes ownership of the sleeper and closes it when closed.
49+
*/
50+
val sleeper: Sleeper,
4151
/**
4252
* The clock to use for operations that require timing, like retries.
4353
*
@@ -129,6 +139,7 @@ private constructor(
129139
private var httpClient: HttpClient? = null
130140
private var checkJacksonVersionCompatibility: Boolean = true
131141
private var jsonMapper: JsonMapper = jsonMapper()
142+
private var sleeper: Sleeper? = null
132143
private var clock: Clock = Clock.systemUTC()
133144
private var baseUrl: String? = null
134145
private var headers: Headers.Builder = Headers.builder()
@@ -142,6 +153,7 @@ private constructor(
142153
httpClient = clientOptions.originalHttpClient
143154
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
144155
jsonMapper = clientOptions.jsonMapper
156+
sleeper = clientOptions.sleeper
145157
clock = clientOptions.clock
146158
baseUrl = clientOptions.baseUrl
147159
headers = clientOptions.headers.toBuilder()
@@ -182,6 +194,17 @@ private constructor(
182194
*/
183195
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
184196

197+
/**
198+
* The interface to use for delaying execution, like during retries.
199+
*
200+
* This is primarily useful for using fake delays in tests.
201+
*
202+
* Defaults to real execution delays.
203+
*
204+
* This class takes ownership of the sleeper and closes it when closed.
205+
*/
206+
fun sleeper(sleeper: Sleeper) = apply { this.sleeper = PhantomReachableSleeper(sleeper) }
207+
185208
/**
186209
* The clock to use for operations that require timing, like retries.
187210
*
@@ -361,6 +384,7 @@ private constructor(
361384
*/
362385
fun build(): ClientOptions {
363386
val httpClient = checkRequired("httpClient", httpClient)
387+
val sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper())
364388
val apiKey = checkRequired("apiKey", apiKey)
365389

366390
val headers = Headers.builder()
@@ -384,11 +408,13 @@ private constructor(
384408
httpClient,
385409
RetryingHttpClient.builder()
386410
.httpClient(httpClient)
411+
.sleeper(sleeper)
387412
.clock(clock)
388413
.maxRetries(maxRetries)
389414
.build(),
390415
checkJacksonVersionCompatibility,
391416
jsonMapper,
417+
sleeper,
392418
clock,
393419
baseUrl,
394420
headers.build(),
@@ -413,5 +439,6 @@ private constructor(
413439
*/
414440
fun close() {
415441
httpClient.close()
442+
sleeper.close()
416443
}
417444
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.inty.api.core
2+
3+
import java.time.Duration
4+
import kotlin.time.toKotlinDuration
5+
import kotlinx.coroutines.delay
6+
7+
class DefaultSleeper : Sleeper {
8+
9+
override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())
10+
11+
override suspend fun sleepAsync(duration: Duration) = delay(duration.toKotlinDuration())
12+
13+
override fun close() {}
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.inty.api.core
2+
3+
import java.time.Duration
4+
5+
/**
6+
* A delegating wrapper around a [Sleeper] that closes it once it's only phantom reachable.
7+
*
8+
* This class ensures the [Sleeper] is closed even if the user forgets to do it.
9+
*/
10+
internal class PhantomReachableSleeper(private val sleeper: Sleeper) : Sleeper {
11+
12+
init {
13+
closeWhenPhantomReachable(this, sleeper)
14+
}
15+
16+
override fun sleep(duration: Duration) = sleeper.sleep(duration)
17+
18+
override suspend fun sleepAsync(duration: Duration) = sleeper.sleepAsync(duration)
19+
20+
override fun close() = sleeper.close()
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.inty.api.core
2+
3+
import java.time.Duration
4+
5+
/**
6+
* An interface for delaying execution for a specified amount of time.
7+
*
8+
* Useful for testing and cleaning up resources.
9+
*/
10+
interface Sleeper : AutoCloseable {
11+
12+
/** Synchronously pauses execution for the given [duration]. */
13+
fun sleep(duration: Duration)
14+
15+
/** Asynchronously pauses execution for the given [duration]. */
16+
suspend fun sleepAsync(duration: Duration)
17+
}

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

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.inty.api.core.http
22

3+
import com.inty.api.core.DefaultSleeper
34
import com.inty.api.core.RequestOptions
5+
import com.inty.api.core.Sleeper
46
import com.inty.api.core.checkRequired
57
import com.inty.api.errors.IntyIoException
68
import com.inty.api.errors.IntyRetryableException
@@ -16,8 +18,6 @@ import java.util.concurrent.ThreadLocalRandom
1618
import java.util.concurrent.TimeUnit
1719
import kotlin.math.min
1820
import kotlin.math.pow
19-
import kotlin.time.toKotlinDuration
20-
import kotlinx.coroutines.delay
2121

2222
class RetryingHttpClient
2323
private constructor(
@@ -113,7 +113,10 @@ private constructor(
113113
}
114114
}
115115

116-
override fun close() = httpClient.close()
116+
override fun close() {
117+
httpClient.close()
118+
sleeper.close()
119+
}
117120

118121
private fun isRetryable(request: HttpRequest): Boolean =
119122
// Some requests, such as when a request body is being streamed, cannot be retried because
@@ -218,21 +221,14 @@ private constructor(
218221
class Builder internal constructor() {
219222

220223
private var httpClient: HttpClient? = null
221-
private var sleeper: Sleeper =
222-
object : Sleeper {
223-
224-
override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())
225-
226-
override suspend fun sleepAsync(duration: Duration) =
227-
delay(duration.toKotlinDuration())
228-
}
224+
private var sleeper: Sleeper? = null
229225
private var clock: Clock = Clock.systemUTC()
230226
private var maxRetries: Int = 2
231227
private var idempotencyHeader: String? = null
232228

233229
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
234230

235-
internal fun sleeper(sleeper: Sleeper) = apply { this.sleeper = sleeper }
231+
fun sleeper(sleeper: Sleeper) = apply { this.sleeper = sleeper }
236232

237233
fun clock(clock: Clock) = apply { this.clock = clock }
238234

@@ -243,17 +239,10 @@ private constructor(
243239
fun build(): HttpClient =
244240
RetryingHttpClient(
245241
checkRequired("httpClient", httpClient),
246-
sleeper,
242+
sleeper ?: DefaultSleeper(),
247243
clock,
248244
maxRetries,
249245
idempotencyHeader,
250246
)
251247
}
252-
253-
internal interface Sleeper {
254-
255-
fun sleep(duration: Duration)
256-
257-
suspend fun sleepAsync(duration: Duration)
258-
}
259248
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest
66
import com.github.tomakehurst.wiremock.stubbing.Scenario
77
import com.inty.api.client.okhttp.OkHttpClient
88
import com.inty.api.core.RequestOptions
9+
import com.inty.api.core.Sleeper
910
import com.inty.api.errors.IntyRetryableException
1011
import java.io.InputStream
1112
import java.time.Duration
@@ -289,11 +290,13 @@ internal class RetryingHttpClientTest {
289290
.httpClient(failingHttpClient)
290291
.maxRetries(2)
291292
.sleeper(
292-
object : RetryingHttpClient.Sleeper {
293+
object : Sleeper {
293294

294295
override fun sleep(duration: Duration) {}
295296

296297
override suspend fun sleepAsync(duration: Duration) {}
298+
299+
override fun close() {}
297300
}
298301
)
299302
.build()
@@ -327,11 +330,13 @@ internal class RetryingHttpClientTest {
327330
.httpClient(httpClient)
328331
// Use a no-op `Sleeper` to make the test fast.
329332
.sleeper(
330-
object : RetryingHttpClient.Sleeper {
333+
object : Sleeper {
331334

332335
override fun sleep(duration: Duration) {}
333336

334337
override suspend fun sleepAsync(duration: Duration) {}
338+
339+
override fun close() {}
335340
}
336341
)
337342

0 commit comments

Comments
 (0)