package de.is24.infrastructure.gridfs.http.maintenance;
import com.mongodb.DBObject;
import de.is24.infrastructure.gridfs.http.domain.YumEntry;
import de.is24.infrastructure.gridfs.http.domain.yum.YumPackage;
import de.is24.infrastructure.gridfs.http.domain.yum.YumPackageReducedView;
import de.is24.infrastructure.gridfs.http.gridfs.StorageService;
import de.is24.infrastructure.gridfs.http.metadata.YumEntriesRepository;
import de.is24.infrastructure.gridfs.http.rpm.version.YumPackageVersionComparator;
import de.is24.infrastructure.gridfs.http.storage.FileDescriptor;
import de.is24.infrastructure.gridfs.http.storage.FileStorageItem;
import de.is24.infrastructure.gridfs.http.storage.FileStorageService;
import de.is24.infrastructure.gridfs.http.utils.MDCHelper;
import de.is24.util.monitoring.spring.TimeMeasurement;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.DocumentCallbackHandler;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsOperations;
import org.springframework.data.mongodb.tx.MongoTx;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ScheduledExecutorService;
import static de.is24.infrastructure.gridfs.http.mongo.DatabaseStructure.GRIDFS_FILES_COLLECTION;
import static de.is24.infrastructure.gridfs.http.mongo.DatabaseStructure.YUM_ENTRY_COLLECTION;
import static java.util.stream.Collectors.toSet;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@Service
@TimeMeasurement
public class MaintenanceService {
private static final Logger LOGGER = LoggerFactory.getLogger(MaintenanceService.class);
private final Filter obsoleteRpmFiler = new ObsoleteRpmFilter();
private final Filter propagatableRpmFilter = new PropagatableRpmFilter();
private YumPackageVersionComparator versionComparator = new YumPackageVersionComparator();
private ScheduledExecutorService scheduledExecutorService;
private YumEntriesRepository yumEntriesRepository;
private FileStorageService fileStorageService;
private StorageService storageService;
private MongoTemplate mongoTemplate;
private GridFsOperations gridFsTemplate;
/* for AOP autoproxying */
protected MaintenanceService() {
}
@Autowired
public MaintenanceService(ScheduledExecutorService scheduledExecutorService,
YumEntriesRepository yumEntriesRepository,
FileStorageService fileStorageService, StorageService storageService,
MongoTemplate mongoTemplate,
GridFsOperations gridFsTemplate) {
this.scheduledExecutorService = scheduledExecutorService;
this.yumEntriesRepository = yumEntriesRepository;
this.fileStorageService = fileStorageService;
this.storageService = storageService;
this.mongoTemplate = mongoTemplate;
this.gridFsTemplate = gridFsTemplate;
}
public Set<YumPackageReducedView> getPropagatableRPMs(String targetRepo,
String sourceRepo) {
return filterRPMsFromPropagationChain(propagatableRpmFilter, targetRepo, sourceRepo);
}
public Set<YumPackageReducedView> getObsoleteRPMs(String targetRepo,
String sourceRepo) {
return filterRPMsFromPropagationChain(obsoleteRpmFiler, targetRepo, sourceRepo);
}
public void triggerDeletionOfObsoleteRPMs(String targetRepo, String sourceRepo) {
DeleteObsoleteRPMsJob job = new DeleteObsoleteRPMsJob(sourceRepo,
targetRepo);
scheduledExecutorService.submit(job);
LOGGER.info("triggered delete obsolete RPMs in propagation chain from {} to {}", sourceRepo, targetRepo);
}
public Map<ObjectId, YumPackageReducedView> getYumEntriesWithoutAssociatedFiles() {
final Query query = new Query();
query.fields().include("_id");
CheckForMissingFsFilesCallbackHandler callbackHandler = new CheckForMissingFsFilesCallbackHandler();
mongoTemplate.executeQuery(query, YUM_ENTRY_COLLECTION, callbackHandler);
Map<ObjectId, YumPackageReducedView> result = new HashMap<>();
for (ObjectId id : callbackHandler.getEntriesWithMissingFile()) {
YumEntry yumEntry = yumEntriesRepository.findOne(id);
result.put(yumEntry.getId(), new YumPackageReducedView(yumEntry.getYumPackage()));
}
return result;
}
public Set<FileStorageItem> getFilesWithoutYumEntry() {
final Query query = new Query(where("metadata.arch").ne("repodata"));
query.addCriteria(where("metadata.markedAsDeleted").exists(false));
CheckForMissingEntriesCallbackHandler callbackHandler = new CheckForMissingEntriesCallbackHandler();
mongoTemplate.executeQuery(query, GRIDFS_FILES_COLLECTION, callbackHandler);
return callbackHandler.getFilesWithMissingEntry().stream().map(fileStorageService::findById).collect(toSet());
}
private void deleteObsoleteRPMs(String targetRepo, String sourceRepo) {
Set<YumPackageReducedView> obsoleteRPMs = filterRPMsFromPropagationChain(obsoleteRpmFiler, targetRepo, sourceRepo);
for (YumPackageReducedView obsoletePackage : obsoleteRPMs) {
String[] strings = obsoletePackage.getLocation().getHref().split("/");
FileDescriptor descriptor = new FileDescriptor(sourceRepo, strings[0], strings[1]);
LOGGER.info("delete obsolete RPM {} obsoleted by a RPM in {}", descriptor.getPath(), targetRepo);
try {
storageService.delete(descriptor);
} catch (RuntimeException e) {
LOGGER.warn("oops", e);
}
}
}
private Set<YumPackageReducedView> filterRPMsFromPropagationChain(Filter filter, String targetRepo,
String sourceRepo) {
Map<String, Map<String, YumPackage>> newestTargetPackages = findNewestPackages(
yumEntriesRepository.findByRepo(
targetRepo));
List<YumEntry> sourceRepoEntries = yumEntriesRepository.findByRepo(sourceRepo);
return filterRPMs(filter, newestTargetPackages,
sourceRepoEntries);
}
private Set<YumPackageReducedView> filterRPMs(Filter filter,
Map<String, Map<String, YumPackage>> newestTargetPackagesByNameAndArch,
List<YumEntry> sourceRepoEntries) {
Set<YumPackageReducedView> result = new TreeSet<>();
for (YumEntry entry : sourceRepoEntries) {
YumPackage yumPackage = entry.getYumPackage();
YumPackage newestPackageInTargetRepo = getMatchingYumPackageByNameAndArchIfAny(newestTargetPackagesByNameAndArch,
yumPackage);
if (filter.select(newestPackageInTargetRepo, yumPackage)) {
LOGGER.info("found a {} version of {}", filter.getFilterDescription(), yumPackage.getName());
result.add(new YumPackageReducedView(yumPackage));
}
}
return result;
}
private YumPackage getMatchingYumPackageByNameAndArchIfAny(Map<String, Map<String, YumPackage>> packagesByNameAndArch,
YumPackage yumPackage) {
Map<String, YumPackage> rpmsByArch = packagesByNameAndArch.get(yumPackage.getName());
if (rpmsByArch != null) {
return rpmsByArch.get(yumPackage.getArch());
}
return null;
}
/**
* determine newest RPMs by name and architecture
* @param inputList list of yum entries in repo
* @return a map of maps, first map key is rpm name, second maps key is arch
*/
private Map<String, Map<String, YumPackage>> findNewestPackages(List<YumEntry> inputList) {
Map<String, Map<String, YumPackage>> result = new HashMap<>();
for (YumEntry entry : inputList) {
YumPackage yumPackage = entry.getYumPackage();
Map<String, YumPackage> packageMap = result.get(yumPackage.getName());
YumPackage packageForArchInMap = null;
if (packageMap == null) {
packageMap = new HashMap<>();
result.put(yumPackage.getName(), packageMap);
} else {
packageForArchInMap = packageMap.get(yumPackage.getArch());
}
if ((packageForArchInMap == null) ||
(versionComparator.compare(yumPackage.getVersion(), packageForArchInMap.getVersion()) > 0)) {
packageMap.put(yumPackage.getArch(), yumPackage);
}
}
return result;
}
@MongoTx
public Map<ObjectId, YumPackageReducedView> deleteYumEntriesWithoutAssociatedFiles() {
Map<ObjectId, YumPackageReducedView> result = getYumEntriesWithoutAssociatedFiles();
result.keySet().forEach(yumEntriesRepository::delete);
return result;
}
private interface Filter {
boolean select(YumPackage newestTargetPackage, YumPackage sourcePackage);
String getFilterDescription();
}
private class ObsoleteRpmFilter implements Filter {
@Override
public boolean select(YumPackage newestTargetPackage, YumPackage sourcePackage) {
return (newestTargetPackage != null) &&
(versionComparator.compare(newestTargetPackage.getVersion(), sourcePackage.getVersion()) > 0);
}
@Override
public String getFilterDescription() {
return "is obsolete";
}
}
private class PropagatableRpmFilter implements Filter {
@Override
public boolean select(YumPackage newestTargetPackage, YumPackage sourcePackage) {
return (newestTargetPackage == null) ||
(versionComparator.compare(newestTargetPackage.getVersion(), sourcePackage.getVersion()) < 0);
}
@Override
public String getFilterDescription() {
return "is propagatable";
}
}
private class DeleteObsoleteRPMsJob implements Runnable {
private final String sourceRepo;
private final String propagationTargetRepo;
public DeleteObsoleteRPMsJob(String sourceRepo,
String propagationTargetRepo) {
this.sourceRepo = sourceRepo;
this.propagationTargetRepo = propagationTargetRepo;
}
@Override
public void run() {
new MDCHelper(this.getClass()).run(() -> {
deleteObsoleteRPMs(propagationTargetRepo, sourceRepo);
LOGGER.info("finished deleting Obsolete RPMs without Exception");
});
}
}
private class CheckForMissingFsFilesCallbackHandler implements DocumentCallbackHandler {
private List<ObjectId> entriesWithMissingFile = new ArrayList<>();
@Override
public void processDocument(DBObject dbObject) {
ObjectId id = getId(dbObject);
if (gridFsTemplate.findOne(Query.query(where("_id").is(id))) == null) {
entriesWithMissingFile.add(id);
}
}
private ObjectId getId(DBObject dbObject) {
return (ObjectId) dbObject.get("_id");
}
private List<ObjectId> getEntriesWithMissingFile() {
return entriesWithMissingFile;
}
}
private class CheckForMissingEntriesCallbackHandler implements DocumentCallbackHandler {
private List<ObjectId> filesWithMissingEntry = new ArrayList<>();
@Override
public void processDocument(DBObject dbObject) {
ObjectId id = getId(dbObject);
if (yumEntriesRepository.findOne(id) == null) {
filesWithMissingEntry.add(id);
}
}
private ObjectId getId(DBObject dbObject) {
return (ObjectId) dbObject.get("_id");
}
private List<ObjectId> getFilesWithMissingEntry() {
return filesWithMissingEntry;
}
}
}