Skip to content

Commit 50c9e46

Browse files
Merge pull request #20 from hbelmiro/KOGITO-6685
Added Circuit Breaker feature
2 parents e5cca2d + 7d50093 commit 50c9e46

File tree

21 files changed

+665
-3
lines changed

21 files changed

+665
-3
lines changed

README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,122 @@ Similarly to bearer token, the API Key Authentication also has the token entry k
152152

153153
The API Key scheme has an additional property that requires where to add the API key in the request token: header, cookie or query. The inner provider takes care of that for you.
154154

155+
## Circuit Breaker
156+
157+
You can define the [CircuitBreaker annotation from MicroProfile Fault Tolerance](https://microprofile.io/project/eclipse/microprofile-fault-tolerance/spec/src/main/asciidoc/circuitbreaker.asciidoc)
158+
in your generated classes by setting the desired configuration in `application.properties`.
159+
160+
Let's say you have the following OpenAPI definition:
161+
````json
162+
{
163+
"openapi": "3.0.3",
164+
"info": {
165+
"title": "Simple API",
166+
"version": "1.0.0-SNAPSHOT"
167+
},
168+
"paths": {
169+
"/hello": {
170+
"get": {
171+
"responses": {
172+
"200": {
173+
"description": "OK",
174+
"content": {
175+
"text/plain": {
176+
"schema": {
177+
"type": "string"
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}
184+
},
185+
"/bye": {
186+
"get": {
187+
"responses": {
188+
"200": {
189+
"description": "OK",
190+
"content": {
191+
"text/plain": {
192+
"schema": {
193+
"type": "string"
194+
}
195+
}
196+
}
197+
}
198+
}
199+
}
200+
}
201+
}
202+
}
203+
````
204+
205+
And you want to configure Circuit Breaker for the `/bye` endpoint, you can do it in the following way:
206+
207+
Add the [SmallRye Fault Tolerance extension](https://quarkus.io/guides/smallrye-fault-tolerance) to your project's `pom.xml` file:
208+
209+
````xml
210+
<dependency>
211+
<groupId>io.quarkus</groupId>
212+
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
213+
</dependency>
214+
````
215+
216+
Assuming your Open API spec file is in `src/main/openapi/simple-openapi.json`, add the following configuration to your `application.properties` file:
217+
218+
````properties
219+
quarkus.openapi-generator.codegen.spec."simple-openapi.json".base-package=org.acme.openapi.simple
220+
221+
# Enables the CircuitBreaker extension for the byeGet method from the DefaultApi class
222+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/enabled=true
223+
````
224+
225+
With the above configuration, your Rest Clients will be created with a code similar to the following:
226+
227+
````java
228+
package org.acme.openapi.simple.api;
229+
230+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
231+
232+
import java.io.InputStream;
233+
import java.io.OutputStream;
234+
import java.util.List;
235+
import java.util.Map;
236+
import javax.ws.rs.*;
237+
import javax.ws.rs.core.Response;
238+
import javax.ws.rs.core.MediaType;
239+
240+
@Path("")
241+
@RegisterRestClient
242+
public interface DefaultApi {
243+
244+
@GET
245+
@Path("/bye")
246+
@Produces({"text/plain"})
247+
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
248+
public String byeGet();
249+
250+
@GET
251+
@Path("/hello")
252+
@Produces({"text/plain"})
253+
public String helloGet();
254+
255+
}
256+
````
257+
258+
You can also override the default Circuit Breaker configuration by setting the properties in `application.properties` [just as you would for a traditional MicroProfile application](https://quarkus.io/guides/smallrye-fault-tolerance#runtime-configuration):
259+
260+
````properties
261+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failOn = java.lang.IllegalArgumentException,java.lang.NullPointerException
262+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/skipOn = java.lang.NumberFormatException
263+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delay = 33
264+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delayUnit = MILLIS
265+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/requestVolumeThreshold = 42
266+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failureRatio = 3.14
267+
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/successThreshold = 22
268+
````
269+
270+
155271
## Known Limitations
156272

157273
These are the known limitations of this pre-release version:

deployment/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@
9797
</dependency>
9898

9999
<!-- Tests -->
100+
<dependency>
101+
<groupId>io.quarkiverse.openapi.generator</groupId>
102+
<artifactId>quarkus-openapi-generator-test-utils</artifactId>
103+
<version>${project.version}</version>
104+
<scope>test</scope>
105+
</dependency>
106+
100107
<dependency>
101108
<groupId>io.quarkus</groupId>
102109
<artifactId>quarkus-junit5-internal</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.quarkiverse.openapi.generator.deployment.circuitbreaker;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.function.Function;
6+
import java.util.stream.Collectors;
7+
import java.util.stream.StreamSupport;
8+
9+
import org.eclipse.microprofile.config.Config;
10+
11+
public final class CircuitBreakerConfigurationParser {
12+
13+
private CircuitBreakerConfigurationParser() {
14+
}
15+
16+
/**
17+
* Parses the {@link Config} and returns a {@link Map} of class names and their methods that should be configured with
18+
* circuit breaker.
19+
*
20+
* @return a {@link Map} of class names and their methods that should be configured with circuit breaker
21+
*/
22+
public static Map<String, List<String>> parse(Config config) {
23+
List<String> filteredPropertyNames = filterPropertyNames(config.getPropertyNames()).stream()
24+
.filter(property -> config.getOptionalValue(property, Boolean.class).orElse(false))
25+
.collect(Collectors.toList());
26+
27+
return filteredPropertyNames.stream()
28+
.map(CircuitBreakerConfigurationParser::getClassName)
29+
.distinct()
30+
.collect(Collectors.toUnmodifiableMap(
31+
Function.identity(),
32+
className -> getMethodNames(className, filteredPropertyNames)));
33+
}
34+
35+
private static List<String> getMethodNames(String className, List<String> propertyNames) {
36+
return propertyNames.stream()
37+
.filter(propertyName -> propertyName.startsWith(className + "/"))
38+
.map(CircuitBreakerConfigurationParser::getMethodName)
39+
.collect(Collectors.toUnmodifiableList());
40+
}
41+
42+
private static String getClassName(String propertyName) {
43+
return propertyName.substring(0, propertyName.indexOf("/"));
44+
}
45+
46+
private static String getMethodName(String propertyName) {
47+
return propertyName.substring(propertyName.indexOf("/") + 1, propertyName.indexOf("/CircuitBreaker/"));
48+
}
49+
50+
private static List<String> filterPropertyNames(Iterable<String> propertyNames) {
51+
return StreamSupport.stream(propertyNames.spliterator(), false)
52+
.filter(propertyName -> propertyName.matches(".+/.+/CircuitBreaker/enabled"))
53+
.collect(Collectors.toList());
54+
}
55+
}

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.nio.file.Path;
88
import java.util.stream.Stream;
99

10+
import io.quarkiverse.openapi.generator.deployment.circuitbreaker.CircuitBreakerConfigurationParser;
1011
import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiClientGeneratorWrapper;
1112
import io.quarkus.bootstrap.prebuild.CodeGenException;
1213
import io.quarkus.deployment.CodeGenContext;
@@ -42,7 +43,10 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
4243
.filter(s -> s.endsWith(this.inputExtension()))
4344
.map(Path::of).forEach(openApiFilePath -> {
4445
final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(
45-
openApiFilePath.normalize(), outDir);
46+
openApiFilePath.normalize(), outDir)
47+
.withCircuitBreakerConfiguration(CircuitBreakerConfigurationParser.parse(
48+
context.config()));
49+
4650
context.config()
4751
.getOptionalValue(getResolvedBasePackageProperty(openApiFilePath), String.class)
4852
.ifPresent(generator::withBasePackage);

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.File;
77
import java.nio.file.Path;
88
import java.util.List;
9+
import java.util.Map;
910

1011
import org.openapitools.codegen.CodegenConstants;
1112
import org.openapitools.codegen.DefaultGenerator;
@@ -68,6 +69,17 @@ public OpenApiClientGeneratorWrapper withBasePackage(final String pkg) {
6869
return this;
6970
}
7071

72+
/**
73+
* Adds the circuit breaker configuration to the generator.
74+
*
75+
* @param config a map of class names and their methods that should be configured with circuit breaker
76+
* @return this wrapper
77+
*/
78+
public OpenApiClientGeneratorWrapper withCircuitBreakerConfiguration(final Map<String, List<String>> config) {
79+
configurator.addAdditionalProperty("circuit-breaker", config);
80+
return this;
81+
}
82+
7183
public List<File> generate() {
7284
this.consolidatePackageNames();
7385
return generator.opts(configurator.toClientOptInput()).generate();

deployment/src/main/resources/templates/api.qute

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public interface {classname} {
5353
{#if op.hasProduces}
5454
@Produces(\{{#for produce in op.produces}"{produce.mediaType}"{#if produce_hasNext}, {/if}{/for}\})
5555
{/if}
56+
{#for cbClassConfig in circuit-breaker.orEmpty}{#if cbClassConfig.key == package + classname}
57+
{#for cbMethod in cbClassConfig.value.orEmpty}{#if cbMethod == op.nickname}
58+
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
59+
{/if}{/for}
60+
{/if}{/for}
5661
public {#if op.returnType}{op.returnType}{#else}void{/if} {op.nickname}({#for p in op.allParams}{#include pathParams.qute param=p/}{#include queryParams.qute param=p/}{#include bodyParams.qute param=p/}{#include formParams.qute param=p/}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for});
5762

5863
{/for}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.quarkiverse.openapi.generator.deployment.assertions;
2+
3+
import com.github.javaparser.ast.body.MethodDeclaration;
4+
5+
import io.quarkiverse.openapi.generator.testutils.circuitbreaker.assertions.CircuitBreakerMethodAssert;
6+
7+
public final class Assertions extends org.assertj.core.api.Assertions {
8+
9+
private Assertions() {
10+
}
11+
12+
public static CircuitBreakerMethodAssert assertThat(MethodDeclaration actual) {
13+
return CircuitBreakerMethodAssert.assertThat(actual);
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.quarkiverse.openapi.generator.deployment.circuitbreaker;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.io.IOException;
6+
import java.io.UncheckedIOException;
7+
import java.net.URL;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Objects;
11+
12+
import org.eclipse.microprofile.config.Config;
13+
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
14+
import org.junit.jupiter.api.Test;
15+
16+
import io.smallrye.config.PropertiesConfigSource;
17+
18+
class CircuitBreakerConfigurationParserTest {
19+
20+
@Test
21+
void parse() {
22+
Config config = mockConfig("/circuitbreaker/application.properties");
23+
24+
Map<String, List<String>> circuitBreakerConfiguration = CircuitBreakerConfigurationParser.parse(config);
25+
26+
assertThat(circuitBreakerConfiguration.get("org.acme.CountryResource"))
27+
.containsExactlyInAnyOrder("getCountries", "getByCapital");
28+
29+
assertThat(circuitBreakerConfiguration.get("org.acme.CityResource"))
30+
.containsOnly("get");
31+
}
32+
33+
@Test
34+
void circuitBreakerDisabledShouldReturnEmptyConfig() {
35+
Config config = mockConfig("/circuitbreaker/circuit_breaker_disabled_application.properties");
36+
37+
Map<String, List<String>> circuitBreakerConfiguration = CircuitBreakerConfigurationParser.parse(config);
38+
39+
assertThat(circuitBreakerConfiguration).isEmpty();
40+
}
41+
42+
@Test
43+
void missingCircuitBreakerEnabledConfigShouldReturnEmptyConfig() {
44+
Config config = mockConfig("/circuitbreaker/missing_circuit_breaker_enabled_application.properties");
45+
46+
Map<String, List<String>> circuitBreakerConfiguration = CircuitBreakerConfigurationParser.parse(config);
47+
48+
assertThat(circuitBreakerConfiguration).isEmpty();
49+
}
50+
51+
private static Config mockConfig(String propertiesFile) {
52+
PropertiesConfigSource configSource;
53+
try {
54+
configSource = new PropertiesConfigSource(getResource(propertiesFile));
55+
} catch (IOException e) {
56+
throw new UncheckedIOException(e);
57+
}
58+
59+
return ConfigProviderResolver
60+
.instance()
61+
.getBuilder()
62+
.withSources(configSource)
63+
.build();
64+
}
65+
66+
private static URL getResource(String resourcePath) {
67+
return Objects.requireNonNull(CircuitBreakerConfigurationParserTest.class.getResource(resourcePath));
68+
}
69+
}

0 commit comments

Comments
 (0)