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.expunge; 021 022import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 023import ca.uhn.fhir.jpa.api.model.ExpungeOptions; 024import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; 025import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 026import ca.uhn.fhir.rest.api.server.RequestDetails; 027import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 028import ca.uhn.fhir.rest.api.server.storage.IResourceVersionPersistentId; 029import com.google.common.annotations.VisibleForTesting; 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032import org.springframework.beans.factory.annotation.Autowired; 033import org.springframework.context.annotation.Scope; 034import org.springframework.stereotype.Component; 035 036import java.util.List; 037import java.util.concurrent.Callable; 038import java.util.concurrent.atomic.AtomicInteger; 039 040@Component 041@Scope("prototype") 042public class ExpungeOperation<T extends IResourcePersistentId<?>, V extends IResourceVersionPersistentId> 043 implements Callable<ExpungeOutcome> { 044 private static final Logger ourLog = LoggerFactory.getLogger(ExpungeOperation.class); 045 public static final String PROCESS_NAME = "Expunging"; 046 public static final String THREAD_PREFIX = "expunge"; 047 048 @Autowired 049 private IResourceExpungeService<T, V> myResourceExpungeService; 050 051 @Autowired 052 private JpaStorageSettings myStorageSettings; 053 054 private final String myResourceName; 055 private final T myResourceId; 056 private final ExpungeOptions myExpungeOptions; 057 private final RequestDetails myRequestDetails; 058 private final AtomicInteger myRemainingCount; 059 060 @Autowired 061 private HapiTransactionService myTxService; 062 063 public ExpungeOperation( 064 String theResourceName, 065 T theResourceId, 066 ExpungeOptions theExpungeOptions, 067 RequestDetails theRequestDetails) { 068 myResourceName = theResourceName; 069 myResourceId = theResourceId; 070 myExpungeOptions = theExpungeOptions; 071 myRequestDetails = theRequestDetails; 072 myRemainingCount = new AtomicInteger(myExpungeOptions.getLimit()); 073 } 074 075 @Override 076 public ExpungeOutcome call() { 077 if (myExpungeOptions.isExpungeDeletedResources() 078 && (myResourceId == null || myResourceId.getVersion() == null)) { 079 expungeDeletedResources(); 080 if (expungeLimitReached()) { 081 return expungeOutcome(); 082 } 083 } 084 085 if (myExpungeOptions.isExpungeOldVersions()) { 086 expungeOldVersions(); 087 if (expungeLimitReached()) { 088 return expungeOutcome(); 089 } 090 } 091 092 return expungeOutcome(); 093 } 094 095 private void expungeDeletedResources() { 096 List<T> resourceIds = findHistoricalVersionsOfDeletedResources(); 097 098 deleteHistoricalVersions(resourceIds); 099 if (expungeLimitReached()) { 100 return; 101 } 102 103 deleteCurrentVersionsOfDeletedResources(resourceIds); 104 } 105 106 private List<T> findHistoricalVersionsOfDeletedResources() { 107 List<T> retVal = getPartitionAwareSupplier() 108 .supplyInPartitionedContext(() -> myResourceExpungeService.findHistoricalVersionsOfDeletedResources( 109 myResourceName, myResourceId, myRemainingCount.get())); 110 111 ourLog.debug("Found {} historical versions", retVal.size()); 112 return retVal; 113 } 114 115 private boolean expungeLimitReached() { 116 boolean expungeLimitReached = myRemainingCount.get() <= 0; 117 if (expungeLimitReached) { 118 ourLog.debug("Expunge limit has been hit - Stopping operation"); 119 } 120 return expungeLimitReached; 121 } 122 123 private void expungeOldVersions() { 124 List<V> historicalIds = getPartitionAwareSupplier() 125 .supplyInPartitionedContext(() -> myResourceExpungeService.findHistoricalVersionsOfNonDeletedResources( 126 myResourceName, myResourceId, myRemainingCount.get())); 127 128 getPartitionRunner() 129 .runInPartitionedThreads( 130 historicalIds, 131 partition -> myResourceExpungeService.expungeHistoricalVersions( 132 myRequestDetails, partition, myRemainingCount)); 133 } 134 135 private PartitionAwareSupplier getPartitionAwareSupplier() { 136 return new PartitionAwareSupplier(myTxService, myRequestDetails); 137 } 138 139 private PartitionRunner getPartitionRunner() { 140 return new PartitionRunner( 141 PROCESS_NAME, 142 THREAD_PREFIX, 143 myStorageSettings.getExpungeBatchSize(), 144 myStorageSettings.getExpungeThreadCount(), 145 myTxService, 146 myRequestDetails); 147 } 148 149 private void deleteCurrentVersionsOfDeletedResources(List<T> theResourceIds) { 150 getPartitionRunner() 151 .runInPartitionedThreads( 152 theResourceIds, 153 partition -> myResourceExpungeService.expungeCurrentVersionOfResources( 154 myRequestDetails, partition, myRemainingCount)); 155 } 156 157 private void deleteHistoricalVersions(List<T> theResourceIds) { 158 getPartitionRunner() 159 .runInPartitionedThreads( 160 theResourceIds, 161 partition -> myResourceExpungeService.expungeHistoricalVersionsOfIds( 162 myRequestDetails, partition, myRemainingCount)); 163 } 164 165 private ExpungeOutcome expungeOutcome() { 166 return new ExpungeOutcome().setDeletedCount(myExpungeOptions.getLimit() - myRemainingCount.get()); 167 } 168 169 @VisibleForTesting 170 public void setHapiTransactionServiceForTesting(HapiTransactionService theHapiTransactionService) { 171 myTxService = theHapiTransactionService; 172 } 173 174 @VisibleForTesting 175 public void setStorageSettingsForTesting(JpaStorageSettings theStorageSettings) { 176 myStorageSettings = theStorageSettings; 177 } 178 179 @SuppressWarnings({"unchecked", "rawtypes"}) 180 @VisibleForTesting 181 public void setExpungeDaoServiceForTesting(IResourceExpungeService theIResourceExpungeService) { 182 myResourceExpungeService = theIResourceExpungeService; 183 } 184}