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