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}