/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.lock.service.test; import com.liferay.arquillian.extension.junit.bridge.junit.Arquillian; import com.liferay.portal.kernel.dao.db.DB; import com.liferay.portal.kernel.dao.db.DBManagerUtil; import com.liferay.portal.kernel.dao.db.DBType; import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.model.BaseModelListener; import com.liferay.portal.kernel.model.ModelListener; import com.liferay.portal.kernel.test.util.TestPropsValues; import com.liferay.portal.kernel.util.ReflectionUtil; import com.liferay.portal.kernel.util.Time; import com.liferay.portal.lock.exception.DuplicateLockException; import com.liferay.portal.lock.model.Lock; import com.liferay.portal.lock.service.LockLocalServiceUtil; import com.liferay.portal.test.rule.ExpectedDBType; import com.liferay.portal.test.rule.ExpectedLog; import com.liferay.portal.test.rule.ExpectedLogs; import com.liferay.portal.test.rule.ExpectedType; import com.liferay.portal.test.rule.LiferayIntegrationTestRule; import java.sql.BatchUpdateException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.GenericJDBCException; import org.hibernate.util.JDBCExceptionReporter; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceRegistration; /** * @author Shuyang Zhou */ @RunWith(Arquillian.class) public class LockLocalServiceTest { @ClassRule @Rule public static final LiferayIntegrationTestRule liferayIntegrationTestRule = new LiferayIntegrationTestRule(); @Before public void setUp() { LockLocalServiceUtil.unlock("className", "key"); } @ExpectedLogs( expectedLogs = { @ExpectedLog( expectedDBType = ExpectedDBType.DB2, expectedLog = "Error for batch element", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.DB2, expectedLog = "[jcc][t4][102][10040][4.16.53] Batch failure.", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.HYPERSONIC, expectedLog = "integrity constraint violation: unique constraint or index violation: IX_228562AD", expectedType = ExpectedType.EXACT ), @ExpectedLog( expectedDBType = ExpectedDBType.MYSQL, expectedLog = "Deadlock found when trying to get lock; try restarting transaction", expectedType = ExpectedType.EXACT ), @ExpectedLog( expectedDBType = ExpectedDBType.MYSQL, expectedLog = "Duplicate entry", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.ORACLE, expectedLog = "ORA-00001: unique constraint", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.POSTGRESQL, expectedLog = "Batch entry 0 insert into Lock_ ", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.POSTGRESQL, expectedLog = "ERROR: duplicate key value violates unique constraint ", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.SYBASE, expectedLog = "Attempt to insert duplicate key row", expectedType = ExpectedType.CONTAINS ) }, level = "ERROR", loggerClass = JDBCExceptionReporter.class ) @Test public void testLock() throws Exception { final long userId = TestPropsValues.getUserId(); // Lock is new Lock lock = LockLocalServiceUtil.lock( userId, "className", "key", "owner", false, Time.DAY, false); Assert.assertTrue(lock.isNew()); Date expirationDate = lock.getExpirationDate(); // Lock already exist, don't renew lock = LockLocalServiceUtil.lock( userId, "className", "key", "owner", false, Time.DAY, false); Assert.assertFalse(lock.isNew()); Assert.assertEquals(expirationDate, lock.getExpirationDate()); // Lock already exist, renew lock = LockLocalServiceUtil.lock( userId, "className", "key", "owner", false, 2 * Time.DAY, true); Assert.assertFalse(lock.isNew()); Assert.assertTrue(expirationDate.before(lock.getExpirationDate())); // Duplicate lock exception try { LockLocalServiceUtil.lock( 0, "className", "key", "owner", false, Time.DAY, false); Assert.fail(); } catch (DuplicateLockException dle) { } // Set lock to be expired expirationDate = new Date(System.currentTimeMillis() - 10 * Time.DAY); lock.setExpirationDate(expirationDate); lock = LockLocalServiceUtil.updateLock(lock); Assert.assertEquals(expirationDate, lock.getExpirationDate()); // Lock with auto expiration lock = LockLocalServiceUtil.lock( userId, "className", "key", "owner", false, Time.DAY, false); Assert.assertTrue(lock.isNew()); Assert.assertTrue(expirationDate.before(lock.getExpirationDate())); // Remove lock LockLocalServiceUtil.unlock("className", "key"); // Concurrent locking Bundle bundle = FrameworkUtil.getBundle(LockLocalServiceTest.class); BundleContext bundleContext = bundle.getBundleContext(); final Thread testThread = Thread.currentThread(); final CountDownLatch createdCountDownLatch = new CountDownLatch(1); final CountDownLatch continueCountDownLatch = new CountDownLatch(1); ServiceRegistration<ModelListener> serviceRegistration = bundleContext.registerService( ModelListener.class, new BaseModelListener<Lock>() { @Override public void onAfterCreate(Lock model) { if (Thread.currentThread() != testThread) { createdCountDownLatch.countDown(); try { continueCountDownLatch.await(); } catch (InterruptedException ie) { ReflectionUtil.throwException(ie); } } } }, null); FutureTask<Lock> futureTask = new FutureTask<>( new Callable<Lock>() { @Override public Lock call() throws PortalException { return LockLocalServiceUtil.lock( userId, "className", "key", "owner", false, Time.DAY, false); } }); Thread lockCreateThread = new Thread(futureTask, "Lock create thread"); lockCreateThread.start(); createdCountDownLatch.await(); serviceRegistration.unregister(); lock = LockLocalServiceUtil.lock( userId, "className", "key", "owner", false, Time.DAY, false); Assert.assertTrue(lock.isNew()); continueCountDownLatch.countDown(); try { futureTask.get(); Assert.fail(); } catch (ExecutionException ee) { Throwable throwable = ee.getCause(); Assert.assertSame( ConstraintViolationException.class, throwable.getClass()); } } @ExpectedLogs( expectedLogs = { @ExpectedLog( expectedDBType = ExpectedDBType.DB2, expectedLog = "Error for batch element", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.DB2, expectedLog = "[jcc][t4][102][10040][4.16.53] Batch failure.", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.HYPERSONIC, expectedLog = "integrity constraint violation: unique constraint or index violation: IX_228562AD", expectedType = ExpectedType.EXACT ), @ExpectedLog( expectedDBType = ExpectedDBType.MYSQL, expectedLog = "Deadlock found when trying to get lock; try restarting transaction", expectedType = ExpectedType.EXACT ), @ExpectedLog( expectedDBType = ExpectedDBType.MYSQL, expectedLog = "Duplicate entry", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.ORACLE, expectedLog = "ORA-00001: unique constraint", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.POSTGRESQL, expectedLog = "Batch entry 0 insert into Lock_ ", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.POSTGRESQL, expectedLog = "ERROR: duplicate key value violates unique constraint ", expectedType = ExpectedType.PREFIX ), @ExpectedLog( expectedDBType = ExpectedDBType.SYBASE, expectedLog = "Attempt to insert duplicate key row", expectedType = ExpectedType.CONTAINS ) }, level = "ERROR", loggerClass = JDBCExceptionReporter.class ) @Test public void testMutualExcludeLockingParallel() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(10); List<Future<Void>> futures = new ArrayList<>(); for (int i = 0; i < 10; i++) { LockingJob lockingJob = new LockingJob( "className", "key", "owner-" + i, 10); futures.add(executorService.submit(lockingJob)); } executorService.shutdown(); Assert.assertTrue( executorService.awaitTermination(600, TimeUnit.SECONDS)); for (Future<Void> future : futures) { future.get(); } Assert.assertFalse(LockLocalServiceUtil.isLocked("className", "key")); } @Test public void testMutualExcludeLockingSerial() throws Exception { String className = "testClassName"; String key = "testKey"; String owner1 = "testOwner1"; Lock lock1 = LockLocalServiceUtil.lock(className, key, owner1); Assert.assertEquals(owner1, lock1.getOwner()); Assert.assertTrue(lock1.isNew()); String owner2 = "owner2"; Lock lock2 = LockLocalServiceUtil.lock(className, key, owner2); Assert.assertEquals(owner1, lock2.getOwner()); Assert.assertFalse(lock2.isNew()); LockLocalServiceUtil.unlock(className, key, owner1); lock2 = LockLocalServiceUtil.lock(className, key, owner2); Assert.assertEquals(owner2, lock2.getOwner()); Assert.assertTrue(lock2.isNew()); LockLocalServiceUtil.unlock(className, key, owner2); } private static class LockingJob implements Callable<Void> { @Override public Void call() { int count = 0; while (true) { try { Lock lock = LockLocalServiceUtil.lock( _className, _key, _owner); if (lock.isNew()) { // The lock creator is responsible for unlocking. Try to // unlock many times because some databases like SQL // Server may randomly choke and rollback an unlock. while (true) { try { LockLocalServiceUtil.unlock( _className, _key, _owner); if (++count >= _requiredSuccessCount) { return null; } break; } catch (RuntimeException re) { if (_isExpectedException(re)) { continue; } throw re; } } } } catch (RuntimeException re) { if (_isExpectedException(re)) { continue; } throw re; } } } private LockingJob( String className, String key, String owner, int requiredSuccessCount) { _className = className; _key = key; _owner = owner; _requiredSuccessCount = requiredSuccessCount; } private boolean _isExpectedException(RuntimeException re) { Throwable cause = re.getCause(); DB db = DBManagerUtil.getDB(); if ((db.getDBType() == DBType.SYBASE) && (cause instanceof GenericJDBCException)) { cause = cause.getCause(); String message = cause.getMessage(); if ((cause instanceof BatchUpdateException) && message.contains( "Attempt to insert duplicate key row in object " + "'Lock_' with unique index 'IX_228562AD'\n")) { return true; } } return false; } private final String _className; private final String _key; private final String _owner; private final int _requiredSuccessCount; } }