001package ca.uhn.fhir.jpa.delete.job;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.jpa.api.config.DaoConfig;
024import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
025import ca.uhn.fhir.jpa.dao.expunge.PartitionRunner;
026import ca.uhn.fhir.jpa.dao.expunge.ResourceForeignKey;
027import ca.uhn.fhir.jpa.dao.expunge.ResourceTableFKProvider;
028import ca.uhn.fhir.jpa.dao.index.IdHelperService;
029import ca.uhn.fhir.jpa.model.entity.ResourceLink;
030import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.batch.item.ItemProcessor;
034import org.springframework.beans.factory.annotation.Autowired;
035import org.springframework.data.domain.Slice;
036import org.springframework.data.domain.SliceImpl;
037
038import java.util.ArrayList;
039import java.util.Collections;
040import java.util.List;
041import java.util.stream.Collectors;
042
043/**
044 * Input: list of pids of resources to be deleted and expunged
045 * Output: list of sql statements to be executed
046 */
047public class DeleteExpungeProcessor implements ItemProcessor<List<Long>, List<String>> {
048        private static final Logger ourLog = LoggerFactory.getLogger(DeleteExpungeProcessor.class);
049
050        public static final String PROCESS_NAME = "Delete Expunging";
051        public static final String THREAD_PREFIX = "delete-expunge";
052
053        @Autowired
054        ResourceTableFKProvider myResourceTableFKProvider;
055        @Autowired
056        DaoConfig myDaoConfig;
057        @Autowired
058        IdHelperService myIdHelper;
059        @Autowired
060        IResourceLinkDao myResourceLinkDao;
061
062        @Override
063        public List<String> process(List<Long> thePids) throws Exception {
064                validateOkToDeleteAndExpunge(new SliceImpl<>(thePids));
065
066                List<String> retval = new ArrayList<>();
067
068                String pidListString = thePids.toString().replace("[", "(").replace("]", ")");
069                List<ResourceForeignKey> resourceForeignKeys = myResourceTableFKProvider.getResourceForeignKeys();
070
071                for (ResourceForeignKey resourceForeignKey : resourceForeignKeys) {
072                        retval.add(deleteRecordsByColumnSql(pidListString, resourceForeignKey));
073                }
074
075                // Lastly we need to delete records from the resource table all of these other tables link to:
076                ResourceForeignKey resourceTablePk = new ResourceForeignKey("HFJ_RESOURCE", "RES_ID");
077                retval.add(deleteRecordsByColumnSql(pidListString, resourceTablePk));
078                return retval;
079        }
080
081        public void validateOkToDeleteAndExpunge(Slice<Long> thePids) {
082                if (!myDaoConfig.isEnforceReferentialIntegrityOnDelete()) {
083                        ourLog.info("Referential integrity on delete disabled.  Skipping referential integrity check.");
084                        return;
085                }
086
087                List<ResourceLink> conflictResourceLinks = Collections.synchronizedList(new ArrayList<>());
088                PartitionRunner partitionRunner = new PartitionRunner(PROCESS_NAME, THREAD_PREFIX, myDaoConfig.getExpungeBatchSize(), myDaoConfig.getExpungeThreadCount());
089                partitionRunner.runInPartitionedThreads(thePids, someTargetPids -> findResourceLinksWithTargetPidIn(thePids.getContent(), someTargetPids, conflictResourceLinks));
090
091                if (conflictResourceLinks.isEmpty()) {
092                        return;
093                }
094
095                ResourceLink firstConflict = conflictResourceLinks.get(0);
096
097                //NB-GGG: We previously instantiated these ID values from firstConflict.getSourceResource().getIdDt(), but in a situation where we
098                //actually had to run delete conflict checks in multiple partitions, the executor service starts its own sessions on a per thread basis, and by the time
099                //we arrive here, those sessions are closed. So instead, we resolve them from PIDs, which are eagerly loaded.
100                String sourceResourceId = myIdHelper.resourceIdFromPidOrThrowException(firstConflict.getSourceResourcePid()).toVersionless().getValue();
101                String targetResourceId = myIdHelper.resourceIdFromPidOrThrowException(firstConflict.getTargetResourcePid()).toVersionless().getValue();
102
103                throw new InvalidRequestException("DELETE with _expunge=true failed.  Unable to delete " +
104                        targetResourceId + " because " + sourceResourceId + " refers to it via the path " + firstConflict.getSourcePath());
105        }
106
107        public void findResourceLinksWithTargetPidIn(List<Long> theAllTargetPids, List<Long> theSomeTargetPids, List<ResourceLink> theConflictResourceLinks) {
108                // We only need to find one conflict, so if we found one already in an earlier partition run, we can skip the rest of the searches
109                if (theConflictResourceLinks.isEmpty()) {
110                        List<ResourceLink> conflictResourceLinks = myResourceLinkDao.findWithTargetPidIn(theSomeTargetPids).stream()
111                                // Filter out resource links for which we are planning to delete the source.
112                                // theAllTargetPids contains a list of all the pids we are planning to delete.  So we only want
113                                // to consider a link to be a conflict if the source of that link is not in theAllTargetPids.
114                                .filter(link -> !theAllTargetPids.contains(link.getSourceResourcePid()))
115                                .collect(Collectors.toList());
116
117                        // We do this in two steps to avoid lock contention on this synchronized list
118                        theConflictResourceLinks.addAll(conflictResourceLinks);
119                }
120        }
121
122        private String deleteRecordsByColumnSql(String thePidListString, ResourceForeignKey theResourceForeignKey) {
123                return "DELETE FROM " + theResourceForeignKey.table + " WHERE " + theResourceForeignKey.key + " IN " + thePidListString;
124        }
125}