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