
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 RequestPartitionId effectiveRequestPartitionId = theExecutionBuilder.getEffectiveRequestPartitionId(); 254 final RequestPartitionId requestPartitionId; 255 if (effectiveRequestPartitionId != null 256 && myPartitionSettings.isDefaultPartition(effectiveRequestPartitionId)) { 257 requestPartitionId = myPartitionSettings.getDefaultRequestPartitionId(); 258 } else { 259 requestPartitionId = effectiveRequestPartitionId; 260 } 261 262 RequestPartitionId previousRequestPartitionId = null; 263 if (requestPartitionId != null) { 264 previousRequestPartitionId = ourRequestPartitionThreadLocal.get(); 265 ourRequestPartitionThreadLocal.set(requestPartitionId); 266 } 267 268 ourLog.trace("Starting doExecute for RequestPartitionId {}", requestPartitionId); 269 if (isCompatiblePartition(previousRequestPartitionId, requestPartitionId)) { 270 if (ourExistingTransaction.get() == this && canReuseExistingTransaction(theExecutionBuilder)) { 271 /* 272 * If we're already in an active transaction, and it's for the right partition, 273 * and it's not a read-only transaction, we don't need to open a new transaction 274 * so let's just add a method to the stack trace that makes this obvious. 275 */ 276 return executeInExistingTransaction(theCallback); 277 } 278 } 279 280 HapiTransactionService previousExistingTransaction = ourExistingTransaction.get(); 281 try { 282 ourExistingTransaction.set(this); 283 284 if (isRequiresNewTransactionWhenChangingPartitions()) { 285 return executeInNewTransactionForPartitionChange( 286 theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); 287 } else { 288 return doExecuteInTransaction( 289 theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); 290 } 291 } finally { 292 ourExistingTransaction.set(previousExistingTransaction); 293 } 294 } 295 296 protected boolean isRequiresNewTransactionWhenChangingPartitions() { 297 return myTransactionPropagationWhenChangingPartitions == Propagation.REQUIRES_NEW; 298 } 299 300 @Override 301 public boolean isCompatiblePartition( 302 RequestPartitionId theRequestPartitionId, RequestPartitionId theOtherRequestPartitionId) { 303 return !myPartitionSettings.isPartitioningEnabled() 304 || !isRequiresNewTransactionWhenChangingPartitions() 305 || Objects.equals(theRequestPartitionId, theOtherRequestPartitionId); 306 } 307 308 @Nullable 309 private <T> T executeInNewTransactionForPartitionChange( 310 ExecutionBuilder theExecutionBuilder, 311 TransactionCallback<T> theCallback, 312 RequestPartitionId requestPartitionId, 313 RequestPartitionId previousRequestPartitionId) { 314 ourLog.trace("executeInNewTransactionForPartitionChange"); 315 theExecutionBuilder.myPropagation = myTransactionPropagationWhenChangingPartitions; 316 return doExecuteInTransaction(theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); 317 } 318 319 private boolean isThrowableOrItsSubclassPresent(Throwable theThrowable, Class<? extends Throwable> theClass) { 320 return ExceptionUtils.indexOfType(theThrowable, theClass) != -1; 321 } 322 323 private boolean isThrowablePresent(Throwable theThrowable, Class<? extends Throwable> theClass) { 324 return ExceptionUtils.indexOfThrowable(theThrowable, theClass) != -1; 325 } 326 327 private boolean isRetriable(Throwable theThrowable) { 328 return isThrowablePresent(theThrowable, ResourceVersionConflictException.class) 329 || isThrowablePresent(theThrowable, DataIntegrityViolationException.class) 330 || isThrowablePresent(theThrowable, ConstraintViolationException.class) 331 || isThrowablePresent(theThrowable, ObjectOptimisticLockingFailureException.class) 332 // calling isThrowableOrItsSubclassPresent instead of isThrowablePresent for 333 // PessimisticLockingFailureException, because we want to retry on its subclasses as well, especially 334 // CannotAcquireLockException, which is thrown in some deadlock situations which we want to retry 335 || isThrowableOrItsSubclassPresent(theThrowable, PessimisticLockingFailureException.class) 336 || isThrowableOrItsSubclassPresent(theThrowable, PessimisticLockException.class); 337 } 338 339 @Nullable 340 private <T> T doExecuteInTransaction( 341 ExecutionBuilder theExecutionBuilder, 342 TransactionCallback<T> theCallback, 343 RequestPartitionId requestPartitionId, 344 RequestPartitionId previousRequestPartitionId) { 345 ourLog.trace("doExecuteInTransaction"); 346 try { 347 // retry loop 348 for (int i = 0; ; i++) { 349 try { 350 351 return doExecuteCallback(theExecutionBuilder, theCallback); 352 353 } catch (Exception e) { 354 int retriesRemaining = 0; 355 int maxRetries = 0; 356 boolean exceptionIsRetriable = isRetriable(e); 357 if (exceptionIsRetriable) { 358 maxRetries = calculateMaxRetries(theExecutionBuilder.myRequestDetails, e); 359 retriesRemaining = maxRetries - i; 360 } 361 362 // we roll back on all exceptions. 363 theExecutionBuilder.rollbackTransactionProcessingChanges(retriesRemaining > 0); 364 365 if (!exceptionIsRetriable) { 366 ourLog.debug("Unexpected transaction exception. Will not be retried.", e); 367 throw e; 368 } else { 369 // We have several exceptions that we consider retriable, call all of them "version conflicts" 370 ourLog.debug("Version conflict detected", e); 371 372 // should we retry? 373 if (retriesRemaining > 0) { 374 // We are retrying. 375 sleepForRetry(i); 376 } else { 377 throwResourceVersionConflictException(i, maxRetries, e); 378 } 379 } 380 } 381 } 382 } finally { 383 if (requestPartitionId != null) { 384 ourRequestPartitionThreadLocal.set(previousRequestPartitionId); 385 } 386 } 387 } 388 389 private static void throwResourceVersionConflictException( 390 int theAttemptIndex, int theMaxRetries, Exception theCause) { 391 IBaseOperationOutcome oo = null; 392 if (theCause instanceof ResourceVersionConflictException) { 393 oo = ((ResourceVersionConflictException) theCause).getOperationOutcome(); 394 } 395 396 if (theAttemptIndex > 0) { 397 // log if we tried to retry, but still failed 398 String msg = "Max retries (" + theMaxRetries + ") exceeded for version conflict: " + theCause.getMessage(); 399 ourLog.info(msg); 400 throw new ResourceVersionConflictException(Msg.code(549) + msg, theCause, oo); 401 } 402 403 throw new ResourceVersionConflictException(Msg.code(550) + theCause.getMessage(), theCause, oo); 404 } 405 406 /** 407 * Sleep a bit more each time, with 0 sleep on first retry and some random dither. 408 * @param theAttemptIndex 0-index for the first attempt, 1 for second, etc. 409 */ 410 private void sleepForRetry(int theAttemptIndex) { 411 double sleepAmount = (250.0d * theAttemptIndex) * Math.random(); 412 long sleepAmountLong = (long) sleepAmount; 413 ourLog.info( 414 "About to start a transaction retry due to conflict or constraint error. Sleeping {}ms first.", 415 sleepAmountLong); 416 mySleepUtil.sleepAtLeast(sleepAmountLong, false); 417 } 418 419 public void setTransactionPropagationWhenChangingPartitions( 420 Propagation theTransactionPropagationWhenChangingPartitions) { 421 Objects.requireNonNull(theTransactionPropagationWhenChangingPartitions); 422 myTransactionPropagationWhenChangingPartitions = theTransactionPropagationWhenChangingPartitions; 423 } 424 425 @Nullable 426 protected <T> T doExecuteCallback(ExecutionBuilder theExecutionBuilder, TransactionCallback<T> theCallback) { 427 try { 428 TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); 429 430 if (theExecutionBuilder.myPropagation != null) { 431 txTemplate.setPropagationBehavior(theExecutionBuilder.myPropagation.value()); 432 } 433 434 if (isCustomIsolationSupported() 435 && theExecutionBuilder.myIsolation != null 436 && theExecutionBuilder.myIsolation != Isolation.DEFAULT) { 437 txTemplate.setIsolationLevel(theExecutionBuilder.myIsolation.value()); 438 } 439 440 if (theExecutionBuilder.myReadOnly) { 441 txTemplate.setReadOnly(true); 442 } 443 444 return txTemplate.execute(theCallback); 445 } catch (MyException e) { 446 if (e.getCause() instanceof RuntimeException) { 447 throw (RuntimeException) e.getCause(); 448 } else { 449 throw new InternalErrorException(Msg.code(551) + e); 450 } 451 } 452 } 453 454 private int calculateMaxRetries(RequestDetails theRequestDetails, Exception e) { 455 int maxRetries = 0; 456 457 /* 458 * If two client threads both concurrently try to add the same tag that isn't 459 * known to the system already, they'll both try to create a row in HFJ_TAG_DEF, 460 * which is the tag definition table. In that case, a constraint error will be 461 * thrown by one of the client threads, so we auto-retry in order to avoid 462 * annoying spurious failures for the client. 463 */ 464 if (DaoFailureUtil.isTagStorageFailure(e)) { 465 maxRetries = 3; 466 } 467 468 // Our default policy is no-retry. 469 // But we often register UserRequestRetryVersionConflictsInterceptor, which supports a retry header 470 // and retry settings on RequestDetails. 471 if (maxRetries == 0) { 472 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 473 this.myInterceptorBroadcaster, theRequestDetails); 474 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_VERSION_CONFLICT)) { 475 HookParams params = new HookParams() 476 .add(RequestDetails.class, theRequestDetails) 477 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 478 ResourceVersionConflictResolutionStrategy conflictResolutionStrategy = 479 (ResourceVersionConflictResolutionStrategy) compositeBroadcaster.callHooksAndReturnObject( 480 Pointcut.STORAGE_VERSION_CONFLICT, params); 481 if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) { 482 maxRetries = conflictResolutionStrategy.getMaxRetries(); 483 } 484 } 485 } 486 return maxRetries; 487 } 488 489 protected class ExecutionBuilder implements IExecutionBuilder, TransactionOperations, Cloneable { 490 private final RequestDetails myRequestDetails; 491 private Isolation myIsolation; 492 private Propagation myPropagation; 493 private boolean myReadOnly; 494 private TransactionDetails myTransactionDetails; 495 private Runnable myOnRollback; 496 protected RequestPartitionId myRequestPartitionId; 497 498 protected ExecutionBuilder(RequestDetails theRequestDetails) { 499 myRequestDetails = theRequestDetails; 500 } 501 502 @Override 503 public ExecutionBuilder withIsolation(Isolation theIsolation) { 504 assert myIsolation == null; 505 myIsolation = theIsolation; 506 return this; 507 } 508 509 @Override 510 public ExecutionBuilder withTransactionDetails(TransactionDetails theTransactionDetails) { 511 assert myTransactionDetails == null; 512 myTransactionDetails = theTransactionDetails; 513 return this; 514 } 515 516 @Override 517 public ExecutionBuilder withPropagation(Propagation thePropagation) { 518 assert myPropagation == null; 519 myPropagation = thePropagation; 520 return this; 521 } 522 523 @Override 524 public ExecutionBuilder withRequestPartitionId(RequestPartitionId theRequestPartitionId) { 525 assert myRequestPartitionId == null; 526 myRequestPartitionId = theRequestPartitionId; 527 return this; 528 } 529 530 @Override 531 public ExecutionBuilder readOnly() { 532 myReadOnly = true; 533 return this; 534 } 535 536 @Override 537 public ExecutionBuilder onRollback(Runnable theOnRollback) { 538 assert myOnRollback == null; 539 myOnRollback = theOnRollback; 540 return this; 541 } 542 543 @Override 544 public void execute(Runnable theTask) { 545 TransactionCallback<Void> task = tx -> { 546 theTask.run(); 547 return null; 548 }; 549 execute(task); 550 } 551 552 @Override 553 public <T> T execute(Callable<T> theTask) { 554 TransactionCallback<T> callback = tx -> invokeCallableAndHandleAnyException(theTask); 555 return execute(callback); 556 } 557 558 @Override 559 public <T> T execute(@Nonnull TransactionCallback<T> callback) { 560 return doExecute(this, callback); 561 } 562 563 @VisibleForTesting 564 public RequestPartitionId getRequestPartitionIdForTesting() { 565 return myRequestPartitionId; 566 } 567 568 @VisibleForTesting 569 public RequestDetails getRequestDetailsForTesting() { 570 return myRequestDetails; 571 } 572 573 public Propagation getPropagation() { 574 return myPropagation; 575 } 576 577 @Nullable 578 protected RequestPartitionId getEffectiveRequestPartitionId() { 579 final RequestPartitionId requestPartitionId; 580 if (myRequestPartitionId != null) { 581 requestPartitionId = myRequestPartitionId; 582 } else if (myRequestDetails != null) { 583 requestPartitionId = myRequestPartitionHelperSvc.determineGenericPartitionForRequest(myRequestDetails); 584 } else { 585 requestPartitionId = null; 586 } 587 return requestPartitionId; 588 } 589 590 /** 591 * This method is called when a transaction has failed, and we need to rollback any changes made to the 592 * state of our objects in RAM. 593 * <p> 594 * This is used to undo any changes made during transaction resolution, such as conditional references, 595 * placeholders, etc. 596 * 597 * @param theWillRetry Should be <code>true</code> if the transaction is about to be automatically retried 598 * by the transaction service. 599 */ 600 void rollbackTransactionProcessingChanges(boolean theWillRetry) { 601 if (myOnRollback != null) { 602 myOnRollback.run(); 603 } 604 605 if (myTransactionDetails != null) { 606 /* 607 * Loop through the rollback undo actions in reverse order so we leave things in the correct 608 * initial state. E.g., the resource ID may get modified twice if a resource is being modified 609 * within a FHIR transaction: first the TransactionProcessor sets a new ID and adds a rollback 610 * item, and then the Resource DAO touches the ID a second time and adds a second rollback item. 611 */ 612 List<Runnable> rollbackUndoActions = myTransactionDetails.getRollbackUndoActions(); 613 for (int i = rollbackUndoActions.size() - 1; i >= 0; i--) { 614 Runnable rollbackUndoAction = rollbackUndoActions.get(i); 615 rollbackUndoAction.run(); 616 } 617 618 /* 619 * If we're about to retry the transaction, we shouldn't clear the rollback undo actions 620 * because we need to re-execute them if the transaction fails a second time. 621 */ 622 if (!theWillRetry) { 623 myTransactionDetails.clearRollbackUndoActions(); 624 } 625 626 myTransactionDetails.clearResolvedItems(); 627 myTransactionDetails.clearUserData(XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS); 628 myTransactionDetails.clearUserData(XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS); 629 } 630 } 631 } 632 633 /** 634 * This is just an unchecked exception so that we can catch checked exceptions inside TransactionTemplate 635 * and rethrow them outside of it 636 */ 637 static class MyException extends RuntimeException { 638 639 public MyException(Throwable theThrowable) { 640 super(theThrowable); 641 } 642 } 643 644 /** 645 * Returns true if we already have an active transaction associated with the current thread, AND 646 * either it's non-read-only or we only need a read-only transaction, AND 647 * the newly requested transaction has a propagation of REQUIRED 648 */ 649 private static boolean canReuseExistingTransaction(ExecutionBuilder theExecutionBuilder) { 650 return TransactionSynchronizationManager.isActualTransactionActive() 651 && (!TransactionSynchronizationManager.isCurrentTransactionReadOnly() || theExecutionBuilder.myReadOnly) 652 && (theExecutionBuilder.myPropagation == null 653 || theExecutionBuilder.myPropagation 654 == DEFAULT_TRANSACTION_PROPAGATION_WHEN_CHANGING_PARTITIONS); 655 } 656 657 @Nullable 658 private static <T> T executeInExistingTransaction(@Nonnull TransactionCallback<T> theCallback) { 659 ourLog.trace("executeInExistingTransaction"); 660 // TODO we could probably track the TransactionStatus we need as a thread local like we do our partition id. 661 return theCallback.doInTransaction(null); 662 } 663 664 /** 665 * Invokes {@link Callable#call()} and rethrows any exceptions thrown by that method. 666 * If the exception extends {@link BaseServerResponseException} it is rethrown unmodified. 667 * Otherwise, it's wrapped in a {@link InternalErrorException}. 668 */ 669 public static <T> T invokeCallableAndHandleAnyException(Callable<T> theTask) { 670 try { 671 return theTask.call(); 672 } catch (BaseServerResponseException e) { 673 throw e; 674 } catch (Exception e) { 675 throw new InternalErrorException(Msg.code(2223) + e.getMessage(), e); 676 } 677 } 678 679 public static <T> T executeWithDefaultPartitionInContext(@Nonnull ICallable<T> theCallback) { 680 RequestPartitionId previousRequestPartitionId = ourRequestPartitionThreadLocal.get(); 681 ourRequestPartitionThreadLocal.set(RequestPartitionId.defaultPartition()); 682 try { 683 return theCallback.call(); 684 } finally { 685 ourRequestPartitionThreadLocal.set(previousRequestPartitionId); 686 } 687 } 688 689 public static RequestPartitionId getRequestPartitionAssociatedWithThread() { 690 return ourRequestPartitionThreadLocal.get(); 691 } 692 693 /** 694 * Throws an {@link IllegalArgumentException} if a transaction is active 695 */ 696 public static void noTransactionAllowed() { 697 Validate.isTrue( 698 !TransactionSynchronizationManager.isActualTransactionActive(), 699 "Transaction must not be active but found an active transaction"); 700 } 701 702 /** 703 * Throws an {@link IllegalArgumentException} if no transaction is active 704 */ 705 public static void requireTransaction() { 706 Validate.isTrue( 707 TransactionSynchronizationManager.isActualTransactionActive(), 708 "Transaction required here but no active transaction found"); 709 } 710}