001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao.tx; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.interceptor.api.HookParams; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy; 028import ca.uhn.fhir.jpa.dao.DaoFailureUtil; 029import ca.uhn.fhir.jpa.model.config.PartitionSettings; 030import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 031import ca.uhn.fhir.rest.api.server.RequestDetails; 032import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 033import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 035import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 036import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 037import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 038import ca.uhn.fhir.util.ICallable; 039import ca.uhn.fhir.util.SleepUtil; 040import com.google.common.annotations.VisibleForTesting; 041import jakarta.annotation.Nonnull; 042import jakarta.annotation.Nullable; 043import org.apache.commons.lang3.Validate; 044import org.apache.commons.lang3.exception.ExceptionUtils; 045import org.hibernate.exception.ConstraintViolationException; 046import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049import org.springframework.beans.factory.annotation.Autowired; 050import org.springframework.dao.DataIntegrityViolationException; 051import org.springframework.dao.PessimisticLockingFailureException; 052import org.springframework.orm.ObjectOptimisticLockingFailureException; 053import org.springframework.transaction.PlatformTransactionManager; 054import org.springframework.transaction.TransactionStatus; 055import org.springframework.transaction.annotation.Isolation; 056import org.springframework.transaction.annotation.Propagation; 057import org.springframework.transaction.support.TransactionCallback; 058import org.springframework.transaction.support.TransactionCallbackWithoutResult; 059import org.springframework.transaction.support.TransactionOperations; 060import org.springframework.transaction.support.TransactionSynchronizationManager; 061import org.springframework.transaction.support.TransactionTemplate; 062 063import java.util.Objects; 064import java.util.concurrent.Callable; 065 066/** 067 * @see IHapiTransactionService for an explanation of this class 068 */ 069public class HapiTransactionService implements IHapiTransactionService { 070 071 public static final String XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS = 072 HapiTransactionService.class.getName() + "_RESOLVED_TAG_DEFINITIONS"; 073 public static final String XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS = 074 HapiTransactionService.class.getName() + "_EXISTING_SEARCH_PARAMS"; 075 private static final Logger ourLog = LoggerFactory.getLogger(HapiTransactionService.class); 076 private static final ThreadLocal<RequestPartitionId> ourRequestPartitionThreadLocal = new ThreadLocal<>(); 077 private static final ThreadLocal<HapiTransactionService> ourExistingTransaction = new ThreadLocal<>(); 078 079 @Autowired 080 protected IInterceptorBroadcaster myInterceptorBroadcaster; 081 082 @Autowired 083 protected PlatformTransactionManager myTransactionManager; 084 085 @Autowired 086 protected IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 087 088 @Autowired 089 protected PartitionSettings myPartitionSettings; 090 091 private Propagation myTransactionPropagationWhenChangingPartitions = Propagation.REQUIRED; 092 093 private SleepUtil mySleepUtil = new SleepUtil(); 094 095 @VisibleForTesting 096 public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) { 097 myInterceptorBroadcaster = theInterceptorBroadcaster; 098 } 099 100 @VisibleForTesting 101 public void setSleepUtil(SleepUtil theSleepUtil) { 102 mySleepUtil = theSleepUtil; 103 } 104 105 @Override 106 public IExecutionBuilder withRequest(@Nullable RequestDetails theRequestDetails) { 107 return buildExecutionBuilder(theRequestDetails); 108 } 109 110 @Override 111 public IExecutionBuilder withSystemRequest() { 112 return buildExecutionBuilder(null); 113 } 114 115 protected IExecutionBuilder buildExecutionBuilder(@Nullable RequestDetails theRequestDetails) { 116 return new ExecutionBuilder(theRequestDetails); 117 } 118 119 /** 120 * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead 121 */ 122 @Deprecated 123 public <T> T execute( 124 @Nullable RequestDetails theRequestDetails, 125 @Nullable TransactionDetails theTransactionDetails, 126 @Nonnull TransactionCallback<T> theCallback) { 127 return execute(theRequestDetails, theTransactionDetails, theCallback, null); 128 } 129 130 /** 131 * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead 132 */ 133 @Deprecated 134 public void execute( 135 @Nullable RequestDetails theRequestDetails, 136 @Nullable TransactionDetails theTransactionDetails, 137 @Nonnull Propagation thePropagation, 138 @Nonnull Isolation theIsolation, 139 @Nonnull Runnable theCallback) { 140 TransactionCallbackWithoutResult callback = new TransactionCallbackWithoutResult() { 141 @Override 142 protected void doInTransactionWithoutResult(TransactionStatus status) { 143 theCallback.run(); 144 } 145 }; 146 execute(theRequestDetails, theTransactionDetails, callback, null, thePropagation, theIsolation); 147 } 148 149 /** 150 * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead 151 */ 152 @Deprecated 153 @Override 154 public <T> T withRequest( 155 @Nullable RequestDetails theRequestDetails, 156 @Nullable TransactionDetails theTransactionDetails, 157 @Nonnull Propagation thePropagation, 158 @Nonnull Isolation theIsolation, 159 @Nonnull ICallable<T> theCallback) { 160 161 TransactionCallback<T> callback = tx -> theCallback.call(); 162 return execute(theRequestDetails, theTransactionDetails, callback, null, thePropagation, theIsolation); 163 } 164 165 /** 166 * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead 167 */ 168 @Deprecated 169 public <T> T execute( 170 @Nullable RequestDetails theRequestDetails, 171 @Nullable TransactionDetails theTransactionDetails, 172 @Nonnull TransactionCallback<T> theCallback, 173 @Nullable Runnable theOnRollback) { 174 return execute(theRequestDetails, theTransactionDetails, theCallback, theOnRollback, null, null); 175 } 176 177 @SuppressWarnings("ConstantConditions") 178 /** 179 * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead 180 */ 181 @Deprecated 182 public <T> T execute( 183 @Nullable RequestDetails theRequestDetails, 184 @Nullable TransactionDetails theTransactionDetails, 185 @Nonnull TransactionCallback<T> theCallback, 186 @Nullable Runnable theOnRollback, 187 @Nullable Propagation thePropagation, 188 @Nullable Isolation theIsolation) { 189 return withRequest(theRequestDetails) 190 .withTransactionDetails(theTransactionDetails) 191 .withPropagation(thePropagation) 192 .withIsolation(theIsolation) 193 .onRollback(theOnRollback) 194 .execute(theCallback); 195 } 196 197 /** 198 * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead 199 */ 200 @Deprecated 201 public <T> T execute( 202 @Nullable RequestDetails theRequestDetails, 203 @Nullable TransactionDetails theTransactionDetails, 204 @Nonnull TransactionCallback<T> theCallback, 205 @Nullable Runnable theOnRollback, 206 @Nonnull Propagation thePropagation, 207 @Nonnull Isolation theIsolation, 208 RequestPartitionId theRequestPartitionId) { 209 return withRequest(theRequestDetails) 210 .withTransactionDetails(theTransactionDetails) 211 .withPropagation(thePropagation) 212 .withIsolation(theIsolation) 213 .withRequestPartitionId(theRequestPartitionId) 214 .onRollback(theOnRollback) 215 .execute(theCallback); 216 } 217 218 public boolean isCustomIsolationSupported() { 219 return false; 220 } 221 222 @VisibleForTesting 223 public void setRequestPartitionSvcForUnitTest(IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { 224 myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; 225 } 226 227 public PlatformTransactionManager getTransactionManager() { 228 return myTransactionManager; 229 } 230 231 @VisibleForTesting 232 public void setTransactionManager(PlatformTransactionManager theTransactionManager) { 233 myTransactionManager = theTransactionManager; 234 } 235 236 @VisibleForTesting 237 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 238 myPartitionSettings = thePartitionSettings; 239 } 240 241 @Nullable 242 protected <T> T doExecute(ExecutionBuilder theExecutionBuilder, TransactionCallback<T> theCallback) { 243 final RequestPartitionId requestPartitionId = theExecutionBuilder.getEffectiveRequestPartitionId(); 244 RequestPartitionId previousRequestPartitionId = null; 245 if (requestPartitionId != null) { 246 previousRequestPartitionId = ourRequestPartitionThreadLocal.get(); 247 ourRequestPartitionThreadLocal.set(requestPartitionId); 248 } 249 250 ourLog.trace("Starting doExecute for RequestPartitionId {}", requestPartitionId); 251 if (!myPartitionSettings.isPartitioningEnabled() 252 || Objects.equals(previousRequestPartitionId, requestPartitionId)) { 253 if (ourExistingTransaction.get() == this && canReuseExistingTransaction(theExecutionBuilder)) { 254 /* 255 * If we're already in an active transaction, and it's for the right partition, 256 * and it's not a read-only transaction, we don't need to open a new transaction 257 * so let's just add a method to the stack trace that makes this obvious. 258 */ 259 return executeInExistingTransaction(theCallback); 260 } 261 } 262 263 HapiTransactionService previousExistingTransaction = ourExistingTransaction.get(); 264 try { 265 ourExistingTransaction.set(this); 266 267 if (myTransactionPropagationWhenChangingPartitions == Propagation.REQUIRES_NEW) { 268 return executeInNewTransactionForPartitionChange( 269 theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); 270 } else { 271 return doExecuteInTransaction( 272 theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); 273 } 274 } finally { 275 ourExistingTransaction.set(previousExistingTransaction); 276 } 277 } 278 279 @Nullable 280 private <T> T executeInNewTransactionForPartitionChange( 281 ExecutionBuilder theExecutionBuilder, 282 TransactionCallback<T> theCallback, 283 RequestPartitionId requestPartitionId, 284 RequestPartitionId previousRequestPartitionId) { 285 ourLog.trace("executeInNewTransactionForPartitionChange"); 286 theExecutionBuilder.myPropagation = myTransactionPropagationWhenChangingPartitions; 287 return doExecuteInTransaction(theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); 288 } 289 290 private boolean isThrowableOrItsSubclassPresent(Throwable theThrowable, Class<? extends Throwable> theClass) { 291 return ExceptionUtils.indexOfType(theThrowable, theClass) != -1; 292 } 293 294 private boolean isThrowablePresent(Throwable theThrowable, Class<? extends Throwable> theClass) { 295 return ExceptionUtils.indexOfThrowable(theThrowable, theClass) != -1; 296 } 297 298 private boolean isRetriable(Throwable theThrowable) { 299 return isThrowablePresent(theThrowable, ResourceVersionConflictException.class) 300 || isThrowablePresent(theThrowable, DataIntegrityViolationException.class) 301 || isThrowablePresent(theThrowable, ConstraintViolationException.class) 302 || isThrowablePresent(theThrowable, ObjectOptimisticLockingFailureException.class) 303 // calling isThrowableOrItsSubclassPresent instead of isThrowablePresent for 304 // PessimisticLockingFailureException, because we want to retry on its subclasses as well, especially 305 // CannotAcquireLockException, which is thrown in some deadlock situations which we want to retry 306 || isThrowableOrItsSubclassPresent(theThrowable, PessimisticLockingFailureException.class); 307 } 308 309 @Nullable 310 private <T> T doExecuteInTransaction( 311 ExecutionBuilder theExecutionBuilder, 312 TransactionCallback<T> theCallback, 313 RequestPartitionId requestPartitionId, 314 RequestPartitionId previousRequestPartitionId) { 315 ourLog.trace("doExecuteInTransaction"); 316 try { 317 for (int i = 0; ; i++) { 318 try { 319 320 return doExecuteCallback(theExecutionBuilder, theCallback); 321 322 } catch (Exception e) { 323 if (!isRetriable(e)) { 324 ourLog.debug("Unexpected transaction exception. Will not be retried.", e); 325 throw e; 326 } else { 327 328 ourLog.debug("Version conflict detected", e); 329 330 if (theExecutionBuilder.myOnRollback != null) { 331 theExecutionBuilder.myOnRollback.run(); 332 } 333 334 int maxRetries = 0; 335 336 /* 337 * If two client threads both concurrently try to add the same tag that isn't 338 * known to the system already, they'll both try to create a row in HFJ_TAG_DEF, 339 * which is the tag definition table. In that case, a constraint error will be 340 * thrown by one of the client threads, so we auto-retry in order to avoid 341 * annoying spurious failures for the client. 342 */ 343 if (DaoFailureUtil.isTagStorageFailure(e)) { 344 maxRetries = 3; 345 } 346 347 if (maxRetries == 0) { 348 HookParams params = new HookParams() 349 .add(RequestDetails.class, theExecutionBuilder.myRequestDetails) 350 .addIfMatchesType( 351 ServletRequestDetails.class, theExecutionBuilder.myRequestDetails); 352 ResourceVersionConflictResolutionStrategy conflictResolutionStrategy = 353 (ResourceVersionConflictResolutionStrategy) 354 CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( 355 myInterceptorBroadcaster, 356 theExecutionBuilder.myRequestDetails, 357 Pointcut.STORAGE_VERSION_CONFLICT, 358 params); 359 if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) { 360 maxRetries = conflictResolutionStrategy.getMaxRetries(); 361 } 362 } 363 364 if (i < maxRetries) { 365 if (theExecutionBuilder.myTransactionDetails != null) { 366 theExecutionBuilder 367 .myTransactionDetails 368 .getRollbackUndoActions() 369 .forEach(Runnable::run); 370 theExecutionBuilder.myTransactionDetails.clearRollbackUndoActions(); 371 theExecutionBuilder.myTransactionDetails.clearResolvedItems(); 372 theExecutionBuilder.myTransactionDetails.clearUserData( 373 XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS); 374 theExecutionBuilder.myTransactionDetails.clearUserData( 375 XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS); 376 } 377 double sleepAmount = (250.0d * i) * Math.random(); 378 long sleepAmountLong = (long) sleepAmount; 379 mySleepUtil.sleepAtLeast(sleepAmountLong, false); 380 381 ourLog.info( 382 "About to start a transaction retry due to conflict or constraint error. Sleeping {}ms first.", 383 sleepAmountLong); 384 continue; 385 } 386 387 IBaseOperationOutcome oo = null; 388 if (e instanceof ResourceVersionConflictException) { 389 oo = ((ResourceVersionConflictException) e).getOperationOutcome(); 390 } 391 392 if (maxRetries > 0) { 393 String msg = 394 "Max retries (" + maxRetries + ") exceeded for version conflict: " + e.getMessage(); 395 ourLog.info(msg, maxRetries); 396 throw new ResourceVersionConflictException(Msg.code(549) + msg); 397 } 398 399 throw new ResourceVersionConflictException(Msg.code(550) + e.getMessage(), e, oo); 400 } 401 } 402 } 403 } finally { 404 if (requestPartitionId != null) { 405 ourRequestPartitionThreadLocal.set(previousRequestPartitionId); 406 } 407 } 408 } 409 410 public void setTransactionPropagationWhenChangingPartitions( 411 Propagation theTransactionPropagationWhenChangingPartitions) { 412 Validate.notNull(theTransactionPropagationWhenChangingPartitions); 413 myTransactionPropagationWhenChangingPartitions = theTransactionPropagationWhenChangingPartitions; 414 } 415 416 @Nullable 417 protected <T> T doExecuteCallback(ExecutionBuilder theExecutionBuilder, TransactionCallback<T> theCallback) { 418 try { 419 TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); 420 421 if (theExecutionBuilder.myPropagation != null) { 422 txTemplate.setPropagationBehavior(theExecutionBuilder.myPropagation.value()); 423 } 424 425 if (isCustomIsolationSupported() 426 && theExecutionBuilder.myIsolation != null 427 && theExecutionBuilder.myIsolation != Isolation.DEFAULT) { 428 txTemplate.setIsolationLevel(theExecutionBuilder.myIsolation.value()); 429 } 430 431 if (theExecutionBuilder.myReadOnly) { 432 txTemplate.setReadOnly(true); 433 } 434 435 return txTemplate.execute(theCallback); 436 } catch (MyException e) { 437 if (e.getCause() instanceof RuntimeException) { 438 throw (RuntimeException) e.getCause(); 439 } else { 440 throw new InternalErrorException(Msg.code(551) + e); 441 } 442 } 443 } 444 445 protected class ExecutionBuilder implements IExecutionBuilder, TransactionOperations, Cloneable { 446 447 private final RequestDetails myRequestDetails; 448 private Isolation myIsolation; 449 private Propagation myPropagation; 450 private boolean myReadOnly; 451 private TransactionDetails myTransactionDetails; 452 private Runnable myOnRollback; 453 protected RequestPartitionId myRequestPartitionId; 454 455 protected ExecutionBuilder(RequestDetails theRequestDetails) { 456 myRequestDetails = theRequestDetails; 457 } 458 459 @Override 460 public ExecutionBuilder withIsolation(Isolation theIsolation) { 461 assert myIsolation == null; 462 myIsolation = theIsolation; 463 return this; 464 } 465 466 @Override 467 public ExecutionBuilder withTransactionDetails(TransactionDetails theTransactionDetails) { 468 assert myTransactionDetails == null; 469 myTransactionDetails = theTransactionDetails; 470 return this; 471 } 472 473 @Override 474 public ExecutionBuilder withPropagation(Propagation thePropagation) { 475 assert myPropagation == null; 476 myPropagation = thePropagation; 477 return this; 478 } 479 480 @Override 481 public ExecutionBuilder withRequestPartitionId(RequestPartitionId theRequestPartitionId) { 482 assert myRequestPartitionId == null; 483 myRequestPartitionId = theRequestPartitionId; 484 return this; 485 } 486 487 @Override 488 public ExecutionBuilder readOnly() { 489 myReadOnly = true; 490 return this; 491 } 492 493 @Override 494 public ExecutionBuilder onRollback(Runnable theOnRollback) { 495 assert myOnRollback == null; 496 myOnRollback = theOnRollback; 497 return this; 498 } 499 500 @Override 501 public void execute(Runnable theTask) { 502 TransactionCallback<Void> task = tx -> { 503 theTask.run(); 504 return null; 505 }; 506 execute(task); 507 } 508 509 @Override 510 public <T> T execute(Callable<T> theTask) { 511 TransactionCallback<T> callback = tx -> invokeCallableAndHandleAnyException(theTask); 512 return execute(callback); 513 } 514 515 @Override 516 public <T> T execute(@Nonnull TransactionCallback<T> callback) { 517 assert callback != null; 518 519 return doExecute(this, callback); 520 } 521 522 @VisibleForTesting 523 public RequestPartitionId getRequestPartitionIdForTesting() { 524 return myRequestPartitionId; 525 } 526 527 @VisibleForTesting 528 public RequestDetails getRequestDetailsForTesting() { 529 return myRequestDetails; 530 } 531 532 public Propagation getPropagation() { 533 return myPropagation; 534 } 535 536 @Nullable 537 protected RequestPartitionId getEffectiveRequestPartitionId() { 538 final RequestPartitionId requestPartitionId; 539 if (myRequestPartitionId != null) { 540 requestPartitionId = myRequestPartitionId; 541 } else if (myRequestDetails != null) { 542 requestPartitionId = myRequestPartitionHelperSvc.determineGenericPartitionForRequest(myRequestDetails); 543 } else { 544 requestPartitionId = null; 545 } 546 return requestPartitionId; 547 } 548 } 549 550 /** 551 * This is just an unchecked exception so that we can catch checked exceptions inside TransactionTemplate 552 * and rethrow them outside of it 553 */ 554 static class MyException extends RuntimeException { 555 556 public MyException(Throwable theThrowable) { 557 super(theThrowable); 558 } 559 } 560 561 /** 562 * Returns true if we alreadyt have an active transaction associated with the current thread, AND 563 * either it's non-read-only or we only need a read-only transaction, AND 564 * the newly requested transaction has a propagation of REQUIRED 565 */ 566 private static boolean canReuseExistingTransaction(ExecutionBuilder theExecutionBuilder) { 567 return TransactionSynchronizationManager.isActualTransactionActive() 568 && (!TransactionSynchronizationManager.isCurrentTransactionReadOnly() || theExecutionBuilder.myReadOnly) 569 && (theExecutionBuilder.myPropagation == null 570 || theExecutionBuilder.myPropagation == Propagation.REQUIRED); 571 } 572 573 @Nullable 574 private static <T> T executeInExistingTransaction(@Nonnull TransactionCallback<T> theCallback) { 575 ourLog.trace("executeInExistingTransaction"); 576 // TODO we could probably track the TransactionStatus we need as a thread local like we do our partition id. 577 return theCallback.doInTransaction(null); 578 } 579 580 /** 581 * Invokes {@link Callable#call()} and rethrows any exceptions thrown by that method. 582 * If the exception extends {@link BaseServerResponseException} it is rethrown unmodified. 583 * Otherwise, it's wrapped in a {@link InternalErrorException}. 584 */ 585 public static <T> T invokeCallableAndHandleAnyException(Callable<T> theTask) { 586 try { 587 return theTask.call(); 588 } catch (BaseServerResponseException e) { 589 throw e; 590 } catch (Exception e) { 591 throw new InternalErrorException(Msg.code(2223) + e.getMessage(), e); 592 } 593 } 594 595 public static <T> T executeWithDefaultPartitionInContext(@Nonnull ICallable<T> theCallback) { 596 RequestPartitionId previousRequestPartitionId = ourRequestPartitionThreadLocal.get(); 597 ourRequestPartitionThreadLocal.set(RequestPartitionId.defaultPartition()); 598 try { 599 return theCallback.call(); 600 } finally { 601 ourRequestPartitionThreadLocal.set(previousRequestPartitionId); 602 } 603 } 604 605 public static RequestPartitionId getRequestPartitionAssociatedWithThread() { 606 return ourRequestPartitionThreadLocal.get(); 607 } 608 609 /** 610 * Throws an {@link IllegalArgumentException} if a transaction is active 611 */ 612 public static void noTransactionAllowed() { 613 Validate.isTrue( 614 !TransactionSynchronizationManager.isActualTransactionActive(), 615 "Transaction must not be active but found an active transaction"); 616 } 617 618 /** 619 * Throws an {@link IllegalArgumentException} if no transaction is active 620 */ 621 public static void requireTransaction() { 622 Validate.isTrue( 623 TransactionSynchronizationManager.isActualTransactionActive(), 624 "Transaction required here but no active transaction found"); 625 } 626}