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