Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

package org.springframework.cache.transaction;

import org.jspecify.annotations.Nullable;

import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.support.AbstractCacheManager;

/**
Expand All @@ -25,6 +28,7 @@
* on explicitly through the {@link #setTransactionAware} bean property.
*
* @author Juergen Hoeller
* @author Seonghun Lee
* @since 3.2
* @see #setTransactionAware
* @see TransactionAwareCacheDecorator
Expand All @@ -34,6 +38,8 @@ public abstract class AbstractTransactionSupportingCacheManager extends Abstract

private boolean transactionAware = false;

private @Nullable CacheErrorHandler errorHandler;


/**
* Set whether this CacheManager should expose transaction-aware Cache objects.
Expand All @@ -52,10 +58,35 @@ public boolean isTransactionAware() {
return this.transactionAware;
}

/**
* Set the {@link CacheErrorHandler} for {@link Cache#put}, {@link Cache#evict}
* and {@link Cache#clear} failures in the after-commit phase of a transaction,
* applied when this CacheManager is {@linkplain #setTransactionAware
* transaction-aware}.
* <p>By default, such failures are propagated to the caller of the transaction
* commit, bypassing any error handler configured at the cache interception
* level, since the deferred operation runs outside the intercepted cache
* invocation.
* @since 7.1
* @see TransactionAwareCacheDecorator
*/
public void setErrorHandler(@Nullable CacheErrorHandler errorHandler) {
this.errorHandler = errorHandler;
}

/**
* Return the {@link CacheErrorHandler} for after-commit cache operation
* failures, if any.
* @since 7.1
*/
public @Nullable CacheErrorHandler getErrorHandler() {
return this.errorHandler;
}


@Override
protected Cache decorateCache(Cache cache) {
return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache);
return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache, getErrorHandler()) : cache);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.jspecify.annotations.Nullable;

import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
Expand All @@ -42,21 +43,42 @@
* @author Juergen Hoeller
* @author Stephane Nicoll
* @author Stas Volsky
* @author Seonghun Lee
* @since 3.2
* @see TransactionAwareCacheManagerProxy
*/
public class TransactionAwareCacheDecorator implements Cache {

private final Cache targetCache;

private final @Nullable CacheErrorHandler errorHandler;


/**
* Create a new TransactionAwareCache for the given target Cache.
* @param targetCache the target Cache to decorate
*/
public TransactionAwareCacheDecorator(Cache targetCache) {
this(targetCache, null);
}

/**
* Create a new TransactionAwareCache for the given target Cache, using the
* given {@link CacheErrorHandler} for {@link #put}, {@link #evict} and
* {@link #clear} failures in the after-commit phase of a transaction.
* <p>Without an error handler, such failures are propagated to the caller
* of the transaction commit, bypassing any error handler configured at the
* cache interception level, since the deferred operation runs outside the
* intercepted cache invocation.
* @param targetCache the target Cache to decorate
* @param errorHandler the error handler to invoke for cache operation
* failures in the after-commit phase (may be {@code null} to propagate them)
* @since 7.1
*/
public TransactionAwareCacheDecorator(Cache targetCache, @Nullable CacheErrorHandler errorHandler) {
Assert.notNull(targetCache, "Target Cache must not be null");
this.targetCache = targetCache;
this.errorHandler = errorHandler;
}


Expand Down Expand Up @@ -108,7 +130,16 @@ public void put(final Object key, final @Nullable Object value) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
try {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
catch (RuntimeException ex) {
if (TransactionAwareCacheDecorator.this.errorHandler == null) {
throw ex;
}
TransactionAwareCacheDecorator.this.errorHandler.handleCachePutError(
ex, TransactionAwareCacheDecorator.this.targetCache, key, value);
}
}
});
}
Expand All @@ -128,7 +159,16 @@ public void evict(final Object key) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.evict(key);
try {
TransactionAwareCacheDecorator.this.targetCache.evict(key);
}
catch (RuntimeException ex) {
if (TransactionAwareCacheDecorator.this.errorHandler == null) {
throw ex;
}
TransactionAwareCacheDecorator.this.errorHandler.handleCacheEvictError(
ex, TransactionAwareCacheDecorator.this.targetCache, key);
}
}
});
}
Expand All @@ -148,7 +188,16 @@ public void clear() {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
targetCache.clear();
try {
TransactionAwareCacheDecorator.this.targetCache.clear();
}
catch (RuntimeException ex) {
if (TransactionAwareCacheDecorator.this.errorHandler == null) {
throw ex;
}
TransactionAwareCacheDecorator.this.errorHandler.handleCacheClearError(
ex, TransactionAwareCacheDecorator.this.targetCache);
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,26 @@

package org.springframework.cache.transaction;

import java.util.ArrayList;
import java.util.List;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;

import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.testfixture.CallCountingTransactionManager;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

/**
* @author Stephane Nicoll
* @author Juergen Hoeller
* @author Seonghun Lee
*/
class TransactionAwareCacheDecoratorTests {

Expand Down Expand Up @@ -221,4 +228,100 @@ void invalidateTransactional() { // no transactional support for invalidate
assertThat(target.get(key)).isNull();
}

@Test // gh-28554
void putTransactionalWithFailureAndErrorHandler() {
Cache target = new FailingCache("testCache");
CollectingCacheErrorHandler errorHandler = new CollectingCacheErrorHandler();
Cache cache = new TransactionAwareCacheDecorator(target, errorHandler);
Object key = new Object();

txTemplate.executeWithoutResult(s -> cache.put(key, "123"));

assertThat(errorHandler.handled).singleElement().isInstanceOf(SimulatedFailure.class);
}

@Test // gh-28554
void evictTransactionalWithFailureAndErrorHandler() {
Cache target = new FailingCache("testCache");
CollectingCacheErrorHandler errorHandler = new CollectingCacheErrorHandler();
Cache cache = new TransactionAwareCacheDecorator(target, errorHandler);

txTemplate.executeWithoutResult(s -> cache.evict(new Object()));

assertThat(errorHandler.handled).singleElement().isInstanceOf(SimulatedFailure.class);
}

@Test // gh-28554
void clearTransactionalWithFailureAndErrorHandler() {
Cache target = new FailingCache("testCache");
CollectingCacheErrorHandler errorHandler = new CollectingCacheErrorHandler();
Cache cache = new TransactionAwareCacheDecorator(target, errorHandler);

txTemplate.executeWithoutResult(s -> cache.clear());

assertThat(errorHandler.handled).singleElement().isInstanceOf(SimulatedFailure.class);
}

@Test // gh-28554
void putTransactionalWithFailureAndNoErrorHandler() {
Cache target = new FailingCache("testCache");
Cache cache = new TransactionAwareCacheDecorator(target);
Object key = new Object();

assertThatExceptionOfType(SimulatedFailure.class)
.isThrownBy(() -> txTemplate.executeWithoutResult(s -> cache.put(key, "123")));
}


@SuppressWarnings("serial")
private static class SimulatedFailure extends RuntimeException {
}

private static class FailingCache extends ConcurrentMapCache {

FailingCache(String name) {
super(name);
}

@Override
public void put(Object key, @Nullable Object value) {
throw new SimulatedFailure();
}

@Override
public void evict(Object key) {
throw new SimulatedFailure();
}

@Override
public void clear() {
throw new SimulatedFailure();
}
}

private static class CollectingCacheErrorHandler implements CacheErrorHandler {

final List<RuntimeException> handled = new ArrayList<>();

@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
this.handled.add(exception);
}

@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) {
this.handled.add(exception);
}

@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
this.handled.add(exception);
}

@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
this.handled.add(exception);
}
}

}
Loading