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 org.fcrepo.kernel.api.ContainmentIndex;
021import org.fcrepo.kernel.api.Transaction;
022import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
023import org.fcrepo.kernel.api.exception.TransactionClosedException;
024import org.fcrepo.kernel.api.observer.EventAccumulator;
025import org.fcrepo.kernel.api.services.MembershipService;
026import org.fcrepo.kernel.api.services.ReferenceService;
027import org.fcrepo.persistence.api.PersistentStorageSession;
028import org.fcrepo.persistence.api.exceptions.PersistentStorageException;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import java.time.Duration;
033import java.time.Instant;
034import java.util.concurrent.Callable;
035
036import static java.time.Duration.ofMillis;
037import static java.time.Duration.ofMinutes;
038
039/**
040 * The Fedora Transaction implementation
041 *
042 * @author mohideen
043 */
044public class TransactionImpl implements Transaction {
045
046    public static final String TIMEOUT_SYSTEM_PROPERTY = "fcrepo.session.timeout";
047
048    private static final Logger log = LoggerFactory.getLogger(TransactionImpl.class);
049
050    private static final Duration DEFAULT_TIMEOUT = ofMinutes(3);
051
052    private final String id;
053
054    private final TransactionManagerImpl txManager;
055
056    private boolean shortLived = true;
057
058    private Instant expiration;
059
060    private boolean expired = false;
061
062    private boolean rolledback = false;
063
064    private boolean committed = false;
065
066    private String baseUri;
067
068    private String userAgent;
069
070    protected TransactionImpl(final String id, final TransactionManagerImpl txManager) {
071        if (id == null || id.isEmpty()) {
072            throw new IllegalArgumentException("Transaction id should not be empty!");
073        }
074        this.id = id;
075        this.txManager = txManager;
076        this.expiration = Instant.now().plus(timeout());
077    }
078
079    @Override
080    public synchronized void commit() {
081        failIfExpired();
082        failIfRolledback();
083        if (this.committed) {
084            return;
085        }
086        try {
087            log.debug("Committing transaction {}", id);
088            this.getPersistentSession().commit();
089            this.getContainmentIndex().commitTransaction(id);
090            this.getReferenceService().commitTransaction(id);
091            this.getMembershipService().commitTransaction(id);
092            this.getEventAccumulator().emitEvents(id, baseUri, userAgent);
093            this.committed = true;
094        } catch (final PersistentStorageException ex) {
095            log.error("Failed to commit transaction: {}", id, ex);
096
097            // Rollback on commit failure
098            rollback();
099            throw new RepositoryRuntimeException("Failed to commit transaction " + id, ex);
100        }
101    }
102
103    @Override
104    public synchronized boolean isCommitted() {
105        return committed;
106    }
107
108    @Override
109    public synchronized void rollback() {
110        failIfCommitted();
111        if (this.rolledback) {
112            return;
113        }
114        log.info("Rolling back transaction {}", id);
115        this.rolledback = true;
116
117        execQuietly("Failed to rollback storage in transaction " + id, () -> {
118            this.getPersistentSession().rollback();
119            return null;
120        });
121        execQuietly("Failed to rollback index in transaction " + id, () -> {
122            this.getContainmentIndex().rollbackTransaction(id);
123            return null;
124        });
125        execQuietly("Failed to rollback reference index in transaction " + id, () -> {
126            this.getReferenceService().rollbackTransaction(id);
127            return null;
128        });
129        execQuietly("Failed to rollback membership index in transaction " + id, () -> {
130            this.getMembershipService().rollbackTransaction(id);
131            return null;
132        });
133        execQuietly("Failed to rollback events in transaction " + id, () -> {
134            this.getEventAccumulator().clearEvents(id);
135            return null;
136        });
137    }
138
139    @Override
140    public synchronized boolean isRolledBack() {
141        return rolledback;
142    }
143
144    @Override
145    public String getId() {
146        return id;
147    }
148
149    @Override
150    public void setShortLived(final boolean shortLived) {
151        this.shortLived = shortLived;
152    }
153
154    @Override
155    public boolean isShortLived() {
156        return this.shortLived;
157    }
158
159    @Override
160    public synchronized void expire() {
161        this.expiration = Instant.now();
162        this.expired = true;
163    }
164
165    @Override
166    public boolean hasExpired() {
167        if (this.expired) {
168            return true;
169        }
170        this.expired = this.expiration.isBefore(Instant.now());
171        return this.expired;
172    }
173
174    @Override
175    public synchronized Instant updateExpiry(final Duration amountToAdd) {
176        failIfExpired();
177        failIfCommitted();
178        failIfRolledback();
179        this.expiration = this.expiration.plus(amountToAdd);
180        return this.expiration;
181    }
182
183    @Override
184    public Instant getExpires() {
185        return this.expiration;
186    }
187
188    @Override
189    public void commitIfShortLived() {
190       if (this.isShortLived()) {
191           this.commit();
192       }
193    }
194
195    @Override
196    public void refresh() {
197        updateExpiry(timeout());
198    }
199
200    @Override
201    public void setBaseUri(final String baseUri) {
202        this.baseUri = baseUri;
203    }
204
205    @Override
206    public void setUserAgent(final String userAgent) {
207        this.userAgent = userAgent;
208    }
209
210    private Duration timeout() {
211        // Get the configured timeout
212        final String timeoutProperty = System.getProperty(TIMEOUT_SYSTEM_PROPERTY);
213        if (timeoutProperty != null) {
214            return ofMillis(Long.parseLong(timeoutProperty));
215        } else {
216            // Otherwise, use the default timeout
217            return DEFAULT_TIMEOUT;
218        }
219    }
220
221    private PersistentStorageSession getPersistentSession() {
222        return this.txManager.getPersistentStorageSessionManager().getSession(this.id);
223    }
224
225    private void failIfExpired() {
226        if (hasExpired()) {
227            throw new TransactionClosedException("Transaction with transactionId: " + id + " expired!");
228        }
229    }
230
231    private void failIfCommitted() {
232        if (this.committed) {
233            throw new TransactionClosedException("Transaction with transactionId: " + id + " is already committed!");
234        }
235    }
236
237    private void failIfRolledback() {
238        if (this.rolledback) {
239            throw new TransactionClosedException("Transaction with transactionId: " + id + " is already rolledback!");
240        }
241    }
242
243    /**
244     * Executes the closure, capturing all exceptions, and logging them as errors.
245     *
246     * @param failureMessage what to print if the closure fails
247     * @param callable closure to execute
248     */
249    private void execQuietly(final String failureMessage, final Callable<Void> callable) {
250        try {
251            callable.call();
252        } catch (final Exception e) {
253            log.error(failureMessage, e);
254        }
255    }
256
257    private ContainmentIndex getContainmentIndex() {
258        return this.txManager.getContainmentIndex();
259    }
260
261    private EventAccumulator getEventAccumulator() {
262        return this.txManager.getEventAccumulator();
263    }
264
265    private ReferenceService getReferenceService() {
266        return this.txManager.getReferenceService();
267    }
268
269    private MembershipService getMembershipService() {
270        return this.txManager.getMembershipService();
271    }
272
273}