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}