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