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}