From 62021f7d424bd365e6b60fc689c46e3f31e08bc3 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Wed, 15 Dec 2021 10:26:11 +0100 Subject: [PATCH] Add retry, and ProductId --- .../kotlin/io/arrow/example/Application.kt | 2 +- src/main/kotlin/io/arrow/example/Model.kt | 7 +++-- src/main/kotlin/io/arrow/example/Utils.kt | 1 + .../io/arrow/example/external/Billing.kt | 31 ++++++++++++++----- .../io/arrow/example/external/Warehouse.kt | 7 +++-- .../example/external/impl/WarehouseImpl.kt | 3 +- .../io/arrow/example/validation/Structure.kt | 9 +++--- .../io/arrow/example/ApplicationTest.kt | 6 ++-- .../example/external/test/WarehouseTest.kt | 3 +- 9 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/io/arrow/example/Application.kt b/src/main/kotlin/io/arrow/example/Application.kt index 4818cbe..1104804 100644 --- a/src/main/kotlin/io/arrow/example/Application.kt +++ b/src/main/kotlin/io/arrow/example/Application.kt @@ -59,7 +59,7 @@ class ExampleApp( order.entries.parTraverseValidated { warehouse.validateAvailability(it.id, it.amount) }.mapLeft { availability -> - badRequest("Following productIds weren't available: ${availability.joinToString { it.productId }}") + badRequest("Following productIds weren't available: ${availability.joinToString { it.productId.value }}") }.bind() } when (result) { diff --git a/src/main/kotlin/io/arrow/example/Model.kt b/src/main/kotlin/io/arrow/example/Model.kt index 7f1658c..99a1757 100644 --- a/src/main/kotlin/io/arrow/example/Model.kt +++ b/src/main/kotlin/io/arrow/example/Model.kt @@ -2,12 +2,15 @@ package io.arrow.example import arrow.optics.optics +@JvmInline +value class ProductId(val value: String) + @optics -data class Entry(val id: String, val amount: Int) { +data class Entry(val id: ProductId, val amount: Int) { companion object // required by @optics val asPair: Pair - get() = Pair(id, amount) + get() = Pair(id.value, amount) } @optics diff --git a/src/main/kotlin/io/arrow/example/Utils.kt b/src/main/kotlin/io/arrow/example/Utils.kt index 7cea46d..529680d 100644 --- a/src/main/kotlin/io/arrow/example/Utils.kt +++ b/src/main/kotlin/io/arrow/example/Utils.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.CoroutineScope fun A.ensure(predicate: (A) -> Boolean, problem: () -> E): ValidatedNel = if (predicate(this)) this.validNel() else problem().invalidNel() +// TODO move to Arrow Fx Coroutines suspend fun Iterable.parTraverseValidated( f: suspend CoroutineScope.(A) -> ValidatedNel ): ValidatedNel> = diff --git a/src/main/kotlin/io/arrow/example/external/Billing.kt b/src/main/kotlin/io/arrow/example/external/Billing.kt index e3590a5..8a7120d 100644 --- a/src/main/kotlin/io/arrow/example/external/Billing.kt +++ b/src/main/kotlin/io/arrow/example/external/Billing.kt @@ -2,6 +2,10 @@ package io.arrow.example.external import arrow.fx.coroutines.CircuitBreaker import arrow.fx.coroutines.Schedule +import arrow.fx.coroutines.retry +import java.util.concurrent.ScheduledExecutorService +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime enum class BillingResponse { OK, USER_ERROR, SYSTEM_ERROR @@ -11,14 +15,25 @@ interface Billing { suspend fun processBilling(order: Map): BillingResponse } -class BillingWithBreaker(private val underlying: Billing, private val circuitBreaker: CircuitBreaker, private val retries: Int): Billing { +fun Billing.withBreaker(circuitBreaker: CircuitBreaker, retries: Int): Billing = + BillingWithBreaker(this, circuitBreaker, retries) + +@OptIn(ExperimentalTime::class) +private class BillingWithBreaker( + private val underlying: Billing, + private val circuitBreaker: CircuitBreaker, + private val retries: Int +) : Billing { override suspend fun processBilling(order: Map): BillingResponse = - (Schedule.recurs(retries) zipRight Schedule.doWhile { it == BillingResponse.SYSTEM_ERROR }).repeat { - circuitBreaker.protectOrThrow { - underlying.processBilling(order) + Schedule.recurs(retries) + .zipRight(Schedule.doWhile { it == BillingResponse.SYSTEM_ERROR }) + .repeat { + Schedule.recurs(retries) + .and(Schedule.exponential(20.milliseconds)) + .retry { + circuitBreaker.protectOrThrow { + underlying.processBilling(order) + } + } } - } } - -fun Billing.withBreaker(circuitBreaker: CircuitBreaker, retries: Int): Billing = - BillingWithBreaker(this, circuitBreaker, retries) \ No newline at end of file diff --git a/src/main/kotlin/io/arrow/example/external/Warehouse.kt b/src/main/kotlin/io/arrow/example/external/Warehouse.kt index e7e2a33..6fe6bdf 100644 --- a/src/main/kotlin/io/arrow/example/external/Warehouse.kt +++ b/src/main/kotlin/io/arrow/example/external/Warehouse.kt @@ -4,14 +4,15 @@ import arrow.core.ValidatedNel import arrow.core.invalidNel import arrow.core.validNel import io.arrow.example.Entry +import io.arrow.example.ProductId interface Warehouse { - suspend fun checkAvailability(productId: String, amount: Int): Boolean + suspend fun checkAvailability(productId: ProductId, amount: Int): Boolean } -data class AvailabilityProblem(val productId: String) +data class AvailabilityProblem(val productId: ProductId) -suspend fun Warehouse.validateAvailability(productId: String, amount: Int) +suspend fun Warehouse.validateAvailability(productId: ProductId, amount: Int) : ValidatedNel = if (checkAvailability(productId, amount)) Entry(productId, amount).validNel() diff --git a/src/main/kotlin/io/arrow/example/external/impl/WarehouseImpl.kt b/src/main/kotlin/io/arrow/example/external/impl/WarehouseImpl.kt index 3a3de1e..da718ad 100644 --- a/src/main/kotlin/io/arrow/example/external/impl/WarehouseImpl.kt +++ b/src/main/kotlin/io/arrow/example/external/impl/WarehouseImpl.kt @@ -1,10 +1,11 @@ package io.arrow.example.external.impl +import io.arrow.example.ProductId import io.arrow.example.external.Warehouse import io.ktor.http.* class WarehouseImpl(private val serviceUrl: Url): Warehouse { - override suspend fun checkAvailability(productId: String, amount: Int): Boolean { + override suspend fun checkAvailability(productId: ProductId, amount: Int): Boolean { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/src/main/kotlin/io/arrow/example/validation/Structure.kt b/src/main/kotlin/io/arrow/example/validation/Structure.kt index cebac18..6232865 100644 --- a/src/main/kotlin/io/arrow/example/validation/Structure.kt +++ b/src/main/kotlin/io/arrow/example/validation/Structure.kt @@ -9,6 +9,7 @@ import arrow.core.traverseValidated import arrow.core.zip import io.arrow.example.Entry import io.arrow.example.Order +import io.arrow.example.ProductId import io.arrow.example.ensure import io.arrow.example.flatten @@ -29,10 +30,10 @@ suspend fun validateStructure(order: Order): ValidatedNel = validateEntryId(entry.id).zip(validateEntryAmount(entry.amount), ::Entry) -fun validateEntryId(id: String): ValidatedNel = - eager, String> { - ensure(id.isNotEmpty()) { ValidateStructureProblem.EMPTY_ID.nel() } - ensure(Regex("^ID-(\\d){4}\$").matches(id)) { ValidateStructureProblem.INCORRECT_ID.nel() } +fun validateEntryId(id: ProductId): ValidatedNel = + eager, ProductId> { + ensure(id.value.isNotEmpty()) { ValidateStructureProblem.EMPTY_ID.nel() } + ensure(Regex("^ID-(\\d){4}\$").matches(id.value)) { ValidateStructureProblem.INCORRECT_ID.nel() } id }.toValidated() diff --git a/src/test/kotlin/io/arrow/example/ApplicationTest.kt b/src/test/kotlin/io/arrow/example/ApplicationTest.kt index 86d0912..f336362 100644 --- a/src/test/kotlin/io/arrow/example/ApplicationTest.kt +++ b/src/test/kotlin/io/arrow/example/ApplicationTest.kt @@ -12,7 +12,7 @@ import io.ktor.request.* import kotlin.test.* import io.ktor.server.testing.* -class ApplicationTest { +class ApplicationTest() { // inject fake implementations private val app = @@ -40,7 +40,7 @@ class ApplicationTest { @Test fun `wrong id gives error`() = testProcess( - Order(listOf(Entry("NOT-AN-ID", 2))) + Order(listOf(Entry(ProductId("NOT-AN-ID"), 2))) ) { assertEquals(HttpStatusCode.BadRequest, response.status()) assertEquals("INCORRECT_ID", response.content) @@ -48,7 +48,7 @@ class ApplicationTest { @Test fun `reasonable order`() = testProcess( - Order(listOf(Entry("ID-1234", 2))) + Order(listOf(Entry(ProductId("ID-1234"), 2))) ) { assertEquals(HttpStatusCode.OK, response.status()) } diff --git a/src/test/kotlin/io/arrow/example/external/test/WarehouseTest.kt b/src/test/kotlin/io/arrow/example/external/test/WarehouseTest.kt index 9445547..01cf03b 100644 --- a/src/test/kotlin/io/arrow/example/external/test/WarehouseTest.kt +++ b/src/test/kotlin/io/arrow/example/external/test/WarehouseTest.kt @@ -1,8 +1,9 @@ package io.arrow.example.external.test +import io.arrow.example.ProductId import io.arrow.example.external.Warehouse class WarehouseTest(private val maxAmount: Int): Warehouse { - override suspend fun checkAvailability(productId: String, amount: Int): Boolean = + override suspend fun checkAvailability(productId: ProductId, amount: Int): Boolean = amount < maxAmount } \ No newline at end of file