From cf99650adde67eb93602e2167402c0817114b60d Mon Sep 17 00:00:00 2001 From: John Blum Date: Wed, 10 Dec 2014 21:32:35 -0800 Subject: [PATCH] SGF-354 - SimpleGemfireRepository.deleteAll that supports transactions. --- .../support/SimpleGemfireRepository.java | 65 ++++++- ...epositoryTransactionalIntegrationTest.java | 178 ++++++++++++++++++ .../SimpleGemfireRepositoryUnitTest.java | 103 +++++++++- ...ryTransactionalIntegrationTest-context.xml | 34 ++++ 4 files changed, 366 insertions(+), 14 deletions(-) create mode 100644 src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest.java create mode 100644 src/test/resources/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest-context.xml diff --git a/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java b/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java index dacc219a..cb4d1064 100644 --- a/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java +++ b/src/main/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepository.java @@ -16,6 +16,9 @@ import org.springframework.data.gemfire.repository.query.QueryString; import org.springframework.data.repository.core.EntityInformation; import org.springframework.util.Assert; +import com.gemstone.gemfire.cache.Cache; +import com.gemstone.gemfire.cache.CacheTransactionManager; +import com.gemstone.gemfire.cache.DataPolicy; import com.gemstone.gemfire.cache.Region; import com.gemstone.gemfire.cache.query.SelectResults; @@ -200,6 +203,52 @@ public class SimpleGemfireRepository implements Gemf } } + /* + * (non-Javadoc) + * + * @see com.gemstone.gemfire.cache.Region#getAttributes() + * @see com.gemstone.gemfire.cache.RegionAttributes#getDataPolicy() + */ + boolean isPartitioned(final Region region) { + return (region != null && region.getAttributes() != null + && isPartitioned(region.getAttributes().getDataPolicy())); + } + + /* + * (non-Javadoc) + * + * @see com.gemstone.gemfire.cache.DataPolicy#withPartitioning() + */ + boolean isPartitioned(final DataPolicy dataPolicy) { + return (dataPolicy != null && dataPolicy.withPartitioning()); + } + + /* + * (non-Javadoc) + * + * @see com.gemstone.gemfire.cache.Region#getRegionService() + * @see com.gemstone.gemfire.cache.Cache#getCacheTransactionManager() + */ + boolean isTransactionPresent(final Region region) { + return (region.getRegionService() instanceof Cache + && isTransactionPresent(((Cache) region.getRegionService()).getCacheTransactionManager())); + } + + /* + * (non-Javadoc) + * + * @see com.gemstone.gemfire.cache.CacheTransactionManager#exists() + */ + boolean isTransactionPresent(final CacheTransactionManager cacheTransactionManager) { + return (cacheTransactionManager != null && cacheTransactionManager.exists()); + } + + void doRegionClear(final Region region) { + for (Object key : region.keySet()) { + region.remove(key); + } + } + /* * (non-Javadoc) * @@ -210,14 +259,16 @@ public class SimpleGemfireRepository implements Gemf template.execute(new GemfireCallback() { @Override @SuppressWarnings("rawtypes") - public Void doInGemfire(Region region) { - //clear() does not work for partitioned regions - try { - region.clear(); + public Void doInGemfire(final Region region) { + if (isPartitioned(region) || isTransactionPresent(region)) { + doRegionClear(region); } - catch (UnsupportedOperationException e) { - for (Object key : region.keySet()) { - region.remove(key); + else { + try { + region.clear(); + } + catch (UnsupportedOperationException ignore) { + doRegionClear(region); } } diff --git a/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest.java new file mode 100644 index 00000000..48dff615 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2010-2013 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.gemfire.repository.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Resource; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.gemfire.repository.GemfireRepository; +import org.springframework.data.gemfire.repository.sample.Customer; +import org.springframework.data.repository.core.support.ReflectionEntityInformation; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import com.gemstone.gemfire.cache.Region; + +/** + * The SimpleGemfireRepositoryTransactionalIntegrationTest class is a test suite of test cases testing + * the SimpleGemfireRepository class and SDC Repository abstraction implementation in the context of + * GemFire "Cache" Transactions. + * + * @author John Blum + * @see org.junit.Test + * @see org.junit.runner.RunWith + * @see org.springframework.data.gemfire.repository.support.SimpleGemfireRepository + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner + * @since 1.6.0 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class SimpleGemfireRepositoryTransactionalIntegrationTest { + + // TODO add additional test cases for SimpleGemfireRepository (Region operations) in the presence of Transactions!!! + + protected static final AtomicLong ID_SEQUENCE = new AtomicLong(0l); + + @Autowired + private CustomerService customerService; + + @Resource(name = "Customers") + private Region customers; + + protected static Customer createCustomer(final String firstName, final String lastName) { + Customer customer = new SerializableCustomer(firstName, lastName); + customer.setId(ID_SEQUENCE.incrementAndGet()); + return customer; + } + + @Before + public void setup() { + assertNotNull("The 'Customers' GemFire Cache Region was not properly configured and initialized!", customers); + assertEquals("Customers", customers.getName()); + assertEquals("/Customers", customers.getFullPath()); + assertTrue(customers.isEmpty()); + } + + @After + public void tearDown() { + customers.clear(); + } + + @Test + public void testDeleteAll() { + Collection expectedCustomers = new ArrayList(4); + + expectedCustomers.add(createCustomer("Jon", "Doe")); + expectedCustomers.add(createCustomer("Jane", "Doe")); + expectedCustomers.add(createCustomer("Pie", "Doe")); + expectedCustomers.add(createCustomer("Cookie", "Doe")); + + customerService.saveAll(expectedCustomers); + + assertFalse(customers.isEmpty()); + assertEquals(expectedCustomers.size(), customers.size()); + + try { + customerService.removeAllCausingTransactionRollback(); + } + catch (RuntimeException ignore) { + // the RuntimeException should cause the Cache Transaction to rollback and avoid the Region modification! + } + + assertFalse(customers.isEmpty()); + assertEquals(expectedCustomers.size(), customers.size()); + + customerService.removeAll(); + + assertTrue(customers.isEmpty()); + } + + public static class SerializableCustomer extends Customer implements Serializable { + + public SerializableCustomer() { + } + + public SerializableCustomer(final Long id) { + super(id); + } + + public SerializableCustomer(final String firstName, final String lastName) { + super(firstName, lastName); + } + } + + public static class CustomerService { + + private GemfireRepository customerRepository; + + private TransactionTemplate transactionTemplate; + + @Autowired + public CustomerService(GemfireTemplate customersTemplate, PlatformTransactionManager transactionManager) { + customerRepository = new SimpleGemfireRepository(customersTemplate, + new ReflectionEntityInformation(Customer.class)); + + transactionTemplate = new TransactionTemplate(transactionManager); + } + + public void saveAll(final Iterable customers) { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { + customerRepository.save(customers); + } + }); + } + + public void removeAllCausingTransactionRollback() { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { + removeAll(); + throw new IllegalStateException("'removeAll' operation not permitted"); + } + }); + } + + public void removeAll() { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { + customerRepository.deleteAll(); + } + }); + } + } + +} diff --git a/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTest.java b/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTest.java index e06a50de..c0805c47 100644 --- a/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTest.java +++ b/src/test/java/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryUnitTest.java @@ -48,7 +48,11 @@ import org.springframework.data.gemfire.repository.sample.Animal; import org.springframework.data.gemfire.test.support.CollectionUtils; import org.springframework.data.repository.core.EntityInformation; +import com.gemstone.gemfire.cache.Cache; +import com.gemstone.gemfire.cache.CacheTransactionManager; +import com.gemstone.gemfire.cache.DataPolicy; import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.cache.RegionAttributes; /** * The SimpleGemfireRepositoryUnitTest class is a test suite of test cases testing the contract and functionality @@ -57,6 +61,8 @@ import com.gemstone.gemfire.cache.Region; * @author John Blum * @see org.junit.Test * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.data.gemfire.repository.Wrapper * @see org.springframework.data.gemfire.repository.support.SimpleGemfireRepository * @since 1.4.5 */ @@ -87,6 +93,18 @@ public class SimpleGemfireRepositoryUnitTest { return new GemfireTemplate(region); } + protected Cache mockCache(final String mockName, final boolean transactionExists) { + Cache mockCache = mock(Cache.class, String.format("%1$s.MockCache", mockName)); + + CacheTransactionManager mockCacheTransactionManager = mock(CacheTransactionManager.class, String.format( + "%1$s.MockCacheTransactionManager", mockName)); + + when(mockCache.getCacheTransactionManager()).thenReturn(mockCacheTransactionManager); + when(mockCacheTransactionManager.exists()).thenReturn(transactionExists); + + return mockCache; + } + protected EntityInformation mockEntityInformation() { EntityInformation mockEntityInformation = mock(EntityInformation.class); @@ -105,6 +123,20 @@ public class SimpleGemfireRepositoryUnitTest { return mockEntityInformation; } + protected Region mockRegion(final String mockName, final Cache mockCache, final DataPolicy dataPolicy) { + Region mockRegion = mock(Region.class, String.format("%1$s.MockRegion", mockName)); + + when(mockRegion.getRegionService()).thenReturn(mockCache); + + RegionAttributes mockRegionAttributes = mock(RegionAttributes.class, String.format( + "%1$s.MockRegionAttributes", mockName)); + + when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); + when(mockRegionAttributes.getDataPolicy()).thenReturn(dataPolicy); + + return mockRegion; + } + @Test public void testSave() { Region mockRegion = mock(Region.class, "testSave"); @@ -272,32 +304,89 @@ public class SimpleGemfireRepositoryUnitTest { @Test public void testDeleteAllWithClear() { - Region mockRegion = mock(Region.class, "testDeleteEntities"); + Cache mockCache = mockCache("testDeleteAllWithClear", false); - SimpleGemfireRepository repository = new SimpleGemfireRepository( + Region mockRegion = mockRegion("testDeleteAllWithClear", mockCache, DataPolicy.REPLICATE); + + SimpleGemfireRepository gemfireRepository = new SimpleGemfireRepository( createGemfireTemplate(mockRegion), mockEntityInformation()); - repository.deleteAll(); + gemfireRepository.deleteAll(); + verify(mockCache, times(1)).getCacheTransactionManager(); + verify(mockRegion, times(2)).getAttributes(); + verify(mockRegion, times(2)).getRegionService(); verify(mockRegion, times(1)).clear(); } @Test - public void testDeleteAllWithKeys() { - Region mockRegion = mock(Region.class, "testDeleteEntities"); + public void testDeleteAllWithKeysWhenClearThrowsUnsupportedOperationException() { + Cache mockCache = mockCache("testDeleteAllWithKeysWhenClearThrowsUnsupportedOperationException", false); + + Region mockRegion = mockRegion("testDeleteAllWithKeysWhenClearThrowsUnsupportedOperationException", + mockCache, DataPolicy.PERSISTENT_REPLICATE); doThrow(new UnsupportedOperationException("Not Implemented!")).when(mockRegion).clear(); when(mockRegion.keySet()).thenReturn(new HashSet(Arrays.asList(1l, 2l, 3l))); - SimpleGemfireRepository repository = new SimpleGemfireRepository( + SimpleGemfireRepository gemfireRepository = new SimpleGemfireRepository( createGemfireTemplate(mockRegion), mockEntityInformation()); - repository.deleteAll(); + gemfireRepository.deleteAll(); + verify(mockCache, times(1)).getCacheTransactionManager(); + verify(mockRegion, times(2)).getAttributes(); + verify(mockRegion, times(2)).getRegionService(); verify(mockRegion, times(1)).clear(); verify(mockRegion, times(1)).remove(eq(1l)); verify(mockRegion, times(1)).remove(eq(2l)); verify(mockRegion, times(1)).remove(eq(3l)); } + @Test + public void testDeleteAllWithKeysWhenPartitionRegion() { + Cache mockCache = mockCache("testDeleteAllWithKeysWhenPartitionRegion", false); + + Region mockRegion = mockRegion("testDeleteAllWithKeysWhenPartitionRegion", mockCache, + DataPolicy.PERSISTENT_PARTITION); + + when(mockRegion.keySet()).thenReturn(new HashSet(Arrays.asList(1l, 2l, 3l))); + + SimpleGemfireRepository gemfireRepository = new SimpleGemfireRepository( + createGemfireTemplate(mockRegion), mockEntityInformation()); + + gemfireRepository.deleteAll(); + + verify(mockCache, times(0)).getCacheTransactionManager(); + verify(mockRegion, times(2)).getAttributes(); + verify(mockRegion, times(0)).getRegionService(); + verify(mockRegion, times(0)).clear(); + verify(mockRegion, times(1)).remove(eq(1l)); + verify(mockRegion, times(1)).remove(eq(2l)); + verify(mockRegion, times(1)).remove(eq(3l)); + } + + @Test + public void testDeleteAllWithKeysWhenTransactionPresent() { + Cache mockCache = mockCache("testDeleteAllWithKeysWhenTransactionPresent", true); + + Region mockRegion = mockRegion("testDeleteAllWithKeysWhenTransactionPresent", mockCache, + DataPolicy.REPLICATE); + + when(mockRegion.keySet()).thenReturn(new HashSet(Arrays.asList(1l, 2l, 3l))); + + SimpleGemfireRepository gemfireRepository = new SimpleGemfireRepository( + createGemfireTemplate(mockRegion), mockEntityInformation()); + + gemfireRepository.deleteAll(); + + verify(mockCache, times(1)).getCacheTransactionManager(); + verify(mockRegion, times(2)).getAttributes(); + verify(mockRegion, times(2)).getRegionService(); + verify(mockRegion, times(0)).clear(); + verify(mockRegion, times(1)).remove(eq(1l)); + verify(mockRegion, times(1)).remove(eq(2l)); + verify(mockRegion, times(1)).remove(eq(3l)); + } + } diff --git a/src/test/resources/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest-context.xml b/src/test/resources/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest-context.xml new file mode 100644 index 00000000..ccb653db --- /dev/null +++ b/src/test/resources/org/springframework/data/gemfire/repository/support/SimpleGemfireRepositoryTransactionalIntegrationTest-context.xml @@ -0,0 +1,34 @@ + + + + + SimpleGemfireRepositoryTransactionalTest + 0 + warning + + + + + + + + + + + + + + +