fi.hsl.parkandride.back.LockDaoTest.java Source code

Java tutorial

Introduction

Here is the source code for fi.hsl.parkandride.back.LockDaoTest.java

Source

// Copyright  2015 HSL <https://www.hsl.fi>
// This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses.

package fi.hsl.parkandride.back;

import com.querydsl.sql.postgresql.PostgreSQLQueryFactory;
import fi.hsl.parkandride.core.domain.Lock;
import fi.hsl.parkandride.core.domain.LockAcquireFailedException;
import fi.hsl.parkandride.core.domain.LockException;
import fi.hsl.parkandride.core.service.ValidationService;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import javax.inject.Inject;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;

// NOTE: class not marked with @Transactional to allow some tests to run
// non-transactionally (and allow the tests to create the transactions within
// separate threads -> thread pool with max 2 connections is enough).
// Those test methods that require transactionality (those that do not run
// operations on LockDao in a separate thread), are annotated with
// @Transactional to have a transaction in test method scope.
public class LockDaoTest extends AbstractDaoTest {

    private static final String LOCK_OWNER_NAME = "test-lock-owner";
    private static final String TEST_LOCK_NAME = "test-lock";
    private static final Duration TEST_LOCK_DURATION = Duration.standardSeconds(10);

    @Inject
    private PostgreSQLQueryFactory queryFactory;

    @Inject
    private ValidationService validationService;

    @Inject
    private PlatformTransactionManager transactionManager;

    private LockDao lockDao;

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void createLockDao() {
        lockDao = new LockDao(queryFactory, validationService, LOCK_OWNER_NAME);
    }

    @Before
    public void clean_database_so_that_test_do_not_need_to_release_locks() {
        cleanup();
    }

    @Test
    @Transactional
    public void lock_acquisition_creates_a_lock_in_database_and_releasing_deletes_it() {
        Lock lock = lockDao.acquireLock(TEST_LOCK_NAME, TEST_LOCK_DURATION);
        assertNotNull(lock);
        Optional<Lock> lockReadFromDatabase = lockDao.selectLockIfExists(TEST_LOCK_NAME);
        assertTrue(lockReadFromDatabase.isPresent());
        assertThat(lock, is(lockReadFromDatabase.get()));

        lockDao.releaseLock(lock);
        lockReadFromDatabase = lockDao.selectLockIfExists(TEST_LOCK_NAME);
        assertFalse(lockReadFromDatabase.isPresent());
    }

    @Test
    public void lock_cannot_be_acquired_by_another_owner_when_it_is_taken_and_still_valid() throws Exception {
        testTakenLockAcquisitionWithAnotherOwnerName("another-owner");
    }

    @Test
    public void lock_cannot_be_acquired_even_by_same_owner_when_it_is_taken_and_still_valid() throws Exception {
        testTakenLockAcquisitionWithAnotherOwnerName(LOCK_OWNER_NAME);
    }

    private void testTakenLockAcquisitionWithAnotherOwnerName(String anotherLockOwnerName) throws Exception {
        // Acquire the lock (win the race for the lock)
        Lock winningLock = runTxInOtherThread(tx -> lockDao.acquireLock(TEST_LOCK_NAME, TEST_LOCK_DURATION)).get();
        assertNotNull(winningLock);
        assertThat(winningLock.owner, is(LOCK_OWNER_NAME));

        // Try to acquire it with another thread (as if another server)
        LockDao anotherLockDao = new LockDao(queryFactory, validationService, anotherLockOwnerName);
        Exception losingLockException = null;
        Lock losingLock = null;
        try {
            losingLock = runTxInOtherThread(tx -> anotherLockDao.acquireLock(TEST_LOCK_NAME, TEST_LOCK_DURATION))
                    .get();
        } catch (Exception e) {
            losingLockException = e;
        }

        // Verify that another thread did not get the lock & threw an exception
        assertNull(losingLock);
        assertThat(losingLockException, instanceOf(ExecutionException.class));
        assertThat(losingLockException.getCause(), instanceOf(LockAcquireFailedException.class));
    }

    @Test
    public void expired_lock_can_be_claimed_and_releasing_expired_lock_does_not_delete_valid_lock()
            throws Exception {
        // Acquire the lock that expires immediately
        Lock expiringLock = runTxInOtherThread(tx -> lockDao.acquireLock(TEST_LOCK_NAME, Duration.ZERO)).get();
        assertNotNull(expiringLock);
        assertThat(expiringLock.owner, is(LOCK_OWNER_NAME));

        // New Lock can be claimed when existing lock has expired
        Lock newLock = runTxInOtherThread(tx -> lockDao.acquireLock(TEST_LOCK_NAME, TEST_LOCK_DURATION)).get();
        assertNotNull(newLock);
        assertThat(newLock.owner, is(LOCK_OWNER_NAME));

        // Trying to release the expired lock does not release the valid newLock
        Boolean wasLockReleased = runTxInOtherThread(tx -> lockDao.releaseLock(expiringLock)).get();
        assertFalse(wasLockReleased);

        // Verify that database still contains the valid lock
        Optional<Lock> lockReadFromDatabase = runTxInOtherThread(tx -> lockDao.selectLockIfExists(TEST_LOCK_NAME))
                .get();
        assertTrue(lockReadFromDatabase.isPresent());
        assertThat(lockReadFromDatabase.get(), equalTo(newLock));
    }

    @Test
    public void cannot_release_lock_that_is_not_owned() {
        thrown.expect(LockException.class);
        thrown.expectMessage("Lock is not owned");

        Lock someoneElsesLock = new Lock(TEST_LOCK_NAME, "someone-else", DateTime.now().plus(TEST_LOCK_DURATION));
        lockDao.releaseLock(someoneElsesLock);
    }

    @Test
    public void lock_acquisition_race_loss_causes_LockAcquireFailedException() throws Exception {
        // Run a thread to acquire a lock: notice that lock is not taken, but wait before inserting the lock to database
        LosingLockDao losingLockDao = new LosingLockDao(queryFactory, validationService, "another-owner");
        Future<Lock> losingLockFuture = runTxInOtherThread(
                tx -> losingLockDao.acquireLock(TEST_LOCK_NAME, TEST_LOCK_DURATION));
        losingLockDao.waitUntilReadyToInsert();

        // Acquire the lock with another thread (win the race for the lock)
        Lock winningLock = runTxInOtherThread(tx -> lockDao.acquireLock(TEST_LOCK_NAME, TEST_LOCK_DURATION)).get();
        assertNotNull(winningLock);
        assertThat(winningLock.owner, is(LOCK_OWNER_NAME));

        // Let the first thread to proceed with an attempt to acquire lock
        losingLockDao.proceedWithInsertLock();

        // Collect the result of the first thread
        Exception losingLockException = null;
        Lock losingLock = null;
        try {
            losingLock = losingLockFuture.get();
        } catch (Exception e) {
            losingLockException = e;
        }

        // Verify that LosingLockDao did not get the lock & threw an exception
        assertNull(losingLock);
        assertThat(losingLockException, instanceOf(ExecutionException.class));
        assertThat(losingLockException.getCause(), instanceOf(LockAcquireFailedException.class));
    }

    private <T> Future<T> runTxInOtherThread(TransactionCallback<T> transactionCallback) {
        return Executors.newSingleThreadExecutor().submit(() -> {
            TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);
            txTemplate.setTimeout(1);
            return txTemplate.execute(transactionCallback);
        });
    }

    public static class LosingLockDao extends LockDao {
        private final Semaphore externalWaitForReadySemaphore = new Semaphore(0);
        private final Semaphore waitForInsertSemaphore = new Semaphore(0);

        public LosingLockDao(PostgreSQLQueryFactory queryFactory, ValidationService validationService,
                String lockOwnerName) {
            super(queryFactory, validationService, lockOwnerName);
        }

        public void waitUntilReadyToInsert() {
            externalWaitForReadySemaphore.acquireUninterruptibly();
        }

        public void proceedWithInsertLock() {
            waitForInsertSemaphore.release();
        }

        @Override
        public Lock insertLock(String lockName, Duration lockDuration) {
            externalWaitForReadySemaphore.release();
            waitForInsertSemaphore.acquireUninterruptibly();
            return super.insertLock(lockName, lockDuration);
        }
    }
}