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