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 static org.junit.Assert.assertFalse;
009import static org.junit.Assert.assertTrue;
010import static org.junit.Assert.fail;
011
012import java.util.UUID;
013import java.util.concurrent.ExecutionException;
014import java.util.concurrent.ExecutorService;
015import java.util.concurrent.Executors;
016import java.util.concurrent.Phaser;
017
018import org.fcrepo.kernel.api.exception.ConcurrentUpdateException;
019import org.fcrepo.kernel.api.identifiers.FedoraId;
020import org.fcrepo.kernel.api.lock.ResourceLockManager;
021
022import org.junit.AfterClass;
023import org.junit.Before;
024import org.junit.BeforeClass;
025import org.junit.Test;
026
027/**
028 * @author pwinckles
029 */
030public class InMemoryResourceLockManagerTest {
031
032    private ResourceLockManager lockManager;
033
034    private static ExecutorService executor;
035
036    private String txId1;
037    private String txId2;
038    private FedoraId resourceId;
039
040    @BeforeClass
041    public static void beforeClass() {
042        executor = Executors.newCachedThreadPool();
043    }
044
045    @AfterClass
046    public static void afterClass() {
047        executor.shutdown();
048    }
049
050    @Before
051    public void setup() {
052        lockManager = new InMemoryResourceLockManager();
053        txId1 = UUID.randomUUID().toString();
054        txId2 = UUID.randomUUID().toString();
055        resourceId = randomResourceId();
056    }
057
058    @Test
059    public void shouldLockResourceWhenNotAlreadyLockedExclusive() {
060        lockManager.acquireExclusive(txId1, resourceId);
061    }
062
063    @Test
064    public void shouldLockResourceWhenNotAlreadyLockedNonExclusive() {
065        lockManager.acquireNonExclusive(txId1, resourceId);
066    }
067
068    @Test
069    public void sameTxShouldBeAbleToReacquireLockItAlreadyHoldsExclusive() {
070        lockManager.acquireExclusive(txId1, resourceId);
071        lockManager.acquireExclusive(txId1, resourceId);
072    }
073
074    @Test
075    public void sameTxShouldBeAbleToReacquireLockItAlreadyHoldsNonExclusive() {
076        lockManager.acquireNonExclusive(txId1, resourceId);
077        lockManager.acquireNonExclusive(txId1, resourceId);
078    }
079
080    @Test
081    public void shouldFailToAcquireLockWhenHeldByAnotherTxExclusive() {
082        lockManager.acquireExclusive(txId1, resourceId);
083        assertLockException(() -> {
084            lockManager.acquireExclusive(txId2, resourceId);
085        });
086        assertLockException(() -> {
087            lockManager.acquireNonExclusive(txId2, resourceId);
088        });
089    }
090
091    @Test
092    public void shouldFailToAcquireLockWhenHeldByAnotherTxSecondExclusive() {
093        lockManager.acquireNonExclusive(txId1, resourceId);
094        assertLockException(() -> {
095            lockManager.acquireExclusive(txId2, resourceId);
096        });
097    }
098
099    @Test
100    public void shouldSucceedToAcquireNonExclusiveLockWhenHeldByAnotherTxNonExclusive() {
101        lockManager.acquireNonExclusive(txId1, resourceId);
102        lockManager.acquireNonExclusive(txId2, resourceId);
103    }
104
105    @Test
106    public void shouldAcquireLockAfterReleasedByAnotherTx1() {
107        lockManager.acquireExclusive(txId1, resourceId);
108        lockManager.releaseAll(txId1);
109        lockManager.acquireExclusive(txId2, resourceId);
110    }
111
112    @Test
113    public void shouldAcquireLockAfterReleasedByAnotherTx2() {
114        lockManager.acquireExclusive(txId1, resourceId);
115        lockManager.releaseAll(txId1);
116        lockManager.acquireNonExclusive(txId2, resourceId);
117    }
118
119    @Test
120    public void shouldAcquireLockAfterReleasedByAnotherTx3() {
121        lockManager.acquireNonExclusive(txId1, resourceId);
122        lockManager.releaseAll(txId1);
123        lockManager.acquireExclusive(txId2, resourceId);
124    }
125
126    @Test
127    public void shouldAcquireLockAfterReleasedByAnotherTx4() {
128        lockManager.acquireNonExclusive(txId1, resourceId);
129        lockManager.releaseAll(txId1);
130        lockManager.acquireNonExclusive(txId2, resourceId);
131    }
132
133    @Test
134    public void concurrentRequestsFromSameTxShouldBothSucceedWhenLockAvailable()
135            throws ExecutionException, InterruptedException {
136        final var phaser = new Phaser(3);
137
138        final var future1 = executor.submit(() -> {
139            phaser.arriveAndAwaitAdvance();
140            lockManager.acquireExclusive(txId1, resourceId);
141            return true;
142        });
143        final var future2 = executor.submit(() -> {
144            phaser.arriveAndAwaitAdvance();
145            lockManager.acquireExclusive(txId1, resourceId);
146            return true;
147        });
148
149        phaser.arriveAndAwaitAdvance();
150
151        assertTrue(future1.get());
152        assertTrue(future2.get());
153    }
154
155    @Test
156    public void concurrentExclusiveRequestsFromDifferentTxesOnlyOneShouldSucceed()
157            throws ExecutionException, InterruptedException {
158        final var phaser = new Phaser(3);
159
160        final var future1 = executor.submit(() -> {
161            phaser.arriveAndAwaitAdvance();
162            try {
163                lockManager.acquireExclusive(txId1, resourceId);
164                return true;
165            } catch (final ConcurrentUpdateException e) {
166                return false;
167            }
168        });
169        final var future2 = executor.submit(() -> {
170            phaser.arriveAndAwaitAdvance();
171            try {
172                lockManager.acquireExclusive(txId2, resourceId);
173                return true;
174            } catch (final ConcurrentUpdateException e) {
175                return false;
176            }
177        });
178
179        phaser.arriveAndAwaitAdvance();
180
181        if (future1.get()) {
182            assertFalse("Only one tx should have acquired a lock", future2.get());
183        } else {
184            assertTrue("Only one tx should have acquired a lock", future2.get());
185        }
186    }
187
188    @Test
189    public void concurrentOneExclusiveRequestsFromDifferentTxesOnlyOneShouldSucceed()
190            throws ExecutionException, InterruptedException {
191        final var phaser = new Phaser(3);
192
193        final var future1 = executor.submit(() -> {
194            phaser.arriveAndAwaitAdvance();
195            try {
196                lockManager.acquireExclusive(txId1, resourceId);
197                return true;
198            } catch (final ConcurrentUpdateException e) {
199                return false;
200            }
201        });
202        final var future2 = executor.submit(() -> {
203            phaser.arriveAndAwaitAdvance();
204            try {
205                lockManager.acquireNonExclusive(txId2, resourceId);
206                return true;
207            } catch (final ConcurrentUpdateException e) {
208                return false;
209            }
210        });
211
212        phaser.arriveAndAwaitAdvance();
213
214        if (future1.get()) {
215            assertFalse("Only one tx should have acquired a lock", future2.get());
216        } else {
217            assertTrue("Only one tx should have acquired a lock", future2.get());
218        }
219    }
220
221    @Test
222    public void concurrentNonexclusiveRequestsFromDifferentTxesBothShouldSucceed()
223            throws ExecutionException, InterruptedException {
224        final var phaser = new Phaser(3);
225
226        final var future1 = executor.submit(() -> {
227            phaser.arriveAndAwaitAdvance();
228            try {
229                lockManager.acquireNonExclusive(txId1, resourceId);
230                return true;
231            } catch (final ConcurrentUpdateException e) {
232                return false;
233            }
234        });
235        final var future2 = executor.submit(() -> {
236            phaser.arriveAndAwaitAdvance();
237            try {
238                lockManager.acquireNonExclusive(txId2, resourceId);
239                return true;
240            } catch (final ConcurrentUpdateException e) {
241                return false;
242            }
243        });
244
245        phaser.arriveAndAwaitAdvance();
246        // Both should succeed.
247        assertTrue(future1.get());
248        assertTrue(future2.get());
249    }
250
251    @Test
252    public void releasingAlreadyReleasedLocksShouldDoNothing() {
253        lockManager.acquireExclusive(txId1, resourceId);
254        lockManager.releaseAll(txId1);
255        lockManager.releaseAll(txId1);
256        lockManager.acquireExclusive(txId2, resourceId);
257    }
258
259    private void assertLockException(final Runnable runnable) {
260        try {
261            runnable.run();
262            fail("acquire should have thrown an exception");
263        } catch (final ConcurrentUpdateException e) {
264            // expected exception
265        }
266    }
267
268    private FedoraId randomResourceId() {
269        return FedoraId.create(UUID.randomUUID().toString());
270    }
271
272}