Add statistics support via a listener

This commit is contained in:
Dave Syer
2016-06-21 12:07:35 +01:00
parent 8fcd854365
commit 6c2cc11264
10 changed files with 622 additions and 8 deletions

View File

@@ -32,7 +32,27 @@ public interface RetryContext extends AttributeAccessor {
* for instance in a retry listener, to accumulate data about the performance of a
* retry.
*/
String STATS_NAME = "stats.name";
String NAME = "context.name";
/**
* Retry context attribute name for state key. Can be used to identify a stateful retry from its context.
*/
String STATE_KEY = "context.state";
/**
* Retry context attribute that is non-null (and true) if the context has been closed.
*/
String CLOSED = "context.closed";
/**
* Retry context attribute that is non-null (and true) if the recovery path was taken.
*/
String RECOVERED = "context.recovered";
/**
* Retry context attribute that is non-null (and true) if the retry was exhausted.
*/
String EXHAUSTED = "context.exhausted";
/**
* Signal to the framework that no more attempts should be made to try or retry the

View File

@@ -26,7 +26,7 @@ package org.springframework.retry;
public interface RetryStatistics {
/**
* @return the number of completed retry attempts (successful or not).
* @return the number of completed successful retry attempts.
*/
int getCompleteCount();
@@ -54,6 +54,13 @@ public interface RetryStatistics {
*/
int getAbortCount();
/**
* Get the number of times a recovery callback was applied.
*
* @return the number of recovered attempts.
*/
int getRecoveryCount();
/**
* Get an identifier for the retry block for reporting purposes.
*

View File

@@ -77,7 +77,7 @@ public class RetryOperationsInterceptor implements MethodInterceptor {
public Object doWithRetry(RetryContext context) throws Exception {
context.setAttribute(RetryContext.STATS_NAME, label);
context.setAttribute(RetryContext.NAME, label);
/*
* If we don't copy the invocation carefully it won't keep a reference to

View File

@@ -170,7 +170,7 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
}
public Object doWithRetry(RetryContext context) throws Exception {
context.setAttribute(RetryContext.STATS_NAME, label);
context.setAttribute(RetryContext.NAME, label);
try {
return invocation.proceed();
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2012-2015 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
*
* 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 org.springframework.retry.stats;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.retry.RetryStatistics;
/**
* @author Dave Syer
*
*/
public class DefaultRetryStatistics implements RetryStatistics {
private String name;
private AtomicInteger startedCount = new AtomicInteger();
private AtomicInteger completeCount = new AtomicInteger();
private AtomicInteger recoveryCount = new AtomicInteger();
private AtomicInteger errorCount = new AtomicInteger();
private AtomicInteger abortCount = new AtomicInteger();
DefaultRetryStatistics() {
}
public DefaultRetryStatistics(String name) {
this.name = name;
}
public DefaultRetryStatistics(String name, int startedCount, int completeCount,
int recoveryCount, int errorCount, int abortCount) {
this.name = name;
this.startedCount = new AtomicInteger(startedCount);
this.completeCount = new AtomicInteger(completeCount);
this.recoveryCount = new AtomicInteger(recoveryCount);
this.errorCount = new AtomicInteger(errorCount);
this.abortCount = new AtomicInteger(abortCount);
}
@Override
public int getCompleteCount() {
return completeCount.get();
}
@Override
public int getStartedCount() {
return startedCount.get();
}
@Override
public int getErrorCount() {
return errorCount.get();
}
@Override
public int getAbortCount() {
return abortCount.get();
}
@Override
public String getName() {
return name;
}
@Override
public int getRecoveryCount() {
return recoveryCount.get();
}
public void setName(String name) {
this.name = name;
}
public void setStartedCount(int startedCount) {
this.startedCount.set(startedCount);
}
public void setCompleteCount(int completeCount) {
this.completeCount.set(completeCount);
}
public void setRecoveryCount(int recoveryCount) {
this.recoveryCount.set(recoveryCount);
}
public void setErrorCount(int errorCount) {
this.errorCount.set(errorCount);
}
public void setAbortCount(int abortCount) {
this.abortCount.set(abortCount);
}
public void incrementStartedCount() {
this.startedCount.incrementAndGet();
}
public void incrementCompleteCount() {
this.completeCount.incrementAndGet();
}
public void incrementRecoveryCount() {
this.recoveryCount.incrementAndGet();
}
public void incrementErrorCount() {
this.errorCount.incrementAndGet();
}
public void incrementAbortCount() {
this.abortCount.incrementAndGet();
}
@Override
public String toString() {
return "DefaultRetryStatistics [name=" + name + ", startedCount=" + startedCount
+ ", completeCount=" + completeCount + ", recoveryCount=" + recoveryCount
+ ", errorCount=" + errorCount + ", abortCount=" + abortCount + "]";
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2015 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
*
* 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 org.springframework.retry.stats;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.retry.RetryStatistics;
/**
* @author Dave Syer
*
*/
public class DefaultStatisticsRepository implements StatisticsRepository {
private ConcurrentMap<String, DefaultRetryStatistics> map = new ConcurrentHashMap<String, DefaultRetryStatistics>();
@Override
public RetryStatistics findOne(String name) {
return map.get(name);
}
@Override
public Iterable<RetryStatistics> findAll() {
return new ArrayList<RetryStatistics>(map.values());
}
@Override
public void addStarted(String name) {
getStatistics(name).incrementStartedCount();
}
@Override
public void addError(String name) {
getStatistics(name).incrementErrorCount();
}
@Override
public void addRecovery(String name) {
getStatistics(name).incrementRecoveryCount();
}
@Override
public void addComplete(String name) {
getStatistics(name).incrementCompleteCount();
}
@Override
public void addAbort(String name) {
getStatistics(name).incrementAbortCount();
}
private DefaultRetryStatistics getStatistics(String name) {
DefaultRetryStatistics stats;
if (!map.containsKey(name)) {
map.putIfAbsent(name, new DefaultRetryStatistics(name));
}
stats = map.get(name);
return stats;
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2012-2015 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
*
* 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 org.springframework.retry.stats;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.RetryListenerSupport;
/**
* @author Dave Syer
*
*/
public class StatisticsListener extends RetryListenerSupport {
private final StatisticsRepository repository;
public StatisticsListener(StatisticsRepository repository) {
this.repository = repository;
}
@Override
public <T, E extends Throwable> void close(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
String name = getName(context);
if (name != null) {
if (!isExhausted(context)) {
// If exhausted and stateful then the retry callback was not called. If
// exhausted and stateless it was called, but the started counter was
// already incremented.
repository.addStarted(name);
}
if (isRecovered(context)) {
repository.addRecovery(name);
}
else if (isExhausted(context)) {
repository.addAbort(name);
}
else if (isClosed(context)) {
repository.addComplete(name);
}
}
}
@Override
public <T, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
String name = getName(context);
if (name != null) {
if (!hasState(context)) {
// Stateless retry involves starting the retry callback once per error
// without closing the context, so we need to increment the started count
repository.addStarted(name);
}
repository.addError(name);
}
}
private boolean isExhausted(RetryContext context) {
return context.hasAttribute(RetryContext.EXHAUSTED);
}
private boolean isClosed(RetryContext context) {
return context.hasAttribute(RetryContext.CLOSED);
}
private boolean isRecovered(RetryContext context) {
return context.hasAttribute(RetryContext.RECOVERED);
}
private boolean hasState(RetryContext context) {
return context.hasAttribute(RetryContext.STATE_KEY);
}
private String getName(RetryContext context) {
return (String) context.getAttribute(RetryContext.NAME);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2015 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
*
* 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 org.springframework.retry.stats;
import org.springframework.retry.RetryStatistics;
/**
* @author Dave Syer
*
*/
public interface StatisticsRepository {
RetryStatistics findOne(String name);
Iterable<RetryStatistics> findAll();
void addStarted(String name);
void addError(String name);
void addRecovery(String name);
void addComplete(String name);
void addAbort(String name);
}

View File

@@ -371,10 +371,12 @@ public class RetryTemplate implements RetryOperations {
if (succeeded) {
this.retryContextCache.remove(state.getKey());
retryPolicy.close(context);
context.setAttribute(RetryContext.CLOSED, true);
}
}
else {
retryPolicy.close(context);
context.setAttribute(RetryContext.CLOSED, true);
}
}
@@ -417,14 +419,14 @@ public class RetryTemplate implements RetryOperations {
Object key = state.getKey();
if (state.isForceRefresh()) {
return doOpenInternal(retryPolicy);
return doOpenInternal(retryPolicy, state);
}
// If there is no cache hit we can avoid the possible expense of the
// cache re-hydration.
if (!this.retryContextCache.containsKey(key)) {
// The cache is only used if there is a failure.
return doOpenInternal(retryPolicy);
return doOpenInternal(retryPolicy, state);
}
RetryContext context = this.retryContextCache.get(key);
@@ -437,15 +439,23 @@ public class RetryTemplate implements RetryOperations {
}
// The cache could have been expired in between calls to
// containsKey(), so we have to live with this:
return doOpenInternal(retryPolicy);
return doOpenInternal(retryPolicy, state);
}
return context;
}
private RetryContext doOpenInternal(RetryPolicy retryPolicy, RetryState state) {
RetryContext context = retryPolicy.open(RetrySynchronizationManager.getContext());
if (state!=null) {
context.setAttribute(RetryContext.STATE_KEY, state.getKey());
}
return context;
}
private RetryContext doOpenInternal(RetryPolicy retryPolicy) {
return retryPolicy.open(RetrySynchronizationManager.getContext());
return doOpenInternal(retryPolicy, null);
}
/**
@@ -465,10 +475,12 @@ public class RetryTemplate implements RetryOperations {
*/
protected <T> T handleRetryExhausted(RecoveryCallback<T> recoveryCallback,
RetryContext context, RetryState state) throws Throwable {
context.setAttribute(RetryContext.EXHAUSTED, true);
if (state != null) {
this.retryContextCache.remove(state.getKey());
}
if (recoveryCallback != null) {
context.setAttribute(RetryContext.RECOVERED, true);
return recoveryCallback.recover(context);
}
if (state != null) {

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2012-2015 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
*
* 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 org.springframework.retry.stats;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.Collections;
import org.junit.Test;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryState;
import org.springframework.retry.RetryStatistics;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.DefaultRetryState;
import org.springframework.retry.support.RetryTemplate;
/**
* @author Dave Syer
*
*/
public class StatisticsListenerTests {
private StatisticsRepository repository = new DefaultStatisticsRepository();
private StatisticsListener listener = new StatisticsListener(repository);
@Test
public void testStatelessSuccessful() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setListeners(new RetryListenerSupport[] { listener });
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x,
Collections.<Class<? extends Throwable>, Boolean>singletonMap(
Exception.class, true)));
retryTemplate.execute(callback);
assertEquals(x, callback.attempts);
RetryStatistics stats = repository.findOne("test");
// System.err.println(stats);
assertNotNull(stats);
assertEquals(x, stats.getCompleteCount());
assertEquals((x + 1) * x / 2, stats.getStartedCount());
assertEquals(stats.getStartedCount(), stats.getErrorCount() + x);
}
}
@Test
public void testStatefulSuccessful() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setListeners(new RetryListenerSupport[] { listener });
RetryState state = new DefaultRetryState("foo");
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x,
Collections.<Class<? extends Throwable>, Boolean>singletonMap(
Exception.class, true)));
for (int i = 0; i < x; i++) {
try {
retryTemplate.execute(callback, state);
}
catch (Exception e) {
// don't care
}
}
assertEquals(x, callback.attempts);
RetryStatistics stats = repository.findOne("test");
// System.err.println(stats);
assertNotNull(stats);
assertEquals(x, stats.getCompleteCount());
assertEquals((x + 1) * x / 2, stats.getStartedCount());
assertEquals(stats.getStartedCount(), stats.getErrorCount() + x);
}
}
@Test
public void testStatelessUnsuccessful() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setListeners(new RetryListenerSupport[] { listener });
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x + 1);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x,
Collections.<Class<? extends Throwable>, Boolean>singletonMap(
Exception.class, true)));
try {
retryTemplate.execute(callback);
}
catch (Exception e) {
// not interested
}
assertEquals(x, callback.attempts);
RetryStatistics stats = repository.findOne("test");
assertNotNull(stats);
assertEquals(x, stats.getAbortCount());
assertEquals((x + 1) * x / 2, stats.getStartedCount());
assertEquals(stats.getStartedCount(), stats.getErrorCount());
}
}
@Test
public void testStatefulUnsuccessful() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setListeners(new RetryListenerSupport[] { listener });
RetryState state = new DefaultRetryState("foo");
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x + 1);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x,
Collections.<Class<? extends Throwable>, Boolean>singletonMap(
Exception.class, true)));
for (int i = 0; i < x+1; i++) {
try {
retryTemplate.execute(callback, state);
}
catch (Exception e) {
// don't care
}
}
assertEquals(x, callback.attempts);
RetryStatistics stats = repository.findOne("test");
// System.err.println(stats);
assertNotNull(stats);
assertEquals(x, stats.getAbortCount());
assertEquals((x + 1) * x / 2, stats.getStartedCount());
assertEquals(stats.getStartedCount(), stats.getErrorCount());
}
}
@Test
public void testStatelessRecovery() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setListeners(new RetryListenerSupport[] { listener });
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x + 1);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x,
Collections.<Class<? extends Throwable>, Boolean>singletonMap(
Exception.class, true)));
retryTemplate.execute(callback, new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
return null;
}
});
assertEquals(x, callback.attempts);
RetryStatistics stats = repository.findOne("test");
// System.err.println(stats);
assertNotNull(stats);
assertEquals(x, stats.getRecoveryCount());
assertEquals((x + 1) * x / 2, stats.getStartedCount());
assertEquals(stats.getStartedCount(), stats.getErrorCount());
}
}
@Test
public void testStatefulRecovery() throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setListeners(new RetryListenerSupport[] { listener });
RetryState state = new DefaultRetryState("foo");
for (int x = 1; x <= 10; x++) {
MockRetryCallback callback = new MockRetryCallback();
callback.setAttemptsBeforeSuccess(x + 1);
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x,
Collections.<Class<? extends Throwable>, Boolean>singletonMap(
Exception.class, true)));
for (int i = 0; i < x+1; i++) {
try {
retryTemplate.execute(callback, new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
return null;
}
}, state);
}
catch (Exception e) {
// don't care
}
}
assertEquals(x, callback.attempts);
RetryStatistics stats = repository.findOne("test");
// System.err.println(stats);
assertNotNull(stats);
assertEquals(x, stats.getRecoveryCount());
assertEquals((x + 1) * x / 2, stats.getStartedCount());
assertEquals(stats.getStartedCount(), stats.getErrorCount());
}
}
private static class MockRetryCallback implements RetryCallback<Object, Exception> {
private int attempts;
private int attemptsBeforeSuccess;
private Exception exceptionToThrow = new Exception();
@Override
public Object doWithRetry(RetryContext status) throws Exception {
status.setAttribute(RetryContext.NAME, "test");
this.attempts++;
if (this.attempts < this.attemptsBeforeSuccess) {
throw this.exceptionToThrow;
}
return null;
}
public void setAttemptsBeforeSuccess(int attemptsBeforeSuccess) {
this.attemptsBeforeSuccess = attemptsBeforeSuccess;
}
}
}