
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.util; 021 022import ca.uhn.fhir.rest.api.server.RequestDetails; 023import jakarta.annotation.Nonnull; 024import org.apache.commons.lang3.Validate; 025import org.apache.commons.text.StringTokenizer; 026 027/** 028 * This class parses and serializes the <code>X-Transaction-Semantics</code> 029 * header, which is a custom HAPI FHIR extension affecting the way that 030 * FHIR transactions are processed. 031 * 032 * @see ca.uhn.fhir.jpa.dao.BaseTransactionProcessor 033 * @since 8.2.0 034 */ 035public class TransactionSemanticsHeader { 036 037 public static final String RETRY_COUNT = "retryCount"; 038 public static final String MIN_DELAY = "minRetryDelay"; 039 public static final String MAX_DELAY = "maxRetryDelay"; 040 public static final String TRY_BATCH_AS_TRANSACTION_FIRST = "tryBatchAsTransactionFirst"; 041 public static final TransactionSemanticsHeader DEFAULT = newBuilder().build(); 042 public static final String HEADER_NAME = "X-Transaction-Semantics"; 043 044 private final Integer myRetryCount; 045 private final Integer myMinRetryDelay; 046 private final Integer myMaxRetryDelay; 047 private final boolean myTryBatchAsTransactionFirst; 048 049 /** 050 * Non-instantiable, see {@link #newBuilder()} 051 */ 052 private TransactionSemanticsHeader( 053 Integer theRetryCount, 054 Integer theMinRetryDelay, 055 Integer theMaxRetryDelay, 056 boolean theTryAsBatchAsTransactionFirst) { 057 myRetryCount = theRetryCount; 058 myMinRetryDelay = theMinRetryDelay; 059 myMaxRetryDelay = theMaxRetryDelay; 060 myTryBatchAsTransactionFirst = theTryAsBatchAsTransactionFirst; 061 } 062 063 /** 064 * Specifies the number of retry attempts which should be attempted 065 * if the initial transaction processing fails with any kind of error. 066 * A value of 0 (or {@literal null}) means that the transaction will be 067 * attempted only once (i.e. the default behaviour). A value of 2 means 068 * that the transaction will be attempted once, and if it fails, up to 069 * two more attempts will be made before giving up. 070 */ 071 public Integer getRetryCount() { 072 return myRetryCount; 073 } 074 075 /** 076 * When automatically retrying a failed transaction, the system will 077 * first sleep for a minimum of this number of milliseconds. 078 */ 079 public Integer getMinRetryDelay() { 080 return myMinRetryDelay; 081 } 082 083 /** 084 * When automatically retrying a failed transaction, the system will 085 * first sleep for a minimum of this number of milliseconds. 086 */ 087 public Integer getMaxRetryDelay() { 088 return myMaxRetryDelay; 089 } 090 091 /** 092 * When processing a FHIR Batch bundle, try it as a FHIR transaction first, and only switch 093 * to batch mode on the first retry. This option is useful in cases where data can safely be 094 * ingested as a FHIR Batch, since FHIR Transaction processing is generally significantly 095 * faster. However, FHIR transactions exhibit an all-or-nothing failure mode which is not 096 * always desirable for batch ingestion, so this option provides an easy fallback which brings 097 * the benefits of both approaches. 098 */ 099 public boolean isTryBatchAsTransactionFirst() { 100 return myTryBatchAsTransactionFirst; 101 } 102 103 /** 104 * Serializes the values as a header value (not including the header name) 105 */ 106 public String toHeaderValue() { 107 StringBuilder b = new StringBuilder(); 108 109 if (myRetryCount != null) { 110 b.append(RETRY_COUNT).append('=').append(myRetryCount); 111 112 // None of the following settings has any meaning unless a 113 // retry count is specified 114 if (myMinRetryDelay != null) { 115 b.append("; "); 116 b.append(MIN_DELAY).append('=').append(myMinRetryDelay); 117 } 118 if (myMaxRetryDelay != null) { 119 b.append("; "); 120 b.append(MAX_DELAY).append('=').append(myMaxRetryDelay); 121 } 122 if (myTryBatchAsTransactionFirst) { 123 b.append("; "); 124 b.append(TRY_BATCH_AS_TRANSACTION_FIRST).append('=').append("true"); 125 } 126 } 127 128 return b.toString(); 129 } 130 131 /** 132 * Add this header to a RequestDetails object 133 * 134 * @since 8.6.0 135 */ 136 public void applyToRequest(RequestDetails theRequestDetails) { 137 theRequestDetails.addHeader(HEADER_NAME, toHeaderValue()); 138 } 139 140 /** 141 * Parses a header value (not including the header name) into a new 142 * {@link TransactionSemanticsHeader} instance. 143 */ 144 public static TransactionSemanticsHeader parse(@Nonnull String theHeaderValue) { 145 Validate.notNull(theHeaderValue, "theHeaderValue must not be null"); 146 Integer retryCount = null; 147 Integer minRetryDelay = null; 148 Integer maxRetryDelay = null; 149 boolean tryBatchAsTransactionFirst = false; 150 151 StringTokenizer tok = new StringTokenizer(theHeaderValue, ";"); 152 while (tok.hasNext()) { 153 String next = tok.nextToken(); 154 int equalsIdx = next.indexOf('='); 155 if (equalsIdx == -1) { 156 continue; 157 } 158 159 String name = next.substring(0, equalsIdx).trim(); 160 String value = next.substring(equalsIdx + 1).trim(); 161 162 switch (name) { 163 case RETRY_COUNT: 164 retryCount = parsePositiveInteger(value); 165 break; 166 case MIN_DELAY: 167 minRetryDelay = parsePositiveInteger(value); 168 break; 169 case MAX_DELAY: 170 maxRetryDelay = parsePositiveInteger(value); 171 break; 172 case TRY_BATCH_AS_TRANSACTION_FIRST: 173 tryBatchAsTransactionFirst = parseBoolean(value); 174 break; 175 } 176 } 177 178 return new TransactionSemanticsHeader(retryCount, minRetryDelay, maxRetryDelay, tryBatchAsTransactionFirst); 179 } 180 181 /** 182 * Begin building a new {@link TransactionSemanticsHeader} instance 183 */ 184 public static Builder newBuilder() { 185 return new Builder(); 186 } 187 188 private static Integer parsePositiveInteger(String theValue) { 189 try { 190 int retVal = Integer.parseInt(theValue); 191 if (retVal <= 0) { 192 return null; 193 } 194 return retVal; 195 } catch (NumberFormatException e) { 196 return null; 197 } 198 } 199 200 private static boolean parseBoolean(String theValue) { 201 return "true".equalsIgnoreCase(theValue); 202 } 203 204 public static final class Builder { 205 206 private Integer myRetryCount; 207 private Integer myMinRetryDelay; 208 private Integer myMaxRetryDelay; 209 private boolean myTryBatchAsTransactionFirst; 210 211 private Builder() {} 212 213 /** 214 * Specifies the number of retry attempts which should be attempted 215 * if the initial transaction processing fails with any kind of error. 216 * A value of 0 (or {@literal null} means that the transaction will be 217 * attempted once only (i.e. the default behaviour). A value of 2 means 218 * that the transaction will be attempted once, and if it fails, up to 219 * two more attempts will be made before giving up. 220 */ 221 public Builder withRetryCount(Integer theRetryCount) { 222 Validate.isTrue( 223 theRetryCount == null || theRetryCount >= 0, "Retry count must be null or a non-negative integer"); 224 myRetryCount = theRetryCount; 225 return this; 226 } 227 228 /** 229 * When automatically retrying a failed transaction, the system will 230 * first sleep for a minimum of this number of milliseconds. 231 */ 232 public Builder withMinRetryDelay(Integer theMinRetryDelay) { 233 Validate.isTrue( 234 theMinRetryDelay == null || theMinRetryDelay >= 0, 235 "Retry delay must be null or a non-negative integer"); 236 myMinRetryDelay = theMinRetryDelay; 237 return this; 238 } 239 240 /** 241 * When automatically retrying a failed transaction, the system will 242 * first sleep for a minimum of this number of milliseconds. 243 */ 244 public Builder withMaxRetryDelay(Integer theMaxRetryDelay) { 245 Validate.isTrue( 246 theMaxRetryDelay == null || theMaxRetryDelay >= 0, 247 "Retry delay must be null or a non-negative integer"); 248 myMaxRetryDelay = theMaxRetryDelay; 249 return this; 250 } 251 252 /** 253 * When processing a FHIR Batch bundle, try it as a FHIR transaction first, and only switch 254 * to batch mode on the first retry. This option is useful in cases where data can safely be 255 * ingested as a FHIR Batch, since FHIR Transaction processing is generally significantly 256 * faster. However, FHIR transactions exhibit an all-or-nothing failure mode which is not 257 * always desirable for batch ingestion, so this option provides an easy fallback which brings 258 * the benefits of both approaches. 259 */ 260 public Builder withTryBatchAsTransactionFirst(boolean theTryBatchAsTransactionFirst) { 261 myTryBatchAsTransactionFirst = theTryBatchAsTransactionFirst; 262 return this; 263 } 264 265 /** 266 * Construct the header 267 */ 268 public TransactionSemanticsHeader build() { 269 return new TransactionSemanticsHeader( 270 myRetryCount, myMinRetryDelay, myMaxRetryDelay, myTryBatchAsTransactionFirst); 271 } 272 } 273}