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;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertTrue;
022import static org.junit.Assert.fail;
023import static org.mockito.Mockito.doThrow;
024import static org.mockito.Mockito.never;
025import static org.mockito.Mockito.times;
026import static org.mockito.Mockito.verify;
027import static org.mockito.Mockito.when;
028
029import java.time.Duration;
030import java.time.Instant;
031import java.util.concurrent.Executors;
032import java.util.concurrent.Phaser;
033import java.util.concurrent.TimeUnit;
034
035import org.fcrepo.common.db.DbTransactionExecutor;
036import org.fcrepo.kernel.api.ContainmentIndex;
037import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
038import org.fcrepo.kernel.api.exception.TransactionClosedException;
039import org.fcrepo.kernel.api.lock.ResourceLockManager;
040import org.fcrepo.kernel.api.observer.EventAccumulator;
041import org.fcrepo.kernel.api.services.MembershipService;
042import org.fcrepo.kernel.api.services.ReferenceService;
043import org.fcrepo.persistence.api.PersistentStorageSession;
044import org.fcrepo.persistence.api.PersistentStorageSessionManager;
045import org.fcrepo.persistence.api.exceptions.PersistentStorageException;
046import org.fcrepo.search.api.SearchIndex;
047import org.junit.Before;
048import org.junit.Test;
049import org.junit.runner.RunWith;
050import org.mockito.Mock;
051import org.mockito.junit.MockitoJUnitRunner;
052
053import com.google.common.base.Stopwatch;
054
055/**
056 * <p>
057 * TransactionTest class.
058 * </p>
059 *
060 * @author mohideen
061 */
062@RunWith(MockitoJUnitRunner.Silent.class)
063public class TransactionImplTest {
064
065    private TransactionImpl testTx;
066
067    @Mock
068    private TransactionManagerImpl txManager;
069
070    @Mock
071    private PersistentStorageSessionManager pssManager;
072
073    @Mock
074    private PersistentStorageSession psSession;
075
076    @Mock
077    private ContainmentIndex containmentIndex;
078
079    @Mock
080    private SearchIndex searchIndex;
081
082    @Mock
083    private EventAccumulator eventAccumulator;
084
085    @Mock
086    private ReferenceService referenceService;
087
088    @Mock
089    private MembershipService membershipService;
090
091    @Mock
092    private ResourceLockManager resourceLockManager;
093
094    @Before
095    public void setUp() {
096        testTx = new TransactionImpl("123", txManager, Duration.ofMillis(180000));
097        when(pssManager.getSession(testTx)).thenReturn(psSession);
098        when(txManager.getPersistentStorageSessionManager()).thenReturn(pssManager);
099        when(txManager.getContainmentIndex()).thenReturn(containmentIndex);
100        when(txManager.getEventAccumulator()).thenReturn(eventAccumulator);
101        when(txManager.getReferenceService()).thenReturn(referenceService);
102        when(txManager.getMembershipService()).thenReturn(membershipService);
103        when(txManager.getSearchIndex()).thenReturn(this.searchIndex);
104        when(txManager.getDbTransactionExecutor()).thenReturn(new DbTransactionExecutor());
105        when(txManager.getResourceLockManager()).thenReturn(resourceLockManager);
106    }
107
108    @Test
109    public void testGetId() {
110        assertEquals("123", testTx.getId());
111    }
112
113    @Test
114    public void testDefaultShortLived() {
115        assertEquals(true, testTx.isShortLived());
116    }
117
118    @Test
119    public void testSetShortLived() {
120        testTx.setShortLived(false);
121        assertEquals(false, testTx.isShortLived());
122    }
123
124    @Test
125    public void testCommit() throws Exception {
126        testTx.commit();
127        verify(psSession).commit();
128    }
129
130    @Test
131    public void testCommitIfShortLived() throws Exception {
132        testTx.setShortLived(true);
133        testTx.commitIfShortLived();
134        verify(psSession).commit();
135    }
136
137    @Test
138    public void testCommitIfShortLivedOnNonShortLived() throws Exception {
139        testTx.setShortLived(false);
140        testTx.commitIfShortLived();
141        verify(psSession, never()).commit();
142    }
143
144    @Test(expected = TransactionClosedException.class)
145    public void testCommitExpired() throws Exception {
146        testTx.expire();
147        try {
148            testTx.commit();
149        } finally {
150            verify(psSession, never()).commit();
151        }
152    }
153
154    @Test(expected = TransactionClosedException.class)
155    public void testCommitRolledbackTx() throws Exception {
156        testTx.rollback();
157        try {
158            testTx.commit();
159        } finally {
160            verify(psSession, never()).commit();
161        }
162    }
163
164    @Test(expected = RepositoryRuntimeException.class)
165    public void testEnsureRollbackOnFailedCommit() throws Exception {
166        doThrow(new PersistentStorageException("Failed")).when(psSession).commit();
167        try {
168            testTx.commit();
169        } finally {
170            verify(psSession).commit();
171            verify(psSession).rollback();
172        }
173    }
174
175    @Test
176    public void testCommitAlreadyCommittedTx() throws Exception {
177        testTx.commit();
178        testTx.commit();
179        verify(psSession, times(1)).commit();
180    }
181
182    @Test
183    public void testRollback() throws Exception {
184        testTx.rollback();
185        verify(psSession).rollback();
186    }
187
188    @Test
189    public void shouldRollbackAllWhenStorageThrowsException() throws Exception {
190        doThrow(new PersistentStorageException("storage")).when(psSession).rollback();
191        testTx.rollback();
192        verifyRollback();
193    }
194
195    @Test
196    public void shouldRollbackAllWhenContainmentThrowsException() throws Exception {
197        doThrow(new RuntimeException()).when(containmentIndex).rollbackTransaction(testTx);
198        testTx.rollback();
199        verifyRollback();
200    }
201
202    @Test
203    public void shouldRollbackAllWhenEventsThrowsException() throws Exception {
204        doThrow(new RuntimeException()).when(eventAccumulator).clearEvents(testTx);
205        testTx.rollback();
206        verifyRollback();
207    }
208
209    @Test(expected = TransactionClosedException.class)
210    public void testRollbackCommited() throws Exception {
211        testTx.commit();
212        try {
213            testTx.rollback();
214        } finally {
215            verify(psSession, never()).rollback();
216        }
217    }
218
219    @Test
220    public void testRollbackAlreadyRolledbackTx() throws Exception {
221        testTx.rollback();
222        testTx.rollback();
223        verify(psSession, times(1)).rollback();
224    }
225
226    @Test
227    public void testExpire() {
228        testTx.expire();
229        assertTrue(testTx.hasExpired());
230    }
231
232    @Test
233    public void testUpdateExpiry() {
234        final Instant previousExpiry = testTx.getExpires();
235        testTx.updateExpiry(Duration.ofSeconds(1));
236        assertTrue(testTx.getExpires().isAfter(previousExpiry));
237    }
238
239    @Test(expected = TransactionClosedException.class)
240    public void testUpdateExpiryOnExpired() {
241        testTx.expire();
242        final Instant previousExpiry = testTx.getExpires();
243        try {
244            testTx.updateExpiry(Duration.ofSeconds(1));
245        } finally {
246            assertEquals(testTx.getExpires(), previousExpiry);
247        }
248    }
249
250    @Test
251    public void testRefresh() {
252        final Instant previousExpiry = testTx.getExpires();
253        testTx.refresh();
254        assertTrue(testTx.getExpires().isAfter(previousExpiry));
255    }
256
257    @Test(expected = TransactionClosedException.class)
258    public void testRefreshOnExpired() {
259        testTx.expire();
260        final Instant previousExpiry = testTx.getExpires();
261        try {
262            testTx.refresh();
263        } finally {
264            assertEquals(testTx.getExpires(), previousExpiry);
265        }
266    }
267
268    @Test
269    public void testNewTransactionNotExpired() {
270        assertTrue(testTx.getExpires().isAfter(Instant.now()));
271    }
272
273    @Test(expected = TransactionClosedException.class)
274    public void operationsShouldFailWhenTxNotOpen() {
275        testTx.commit();
276        testTx.doInTx(() -> {
277            fail("This code should not be executed");
278        });
279    }
280
281    @Test
282    public void commitShouldWaitTillAllOperationsComplete() {
283        final var executor = Executors.newCachedThreadPool();
284        final var phaser = new Phaser(2);
285
286        executor.submit(() -> {
287            testTx.doInTx(() -> {
288                phaser.arriveAndAwaitAdvance();
289                try {
290                    TimeUnit.SECONDS.sleep(2);
291                } catch (InterruptedException e) {
292                    throw new RuntimeException(e);
293                }
294            });
295        });
296
297        phaser.arriveAndAwaitAdvance();
298        final var stopwatch = Stopwatch.createStarted();
299        testTx.commit();
300        final var duration = stopwatch.stop().elapsed().toMillis();
301
302        assertTrue(duration < 3000 && duration > 1000);
303    }
304
305    private void verifyRollback() throws PersistentStorageException {
306        verify(psSession).rollback();
307        verify(containmentIndex).rollbackTransaction(testTx);
308        verify(eventAccumulator).clearEvents(testTx);
309    }
310}