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