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