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}