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}