Skip to content

Commit 8630741

Browse files
committed
Added Circuit Breaker feature
Signed-off-by: Helber Belmiro <[email protected]>
1 parent e5cca2d commit 8630741

File tree

23 files changed

+944
-5
lines changed

23 files changed

+944
-5
lines changed

README.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,114 @@ 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 add [CircuitBreaker annotation from MicroProfile Fault Tolerance](https://microprofile.io/project/eclipse/microprofile-fault-tolerance/spec/src/main/asciidoc/circuitbreaker.asciidoc)
158+
to your generated classes by defining 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+
186+
"/bye": {
187+
"get": {
188+
"responses": {
189+
"200": {
190+
"description": "OK",
191+
"content": {
192+
"text/plain": {
193+
"schema": {
194+
"type": "string"
195+
}
196+
}
197+
}
198+
}
199+
}
200+
}
201+
}
202+
}
203+
}
204+
````
205+
206+
And you want to configure Circuit Breaker for the `/bye` endpoint, you can do it in the following way:
207+
208+
Add the [SmallRye Fault Tolerance extension](https://quarkus.io/guides/smallrye-fault-tolerance) to your project's `pom.xml` file:
209+
210+
````xml
211+
<dependency>
212+
<groupId>io.quarkus</groupId>
213+
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
214+
</dependency>
215+
````
216+
217+
Assuming your Open API spec file is in `src/main/openapi/simple-openapi.json`, add the following configuration to your `application.properties` file:
218+
219+
````properties
220+
quarkus.openapi-generator.spec."simple-openapi.json".base-package=org.acme.openapi.simple
221+
222+
# Enables the CircuitBreaker extension
223+
quarkus.openapi-generator.CircuitBreaker.enabled=true
224+
225+
# Defines the configuration for the GET method to the /bye endpoint
226+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/failOn = java.lang.IllegalArgumentException,java.lang.NullPointerException
227+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/skipOn = java.lang.NumberFormatException
228+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/delay = 33
229+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/delayUnit = MILLIS
230+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/requestVolumeThreshold = 42
231+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/failureRatio = 3.14
232+
quarkus.openapi-generator.spec."simple-openapi.json"/byeGet/CircuitBreaker/successThreshold = 22
233+
````
234+
235+
With the above configuration, your Rest Clients will be created with a code similar to the following:
236+
237+
````java
238+
@Path("")
239+
@RegisterRestClient
240+
public interface DefaultApi {
241+
242+
@GET
243+
@Path("/bye")
244+
@Produces({"text/plain"})
245+
@org.eclipse.microprofile.faulttolerance.CircuitBreaker(
246+
delay = 33,
247+
delayUnit = java.time.temporal.ChronoUnit.MILLIS,
248+
failOn = { java.lang.IllegalArgumentException.class, java.lang.NullPointerException.class },
249+
failureRatio = 3.14,
250+
requestVolumeThreshold = 42,
251+
skipOn = java.lang.NumberFormatException.class,
252+
successThreshold = 22)
253+
public String byeGet();
254+
255+
@GET
256+
@Path("/hello")
257+
@Produces({"text/plain"})
258+
public String helloGet();
259+
260+
}
261+
````
262+
155263
## Known Limitations
156264

157265
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>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.quarkiverse.openapi.generator.deployment.circuitbreaker;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.Objects;
6+
import java.util.stream.Collectors;
7+
import java.util.stream.Stream;
8+
9+
public final class CircuitBreakerConfiguration {
10+
11+
private static final CircuitBreakerConfiguration EMPTY = CircuitBreakerConfiguration.builder()
12+
.enabled(false)
13+
.operations(List.of())
14+
.build();
15+
16+
private final Boolean enabled;
17+
18+
private final List<Operation> operations;
19+
20+
private CircuitBreakerConfiguration(Builder builder) {
21+
enabled = Objects.requireNonNull(builder.enabled);
22+
operations = Objects.requireNonNull(builder.operations);
23+
}
24+
25+
public Boolean getEnabled() {
26+
return enabled;
27+
}
28+
29+
public List<Operation> getOperations() {
30+
return operations;
31+
}
32+
33+
public static CircuitBreakerConfiguration empty() {
34+
return EMPTY;
35+
}
36+
37+
public static Builder builder() {
38+
return new Builder();
39+
}
40+
41+
public static final class Operation {
42+
private final String name;
43+
private final Map<String, String> attributes;
44+
45+
public Operation(String name, Map<String, String> attributes) {
46+
this.name = Objects.requireNonNull(name);
47+
this.attributes = Objects.requireNonNull(attributes);
48+
}
49+
50+
public String getName() {
51+
return name;
52+
}
53+
54+
public Map<String, String> getAttributes() {
55+
return attributes;
56+
}
57+
58+
public String getAttributesAsString() {
59+
return attributes.entrySet().stream()
60+
.sorted(Map.Entry.comparingByKey())
61+
.map(entry -> {
62+
switch (entry.getKey()) {
63+
case "failOn":
64+
case "skipOn":
65+
List<String> classes = Stream.of(entry.getValue().split(","))
66+
.map(String::trim)
67+
.map(value -> value + ".class")
68+
.collect(Collectors.toUnmodifiableList());
69+
70+
if (classes.size() == 1) {
71+
return entry.getKey() + " = " + classes.get(0);
72+
} else {
73+
return entry.getKey() + " = { " + String.join(", ", classes) + " }";
74+
}
75+
case "delayUnit":
76+
return "delayUnit = java.time.temporal.ChronoUnit." + entry.getValue();
77+
default:
78+
return entry.getKey() + " = " + entry.getValue().trim();
79+
}
80+
}).collect(Collectors.joining(", "));
81+
}
82+
}
83+
84+
public static final class Builder {
85+
private List<Operation> operations;
86+
private Boolean enabled;
87+
88+
private Builder() {
89+
}
90+
91+
public Builder enabled(boolean enabled) {
92+
this.enabled = enabled;
93+
return this;
94+
}
95+
96+
public Builder operations(List<Operation> operations) {
97+
this.operations = List.copyOf(operations);
98+
return this;
99+
}
100+
101+
public CircuitBreakerConfiguration build() {
102+
return new CircuitBreakerConfiguration(this);
103+
}
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.quarkiverse.openapi.generator.deployment.circuitbreaker;
2+
3+
import java.util.Collection;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.function.UnaryOperator;
8+
import java.util.stream.Collectors;
9+
10+
public final class CircuitBreakerConfigurationParser {
11+
12+
private static final String CONFIG_PREFIX = "quarkus.openapi-generator.spec.";
13+
14+
private static final String PROPERTY_REGEX = CONFIG_PREFIX + "\".+\"/.+/CircuitBreaker/.+";
15+
16+
private static final String CIRCUIT_BREAKER_ENABLED_PROPERTY_NAME = "quarkus.openapi-generator.CircuitBreaker.enabled";
17+
18+
private final UnaryOperator<String> nameToValuePropertyMapper;
19+
20+
private final String openApiFileName;
21+
22+
private final int operationIndex;
23+
24+
public CircuitBreakerConfigurationParser(String openApiFileName, UnaryOperator<String> nameToValuePropertyMapper) {
25+
this.openApiFileName = openApiFileName;
26+
this.nameToValuePropertyMapper = nameToValuePropertyMapper;
27+
operationIndex = CONFIG_PREFIX.length() + openApiFileName.length() + 3;
28+
}
29+
30+
public CircuitBreakerConfiguration parse(Collection<String> propertyNames) {
31+
if (propertyNames.contains(CIRCUIT_BREAKER_ENABLED_PROPERTY_NAME)
32+
&& Boolean.parseBoolean(nameToValuePropertyMapper.apply(CIRCUIT_BREAKER_ENABLED_PROPERTY_NAME))) {
33+
return CircuitBreakerConfiguration.builder()
34+
.enabled(true)
35+
.operations(getOperations(propertyNames))
36+
.build();
37+
} else {
38+
return CircuitBreakerConfiguration.empty();
39+
}
40+
}
41+
42+
private List<CircuitBreakerConfiguration.Operation> getOperations(Collection<String> propertyNames) {
43+
Map<String, Map<String, String>> operationsMap = new HashMap<>();
44+
45+
for (String propertyName : filterPropertyNames(propertyNames)) {
46+
String operationName = getOperationName(propertyName);
47+
String circuitBreakerAttributeName = getCircuitBreakerAttributeName(propertyName);
48+
String circuitBreakerAttributeValue = nameToValuePropertyMapper.apply(propertyName).trim();
49+
50+
operationsMap.computeIfAbsent(operationName, k -> new HashMap<>())
51+
.put(circuitBreakerAttributeName, circuitBreakerAttributeValue);
52+
}
53+
54+
return operationsMap.entrySet().stream()
55+
.map(entry -> new CircuitBreakerConfiguration.Operation(entry.getKey(), entry.getValue()))
56+
.collect(Collectors.toUnmodifiableList());
57+
}
58+
59+
private String getCircuitBreakerAttributeName(String propertyName) {
60+
return propertyName.substring(propertyName.lastIndexOf("/") + 1);
61+
}
62+
63+
private String getOperationName(String propertyName) {
64+
return propertyName.substring(operationIndex, propertyName.indexOf("/CircuitBreaker/"));
65+
}
66+
67+
private List<String> filterPropertyNames(Collection<String> propertyNames) {
68+
return propertyNames.stream()
69+
.filter(propertyName -> propertyName.matches(PROPERTY_REGEX))
70+
.filter(propertyName -> propertyName
71+
.matches(CONFIG_PREFIX + "\"" + openApiFileName + "\"/.+/CircuitBreaker/.+"))
72+
.collect(Collectors.toList());
73+
}
74+
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
import java.io.IOException;
66
import java.nio.file.Files;
77
import java.nio.file.Path;
8+
import java.util.List;
9+
import java.util.function.UnaryOperator;
10+
import java.util.stream.Collectors;
811
import java.util.stream.Stream;
12+
import java.util.stream.StreamSupport;
913

14+
import io.quarkiverse.openapi.generator.deployment.circuitbreaker.CircuitBreakerConfiguration;
15+
import io.quarkiverse.openapi.generator.deployment.circuitbreaker.CircuitBreakerConfigurationParser;
1016
import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiClientGeneratorWrapper;
1117
import io.quarkus.bootstrap.prebuild.CodeGenException;
1218
import io.quarkus.deployment.CodeGenContext;
@@ -41,8 +47,14 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
4147
.map(Path::toString)
4248
.filter(s -> s.endsWith(this.inputExtension()))
4349
.map(Path::of).forEach(openApiFilePath -> {
50+
final CircuitBreakerConfiguration circuitBreakerConfiguration = getCircuitBreakerConfiguration(
51+
context,
52+
openApiFilePath);
53+
4454
final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(
45-
openApiFilePath.normalize(), outDir);
55+
openApiFilePath.normalize(), outDir)
56+
.withCircuitBreakerConfiguration(circuitBreakerConfiguration);
57+
4658
context.config()
4759
.getOptionalValue(getResolvedBasePackageProperty(openApiFilePath), String.class)
4860
.ifPresent(generator::withBasePackage);
@@ -56,4 +68,17 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
5668
}
5769
return false;
5870
}
71+
72+
private CircuitBreakerConfiguration getCircuitBreakerConfiguration(CodeGenContext context, Path openApiFilePath) {
73+
UnaryOperator<String> nameToValuePropertyMapper = propertyName -> context.config().getValue(propertyName,
74+
String.class);
75+
76+
return new CircuitBreakerConfigurationParser(openApiFilePath.toFile().getName(), nameToValuePropertyMapper)
77+
.parse(getConfigPropertyNames(context));
78+
}
79+
80+
private static List<String> getConfigPropertyNames(CodeGenContext context) {
81+
return StreamSupport.stream(context.config().getPropertyNames().spliterator(), false)
82+
.collect(Collectors.toUnmodifiableList());
83+
}
5984
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package io.quarkiverse.openapi.generator.deployment.wrapper;
22

3-
import static java.lang.Boolean.FALSE;
4-
import static java.lang.Boolean.TRUE;
5-
63
import java.io.File;
74
import java.nio.file.Path;
85
import java.util.List;
@@ -13,6 +10,9 @@
1310

1411
import io.quarkiverse.openapi.generator.deployment.SpecConfig;
1512

13+
import static java.lang.Boolean.FALSE;
14+
import static java.lang.Boolean.TRUE;
15+
1616
/**
1717
* Wrapper for the OpenAPIGen tool.
1818
* This is the same as calling the Maven plugin or the CLI.
@@ -68,6 +68,11 @@ public OpenApiClientGeneratorWrapper withBasePackage(final String pkg) {
6868
return this;
6969
}
7070

71+
public OpenApiClientGeneratorWrapper withCircuitBreakerConfiguration(final CircuitBreakerConfiguration config) {
72+
configurator.addAdditionalProperty("circuit-breaker", config);
73+
return this;
74+
}
75+
7176
public List<File> generate() {
7277
this.consolidatePackageNames();
7378
return generator.opts(configurator.toClientOptInput()).generate();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public interface {classname} {
5353
{#if op.hasProduces}
5454
@Produces(\{{#for produce in op.produces}"{produce.mediaType}"{#if produce_hasNext}, {/if}{/for}\})
5555
{/if}
56+
{#if circuit-breaker.enabled}{#for cbOperationConfig in circuit-breaker.operations}{#if cbOperationConfig.name == op.nickname}
57+
@org.eclipse.microprofile.faulttolerance.CircuitBreaker({cbOperationConfig.attributesAsString})
58+
{/if}{/for}{/if}
5659
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});
5760

5861
{/for}

0 commit comments

Comments
 (0)