/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.core.plugins.orchestrate; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.roda.core.RodaCoreFactory; import org.roda.core.data.common.RodaConstants; import org.roda.core.data.common.RodaConstants.NodeType; import org.roda.core.data.exceptions.AuthorizationDeniedException; import org.roda.core.data.exceptions.GenericException; import org.roda.core.data.exceptions.InvalidParameterException; import org.roda.core.data.exceptions.NotFoundException; import org.roda.core.data.exceptions.RODAException; import org.roda.core.data.exceptions.RequestNotValidException; import org.roda.core.data.v2.IsRODAObject; import org.roda.core.data.v2.index.IsIndexed; import org.roda.core.data.v2.index.filter.Filter; import org.roda.core.data.v2.index.filter.OneOfManyFilterParameter; import org.roda.core.data.v2.index.filter.SimpleFilterParameter; import org.roda.core.data.v2.ip.AIP; import org.roda.core.data.v2.ip.AIPState; import org.roda.core.data.v2.ip.File; import org.roda.core.data.v2.ip.IndexedAIP; import org.roda.core.data.v2.ip.IndexedFile; import org.roda.core.data.v2.ip.IndexedRepresentation; import org.roda.core.data.v2.ip.Representation; import org.roda.core.data.v2.ip.TransferredResource; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.Job.JOB_STATE; import org.roda.core.data.v2.jobs.JobStats; import org.roda.core.index.IndexService; import org.roda.core.index.utils.IterableIndexResult; import org.roda.core.model.ModelService; import org.roda.core.plugins.Plugin; import org.roda.core.plugins.orchestrate.akka.Messages; import org.roda.core.plugins.plugins.PluginHelper; import org.roda.core.storage.fs.FSUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class JobsHelper { private static final Logger LOGGER = LoggerFactory.getLogger(JobsHelper.class); private static final String NUMBER_OF_JOB_WORKERS_PROPERTY = "core.orchestrator.nr_of_jobs_workers"; private static final String BLOCK_SIZE_PROPERTY = "core.orchestrator.block_size"; private static final int DEFAULT_BLOCK_SIZE = 100; private static final String SYNC_TIMEOUT_PROPERTY = "core.orchestrator.sync_timeout"; private static final int DEFAULT_SYNC_TIMEOUT = 600; private JobsHelper() { // do nothing } public static int getMaxNumberOfJobsInParallel() { int defaultMaxNumberOfJobsInParallel = Runtime.getRuntime().availableProcessors() + 1; return RodaCoreFactory.getRodaConfiguration().getInt("core.orchestrator.max_jobs_in_parallel", defaultMaxNumberOfJobsInParallel); } public static void setNumberOfJobsWorkers(int numberOfJobWorkers) { RodaCoreFactory.getRodaConfiguration().setProperty(NUMBER_OF_JOB_WORKERS_PROPERTY, numberOfJobWorkers); } public static int getNumberOfJobsWorkers() { int defaultNumberOfJobsWorkers = Runtime.getRuntime().availableProcessors() + 1; return RodaCoreFactory.getRodaConfiguration().getInt(NUMBER_OF_JOB_WORKERS_PROPERTY, defaultNumberOfJobsWorkers); } public static int getBlockSize() { return RodaCoreFactory.getRodaConfiguration().getInt(BLOCK_SIZE_PROPERTY, DEFAULT_BLOCK_SIZE); } public static void setBlockSize(int blockSize) { RodaCoreFactory.getRodaConfiguration().setProperty(BLOCK_SIZE_PROPERTY, blockSize); } public static int getSyncTimeout() { return RodaCoreFactory.getRodaConfiguration().getInt(SYNC_TIMEOUT_PROPERTY, DEFAULT_SYNC_TIMEOUT); } public static void setSyncTimeout(int syncTimeout) { RodaCoreFactory.getRodaConfiguration().setProperty(SYNC_TIMEOUT_PROPERTY, syncTimeout); } public static <T extends IsRODAObject> void updateJobState(Plugin<T> plugin, ModelService model, JOB_STATE state, Optional<String> stateDetails) { try { Job job = PluginHelper.getJob(plugin, model); job.setState(state); if (stateDetails.isPresent()) { job.setStateDetails(stateDetails.get()); } if (job.isInFinalState()) { job.setEndDate(new Date()); } model.createOrUpdateJob(job); } catch (NotFoundException | GenericException | RequestNotValidException | AuthorizationDeniedException e) { LOGGER.error("Unable to get or update Job from model", e); } } public static void updateJobState(Job job, ModelService model, JOB_STATE state, Optional<String> stateDetails) { try { Job jobFromModel = PluginHelper.getJob(job.getId(), model); jobFromModel.setState(state); if (stateDetails.isPresent()) { jobFromModel.setStateDetails(stateDetails.get()); } if (jobFromModel.isInFinalState()) { jobFromModel.setEndDate(new Date()); } model.createOrUpdateJob(jobFromModel); } catch (NotFoundException | GenericException | RequestNotValidException | AuthorizationDeniedException e) { LOGGER.error("Unable to get or update Job from model", e); } } public static <T extends IsRODAObject> void updateJobObjectsCount(Plugin<T> plugin, ModelService model, Long objectsCount) { try { Job job = PluginHelper.getJob(plugin, model); job.getJobStats().setSourceObjectsCount(objectsCount.intValue()) .setSourceObjectsWaitingToBeProcessed(objectsCount.intValue()); model.createOrUpdateJob(job); } catch (NotFoundException | GenericException | RequestNotValidException | AuthorizationDeniedException e) { LOGGER.error("Unable to get or update Job from model", e); } } public static <T extends IsRODAObject> void updateJobInformation(Plugin<T> plugin, ModelService model, JobPluginInfo jobPluginInfo) { // update job try { LOGGER.debug("New job completionPercentage: {}", jobPluginInfo.getCompletionPercentage()); Job job = PluginHelper.getJob(plugin, model); job = setJobCounters(job, jobPluginInfo); model.createOrUpdateJob(job); } catch (NotFoundException | GenericException | RequestNotValidException | AuthorizationDeniedException e) { LOGGER.error("Unable to get or update Job from model", e); } } private static Job setJobCounters(Job job, JobPluginInfo jobPluginInfo) { JobStats jobStats = job.getJobStats(); jobStats.setCompletionPercentage(jobPluginInfo.getCompletionPercentage()); jobStats.setSourceObjectsCount(jobPluginInfo.getSourceObjectsCount()); jobStats.setSourceObjectsBeingProcessed(jobPluginInfo.getSourceObjectsBeingProcessed()); jobStats.setSourceObjectsProcessedWithSuccess(jobPluginInfo.getSourceObjectsProcessedWithSuccess()); jobStats.setSourceObjectsProcessedWithFailure(jobPluginInfo.getSourceObjectsProcessedWithFailure()); jobStats .setSourceObjectsWaitingToBeProcessed(jobStats.getSourceObjectsCount() - jobStats.getSourceObjectsBeingProcessed() - jobStats.getSourceObjectsProcessedWithFailure() - jobStats.getSourceObjectsProcessedWithSuccess()); jobStats.setOutcomeObjectsWithManualIntervention(jobPluginInfo.getOutcomeObjectsWithManualIntervention()); return job; } /** * Updates the job state */ public static <T extends IsRODAObject> void updateJobState(Plugin<T> plugin, JOB_STATE state, Optional<String> stateDetails) { RodaCoreFactory.getPluginOrchestrator().updateJob(plugin, new Messages.JobStateUpdated(plugin, state, stateDetails)); } public static <T extends IsRODAObject> void updateJobState(Plugin<T> plugin, JOB_STATE state, Throwable throwable) { updateJobState(plugin, state, Optional.ofNullable(throwable.getClass().getName() + ": " + throwable.getMessage())); } public static Job updateJobInTheStateStartedOrCreated(Job job) { job.setState(JOB_STATE.FAILED_TO_COMPLETE); JobStats jobStats = job.getJobStats(); jobStats.setSourceObjectsBeingProcessed(0); jobStats.setSourceObjectsWaitingToBeProcessed(0); jobStats.setSourceObjectsProcessedWithFailure( jobStats.getSourceObjectsCount() - jobStats.getSourceObjectsProcessedWithSuccess()); job.setEndDate(new Date()); return job; } public static <T extends IsRODAObject> void setPluginParameters(Plugin<T> plugin, Job job) { Map<String, String> parameters = new HashMap<>(job.getPluginParameters()); parameters.put(RodaConstants.PLUGIN_PARAMS_JOB_ID, job.getId()); try { plugin.setParameterValues(parameters); } catch (InvalidParameterException e) { LOGGER.error("Error setting plugin parameters", e); } } public static List<TransferredResource> getTransferredResources(IndexService index, List<String> uuids) throws NotFoundException, GenericException { List<TransferredResource> ret = index.retrieve(TransferredResource.class, uuids, new ArrayList<>()); if (ret.isEmpty()) { throw new NotFoundException("Could not retrieve the Transferred Resources"); } return ret; } public static List<AIP> getAIPs(ModelService model, List<String> uuids) throws NotFoundException { List<AIP> aipsToReturn = new ArrayList<>(); if (!uuids.isEmpty()) { for (String uuid : uuids) { try { aipsToReturn.add(model.retrieveAIP(uuid)); } catch (RODAException | RuntimeException e) { LOGGER.error("Error while retrieving AIP from model", e); } } } if (aipsToReturn.isEmpty()) { throw new NotFoundException("Could not retrieve the AIPs"); } return aipsToReturn; } public static List<Representation> getRepresentations(ModelService model, IndexService index, List<String> uuids) throws NotFoundException { if (!uuids.isEmpty()) { try { List<IndexedRepresentation> retrieve = index.retrieve(IndexedRepresentation.class, uuids, new ArrayList<>()); List<Representation> representationsToReturn = getRepresentationFromList(model, retrieve); if (representationsToReturn.isEmpty()) { throw new NotFoundException("Could not retrieve the Representations"); } return representationsToReturn; } catch (RODAException | RuntimeException e) { LOGGER.error("Error while retrieving representations from index", e); } } throw new NotFoundException("Could not retrieve the Representations"); } private static List<Representation> getRepresentationFromList(ModelService model, List<IndexedRepresentation> retrieve) throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException { List<Representation> representationsToReturn = new ArrayList<>(); for (IndexedRepresentation indexedRepresentation : retrieve) { representationsToReturn .add(model.retrieveRepresentation(indexedRepresentation.getAipId(), indexedRepresentation.getId())); } return representationsToReturn; } public static List<File> getFiles(ModelService model, IndexService index, List<String> uuids) throws NotFoundException { if (!uuids.isEmpty()) { try { List<IndexedFile> retrieve = index.retrieve(IndexedFile.class, uuids, new ArrayList<>()); List<File> filesToReturn = getFilesFromList(model, retrieve); if (filesToReturn.isEmpty()) { throw new NotFoundException("Could not retrieve the Files"); } return filesToReturn; } catch (RODAException | RuntimeException e) { LOGGER.error("Error while retrieving files from index", e); } } throw new NotFoundException("Could not retrieve the Files"); } private static List<File> getFilesFromList(ModelService model, List<IndexedFile> retrieve) throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException { List<File> filesToReturn = new ArrayList<>(); for (IndexedFile indexedFile : retrieve) { filesToReturn.add(model.retrieveFile(indexedFile.getAipId(), indexedFile.getRepresentationId(), indexedFile.getPath(), indexedFile.getId())); } return filesToReturn; } public static <T extends IsRODAObject> List<T> getObjectsFromUUID(ModelService model, IndexService index, Class<T> objectClass, List<String> uuids) throws NotFoundException, GenericException { if (AIP.class.equals(objectClass)) { return (List<T>) getAIPs(model, uuids); } else if (Representation.class.equals(objectClass)) { return (List<T>) getRepresentations(model, index, uuids); } else if (File.class.equals(objectClass)) { return (List<T>) getFiles(model, index, uuids); } else { return getObjectsFromIndex(index, objectClass, uuids); } } public static <T extends IsRODAObject, T1 extends IsIndexed> List<T> getObjectsFromIndexObjects(ModelService model, Class<T> objectClass, List<T1> indexObjects) throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException { if (AIP.class.equals(objectClass)) { return (List<T>) getAIPs(model, indexObjects.stream().map(e -> e.getUUID()).collect(Collectors.toList())); } else if (Representation.class.equals(objectClass)) { return (List<T>) getRepresentationFromList(model, (List<IndexedRepresentation>) indexObjects); } else if (File.class.equals(objectClass)) { return (List<T>) getFilesFromList(model, (List<IndexedFile>) indexObjects); } else { return (List<T>) indexObjects; } } public static <T extends IsRODAObject> List<T> getObjectsFromIndex(IndexService index, Class<T> objectClass, List<String> uuids) throws NotFoundException, GenericException { List<T> ret = (List<T>) index.retrieve((Class<IsIndexed>) objectClass, uuids, new ArrayList<>()); if (ret.isEmpty()) { throw new NotFoundException("Could not retrieve the " + objectClass.getSimpleName()); } return ret; } public static Class<IsRODAObject> getSelectedClassFromString(String selectedClass) throws GenericException { try { Class<?> clazz = Class.forName(selectedClass); if (IsRODAObject.class.isAssignableFrom(clazz)) { return (Class<IsRODAObject>) clazz; } else { throw new GenericException("Error while getting class from string"); } } catch (ClassNotFoundException e) { throw new GenericException("Error while getting class from string"); } } public static Class<IsIndexed> getIsIndexedSelectedClassFromString(String selectedClass) throws GenericException { try { Class<?> clazz = Class.forName(selectedClass); if (IsIndexed.class.isAssignableFrom(clazz)) { try { return (Class<IsIndexed>) clazz; } catch (ClassCastException e) { LOGGER.error("Error while casting class to IsIndexed", e); // do nothing and let exception in the end of the method be thrown } } } catch (ClassNotFoundException e) { // do nothing and let exception in the end of the method be thrown LOGGER.error("Class not found", e); } throw new GenericException("Error while getting class from string"); } public static void doJobObjectsCleanup(Job job, ModelService model, IndexService index) { if (RodaCoreFactory.getNodeType() == NodeType.MASTER) { try { // make sure the index is up to date index.commit(IndexedAIP.class); // find all AIPs that should be removed Filter filter = new Filter(); // FIXME 20161128 hsilva: perhaps we should avoid ghosts??? // FIXME 20170308 nvieira: it should rollback if job is update filter.add(new SimpleFilterParameter(RodaConstants.INGEST_JOB_ID, job.getId())); filter.add(new OneOfManyFilterParameter(RodaConstants.AIP_STATE, Arrays.asList(AIPState.CREATED.toString(), AIPState.INGEST_PROCESSING.toString()))); doJobCleanup(model, index.findAll(IndexedAIP.class, filter, false, Arrays.asList(RodaConstants.INDEX_UUID))); } catch (GenericException e) { LOGGER.error("Error doing Job cleanup", e); } } } private static void doJobCleanup(ModelService model, IterableIndexResult<IndexedAIP> results) { for (IndexedAIP indexedAIP : results) { try { LOGGER.info("Job cleanup: deleting AIP {}", indexedAIP.getId()); model.deleteAIP(indexedAIP.getId()); } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException e) { LOGGER.error("Error doing deleting AIP during Job cleanup", e); } } } public static void createJobWorkingDirectory(String jobId) { Path path = RodaCoreFactory.getWorkingDirectory().resolve(jobId); try { Files.createDirectory(path); } catch (IOException e) { LOGGER.error("Error while creating job working directory (path='{}')", path); } } public static void deleteJobWorkingDirectory(String jobId) { Path path = RodaCoreFactory.getWorkingDirectory().resolve(jobId); try { FSUtils.deletePath(path); } catch (NotFoundException | GenericException e) { LOGGER.error("Error while deleting job working directory (path='{}')", path); } } }