001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.kernel.impl.lock;
007
008import org.fcrepo.kernel.api.exception.ConcurrentUpdateException;
009import org.fcrepo.kernel.api.identifiers.FedoraId;
010import org.fcrepo.kernel.api.lock.ResourceLockManager;
011import org.junit.AfterClass;
012import org.junit.Before;
013import org.junit.BeforeClass;
014import org.junit.Test;
015
016import java.util.UUID;
017import java.util.concurrent.ExecutionException;
018import java.util.concurrent.ExecutorService;
019import java.util.concurrent.Executors;
020import java.util.concurrent.Phaser;
021
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertTrue;
024import static org.junit.Assert.fail;
025
026/**
027 * @author pwinckles
028 */
029public class InMemoryResourceLockManagerTest {
030
031    private ResourceLockManager lockManager;
032
033    private static ExecutorService executor;
034
035    private String txId1;
036    private String txId2;
037    private FedoraId resourceId;
038
039    @BeforeClass
040    public static void beforeClass() {
041        executor = Executors.newCachedThreadPool();
042    }
043
044    @AfterClass
045    public static void afterClass() {
046        executor.shutdown();
047    }
048
049    @Before
050    public void setup() {
051        lockManager = new InMemoryResourceLockManager();
052        txId1 = UUID.randomUUID().toString();
053        txId2 = UUID.randomUUID().toString();
054        resourceId = randomResourceId();
055    }
056
057    @Test
058    public void shouldLockResourceWhenNotAlreadyLocked() {
059        lockManager.acquire(txId1, resourceId);
060    }
061
062    @Test
063    public void sameTxShouldBeAbleToReacquireLockItAlreadyHolds() {
064        lockManager.acquire(txId1, resourceId);
065        lockManager.acquire(txId1, resourceId);
066    }
067
068    @Test
069    public void shouldFailToAcquireLockWhenHeldByAnotherTx() {
070        lockManager.acquire(txId1, resourceId);
071        assertLockException(() -> {
072            lockManager.acquire(txId2, resourceId);
073        });
074    }
075
076    @Test
077    public void shouldAcquireLockAfterReleasedByAnotherTx() {
078        lockManager.acquire(txId1, resourceId);
079        lockManager.releaseAll(txId1);
080        lockManager.acquire(txId2, resourceId);
081    }
082
083    @Test
084    public void concurrentRequestsFromSameTxShouldBothSucceedWhenLockAvailable()
085            throws ExecutionException, InterruptedException {
086        final var phaser = new Phaser(3);
087
088        final var future1 = executor.submit(() -> {
089            phaser.arriveAndAwaitAdvance();
090            lockManager.acquire(txId1, resourceId);
091            return true;
092        });
093        final var future2 = executor.submit(() -> {
094            phaser.arriveAndAwaitAdvance();
095            lockManager.acquire(txId1, resourceId);
096            return true;
097        });
098
099        phaser.arriveAndAwaitAdvance();
100
101        assertTrue(future1.get());
102        assertTrue(future2.get());
103    }
104
105    @Test
106    public void concurrentRequestsFromDifferentTxesOnlyOneShouldSucceed()
107            throws ExecutionException, InterruptedException {
108        final var phaser = new Phaser(3);
109
110        final var future1 = executor.submit(() -> {
111            phaser.arriveAndAwaitAdvance();
112            try {
113                lockManager.acquire(txId1, resourceId);
114                return true;
115            } catch (ConcurrentUpdateException e) {
116                return false;
117            }
118        });
119        final var future2 = executor.submit(() -> {
120            phaser.arriveAndAwaitAdvance();
121            try {
122                lockManager.acquire(txId2, resourceId);
123                return true;
124            } catch (ConcurrentUpdateException e) {
125                return false;
126            }
127        });
128
129        phaser.arriveAndAwaitAdvance();
130
131        if (future1.get()) {
132            assertFalse("Only one tx should have acquired a lock", future2.get());
133        } else {
134            assertTrue("Only one tx should have acquired a lock", future2.get());
135        }
136    }
137
138    @Test
139    public void releasingAlreadyReleasedLocksShouldDoNothing() {
140        lockManager.acquire(txId1, resourceId);
141        lockManager.releaseAll(txId1);
142        lockManager.releaseAll(txId1);
143        lockManager.acquire(txId2, resourceId);
144    }
145
146    private void assertLockException(final Runnable runnable) {
147        try {
148            runnable.run();
149            fail("acquire should have thrown an exception");
150        } catch (ConcurrentUpdateException e) {
151            // expected exception
152        }
153    }
154
155    private FedoraId randomResourceId() {
156        return FedoraId.create(UUID.randomUUID().toString());
157    }
158
159}