Skip to content

Commit

Permalink
Don't catch and change Exceptions in outgoing watchers to allow for c…
Browse files Browse the repository at this point in the history
…ustom exceptions
  • Loading branch information
F43nd1r committed Oct 27, 2023
1 parent dc6ea84 commit 091211c
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 139 deletions.
1 change: 1 addition & 0 deletions chaos-monkey-docs/src/main/asciidoc/changes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Built with Spring Boot {spring-boot-version}

=== Improvements
// - https://github.com/codecentric/chaos-monkey-spring-boot/pull/xxx[#xxx] Added example entry. Please don't remove.
- https://github.com/codecentric/chaos-monkey-spring-boot/pull/411[#411] Allow custom exceptions to fall through in outgoing watchers (rest template, webclient). This could also slightly change the behaviour/output of outgoing watchers when not using a custom exception.

=== New Features
// - https://github.com/codecentric/chaos-monkey-spring-boot/pull/xxx[#xxx] Added example entry. Please don't remove.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ChaosMonkeyRestTemplateCustomizer chaosMonkeyRestTemplateCustomizer(final
@Bean
@DependsOn("chaosMonkeyRequestScope")
public ChaosMonkeyRestTemplateWatcher chaosMonkeyRestTemplateInterceptor(final ChaosMonkeyRequestScope chaosMonkeyRequestScope,
final WatcherProperties watcherProperties, final AssaultProperties assaultProperties) {
return new ChaosMonkeyRestTemplateWatcher(chaosMonkeyRequestScope, watcherProperties, assaultProperties);
final WatcherProperties watcherProperties) {
return new ChaosMonkeyRestTemplateWatcher(chaosMonkeyRequestScope, watcherProperties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ChaosMonkeyWebClientCustomizer chaosMonkeyWebClientCustomizer(final Chaos
@Bean
@DependsOn("chaosMonkeyRequestScope")
public ChaosMonkeyWebClientWatcher chaosMonkeyWebClientWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope,
final WatcherProperties watcherProperties, final AssaultProperties assaultProperties) {
return new ChaosMonkeyWebClientWatcher(chaosMonkeyRequestScope, watcherProperties, assaultProperties);
final WatcherProperties watcherProperties) {
return new ChaosMonkeyWebClientWatcher(chaosMonkeyRequestScope, watcherProperties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,109 +17,34 @@

import de.codecentric.spring.boot.chaos.monkey.component.ChaosMonkeyRequestScope;
import de.codecentric.spring.boot.chaos.monkey.component.ChaosTarget;
import de.codecentric.spring.boot.chaos.monkey.configuration.AssaultProperties;
import de.codecentric.spring.boot.chaos.monkey.configuration.WatcherProperties;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;

/** @author Marcel Becker */
import java.io.IOException;

/**
* @author Marcel Becker
*/
public class ChaosMonkeyRestTemplateWatcher implements ClientHttpRequestInterceptor {

private final WatcherProperties watcherProperties;
private final ChaosMonkeyRequestScope chaosMonkeyRequestScope;
private final AssaultProperties assaultProperties;

public ChaosMonkeyRestTemplateWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties,
AssaultProperties assaultProperties) {
public ChaosMonkeyRestTemplateWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties) {
this.chaosMonkeyRequestScope = chaosMonkeyRequestScope;
this.watcherProperties = watcherProperties;
this.assaultProperties = assaultProperties;
}

@Override
public ClientHttpResponse intercept(final HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution)
throws IOException {
ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes);
if (watcherProperties.isRestTemplate()) {
try {
chaosMonkeyRequestScope.callChaosMonkey(ChaosTarget.REST_TEMPLATE, httpRequest.getURI().toString());
} catch (final Exception exception) {
try {
if (exception.getClass().equals(assaultProperties.getException().getExceptionClass())) {
response = new ErrorResponse();
} else {
throw exception;
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
chaosMonkeyRequestScope.callChaosMonkey(ChaosTarget.REST_TEMPLATE, httpRequest.getURI().toString());

Check warning on line 46 in chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcher.java

View check run for this annotation

Codecov / codecov/patch

chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyRestTemplateWatcher.java#L46

Added line #L46 was not covered by tests
}
return response;
}

static class ErrorResponse implements ClientHttpResponse {

static final String ERROR_TEXT = "This error is generated by Chaos Monkey for Spring Boot";
static final String ERROR_BODY = "{\"error\": \"This is a Chaos Monkey for Spring Boot generated failure\"}";

private static final Logger Logger = LoggerFactory.getLogger(ErrorResponse.class);

@Nullable
private InputStream responseStream;

@Override
public int getRawStatusCode() {
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}

@Override
public HttpStatusCode getStatusCode() {
return HttpStatusCode.valueOf(getRawStatusCode());
}

@Override
public String getStatusText() {
return ERROR_TEXT;
}

@Override
public void close() {
try {
if (this.responseStream == null) {
getBody();
}
StreamUtils.drain(this.responseStream);
this.responseStream.close();
} catch (Exception ex) {
Logger.debug("Exception while closing the error response", ex);
// ignore {@see
// #org.springframework.http.client.SimpleClientHttpResponse.close()}
}
}

@Override
public InputStream getBody() {
responseStream = new ByteArrayInputStream(ERROR_BODY.getBytes(StandardCharsets.UTF_8));
return responseStream;
}

@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,26 @@

import de.codecentric.spring.boot.chaos.monkey.component.ChaosMonkeyRequestScope;
import de.codecentric.spring.boot.chaos.monkey.component.ChaosTarget;
import de.codecentric.spring.boot.chaos.monkey.configuration.AssaultProperties;
import de.codecentric.spring.boot.chaos.monkey.configuration.WatcherProperties;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import reactor.core.publisher.Mono;

/** @author Marcel Becker */
/**
* @author Marcel Becker
*/
public class ChaosMonkeyWebClientWatcher implements ExchangeFilterFunction {

private final ChaosMonkeyRequestScope chaosMonkeyRequestScope;
private final WatcherProperties watcherProperties;
private final AssaultProperties assaultProperties;

private static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";

public ChaosMonkeyWebClientWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties,
AssaultProperties assaultProperties) {
public ChaosMonkeyWebClientWatcher(final ChaosMonkeyRequestScope chaosMonkeyRequestScope, final WatcherProperties watcherProperties) {
this.chaosMonkeyRequestScope = chaosMonkeyRequestScope;
this.watcherProperties = watcherProperties;
this.assaultProperties = assaultProperties;
}

@Override
Expand All @@ -48,19 +45,10 @@ public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction
Mono<ClientResponse> response = exchangeFunction.exchange(requestFilterWrapper.clientRequest);
if (requestFilterWrapper.filter) {
if (watcherProperties.isWebClient()) {
try {
response = response.map((clientResponse) -> {
chaosMonkeyRequestScope.callChaosMonkey(ChaosTarget.WEB_CLIENT, clientRequest.url().toString());
} catch (final Exception exception) {
try {
if (exception.getClass().equals(assaultProperties.getException().getExceptionClass())) {
response = Mono.just(ErrorClientResponse.getResponse());
} else {
throw exception;
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return clientResponse;

Check warning on line 50 in chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcher.java

View check run for this annotation

Codecov / codecov/patch

chaos-monkey-spring-boot/src/main/java/de/codecentric/spring/boot/chaos/monkey/watcher/outgoing/ChaosMonkeyWebClientWatcher.java#L50

Added line #L50 was not covered by tests
});
}
}
return response;
Expand All @@ -82,13 +70,4 @@ private RequestFilterWrapper handleOncePerRequest(final ClientRequest clientRequ

private record RequestFilterWrapper(ClientRequest clientRequest, Boolean filter) {
}

static class ErrorClientResponse {

static final String ERROR_BODY = "{\"error\": \"This is a Chaos Monkey for Spring Boot generated failure\"}";

private static ClientResponse getResponse() {
return ClientResponse.create(HttpStatus.INTERNAL_SERVER_ERROR).body(ERROR_BODY).build();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,8 +15,6 @@
*/
package de.codecentric.spring.boot.chaos.monkey.watcher.outgoing;

import static de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyRestTemplateWatcher.ErrorResponse.ERROR_BODY;
import static de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyRestTemplateWatcher.ErrorResponse.ERROR_TEXT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;

Expand Down Expand Up @@ -52,7 +50,10 @@ public void testInterceptorIsPresent() {
}

@SpringBootTest(properties = {"chaos.monkey.enabled=true", "chaos.monkey.watcher.rest-template=true",
"chaos.monkey.assaults.exceptions-active=true"}, classes = {ChaosDemoApplication.class})
"chaos.monkey.assaults.exceptions-active=true",
"chaos.monkey.assaults.exception.type=org.springframework.web.client.HttpServerErrorException",
"chaos.monkey.assaults.exception.arguments[0].type=org.springframework.http.HttpStatusCode",
"chaos.monkey.assaults.exception.arguments[0].value=500"}, classes = {ChaosDemoApplication.class})
@ActiveProfiles("chaos-monkey")
@Nested
class ExceptionAssaultIntegrationTest {
Expand All @@ -62,8 +63,7 @@ class ExceptionAssaultIntegrationTest {

@Test
public void testRestTemplateExceptionAssault() {
assertThatThrownBy(() -> this.demoRestTemplateService.callWithRestTemplate())
.hasMessage(500 + " " + ERROR_TEXT + ": \"" + ERROR_BODY + '"');
assertThatThrownBy(() -> this.demoRestTemplateService.callWithRestTemplate()).hasMessage("500 INTERNAL_SERVER_ERROR");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,48 +15,51 @@
*/
package de.codecentric.spring.boot.chaos.monkey.watcher.outgoing;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;

import de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyWebClientWatcher.ErrorClientResponse;
import de.codecentric.spring.boot.demo.chaos.monkey.ChaosDemoApplication;
import de.codecentric.spring.boot.demo.chaos.monkey.service.DemoWebClientService;
import io.netty.handler.timeout.ReadTimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ChaosMonkeyWebClientWatcherIntegrationTest {

@SpringBootTest(properties = {"chaos.monkey.watcher.web-client=true", "chaos.monkey.enabled=true",
"chaos.monkey.assaults.exceptions-active=true"}, classes = {ChaosDemoApplication.class})
@Nested
@SpringBootTest(properties = {"chaos.monkey.watcher.web-client=true", "chaos.monkey.enabled=true", "chaos.monkey.assaults.exceptions-active=true",
"chaos.monkey.assaults.exception.type=org.springframework.web.reactive.function.client.WebClientResponseException",
"chaos.monkey.assaults.exception.method=create", "chaos.monkey.assaults.exception.arguments[0].type=int",
"chaos.monkey.assaults.exception.arguments[0].value=500", "chaos.monkey.assaults.exception.arguments[1].type=java.lang.String",
"chaos.monkey.assaults.exception.arguments[1].value=Failed",
"chaos.monkey.assaults.exception.arguments[2].type=org.springframework.http.HttpHeaders",
"chaos.monkey.assaults.exception.arguments[2].value=null", "chaos.monkey.assaults.exception.arguments[3].type=byte[]",
"chaos.monkey.assaults.exception.arguments[3].value=[70,97,105,108]", // "Fail" in UTF8
"chaos.monkey.assaults.exception.arguments[4].type=java.nio.charset.Charset",
"chaos.monkey.assaults.exception.arguments[4].value=null",}, classes = {ChaosDemoApplication.class})
@ActiveProfiles("chaos-monkey")
static class ExceptionAssaultIntegrationTest {
class ExceptionAssaultIntegrationTest {

@Autowired
private DemoWebClientService demoWebClientService;

@Test
public void testWebClientExceptionAssault() {
try {
this.demoWebClientService.callWithWebClient();
fail("No WebClientResponseException occurred!");
} catch (WebClientResponseException ex) {
String body = ex.getResponseBodyAsString();
assertThat(body).isEqualTo(ErrorClientResponse.ERROR_BODY);
}

assertThatThrownBy(() -> this.demoWebClientService.callWithWebClient()).isInstanceOf(WebClientResponseException.class)
.has(new Condition<>((ex) -> ((WebClientResponseException) ex).getResponseBodyAsString().equals("Fail"), "Body equals fail"));
}
}

@Slf4j
@Nested
@SpringBootTest(properties = {"chaos.monkey.enabled=true", "chaos.monkey.watcher.web-client=true", "chaos.monkey.assaults.latency-active=true",
"chaos.monkey.test.rest-template.time-out=20"}, classes = {ChaosDemoApplication.class})
@ActiveProfiles("chaos-monkey")
static class LatencyAssaultIntegrationTest {
class LatencyAssaultIntegrationTest {

@Autowired
private DemoWebClientService demoWebClientService;
Expand Down

0 comments on commit 091211c

Please sign in to comment.