Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

395 spring retry recover method not getting called in case of exception assault on service class #529

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 @@ -11,6 +11,7 @@ Built with Spring Boot {spring-boot-version}

=== New Features
// - 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/issues/395 Recover method should be called in case of service assault.

=== Contributors
This release was only possible because of these great humans ❤️:
Expand Down
5 changes: 5 additions & 0 deletions chaos-monkey-docs/src/main/asciidoc/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ You can decide which attacks you want to run and which parts of your application
|TRUE or FALSE
|FALSE

|chaos.monkey.assaults.exceptions-ignored-on-recover.
|Exception ignored on `@Recover` annotated methods
|TRUE or FALSE
|FALSE

|chaos.monkey.assaults.exception.type
|Exception to be thrown
|Class name
Expand Down
6 changes: 6 additions & 0 deletions chaos-monkey-spring-boot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.9</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2024 the original author or authors.
* Copyright 2018-2025 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 @@ -20,22 +20,28 @@
import de.codecentric.spring.boot.chaos.monkey.assaults.ChaosMonkeyAssault;
import de.codecentric.spring.boot.chaos.monkey.assaults.ChaosMonkeyRequestAssault;
import de.codecentric.spring.boot.chaos.monkey.assaults.ChaosMonkeyRuntimeAssault;
import de.codecentric.spring.boot.chaos.monkey.assaults.ExceptionAssault;
import de.codecentric.spring.boot.chaos.monkey.configuration.AssaultProperties;
import de.codecentric.spring.boot.chaos.monkey.configuration.ChaosMonkeySettings;
import de.codecentric.spring.boot.chaos.monkey.configuration.MethodFilter;
import de.codecentric.spring.boot.chaos.monkey.configuration.toggles.ChaosToggleNameMapper;
import de.codecentric.spring.boot.chaos.monkey.configuration.toggles.ChaosToggles;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @author Benjamin Wilms
*/
@Slf4j
public class ChaosMonkeyRequestScope {

private final ChaosMonkeySettings chaosMonkeySettings;
Expand All @@ -47,16 +53,14 @@ public class ChaosMonkeyRequestScope {
private final MetricEventPublisher metricEventPublisher;

private final AtomicInteger assaultCounter;
private final MethodFilter methodFilter;

public ChaosMonkeyRequestScope(ChaosMonkeySettings chaosMonkeySettings, List<ChaosMonkeyRequestAssault> assaults,
List<ChaosMonkeyAssault> legacyAssaults, MetricEventPublisher metricEventPublisher, ChaosToggles chaosToggles,
ChaosToggleNameMapper chaosToggleNameMapper) {
List<RequestAssaultAdapter> assaultAdapters = legacyAssaults.stream()
.filter(it -> !(it instanceof ChaosMonkeyRequestAssault || it instanceof ChaosMonkeyRuntimeAssault)).map(RequestAssaultAdapter::new)
.toList();
List<ChaosMonkeyRequestAssault> requestAssaults = new ArrayList<>();
requestAssaults.addAll(assaults);
requestAssaults.addAll(assaultAdapters);
ChaosToggleNameMapper chaosToggleNameMapper, MethodFilter methodFilter) {
this.methodFilter = methodFilter;
List<ChaosMonkeyRequestAssault> requestAssaults = new ArrayList<>(assaults);
requestAssaults.addAll(getLegacyAssaults(legacyAssaults));

this.chaosMonkeySettings = chaosMonkeySettings;
this.assaults = requestAssaults;
Expand All @@ -66,51 +70,55 @@ public ChaosMonkeyRequestScope(ChaosMonkeySettings chaosMonkeySettings, List<Cha
this.assaultCounter = new AtomicInteger(0);
}

public void callChaosMonkey(ChaosTarget type, String simpleName) {
if (isEnabled(type, simpleName) && isTrouble()) {
private static List<RequestAssaultAdapter> getLegacyAssaults(List<ChaosMonkeyAssault> legacyAssaults) {
return legacyAssaults.stream().filter(Predicate.not(ChaosMonkeyRuntimeAssault.class::isInstance))
.filter(Predicate.not(ChaosMonkeyRequestAssault.class::isInstance)).map(RequestAssaultAdapter::new).toList();
}

if (metricEventPublisher != null) {
metricEventPublisher.publishMetricEvent(MetricType.APPLICATION_REQ_COUNT, "type", "total");
}
public void callChaosMonkey(ChaosTarget type, String signature) {
callChaosMonkey(type, signature, null, null);
}

public void callChaosMonkey(ChaosTarget type, String signature, Object target, Method method) {
if (isEnabled(type, signature) && isTrouble()) {
metricEventPublisher.publishMetricEvent(MetricType.APPLICATION_REQ_COUNT, "type", "total");

// Custom watched services can be defined at runtime, if there are any, only
// these will be attacked!
AssaultProperties assaultProps = chaosMonkeySettings.getAssaultProperties();
if (assaultProps.isWatchedCustomServicesActive()) {
if (assaultProps.getWatchedCustomServices().stream().anyMatch(simpleName::startsWith)) {
// only all listed custom methods will be attacked
chooseAndRunAttack();
}
} else {
if (!assaultProps.isWatchedCustomServicesActive() || assaultProps.getWatchedCustomServices().stream().anyMatch(signature::startsWith)) {
// only all listed custom methods will be attacked
// default attack if no custom watched service is defined
chooseAndRunAttack();
chooseAndRunAttack(target, method);
}
}
}

private void chooseAndRunAttack() {
private void chooseAndRunAttack(Object target, Method method) {
List<ChaosMonkeyAssault> activeAssaults = assaults.stream().filter(ChaosMonkeyAssault::isActive).collect(Collectors.toList());
if (isEmpty(activeAssaults)) {
return;
}
getRandomFrom(activeAssaults).attack();

if (metricEventPublisher != null) {
metricEventPublisher.publishMetricEvent(MetricType.APPLICATION_REQ_COUNT, "type", "assaulted");
ChaosMonkeyAssault assault = getRandomFrom(activeAssaults);
if (target != null && method != null && assault instanceof ExceptionAssault && methodFilter.filter(target, method)) {
return;
}
assault.attack();

metricEventPublisher.publishMetricEvent(MetricType.APPLICATION_REQ_COUNT, "type", "assaulted");
}

private ChaosMonkeyAssault getRandomFrom(List<ChaosMonkeyAssault> activeAssaults) {
int exceptionRand = chaosMonkeySettings.getAssaultProperties().chooseAssault(activeAssaults.size());
return activeAssaults.get(exceptionRand);
private ChaosMonkeyAssault getRandomFrom(List<? extends ChaosMonkeyAssault> activeAssaults) {
return activeAssaults.get(chaosMonkeySettings.getAssaultProperties().chooseAssault(activeAssaults.size()));
}

private boolean isTrouble() {
if (chaosMonkeySettings.getAssaultProperties().isDeterministic()) {
return assaultCounter.incrementAndGet() % chaosMonkeySettings.getAssaultProperties().getLevel() == 0;
} else {
return chaosMonkeySettings.getAssaultProperties().getTroubleRandom() >= chaosMonkeySettings.getAssaultProperties().getLevel();
AssaultProperties assaultProperties = chaosMonkeySettings.getAssaultProperties();
if (assaultProperties.isDeterministic()) {
return assaultCounter.incrementAndGet() % assaultProperties.getLevel() == 0;
}
return assaultProperties.getTroubleRandom() >= assaultProperties.getLevel();
}

private boolean isEnabled(ChaosTarget type, String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2023 the original author or authors.
* Copyright 2018-2025 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 Down Expand Up @@ -32,24 +32,27 @@
public class AssaultProperties {
private int level = 1;

private boolean deterministic = false;
private boolean deterministic;

private int latencyRangeStart = 1000;

private int latencyRangeEnd = 3000;

private boolean latencyActive = false;
private boolean latencyActive;

private boolean exceptionsActive = false;
private boolean exceptionsActive;

@JsonIgnore
private boolean exceptionsIgnoredOnRecover;

@NestedConfigurationProperty
private AssaultException exception;

private boolean killApplicationActive = false;
private boolean killApplicationActive;

private String killApplicationCronExpression = "OFF";

private volatile boolean memoryActive = false;
private volatile boolean memoryActive;

private int memoryMillisecondsHoldFilledMemory = 90000;

Expand All @@ -61,7 +64,7 @@ public class AssaultProperties {

private String memoryCronExpression = "OFF";

private volatile boolean cpuActive = false;
private volatile boolean cpuActive;

private int cpuMillisecondsHoldLoad = 90000;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2023 the original author or authors.
* Copyright 2018-2025 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 @@ -24,18 +24,19 @@
import de.codecentric.spring.boot.chaos.monkey.configuration.toggles.DefaultChaosToggles;
import de.codecentric.spring.boot.chaos.monkey.endpoints.ChaosMonkeyJmxEndpoint;
import de.codecentric.spring.boot.chaos.monkey.endpoints.ChaosMonkeyRestEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.retry.annotation.Recover;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
Expand All @@ -53,10 +54,9 @@
@Import({UnleashChaosConfiguration.class, ChaosMonkeyWebClientConfiguration.class, ChaosMonkeyRestTemplateConfiguration.class,
ChaosMonkeyAdvisorConfiguration.class, ChaosMonkeyOpenApiConfiguration.class})
@EnableScheduling
@Slf4j
public class ChaosMonkeyConfiguration {

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

private static final String CHAOS_MONKEY_TASK_SCHEDULER = "chaosMonkeyTaskScheduler";

private final ChaosMonkeyProperties chaosMonkeyProperties;
Expand All @@ -72,10 +72,9 @@ public ChaosMonkeyConfiguration(ChaosMonkeyProperties chaosMonkeyProperties, Wat
this.assaultProperties = assaultProperties;

try {
String chaosLogo = StreamUtils.copyToString(new ClassPathResource("chaos-logo.txt").getInputStream(), Charset.defaultCharset());
Logger.info(chaosLogo);
log.info(StreamUtils.copyToString(new ClassPathResource("chaos-logo.txt").getInputStream(), Charset.defaultCharset()));
} catch (IOException e) {
Logger.info("Chaos Monkey - ready to do evil");
log.info("Chaos Monkey - ready to do evil");
}
}

Expand Down Expand Up @@ -127,8 +126,9 @@ public CpuAssault cpuAssault(ChaosMonkeySettings settings, MetricEventPublisher

@Bean
public ChaosMonkeyRequestScope chaosMonkeyRequestScope(List<ChaosMonkeyRequestAssault> chaosMonkeyAssaults, List<ChaosMonkeyAssault> allAssaults,
ChaosToggles chaosToggles, ChaosToggleNameMapper chaosToggleNameMapper, ChaosMonkeySettings settings, MetricEventPublisher publisher) {
return new ChaosMonkeyRequestScope(settings, chaosMonkeyAssaults, allAssaults, publisher, chaosToggles, chaosToggleNameMapper);
ChaosToggles chaosToggles, ChaosToggleNameMapper chaosToggleNameMapper, ChaosMonkeySettings settings, MetricEventPublisher publisher,
MethodFilter methodFilter) {
return new ChaosMonkeyRequestScope(settings, chaosMonkeyAssaults, allAssaults, publisher, chaosToggles, chaosToggleNameMapper, methodFilter);
}

@Bean
Expand Down Expand Up @@ -175,4 +175,17 @@ public ChaosMonkeyRestEndpoint chaosMonkeyRestEndpoint(ChaosMonkeySettings setti
public ChaosMonkeyJmxEndpoint chaosMonkeyJmxEndpoint(ChaosMonkeySettings settings) {
return new ChaosMonkeyJmxEndpoint(settings);
}

@Bean
@ConditionalOnClass(Recover.class)
@ConditionalOnProperty(name = "chaos.monkey.assaults.exceptions-ignored-on-recover", havingValue = "true")
public MethodFilter recoverMethodFilter() {
return new RecoverMethodFilter();
}

@Bean
@ConditionalOnMissingBean
public MethodFilter methodFilter() {
return (target, method) -> false;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021-2023 the original author or authors.
* Copyright 2021-2025 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 @@ -20,7 +20,6 @@
import de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyRestTemplatePostProcessor;
import de.codecentric.spring.boot.chaos.monkey.watcher.outgoing.ChaosMonkeyRestTemplateWatcher;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* https://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 de.codecentric.spring.boot.chaos.monkey.configuration;

import java.lang.reflect.Method;

public interface MethodFilter {

boolean filter(Object target, Method method);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* https://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 de.codecentric.spring.boot.chaos.monkey.configuration;

import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.retry.annotation.Recover;

import java.lang.reflect.Method;

public class RecoverMethodFilter implements MethodFilter {
@Override
public boolean filter(Object target, Method method) {
Recover recover = AnnotatedElementUtils.findMergedAnnotation(target.getClass(), Recover.class);
if (recover == null) {
recover = findAnnotationOnTarget(target, method);
}
return recover != null;
}

private Recover findAnnotationOnTarget(Object target, Method method) {
try {
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
return AnnotatedElementUtils.findMergedAnnotation(targetMethod, Recover.class);
} catch (RuntimeException | NoSuchMethodException e) {
return null;
}
}
}
Loading
Loading