/* * Copyright 2006-2009 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.batch.support.transaction; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; /** * @author Dave Syer * */ public class ConcurrentTransactionAwareProxyTests { private static Log logger = LogFactory.getLog(ConcurrentTransactionAwareProxyTests.class); private PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); int outerMax = 20; int innerMax = 30; private ExecutorService executor; private CompletionService<List<String>> completionService; @Before public void init() { executor = Executors.newFixedThreadPool(outerMax); completionService = new ExecutorCompletionService<List<String>>(executor); } @After public void close() { executor.shutdown(); } @Test(expected = Throwable.class) public void testConcurrentTransactionalSet() throws Exception { Set<String> set = TransactionAwareProxyFactory.createTransactionalSet(); testSet(set); } @Test public void testConcurrentTransactionalAppendOnlySet() throws Exception { Set<String> set = TransactionAwareProxyFactory.createAppendOnlyTransactionalSet(); testSet(set); } @Test public void testConcurrentTransactionalAppendOnlyList() throws Exception { List<String> list = TransactionAwareProxyFactory.createAppendOnlyTransactionalList(); testList(list, false); } @Ignore("This fails too often and is a false negative") @Test public void testConcurrentTransactionalList() throws Exception { List<String> list = TransactionAwareProxyFactory.createTransactionalList(); try { testList(list, true); fail("Expected ExecutionException or AssertionError (but don't panic if it didn't happen: it probably just means we got lucky for a change)"); } catch (ExecutionException e) { String message = e.getCause().getMessage(); assertTrue("Wrong message: " + message, message.startsWith("Lost update")); } catch (AssertionError e) { String message = e.getMessage(); assertTrue("Wrong message: " + message, message.startsWith("Wrong number of results")); } } @Test public void testConcurrentTransactionalAppendOnlyMap() throws Exception { Map<Long, Map<String, String>> map = TransactionAwareProxyFactory.createAppendOnlyTransactionalMap(); testMap(map); } @Test(expected = ExecutionException.class) public void testConcurrentTransactionalMap() throws Exception { Map<Long, Map<String, String>> map = TransactionAwareProxyFactory.createTransactionalMap(); testMap(map); } @Test public void testTransactionalContains() throws Exception { final Map<Long, Map<String, String>> map = TransactionAwareProxyFactory.createAppendOnlyTransactionalMap(); boolean result = new TransactionTemplate(transactionManager).execute(new TransactionCallback<Boolean>() { @Override public Boolean doInTransaction(TransactionStatus status) { return map.containsKey("foo"); } }); assertFalse(result); } private void testSet(final Set<String> set) throws Exception { for (int i = 0; i < outerMax; i++) { final int count = i; completionService.submit(new Callable<List<String>>() { @Override public List<String> call() throws Exception { List<String> list = new ArrayList<String>(); for (int i = 0; i < innerMax; i++) { String value = count + "bar" + i; saveInSetAndAssert(set, value); list.add(value); } return list; } }); } for (int i = 0; i < outerMax; i++) { List<String> result = completionService.take().get(); assertEquals(innerMax, result.size()); } assertEquals(innerMax * outerMax, set.size()); } private void testList(final List<String> list, final boolean mutate) throws Exception { for (int i = 0; i < outerMax; i++) { completionService.submit(new Callable<List<String>>() { @Override public List<String> call() throws Exception { List<String> result = new ArrayList<String>(); for (int i = 0; i < innerMax; i++) { String value = "bar" + i; saveInListAndAssert(list, value); result.add(value); // Need to slow it down to allow threads to interleave Thread.sleep(10L); if (mutate) { list.remove(value); list.add(value); } } logger.info("Added: " + innerMax + " values"); return result; } }); } for (int i = 0; i < outerMax; i++) { List<String> result = completionService.take().get(); assertEquals("Wrong number of results in inner task", innerMax, result.size()); } assertEquals("Wrong number of results in aggregate", innerMax * outerMax, list.size()); } private void testMap(final Map<Long, Map<String, String>> map) throws Exception { int numberOfKeys = outerMax; for (int i = 0; i < outerMax; i++) { for (int j = 0; j < numberOfKeys; j++) { final long id = j * 1000 + 123L + i; completionService.submit(new Callable<List<String>>() { @Override public List<String> call() throws Exception { List<String> list = new ArrayList<String>(); for (int i = 0; i < innerMax; i++) { String value = "bar" + i; list.add(saveInMapAndAssert(map, id, value).get("foo")); } return list; } }); } for (int j = 0; j < numberOfKeys; j++) { completionService.take().get(); } } } private String saveInSetAndAssert(final Set<String> set, final String value) { new TransactionTemplate(transactionManager).execute(new TransactionCallback<Void>() { @Override public Void doInTransaction(TransactionStatus status) { set.add(value); return null; } }); Assert.state(set.contains(value), "Lost update: value=" + value); return value; } private String saveInListAndAssert(final List<String> list, final String value) { new TransactionTemplate(transactionManager).execute(new TransactionCallback<Void>() { @Override public Void doInTransaction(TransactionStatus status) { list.add(value); return null; } }); Assert.state(list.contains(value), "Lost update: value=" + value); return value; } private Map<String, String> saveInMapAndAssert(final Map<Long, Map<String, String>> map, final Long id, final String value) { new TransactionTemplate(transactionManager).execute(new TransactionCallback<Void>() { @Override public Void doInTransaction(TransactionStatus status) { if (!map.containsKey(id)) { map.put(id, new HashMap<String, String>()); } map.get(id).put("foo", value); return null; } }); Map<String, String> result = map.get(id); Assert.state(result != null, "Lost insert: null String at value=" + value); String foo = result.get("foo"); Assert.state(value.equals(foo), "Lost update: wrong value=" + value + " (found " + foo + ") for id=" + id); return result; } }