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 java.time.Duration.ofMillis; 021import static java.time.Duration.ofMinutes; 022 023import java.time.Duration; 024import java.time.Instant; 025import java.time.temporal.ChronoUnit; 026 027import org.fcrepo.common.lang.CheckedRunnable; 028import org.fcrepo.kernel.api.ContainmentIndex; 029import org.fcrepo.kernel.api.Transaction; 030import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 031import org.fcrepo.kernel.api.exception.TransactionClosedException; 032import org.fcrepo.kernel.api.identifiers.FedoraId; 033import org.fcrepo.kernel.api.lock.ResourceLockManager; 034import org.fcrepo.kernel.api.observer.EventAccumulator; 035import org.fcrepo.kernel.api.services.MembershipService; 036import org.fcrepo.kernel.api.services.ReferenceService; 037import org.fcrepo.persistence.api.PersistentStorageSession; 038 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041import org.springframework.dao.DeadlockLoserDataAccessException; 042import org.springframework.transaction.support.TransactionTemplate; 043 044import net.jodah.failsafe.Failsafe; 045import net.jodah.failsafe.RetryPolicy; 046 047/** 048 * The Fedora Transaction implementation 049 * 050 * @author mohideen 051 */ 052public class TransactionImpl implements Transaction { 053 054 public static final String TIMEOUT_SYSTEM_PROPERTY = "fcrepo.session.timeout"; 055 056 private static final Logger log = LoggerFactory.getLogger(TransactionImpl.class); 057 058 private static final RetryPolicy<Object> DB_RETRY = new RetryPolicy<>() 059 .handleIf(e -> { 060 return e instanceof DeadlockLoserDataAccessException 061 || (e.getCause() != null && e.getCause() instanceof DeadlockLoserDataAccessException); 062 }) 063 .withBackoff(50, 1000, ChronoUnit.MILLIS, 1.5) 064 .withJitter(0.1) 065 .withMaxRetries(5); 066 067 private static final Duration DEFAULT_TIMEOUT = ofMinutes(3); 068 069 private final String id; 070 071 private final TransactionManagerImpl txManager; 072 073 private boolean shortLived = true; 074 075 private Instant expiration; 076 077 private boolean expired = false; 078 079 private boolean rolledback = false; 080 081 private boolean committed = false; 082 083 private String baseUri; 084 085 private String userAgent; 086 087 protected TransactionImpl(final String id, final TransactionManagerImpl txManager) { 088 if (id == null || id.isEmpty()) { 089 throw new IllegalArgumentException("Transaction id should not be empty!"); 090 } 091 this.id = id; 092 this.txManager = txManager; 093 this.expiration = Instant.now().plus(timeout()); 094 } 095 096 @Override 097 public synchronized void commit() { 098 failIfExpired(); 099 failIfRolledback(); 100 if (this.committed) { 101 return; 102 } 103 try { 104 log.debug("Committing transaction {}", id); 105 // MySQL can deadlock when update db records and it must be retried. Unfortunately, the entire transaction 106 // must be retried because something marks the transaction for rollback when the exception is thrown 107 // regardless if you then retry at the query level. 108 Failsafe.with(DB_RETRY).run(() -> { 109 // Cannot use transactional annotations because this class is not managed by spring 110 getTransactionTemplate().executeWithoutResult(status -> { 111 this.getContainmentIndex().commitTransaction(id); 112 this.getReferenceService().commitTransaction(id); 113 this.getMembershipService().commitTransaction(id); 114 this.getPersistentSession().prepare(); 115 // The storage session must be committed last because mutable head changes cannot be rolled back. 116 // The db transaction will remain open until all changes have been written to OCFL. If the changes 117 // are large, or are going to S3, this could take some time. In which case, it is possible the 118 // db's connection timeout may need to be adjusted so that the connection is not closed while 119 // waiting for the OCFL changes to be committed. 120 this.getPersistentSession().commit(); 121 }); 122 }); 123 this.getEventAccumulator().emitEvents(id, baseUri, userAgent); 124 this.committed = true; 125 releaseLocks(); 126 } catch (final Exception ex) { 127 log.error("Failed to commit transaction: {}", id, ex); 128 129 // Rollback on commit failure 130 rollback(); 131 throw new RepositoryRuntimeException("Failed to commit transaction " + id, ex); 132 } 133 } 134 135 @Override 136 public synchronized boolean isCommitted() { 137 return committed; 138 } 139 140 @Override 141 public synchronized void rollback() { 142 failIfCommitted(); 143 if (this.rolledback) { 144 return; 145 } 146 log.info("Rolling back transaction {}", id); 147 this.rolledback = true; 148 149 execQuietly("Failed to rollback storage in transaction " + id, () -> { 150 this.getPersistentSession().rollback(); 151 }); 152 execQuietly("Failed to rollback index in transaction " + id, () -> { 153 this.getContainmentIndex().rollbackTransaction(id); 154 }); 155 execQuietly("Failed to rollback reference index in transaction " + id, () -> { 156 this.getReferenceService().rollbackTransaction(id); 157 }); 158 execQuietly("Failed to rollback membership index in transaction " + id, () -> { 159 this.getMembershipService().rollbackTransaction(id); 160 }); 161 execQuietly("Failed to rollback events in transaction " + id, () -> { 162 this.getEventAccumulator().clearEvents(id); 163 }); 164 165 releaseLocks(); 166 } 167 168 @Override 169 public synchronized boolean isRolledBack() { 170 return rolledback; 171 } 172 173 @Override 174 public String getId() { 175 return id; 176 } 177 178 @Override 179 public void setShortLived(final boolean shortLived) { 180 this.shortLived = shortLived; 181 } 182 183 @Override 184 public boolean isShortLived() { 185 return this.shortLived; 186 } 187 188 @Override 189 public synchronized void expire() { 190 this.expiration = Instant.now(); 191 this.expired = true; 192 } 193 194 @Override 195 public boolean hasExpired() { 196 if (this.expired) { 197 return true; 198 } 199 this.expired = this.expiration.isBefore(Instant.now()); 200 return this.expired; 201 } 202 203 @Override 204 public synchronized Instant updateExpiry(final Duration amountToAdd) { 205 failIfExpired(); 206 failIfCommitted(); 207 failIfRolledback(); 208 this.expiration = this.expiration.plus(amountToAdd); 209 return this.expiration; 210 } 211 212 @Override 213 public Instant getExpires() { 214 return this.expiration; 215 } 216 217 @Override 218 public void commitIfShortLived() { 219 if (this.isShortLived()) { 220 this.commit(); 221 } 222 } 223 224 @Override 225 public void refresh() { 226 updateExpiry(timeout()); 227 } 228 229 @Override 230 public void lockResource(final FedoraId resourceId) { 231 getResourceLockManger().acquire(getId(), resourceId); 232 } 233 234 @Override 235 public void releaseResourceLocksIfShortLived() { 236 if (isShortLived()) { 237 releaseLocks(); 238 } 239 } 240 241 @Override 242 public void setBaseUri(final String baseUri) { 243 this.baseUri = baseUri; 244 } 245 246 @Override 247 public void setUserAgent(final String userAgent) { 248 this.userAgent = userAgent; 249 } 250 251 private Duration timeout() { 252 // Get the configured timeout 253 final String timeoutProperty = System.getProperty(TIMEOUT_SYSTEM_PROPERTY); 254 if (timeoutProperty != null) { 255 return ofMillis(Long.parseLong(timeoutProperty)); 256 } else { 257 // Otherwise, use the default timeout 258 return DEFAULT_TIMEOUT; 259 } 260 } 261 262 private PersistentStorageSession getPersistentSession() { 263 return this.txManager.getPersistentStorageSessionManager().getSession(this.id); 264 } 265 266 private void failIfExpired() { 267 if (hasExpired()) { 268 throw new TransactionClosedException("Transaction with transactionId: " + id + " expired!"); 269 } 270 } 271 272 private void failIfCommitted() { 273 if (this.committed) { 274 throw new TransactionClosedException("Transaction with transactionId: " + id + " is already committed!"); 275 } 276 } 277 278 private void failIfRolledback() { 279 if (this.rolledback) { 280 throw new TransactionClosedException("Transaction with transactionId: " + id + " is already rolledback!"); 281 } 282 } 283 284 private void releaseLocks() { 285 execQuietly("Failed to release resource locks cleanly. You may need to restart Fedora.", () -> { 286 getResourceLockManger().releaseAll(getId()); 287 }); 288 } 289 290 /** 291 * Executes the closure, capturing all exceptions, and logging them as errors. 292 * 293 * @param failureMessage what to print if the closure fails 294 * @param callable closure to execute 295 */ 296 private void execQuietly(final String failureMessage, final CheckedRunnable callable) { 297 try { 298 callable.run(); 299 } catch (final Exception e) { 300 log.error(failureMessage, e); 301 } 302 } 303 304 private ContainmentIndex getContainmentIndex() { 305 return this.txManager.getContainmentIndex(); 306 } 307 308 private EventAccumulator getEventAccumulator() { 309 return this.txManager.getEventAccumulator(); 310 } 311 312 private ReferenceService getReferenceService() { 313 return this.txManager.getReferenceService(); 314 } 315 316 private MembershipService getMembershipService() { 317 return this.txManager.getMembershipService(); 318 } 319 320 private TransactionTemplate getTransactionTemplate() { 321 return this.txManager.getTransactionTemplate(); 322 } 323 324 private ResourceLockManager getResourceLockManger() { 325 return this.txManager.getResourceLockManager(); 326 } 327 328}