DATAREDIS-73 - Add support for spring managed transactions.
RedisTemplate now supports enabling of transaction support, which is disabled by default. In case of enabled transaction support the RedisConnections will be bound to the current thread during ongoing outer transactions. We call MULTI at the beginning and depending on transaction state EXEC or DISCARD at its end. To support read operations during the transaction we wrap a proxy around the bound connection piping read operations to a new (non thread bound) connection obtained by the underlying RedisConnectionFactory. Transaction support is available for jedis, lettuce and srp, while had to be skipped for jredis due to the lack of support for MULTI. Original pull request: #64.
This commit is contained in:
committed by
Thomas Darimont
parent
4e4a2961da
commit
bc1bbd1d39
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2014 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.data.redis.connection;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.hamcrest.core.Is;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.test.annotation.Rollback;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.transaction.AfterTransaction;
|
||||
import org.springframework.test.context.transaction.TransactionConfiguration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@Transactional
|
||||
@TransactionConfiguration(transactionManager = "transactionManager")
|
||||
public abstract class AbstractTransactionalTestBase {
|
||||
|
||||
@Configuration
|
||||
public abstract static class RedisContextConfiguration {
|
||||
|
||||
@Bean
|
||||
public StringRedisTemplate redisTemplate() {
|
||||
|
||||
StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
|
||||
|
||||
// explicitly enable transaction support
|
||||
template.setEnableTransactionSupport(true);
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public abstract RedisConnectionFactory redisConnectionFactory();
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager() throws SQLException {
|
||||
return new DataSourceTransactionManager(dataSource());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() throws SQLException {
|
||||
|
||||
DataSource ds = Mockito.mock(DataSource.class);
|
||||
Mockito.when(ds.getConnection()).thenReturn(Mockito.mock(Connection.class));
|
||||
return ds;
|
||||
}
|
||||
}
|
||||
|
||||
private @Autowired StringRedisTemplate template;
|
||||
|
||||
private @Autowired RedisConnectionFactory factory;
|
||||
|
||||
private List<String> KEYS = Arrays.asList("spring", "data", "redis");
|
||||
private boolean valuesShouldHaveBeenPersisted = false;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
valuesShouldHaveBeenPersisted = false;
|
||||
cleanDataStore();
|
||||
}
|
||||
|
||||
private void cleanDataStore() {
|
||||
|
||||
RedisConnection connection = factory.getConnection();
|
||||
connection.flushDb();
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@AfterTransaction
|
||||
public void verifyTransactionResult() {
|
||||
|
||||
RedisConnection connection = factory.getConnection();
|
||||
for (String key : KEYS) {
|
||||
Assert.assertThat("Values for " + key + " should " + (valuesShouldHaveBeenPersisted ? "" : "NOT ")
|
||||
+ "have been found.", connection.exists(key.getBytes()), Is.is(valuesShouldHaveBeenPersisted));
|
||||
}
|
||||
connection.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Rollback(true)
|
||||
@Test
|
||||
public void valueOperationSetShouldBeRolledBackCorrectly() {
|
||||
|
||||
for (String key : KEYS) {
|
||||
template.opsForValue().set(key, key + "-value");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Rollback(false)
|
||||
@Test
|
||||
public void valueOperationSetShouldBeCommittedCorrectly() {
|
||||
|
||||
this.valuesShouldHaveBeenPersisted = true;
|
||||
for (String key : KEYS) {
|
||||
template.opsForValue().set(key, key + "-value");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Rollback(true)
|
||||
@Test
|
||||
public void listOperationLPushShoudBeRolledBackCorrectly() {
|
||||
|
||||
for (String key : KEYS) {
|
||||
template.opsForList().leftPushAll(key, (String[]) KEYS.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Rollback(false)
|
||||
@Test
|
||||
public void listOperationLPushShoudBeCommittedCorrectly() {
|
||||
|
||||
this.valuesShouldHaveBeenPersisted = true;
|
||||
for (String key : KEYS) {
|
||||
template.opsForList().leftPushAll(key, (String[]) KEYS.toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2014 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.data.redis.connection.jedis;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.AbstractTransactionalTestBase;
|
||||
import org.springframework.data.redis.connection.jedis.TransactionalJedisItegrationTests.JedisContextConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ContextConfiguration(classes = { JedisContextConfiguration.class })
|
||||
public class TransactionalJedisItegrationTests extends AbstractTransactionalTestBase {
|
||||
|
||||
@Configuration
|
||||
public static class JedisContextConfiguration extends RedisContextConfiguration {
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public JedisConnectionFactory redisConnectionFactory() {
|
||||
|
||||
JedisConnectionFactory factory = new JedisConnectionFactory();
|
||||
factory.setHostName("localhost");
|
||||
factory.setPort(6379);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2014 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.data.redis.connection.lettuce;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.AbstractTransactionalTestBase;
|
||||
import org.springframework.data.redis.connection.lettuce.TransactionalLettuceItegrationTests.LettuceContextConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ContextConfiguration(classes = { LettuceContextConfiguration.class })
|
||||
public class TransactionalLettuceItegrationTests extends AbstractTransactionalTestBase {
|
||||
|
||||
@Configuration
|
||||
public static class LettuceContextConfiguration extends RedisContextConfiguration {
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public LettuceConnectionFactory redisConnectionFactory() {
|
||||
|
||||
LettuceConnectionFactory factory = new LettuceConnectionFactory();
|
||||
factory.setHostName("localhost");
|
||||
factory.setPort(6379);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2014 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.data.redis.connection.srp;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.AbstractTransactionalTestBase;
|
||||
import org.springframework.data.redis.connection.srp.TransactionalSrpItegrationTests.SrpContextConfiguration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ContextConfiguration(classes = { SrpContextConfiguration.class })
|
||||
public class TransactionalSrpItegrationTests extends AbstractTransactionalTestBase {
|
||||
|
||||
@Configuration
|
||||
public static class SrpContextConfiguration extends RedisContextConfiguration {
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public SrpConnectionFactory redisConnectionFactory() {
|
||||
|
||||
SrpConnectionFactory factory = new SrpConnectionFactory();
|
||||
factory.setHostName("localhost");
|
||||
factory.setPort(6379);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2014 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.data.redis.core;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Matchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisConnectionUtils.ConnectionSplittingInterceptor;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ConnectionSplittingInterceptorUnitTests {
|
||||
|
||||
private static final Method WRITE_METHOD, READONLY_METHOD;
|
||||
|
||||
private ConnectionSplittingInterceptor interceptor;
|
||||
|
||||
private @Mock RedisConnectionFactory connectionFactoryMock;
|
||||
|
||||
private @Mock RedisConnection freshConnectionMock;
|
||||
|
||||
private @Mock RedisConnection boundConnectionMock;
|
||||
|
||||
static {
|
||||
try {
|
||||
WRITE_METHOD = ClassUtils.getMethod(RedisConnection.class, "expire", byte[].class, long.class);
|
||||
READONLY_METHOD = ClassUtils.getMethod(RedisConnection.class, "keys", byte[].class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
interceptor = new ConnectionSplittingInterceptor(connectionFactoryMock);
|
||||
Mockito.when(connectionFactoryMock.getConnection()).thenReturn(freshConnectionMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void interceptorShouldRequestFreshConnectionForReadonlyCommand() throws Throwable {
|
||||
|
||||
interceptor.intercept(boundConnectionMock, READONLY_METHOD, new Object[] { new byte[] {} }, null);
|
||||
Mockito.verify(connectionFactoryMock, Mockito.times(1)).getConnection();
|
||||
Mockito.verifyZeroInteractions(boundConnectionMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void interceptorShouldUseBoundConnectionForWriteOperations() throws Throwable {
|
||||
|
||||
interceptor.intercept(boundConnectionMock, WRITE_METHOD, new Object[] { new byte[] {}, 0L }, null);
|
||||
Mockito.verify(boundConnectionMock, Mockito.times(1)).expire(Matchers.any(byte[].class), Matchers.anyLong());
|
||||
Mockito.verifyZeroInteractions(connectionFactoryMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test(expected = InvalidDataAccessApiUsageException.class)
|
||||
public void interceptorShouldNotWrapException() throws Throwable {
|
||||
|
||||
Mockito.when(freshConnectionMock.keys(Mockito.any(byte[].class))).thenThrow(
|
||||
InvalidDataAccessApiUsageException.class);
|
||||
interceptor.intercept(boundConnectionMock, READONLY_METHOD, new Object[] { new byte[] {} }, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2014 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.data.redis.core;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
public class RedisCommandUnitTests {
|
||||
|
||||
public @Rule ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldIdentifyAliasCorrectly() {
|
||||
assertThat(RedisCommand.CONFIG_SET.isRepresentedBy("setconfig"), equalTo(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldIdentifyAliasCorrectlyWhenNamePassedInMixedCase() {
|
||||
assertThat(RedisCommand.CONFIG_SET.isRepresentedBy("SetConfig"), equalTo(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotThrowExceptionWhenUsingNullKeyForRepresentationCheck() {
|
||||
assertThat(RedisCommand.CONFIG_SET.isRepresentedBy(null), equalTo(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldIdentifyAliasCorrectlyViaLookup() {
|
||||
assertThat(RedisCommand.failsafeCommandLookup("setconfig"), is(RedisCommand.CONFIG_SET));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldIdentifyAliasCorrectlyWhenNamePassedInMixedCaseViaLookup() {
|
||||
assertThat(RedisCommand.failsafeCommandLookup("SetConfig"), is(RedisCommand.CONFIG_SET));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnUnknownCommandForUnknownCommandString() {
|
||||
assertThat(RedisCommand.failsafeCommandLookup("strangecommand"), is(RedisCommand.UNKNOWN));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotThrowExceptionOnValidArgumentCount() {
|
||||
RedisCommand.AUTH.validateArgumentCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldThrowExceptionOnInvalidArgumentCountWhenExpectedExcatMatch() {
|
||||
|
||||
expectedException.expect(IllegalArgumentException.class);
|
||||
expectedException.expectMessage("AUTH command requires 1 arguments");
|
||||
RedisCommand.AUTH.validateArgumentCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-73
|
||||
*/
|
||||
@Test
|
||||
public void shouldThrowExceptionOnInvalidArgumentCountWhenExpectedMinimalMatch() {
|
||||
|
||||
expectedException.expect(IllegalArgumentException.class);
|
||||
expectedException.expectMessage("DEL command requires at least 1 arguments");
|
||||
RedisCommand.DEL.validateArgumentCount(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user