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.interceptor;
021
022import ca.uhn.fhir.interceptor.api.Hook;
023import ca.uhn.fhir.interceptor.api.Interceptor;
024import ca.uhn.fhir.interceptor.api.Pointcut;
025import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy;
026import ca.uhn.fhir.rest.api.Constants;
027import ca.uhn.fhir.rest.api.server.RequestDetails;
028import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
029import org.apache.commons.lang3.Validate;
030
031/**
032 * This interceptor looks for a header on incoming requests called <code>X-Retry-On-Version-Conflict</code> and
033 * if present, it will instruct the server to automatically retry JPA server operations that would have
034 * otherwise failed with a {@link ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException} (HTTP 409).
035 * <p>
036 * The format of the header is:<br/>
037 * <code>X-Retry-On-Version-Conflict: retry; max-retries=100</code>
038 * </p>
039 */
040@Interceptor
041public class UserRequestRetryVersionConflictsInterceptor {
042
043        /** Deprecated and moved to {@link ca.uhn.fhir.rest.api.Constants#HEADER_RETRY_ON_VERSION_CONFLICT} */
044        @Deprecated
045        public static final String HEADER_NAME = Constants.HEADER_RETRY_ON_VERSION_CONFLICT;
046
047        /** Deprecated and moved to {@link ca.uhn.fhir.rest.api.Constants#HEADER_MAX_RETRIES} */
048        @Deprecated
049        public static final String MAX_RETRIES = "max-retries";
050
051        /** Deprecated and moved to {@link ca.uhn.fhir.rest.api.Constants#HEADER_RETRY} */
052        @Deprecated
053        public static final String RETRY = "retry";
054
055        @Hook(value = Pointcut.STORAGE_VERSION_CONFLICT, order = 100)
056        public ResourceVersionConflictResolutionStrategy check(RequestDetails theRequestDetails) {
057                ResourceVersionConflictResolutionStrategy retVal = new ResourceVersionConflictResolutionStrategy();
058                boolean shouldSetRetries = theRequestDetails != null && theRequestDetails.isRetry();
059                // This can be in HapiTransactionService directly but HapiTransactionService
060                // tackles database logic and doesn't deal with HTTP/client layer. The RequestDetails
061                // is only used here for interceptor broadcasting. Keeping this here is good separation
062                // of concerns.
063                if (shouldSetRetries) {
064                        retVal.setRetry(true);
065                        int maxRetries = Math.min(100, theRequestDetails.getMaxRetries());
066                        retVal.setMaxRetries(maxRetries);
067                }
068
069                return retVal;
070        }
071
072        /**
073         * Convenience method to add a retry header to a system request
074         */
075        public static void addRetryHeader(SystemRequestDetails theRequestDetails, int theMaxRetries) {
076                Validate.inclusiveBetween(1, Integer.MAX_VALUE, theMaxRetries, "Max retries must be > 0");
077                theRequestDetails.setRetry(true);
078                theRequestDetails.setMaxRetries(theMaxRetries);
079        }
080}