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