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