Skip to content

Commit

Permalink
Add resilience4j-kotlin module (ReactiveX#441)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradnewman authored and RobWin committed May 30, 2019
1 parent 82a1f43 commit c846f9e
Show file tree
Hide file tree
Showing 18 changed files with 1,146 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ Add-on modules
* resilience4j-ratpack: Ratpack Starter
* resilience4j-retrofit: Retrofit adapter
* resilience4j-feign: Feign adapter
* resilience4j-vertx: Vertx Future decorator
* resilience4j-consumer: Circular Buffer Event consumer
* resilience4j-kotlin: Kotlin coroutines support

== Spring Boot demo

Expand Down
7 changes: 6 additions & 1 deletion libraries.gradle
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ext {
micrometerVersion = '1.1.4'
hibernateValidatorVersion = '6.0.16.Final'
wiremockVersion = '2.22.0'
kotlinCoroutinesVersion = '1.2.0'

libraries = [
// compile
Expand Down Expand Up @@ -111,7 +112,11 @@ ext {
jaxws: "com.sun.xml.ws:jaxws-ri:2.3.2",

// Groovy
groovy: "org.codehaus.groovy:groovy-all:2.5.6"
groovy: "org.codehaus.groovy:groovy-all:2.5.6",

// Kotlin addon
kotlin_stdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk8",
kotlin_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-core:${kotlinCoroutinesVersion}"
]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
=== Kotlin

==== Introduction

Integration for https://kotlinlang.org/[Kotlin] coroutines that enables executing and decorating `suspend` functions with the various resilience aspects provided by the core modules.

Extension functions that accept Kotlin suspend functions are provided for rate limiter, retry, circuit breaker, time limiter, and semaphore-based bulkheads. No extensions for thread pool bulkheads or caches are currently provided.

==== Gradle

Add the Kotlin module of Resilience4j to your compile dependency, along with whichever core modules are needed:

[source,groovy, subs="attributes"]
----
repositories {
maven { url 'http://oss.jfrog.org/artifactory/oss-snapshot-local/' }
mavenCentral()
}
dependencies {
compile('io.github.resilience4j:resilience4j-kotlin:{release-version}')
// also have a dependency on the core module(s) needed - for example, retry:
compile('io.github.resilience4j:resilience4j-retry:{release-version}')
}
----

==== Basic Usage

Two extension functions are declared for each of `CircuitBreaker`, `RateLimiter`, `Retry`, and `TimeLimiter`: one to execute a suspend function, and one to decorate a suspend function.

[source,kotlin]
----
val circuitBreaker = CircuitBreaker.ofDefaults()
val result = circuitBreaker.executeSuspendFunction {
// call suspending functions here
}
----

[source,kotlin]
----
val function = circuitBreaker.decorateSuspendFunction {
// call suspending functions here
}
val result = function()
----

The suspend functions suspend where usage of the normal methods would block. For example, calls to a `RateLimiter` which need to be delayed to fit within the rate limit suspend before the given function is executed.

No changes are made to the coroutine context of the given suspend functions.

===== Bulkhead

Decorating a suspend function with a `Bulkhead` does not add any additional suspension points. If `maxWaitTime` is non-zero, the call will *block* until the max wait time is reached or permission is obtained. For this reason, it is not recommended to use this extension function with Bulkheads with non-zero max wait times.

No extension functions for thread-pool based bulkheads are provided.

===== Circuit Breaker

Decorating a suspend function with a `CircuitBreaker` does not add any additional suspension points.

===== Rate Limiter

Suspend functions decorated with a `RateLimiter` use `delay()` in order to suspend before executing the decorated function if the rate limit has been reached.

===== Retry

Suspend functions decorated with a `Retry` use `delay()` in order to suspend between retries.

===== Time Limiter

The `TimeLimiter` extension functions simply use `withTimeout()` from `kotlinx-coroutines`, using the timeout from the receiver's configuration. Specifically, this means:

1. On timeout, a `TimeoutCancellationException` is raised, rather than a `TimeoutException` as with methods for non-suspending functions.
1. When a timeout occurs, the coroutine is cancelled, rather than the thread being interrupted as with methods for non-suspending functions.
1. After the timeout, the given block can only be stopped at a cancellable suspending function call.
1. The `cancelRunningFuture` configuration setting is ignored - on timeout, the suspend function is always cancelled even if the `cancelRunningFuture` is set to `false`.
4 changes: 3 additions & 1 deletion resilience4j-documentation/src/docs/asciidoc/addons_guide.adoc
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ include::addon_guides/dropwizard.adoc[]

include::addon_guides/prometheus.adoc[]

include::addon_guides/micrometer.adoc[]
include::addon_guides/micrometer.adoc[]

include::addon_guides/kotlin.adoc[]
1 change: 1 addition & 0 deletions resilience4j-documentation/src/docs/asciidoc/introduction.adoc
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Add-on modules
* resilience4j-retrofit: Retrofit adapter
* resilience4j-feign: Feign adapter
* resilience4j-vertx: Vertx Future decorator
* resilience4j-kotlin: Kotlin coroutines support
* resilience4j-consumer: Circular Buffer Event consumer
To highlight a few differences to Netflix Hystrix:
Expand Down
30 changes: 30 additions & 0 deletions resilience4j-kotlin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.31'
}

dependencies {
implementation(libraries.kotlin_stdlib)
implementation(libraries.kotlin_coroutines)

compileOnly(project(':resilience4j-bulkhead'))
compileOnly(project(':resilience4j-circuitbreaker'))
compileOnly(project(':resilience4j-ratelimiter'))
compileOnly(project(':resilience4j-retry'))
compileOnly(project(':resilience4j-timelimiter'))

testImplementation(project(':resilience4j-bulkhead'))
testImplementation(project(':resilience4j-circuitbreaker'))
testImplementation(project(':resilience4j-ratelimiter'))
testImplementation(project(':resilience4j-retry'))
testImplementation(project(':resilience4j-timelimiter'))
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}

compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}

ext.moduleName='io.github.resilience4j.kotlin'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
*
* Copyright 2019: Brad Newman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
*/
package io.github.resilience4j.kotlin.bulkhead

import io.github.resilience4j.bulkhead.Bulkhead
import io.github.resilience4j.bulkhead.BulkheadConfig

/**
* Decorates and executes the given suspend function [block].
*
* If [BulkheadConfig.maxWaitTime] is non-zero, *blocks* until the max wait time is reached or permission is obtained.
* For this reason, it is not recommended to use this extension function with Bulkheads with non-zero max wait times.
*/
suspend fun <T> Bulkhead.executeSuspendFunction(block: suspend () -> T): T {
acquirePermission()
return try {
block()
} finally {
onComplete()
}
}

/**
* Decorates the given suspend function [block] and returns it.
*
* If [BulkheadConfig.maxWaitTime] is non-zero, *blocks* until the max wait time is reached or permission is obtained.
* For this reason, it is not recommended to use this extension function with Bulkheads with non-zero max wait times.
*/
fun <T> Bulkhead.decorateSuspendFunction(block: suspend () -> T): suspend () -> T = {
executeSuspendFunction(block)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
*
* Copyright 2019: Guido Pio Mariotti, Brad Newman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
*/
package io.github.resilience4j.kotlin.circuitbreaker

import io.github.resilience4j.circuitbreaker.CircuitBreaker

/**
* Decorates and executes the given suspend function [block].
*/
suspend fun <T> CircuitBreaker.executeSuspendFunction(block: suspend () -> T): T {
acquirePermission()
val start = System.nanoTime()
try {
val result = block()
val durationInNanos = System.nanoTime() - start
onSuccess(durationInNanos)
return result
} catch (exception: Exception) {
val durationInNanos = System.nanoTime() - start
onError(durationInNanos, exception)
throw exception
}
}

/**
* Decorates the given *suspend* function [block] and returns it.
*/
fun <T> CircuitBreaker.decorateSuspendFunction(block: suspend () -> T): suspend () -> T = {
executeSuspendFunction { block() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
*
* Copyright 2019: Brad Newman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
*/
package io.github.resilience4j.kotlin.ratelimiter

import io.github.resilience4j.ratelimiter.RateLimiter
import io.github.resilience4j.ratelimiter.RateLimiterConfig
import io.github.resilience4j.ratelimiter.RequestNotPermitted
import kotlinx.coroutines.delay
import java.util.concurrent.TimeUnit

/**
* Decorates and executes the given suspend function [block].
*
* If [RateLimiterConfig.timeoutDuration] is non-zero, the returned function suspends until a permission is available.
*/
suspend fun <T> RateLimiter.executeSuspendFunction(block: suspend () -> T): T {
val waitTimeNs = reservePermission()
if (waitTimeNs < 0) throw RequestNotPermitted(this)
delay(TimeUnit.NANOSECONDS.toMillis(waitTimeNs))
return block()
}

/**
* Decorates the given suspend function [block] and returns it.
*
* If [RateLimiterConfig.timeoutDuration] is non-zero, the returned function suspends until a permission is available.
*/
fun <T> RateLimiter.decorateSuspendFunction(block: suspend () -> T): suspend () -> T = {
executeSuspendFunction(block)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
*
* Copyright 2019: Brad Newman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
*/
package io.github.resilience4j.kotlin.retry

import io.github.resilience4j.retry.Retry
import kotlinx.coroutines.delay

/**
* Decorates and executes the given suspend function [block].
*
* Between attempts, suspends based on the configured interval function.
*/
suspend fun <T> Retry.executeSuspendFunction(block: suspend () -> T): T {
val retryContext = asyncContext<T>()
while (true) {
try {
val result = block()
val delayMs = retryContext.onResult(result)
if (delayMs < 1) {
retryContext.onSuccess()
return result
} else {
delay(delayMs)
}
} catch (e: Exception) {
val delayMs = retryContext.onError(e)
if (delayMs < 1) {
throw e
} else {
delay(delayMs)
}
}
}
}

/**
* Decorates the given suspend function [block] and returns it.
*
* Between attempts, suspends based on the configured interval function.
*/
fun <T> Retry.decorateSuspendFunction(block: suspend () -> T): suspend () -> T = {
executeSuspendFunction(block)
}
Loading

0 comments on commit c846f9e

Please sign in to comment.