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