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.assertNotNull;
010import static org.junit.Assert.assertThrows;
011import static org.junit.Assert.fail;
012import static org.mockito.ArgumentMatchers.any;
013import static org.mockito.Mockito.never;
014import static org.mockito.Mockito.verify;
015import static org.mockito.Mockito.when;
016import static org.springframework.test.util.ReflectionTestUtils.setField;
017
018import java.io.IOException;
019import java.time.Duration;
020
021import org.fcrepo.common.db.DbTransactionExecutor;
022import org.fcrepo.config.FedoraPropsConfig;
023import org.fcrepo.kernel.api.ContainmentIndex;
024import org.fcrepo.kernel.api.cache.UserTypesCache;
025import org.fcrepo.kernel.api.exception.TransactionClosedException;
026import org.fcrepo.kernel.api.exception.TransactionNotFoundException;
027import org.fcrepo.kernel.api.lock.ResourceLockManager;
028import org.fcrepo.kernel.api.observer.EventAccumulator;
029import org.fcrepo.kernel.api.services.MembershipService;
030import org.fcrepo.kernel.api.services.ReferenceService;
031import org.fcrepo.persistence.api.PersistentStorageSession;
032import org.fcrepo.persistence.api.PersistentStorageSessionManager;
033
034import org.fcrepo.search.api.SearchIndex;
035import org.junit.Before;
036import org.junit.Test;
037import org.junit.runner.RunWith;
038import org.mockito.Mock;
039import org.mockito.junit.MockitoJUnitRunner;
040
041/**
042 * <p>TransactionTest class.</p>
043 *
044 * @author mohideen
045 */
046@RunWith(MockitoJUnitRunner.Silent.class)
047public class TransactionManagerImplTest {
048
049    private TransactionImpl testTx;
050
051    private TransactionManagerImpl testTxManager;
052
053    @Mock
054    private PersistentStorageSessionManager pssManager;
055
056    @Mock
057    private PersistentStorageSession psSession;
058
059    @Mock
060    private ContainmentIndex containmentIndex;
061
062    @Mock
063    private EventAccumulator eventAccumulator;
064
065    @Mock
066    private ReferenceService referenceService;
067
068    @Mock
069    private MembershipService membershipService;
070
071    @Mock
072    private SearchIndex searchIndex;
073
074    @Mock
075    private ResourceLockManager resourceLockManager;
076
077    @Mock
078    private UserTypesCache userTypesCache;
079
080    private FedoraPropsConfig fedoraPropsConfig;
081
082    @Before
083    public void setUp() {
084        fedoraPropsConfig = new FedoraPropsConfig();
085        fedoraPropsConfig.setSessionTimeout(Duration.ofMillis(180000));
086        testTxManager = new TransactionManagerImpl();
087        when(pssManager.getSession(any())).thenReturn(psSession);
088        setField(testTxManager, "pSessionManager", pssManager);
089        setField(testTxManager, "containmentIndex", containmentIndex);
090        setField(testTxManager, "searchIndex", searchIndex);
091        setField(testTxManager, "eventAccumulator", eventAccumulator);
092        setField(testTxManager, "referenceService", referenceService);
093        setField(testTxManager, "membershipService", membershipService);
094        setField(testTxManager, "dbTransactionExecutor", new DbTransactionExecutor());
095        setField(testTxManager, "fedoraPropsConfig", fedoraPropsConfig);
096        setField(testTxManager, "resourceLockManager", resourceLockManager);
097        setField(testTxManager, "userTypesCache", userTypesCache);
098        testTx = (TransactionImpl) testTxManager.create();
099    }
100
101    @Test
102    public void testCreateTransaction() {
103        testTx = (TransactionImpl) testTxManager.create();
104        assertNotNull(testTx);
105    }
106
107    @Test
108    public void testGetTransaction() {
109        final TransactionImpl tx = (TransactionImpl) testTxManager.get(testTx.getId());
110        assertNotNull(tx);
111        assertEquals(testTx.getId(), tx.getId());
112    }
113
114    @Test(expected = TransactionNotFoundException.class)
115    public void testGetTransactionWithInvalidID() {
116        testTxManager.get("invalid-id");
117    }
118
119    @Test(expected = TransactionClosedException.class)
120    public void testGetExpiredTransaction() throws Exception {
121        testTx.expire();
122        try {
123            testTxManager.get(testTx.getId());
124        } finally {
125            // Make sure rollback is triggered
126            verify(psSession).rollback();
127        }
128    }
129
130    @Test
131    public void testCleanupClosedTransactions() {
132        fedoraPropsConfig.setSessionTimeout(Duration.ofMillis(10000));
133
134        final var commitTx = testTxManager.create();
135        commitTx.commit();
136        final var continuingTx = testTxManager.create();
137        final var rollbackTx = testTxManager.create();
138        rollbackTx.rollback();
139
140        // verify that transactions retrievable before cleanup
141        try {
142            testTxManager.get(commitTx.getId());
143            fail("Transaction must be committed");
144        } catch (final TransactionClosedException e) {
145            //expected
146        }
147        try {
148            testTxManager.get(rollbackTx.getId());
149            fail("Transaction must be rolled back");
150        } catch (final TransactionClosedException e) {
151            //expected
152        }
153
154        assertNotNull("Continuing transaction must be present",
155                testTxManager.get(continuingTx.getId()));
156
157        testTxManager.cleanupClosedTransactions();
158
159        // Verify that the closed transactions are stick around since they haven't expired yet
160        try {
161            testTxManager.get(commitTx.getId());
162            fail("Transaction must be present but committed");
163        } catch (final TransactionClosedException e) {
164            //expected
165        }
166        try {
167            testTxManager.get(rollbackTx.getId());
168            fail("Transaction must be present but rolled back");
169        } catch (final TransactionClosedException e) {
170            //expected
171        }
172
173        // Force expiration of the closed transactions, rather than waiting for it
174        commitTx.expire();
175        rollbackTx.expire();
176        testTxManager.cleanupClosedTransactions();
177
178        // verify that closed transactions cleanedup
179
180        verify(pssManager).removeSession(commitTx.getId());
181        verify(pssManager).removeSession(rollbackTx.getId());
182        verify(pssManager, never()).removeSession(continuingTx.getId());
183
184        try {
185            testTxManager.get(commitTx.getId());
186            fail("Committed transaction was not cleaned up");
187        } catch (final TransactionNotFoundException e) {
188            //expected
189        }
190        try {
191            testTxManager.get(rollbackTx.getId());
192            fail("Rolled back transaction was not cleaned up");
193        } catch (final TransactionNotFoundException e) {
194            //expected
195        }
196
197        assertNotNull("Continuing transaction must be present",
198                testTxManager.get(continuingTx.getId()));
199    }
200
201    // Check that the scheduled cleanup process rolls back expired transactions, but leaves
202    // them around until the next cleanup call so that they can be queried.
203    @Test
204    public void testCleanupExpiringTransaction() throws Exception {
205        fedoraPropsConfig.setSessionTimeout(Duration.ofMillis(0));
206
207        final var expiringTx = testTxManager.create();
208
209        Thread.sleep(100);
210
211        testTxManager.cleanupClosedTransactions();
212
213        try {
214            testTxManager.get(expiringTx.getId());
215            fail("Transaction must be expired");
216        } catch (final TransactionClosedException e) {
217            //expected
218        }
219
220        verify(psSession).rollback();
221        verify(pssManager).removeSession(expiringTx.getId());
222
223        testTxManager.cleanupClosedTransactions();
224
225        try {
226            testTxManager.get(expiringTx.getId());
227            fail("Expired transaction was not cleaned up");
228        } catch (final TransactionNotFoundException e) {
229            //expected
230        }
231    }
232
233    @Test
234    public void testCleanupAllTransactions() {
235        final var commitTx = testTxManager.create();
236        commitTx.commit();
237        final var continuingTx = testTxManager.create();
238        final var rollbackTx = testTxManager.create();
239        rollbackTx.rollback();
240        final var expiredTx = testTxManager.create();
241        expiredTx.expire();
242        final var expiredAndRolledBackTx = testTxManager.create();
243        expiredAndRolledBackTx.expire();
244        expiredAndRolledBackTx.rollback();
245
246        testTxManager.cleanupAllTransactions();
247
248        // All transactions must now be gone
249        assertThrows(TransactionClosedException.class, () -> testTxManager.get(commitTx.getId()));
250        assertThrows(TransactionClosedException.class, () -> testTxManager.get(rollbackTx.getId()));
251        assertThrows(TransactionClosedException.class, () -> testTxManager.get(continuingTx.getId()));
252        assertThrows(TransactionClosedException.class, () -> testTxManager.get(expiredTx.getId()));
253        // Committed transaction does not get rolled back
254        verify(pssManager, never()).removeSession(commitTx.getId());
255        // Rolled back transactions does not get rolled back (again)
256        verify(pssManager, never()).removeSession(rollbackTx.getId());
257        // Open and expired transactions get rolled back
258        verify(pssManager).removeSession(continuingTx.getId());
259        verify(pssManager).removeSession(expiredTx.getId());
260        // Unless the expired transaction is already rolled back
261        verify(pssManager, never()).removeSession(expiredAndRolledBackTx.getId());
262    }
263
264    @Test
265    public void testPreCleanTransactions() throws IOException {
266        testTxManager.preCleanTransactions();
267        verify(containmentIndex).clearAllTransactions();
268        verify(membershipService).clearAllTransactions();
269        verify(referenceService).clearAllTransactions();
270        verify(searchIndex).clearAllTransactions();
271        verify(pssManager).clearAllSessions();
272    }
273}