All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.chutneytesting.execution.domain.purge.PurgeServiceImpl Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-FileCopyrightText: 2017-2024 Enedis
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 */

package com.chutneytesting.execution.domain.purge;

import static com.chutneytesting.execution.domain.purge.PurgeExecutionsFilters.isExecutionDateBeforeNowMinusOffset;
import static com.chutneytesting.execution.domain.purge.PurgeExecutionsFilters.isScenarioExecutionLinkedWithCampaignExecution;
import static com.chutneytesting.server.core.domain.execution.report.ServerReportStatus.SUCCESS;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Comparator.comparing;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toSet;

import com.chutneytesting.campaign.domain.CampaignExecutionRepository;
import com.chutneytesting.campaign.domain.CampaignRepository;
import com.chutneytesting.server.core.domain.execution.history.ExecutionHistory.ExecutionSummary;
import com.chutneytesting.server.core.domain.execution.history.ExecutionHistoryRepository;
import com.chutneytesting.server.core.domain.execution.history.PurgeService;
import com.chutneytesting.server.core.domain.execution.report.ServerReportStatus;
import com.chutneytesting.server.core.domain.scenario.TestCaseMetadata;
import com.chutneytesting.server.core.domain.scenario.TestCaseRepository;
import com.chutneytesting.server.core.domain.scenario.campaign.Campaign;
import com.chutneytesting.server.core.domain.scenario.campaign.CampaignExecution;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PurgeServiceImpl implements PurgeService {
    private static final Logger LOGGER = LoggerFactory.getLogger(PurgeServiceImpl.class);
    public static final int ONE_DAY_MILLIS = Long.valueOf(Duration.ofDays(1).toMillis()).intValue();
    private final PurgeExecutionService campaignPurgeService;
    private final PurgeExecutionService scenarioPurgeService;

    PurgeServiceImpl(
        TestCaseRepository testCaseRepository,
        ExecutionHistoryRepository executionsRepository,
        CampaignRepository campaignRepository,
        CampaignExecutionRepository campaignExecutionRepository,
        int maxScenarioExecutionsConfiguration,
        int maxCampaignExecutionsConfiguration
    ) {
        this(testCaseRepository,
            executionsRepository,
            campaignRepository,
            campaignExecutionRepository,
            maxScenarioExecutionsConfiguration,
            0,
            maxCampaignExecutionsConfiguration,
            0);
    }

    public PurgeServiceImpl(
        TestCaseRepository testCaseRepository,
        ExecutionHistoryRepository executionsRepository,
        CampaignRepository campaignRepository,
        CampaignExecutionRepository campaignExecutionRepository,
        int maxScenarioExecutionsConfiguration,
        int beforeNowMinusOffsetScenarioExecutionsConfiguration,
        int maxCampaignExecutionsConfiguration,
        int beforeNowMinusOffsetCampaignExecutionsConfiguration
    ) {
        int maxScenarioExecutions = checkPositiveOrDefault(maxScenarioExecutionsConfiguration, "maxScenarioExecutions", 10);
        int maxCampaignExecutions = checkPositiveOrDefault(maxCampaignExecutionsConfiguration, "maxCampaignExecutions", 10);
        int scenarioBeforeHoursTimeExecutions = checkPositiveOrDefault(beforeNowMinusOffsetScenarioExecutionsConfiguration, "beforeNowMinusOffsetScenarioExecutions", ONE_DAY_MILLIS);
        int campaignsBeforeHoursTimeExecutions = checkPositiveOrDefault(beforeNowMinusOffsetCampaignExecutionsConfiguration, "beforeNowMinusOffsetCampaignExecutions", ONE_DAY_MILLIS);

        this.scenarioPurgeService = buildScenarioService(testCaseRepository, executionsRepository, maxScenarioExecutions, scenarioBeforeHoursTimeExecutions);
        this.campaignPurgeService = buildCampaignService(campaignRepository, campaignExecutionRepository, maxCampaignExecutions, campaignsBeforeHoursTimeExecutions);
    }

    private static int checkPositiveOrDefault(
        int configurationLimit,
        String configName,
        int defaultValue
    ) {
        if (configurationLimit < 0) {
            LOGGER.warn("Purge configuration limit must be positive. Defaulting {} to {}", configName, defaultValue);
            return defaultValue;
        }
        return configurationLimit;
    }

    private static PurgeExecutionService buildScenarioService(
        TestCaseRepository testCaseRepository,
        ExecutionHistoryRepository executionsRepository,
        int maxScenarioExecutions,
        int beforeHoursTimeExecutions
    ) {
        return new PurgeExecutionService<>(
            maxScenarioExecutions,
            testCaseRepository::findAll,
            TestCaseMetadata::id,
            executionsRepository::getExecutions,
            isScenarioExecutionLinkedWithCampaignExecution.and(isExecutionDateBeforeNowMinusOffset(ExecutionSummary::time, beforeHoursTimeExecutions)),
            ExecutionSummary::executionId,
            ExecutionSummary::time,
            ExecutionSummary::status,
            ExecutionSummary::environment,
            executionsRepository::deleteExecutions
        );
    }

    private PurgeExecutionService buildCampaignService(
        CampaignRepository campaignRepository,
        CampaignExecutionRepository campaignExecutionRepository,
        int maxCampaignExecutions,
        int beforeHoursTimeExecutions
    ) {
        return new PurgeExecutionService<>(
            maxCampaignExecutions,
            campaignRepository::findAll,
            campaign -> campaign.id,
            campaignExecutionRepository::getExecutionHistory,
            isExecutionDateBeforeNowMinusOffset(cer -> cer.startDate, beforeHoursTimeExecutions),
            cer -> cer.executionId,
            cer -> cer.startDate,
            CampaignExecution::status,
            cer -> cer.executionEnvironment,
            campaignExecutionRepository::deleteExecutions
        ) {
            // Not thread safe
            private Map> campaignExecutionsByPartialExecution;
            private List emptyCampaignExecutions;

            /**
             * Transform executions to filter only those that are not manual replays and not empty.
             */
            @Override
            public List handleExecutionsForOneEnvironment(List executionsFromOneEnvironment) {
                Map> campaignExecutionsByEmpty = executionsFromOneEnvironment.stream()
                    .collect(groupingBy(cer -> cer.scenarioExecutionReports().isEmpty()));
                emptyCampaignExecutions = ofNullable(campaignExecutionsByEmpty.get(true)).orElse(emptyList());

                campaignExecutionsByPartialExecution = ofNullable(campaignExecutionsByEmpty.get(false)).orElse(emptyList()).stream()
                    .collect(groupingBy(cer -> cer.partialExecution));
                return ofNullable(campaignExecutionsByPartialExecution.get(false)).orElse(emptyList());
            }

            /**
             * Select all manual replays (for deletion) that are older than the oldest campaign execution kept and empty executions.
             */
            @Override
            public Collection findExtraExecutionsIdsToDelete(List timeSortedExecutionsForOneEnvironment) {
                return Stream.concat(
                    manualReplaysOlderThanOldestCampaignExecution(timeSortedExecutionsForOneEnvironment),
                    emptyCampaignExecutions.stream().map(ce -> ce.executionId)
                ).toList();
            }

            private Stream manualReplaysOlderThanOldestCampaignExecution(List timeSortedExecutionsForOneEnvironment) {
                LocalDateTime oldestCampaignExecutionToKeptStartDate;
                if (!timeSortedExecutionsForOneEnvironment.isEmpty() && maxCampaignExecutions > 0 && maxCampaignExecutions < timeSortedExecutionsForOneEnvironment.size()) {
                    oldestCampaignExecutionToKeptStartDate = timeSortedExecutionsForOneEnvironment.get(maxCampaignExecutions - 1).startDate;
                } else {
                    oldestCampaignExecutionToKeptStartDate = LocalDateTime.MAX;
                }

                return ofNullable(campaignExecutionsByPartialExecution.get(true)).orElse(emptyList()).stream()
                    .filter(cer -> cer.startDate.isBefore(oldestCampaignExecutionToKeptStartDate))
                    .map(cer -> cer.executionId);
            }
        };
    }

    @Override
    public PurgeReport purge() {
        Set purgedCampaignsExecutionsIds = campaignPurgeService.purgeExecutions();
        Set purgedScenariosExecutionsIds = scenarioPurgeService.purgeExecutions();
        LOGGER.info("Purge report : {} scenarios' executions deleted - {} campaigns' executions deleted", purgedScenariosExecutionsIds.size(), purgedCampaignsExecutionsIds.size());
        return new PurgeReport(purgedScenariosExecutionsIds, purgedCampaignsExecutionsIds);
    }

    /**
     * Core logic to purge executions.
     *
     * @see #purgeExecutions()
     */
    private static class PurgeExecutionService {
        /**
         * The configuration defining the number of executions to keep
         */
        private final int maxExecutionsToKeep;
        /**
         * A supplier of a domain object related to executions.
         *
         * @see TestCaseMetadata
         * @see Campaign
         */
        private final Supplier> baseObject;
        /**
         * A mapper function extracting the domain object id
         */
        private final Function idFunction;
        /**
         * A function returning all executions given an ObjectTypeId
         */
        private final Function> executionsFunction;
        /**
         * An optional executions filter used in {@link #purgeExecutions()} before grouping by environment
         */
        private final Predicate executionsFilter;
        /**
         * A mapper function extracting the execution id
         */
        private final Function executionIdFunction;
        /**
         * A mapper function extracting the execution date for sorting
         */
        private final Function executionDateFunction;
        /**
         * A mapper function extracting the execution status for keeping the last success one
         */
        private final Function statusFunction;
        /**
         * A mapper function extracting the execution environment
         */
        private final Function environmentFunction;
        /**
         * A function deleting executions by ids
         */
        private final Consumer> deleteFunction;

        private PurgeExecutionService(
            int maxExecutionsToKeep,
            Supplier> baseObjectSupplier,
            Function idFunction,
            Function> executionsFunction,
            Predicate executionsFilter,
            Function executionIdFunction,
            Function executionDateFunction,
            Function statusFunction,
            Function environmentFunction,
            Consumer> deleteFunction
        ) {
            this.maxExecutionsToKeep = maxExecutionsToKeep;
            this.baseObject = baseObjectSupplier;
            this.idFunction = idFunction;
            this.executionsFunction = executionsFunction;
            this.executionsFilter = executionsFilter;
            this.executionIdFunction = executionIdFunction;
            this.executionDateFunction = executionDateFunction;
            this.statusFunction = statusFunction;
            this.environmentFunction = environmentFunction;
            this.deleteFunction = deleteFunction;
        }

        /**
         * Purge executions.
         * 

Find all base objects ids and map them to executions.

*

Keep only executions before last 24 hours.

*

For each group of executions, filter then group them by environment.

*

For each group of executions by environment,

*

permits a specific handle via {@link #handleExecutionsForOneEnvironment(List)}

*

then call {@link #purgeOldestExecutionsFromOneEnvironment(List)}

* * @return The list of deleted executions ids. */ Set purgeExecutions() { Set deletedExecutionsIds = new HashSet<>(); baseObject.get().stream() .map(idFunction) .map(executionsFunction) .forEach(executionsReports -> { var executionsByEnvironment = executionsReports.stream() .filter(executionsFilter) .collect(groupingBy(t -> { var env = environmentFunction.apply(t); return env != null ? env : ""; })); for (List executionsOneEnvironment : executionsByEnvironment.values()) { purgeOneBaseObjectExecutionsForOneEnvironment(executionsOneEnvironment, deletedExecutionsIds); } }); return deletedExecutionsIds; } private void purgeOneBaseObjectExecutionsForOneEnvironment(List executionsOneEnvironment, Set deletedExecutionsIds) { try { List executionsToDelete = handleExecutionsForOneEnvironment(executionsOneEnvironment); Set deleteExecutionsIds = purgeOldestExecutionsFromOneEnvironment(executionsToDelete); deletedExecutionsIds.addAll(deleteExecutionsIds); } catch (Exception e) { LOGGER.error("Cannot purge executions {}", executionsOneEnvironment, e); } } /** * Purge the oldest executions according to {@link #maxExecutionsToKeep} configuration * by keeping the last success execution no matter what.
* Check for existence. * Sort by date. * Call {@link #findOldestExecutionsIdsWhileKeepingTheLastSuccess(List)}. * Permits to add other executions ids via {@link #findExtraExecutionsIdsToDelete(List)}. * Delete executions. * * @return The list of deleted executions ids. */ private Set purgeOldestExecutionsFromOneEnvironment(List executionsFromOneEnvironment) { List timeSortedExecutionsForOneEnvironment = executionsFromOneEnvironment.stream() .sorted(comparing(executionDateFunction).reversed()) .toList(); Set deletedExecutionsIdsTmp = new HashSet<>(); deletedExecutionsIdsTmp.addAll( findOldestExecutionsIdsWhileKeepingTheLastSuccess(timeSortedExecutionsForOneEnvironment) ); deletedExecutionsIdsTmp.addAll( findExtraExecutionsIdsToDelete(timeSortedExecutionsForOneEnvironment) ); if (!deletedExecutionsIdsTmp.isEmpty()) { deleteFunction.accept(deletedExecutionsIdsTmp); } return deletedExecutionsIdsTmp; } private Collection findOldestExecutionsIdsWhileKeepingTheLastSuccess(List timeSortedExecutions) { Long youngestSuccessExecutionIdToDelete = youngestSuccessExecutionIdToDelete(timeSortedExecutions); return timeSortedExecutions.stream() .skip(maxExecutionsToKeep) .map(executionIdFunction) .filter(id -> !youngestSuccessExecutionIdToDelete.equals(id)) .collect(toSet()); } // The list parameter must be sorted from younger to oldest private Long youngestSuccessExecutionIdToDelete(List timeSortedExecutions) { boolean isSuccessExecutionKept = timeSortedExecutions.stream() .limit(maxExecutionsToKeep) .map(statusFunction) .anyMatch(SUCCESS::equals); if (!isSuccessExecutionKept) { return timeSortedExecutions.stream() .skip(maxExecutionsToKeep) .filter(es -> SUCCESS.equals(statusFunction.apply(es))) .findFirst() .map(executionIdFunction) .orElse(-1L); } return -1L; } /** * To implement in order to control the list of executions of one {@link #baseObject} for one environment * to be passed down to {@link #purgeOldestExecutionsFromOneEnvironment(List)}.
* This list is not sorted. * * @return Default implementation returns the same list. */ protected List handleExecutionsForOneEnvironment(List executionsFromOneEnvironment) { return executionsFromOneEnvironment; } /** * To implement to find extra executions' ids to delete. * * @param timeSortedExecutionsForOneEnvironment The current list of executions processed by {@link #purgeOldestExecutionsFromOneEnvironment(List)}, sorted by {@link #executionDateFunction} * @return Default implementation returns a {@link Collections#emptySet()} */ protected Collection findExtraExecutionsIdsToDelete(List timeSortedExecutionsForOneEnvironment) { return emptySet(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy