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