001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.impl.lock;
019
020import org.fcrepo.kernel.api.exception.ConcurrentUpdateException;
021import org.fcrepo.kernel.api.identifiers.FedoraId;
022import org.fcrepo.kernel.api.lock.ResourceLockManager;
023import org.junit.AfterClass;
024import org.junit.Before;
025import org.junit.BeforeClass;
026import org.junit.Test;
027
028import java.util.UUID;
029import java.util.concurrent.ExecutionException;
030import java.util.concurrent.ExecutorService;
031import java.util.concurrent.Executors;
032import java.util.concurrent.Phaser;
033
034import static org.junit.Assert.assertFalse;
035import static org.junit.Assert.assertTrue;
036import static org.junit.Assert.fail;
037
038/**
039 * @author pwinckles
040 */
041public class InMemoryResourceLockManagerTest {
042
043    private ResourceLockManager lockManager;
044
045    private static ExecutorService executor;
046
047    private String txId1;
048    private String txId2;
049    private FedoraId resourceId;
050
051    @BeforeClass
052    public static void beforeClass() {
053        executor = Executors.newCachedThreadPool();
054    }
055
056    @AfterClass
057    public static void afterClass() {
058        executor.shutdown();
059    }
060
061    @Before
062    public void setup() {
063        lockManager = new InMemoryResourceLockManager();
064        txId1 = UUID.randomUUID().toString();
065        txId2 = UUID.randomUUID().toString();
066        resourceId = randomResourceId();
067    }
068
069    @Test
070    public void shouldLockResourceWhenNotAlreadyLocked() {
071        lockManager.acquire(txId1, resourceId);
072    }
073
074    @Test
075    public void sameTxShouldBeAbleToReacquireLockItAlreadyHolds() {
076        lockManager.acquire(txId1, resourceId);
077        lockManager.acquire(txId1, resourceId);
078    }
079
080    @Test
081    public void shouldFailToAcquireLockWhenHeldByAnotherTx() {
082        lockManager.acquire(txId1, resourceId);
083        assertLockException(() -> {
084            lockManager.acquire(txId2, resourceId);
085        });
086    }
087
088    @Test
089    public void shouldAcquireLockAfterReleasedByAnotherTx() {
090        lockManager.acquire(txId1, resourceId);
091        lockManager.releaseAll(txId1);
092        lockManager.acquire(txId2, resourceId);
093    }
094
095    @Test
096    public void concurrentRequestsFromSameTxShouldBothSucceedWhenLockAvailable()
097            throws ExecutionException, InterruptedException {
098        final var phaser = new Phaser(3);
099
100        final var future1 = executor.submit(() -> {
101            phaser.arriveAndAwaitAdvance();
102            lockManager.acquire(txId1, resourceId);
103            return true;
104        });
105        final var future2 = executor.submit(() -> {
106            phaser.arriveAndAwaitAdvance();
107            lockManager.acquire(txId1, resourceId);
108            return true;
109        });
110
111        phaser.arriveAndAwaitAdvance();
112
113        assertTrue(future1.get());
114        assertTrue(future2.get());
115    }
116
117    @Test
118    public void concurrentRequestsFromDifferentTxesOnlyOneShouldSucceed()
119            throws ExecutionException, InterruptedException {
120        final var phaser = new Phaser(3);
121
122        final var future1 = executor.submit(() -> {
123            phaser.arriveAndAwaitAdvance();
124            try {
125                lockManager.acquire(txId1, resourceId);
126                return true;
127            } catch (ConcurrentUpdateException e) {
128                return false;
129            }
130        });
131        final var future2 = executor.submit(() -> {
132            phaser.arriveAndAwaitAdvance();
133            try {
134                lockManager.acquire(txId2, resourceId);
135                return true;
136            } catch (ConcurrentUpdateException e) {
137                return false;
138            }
139        });
140
141        phaser.arriveAndAwaitAdvance();
142
143        if (future1.get()) {
144            assertFalse("Only one tx should have acquired a lock", future2.get());
145        } else {
146            assertTrue("Only one tx should have acquired a lock", future2.get());
147        }
148    }
149
150    @Test
151    public void releasingAlreadyReleasedLocksShouldDoNothing() {
152        lockManager.acquire(txId1, resourceId);
153        lockManager.releaseAll(txId1);
154        lockManager.releaseAll(txId1);
155        lockManager.acquire(txId2, resourceId);
156    }
157
158    private void assertLockException(final Runnable runnable) {
159        try {
160            runnable.run();
161            fail("acquire should have thrown an exception");
162        } catch (ConcurrentUpdateException e) {
163            // expected exception
164        }
165    }
166
167    private FedoraId randomResourceId() {
168        return FedoraId.create(UUID.randomUUID().toString());
169    }
170
171}