org.commonjava.indy.promote.data.PromotionManager.java Source code

Java tutorial

Introduction

Here is the source code for org.commonjava.indy.promote.data.PromotionManager.java

Source

/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.commonjava.indy.promote.data;

import org.apache.commons.lang.StringUtils;
import org.commonjava.cdi.util.weft.DrainingExecutorCompletionService;
import org.commonjava.cdi.util.weft.ExecutorConfig;
import org.commonjava.cdi.util.weft.Locker;
import org.commonjava.cdi.util.weft.WeftExecutorService;
import org.commonjava.cdi.util.weft.WeftManaged;
import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.audit.ChangeSummary;
import org.commonjava.indy.content.ContentManager;
import org.commonjava.indy.content.DownloadManager;
import org.commonjava.indy.core.inject.GroupMembershipLocks;
import org.commonjava.indy.core.inject.StoreContentLocks;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.pkg.maven.model.MavenPackageTypeDescriptor;
import org.commonjava.indy.promote.callback.PromotionCallbackHelper;
import org.commonjava.indy.promote.change.event.PathsPromoteCompleteEvent;
import org.commonjava.indy.promote.change.event.PromoteCompleteEvent;
import org.commonjava.indy.promote.conf.PromoteConfig;
import org.commonjava.indy.promote.model.GroupPromoteRequest;
import org.commonjava.indy.promote.model.GroupPromoteResult;
import org.commonjava.indy.promote.model.PathsPromoteRequest;
import org.commonjava.indy.promote.model.PathsPromoteResult;
import org.commonjava.indy.promote.model.ValidationResult;
import org.commonjava.indy.promote.validate.PromotionValidationException;
import org.commonjava.indy.promote.validate.PromotionValidator;
import org.commonjava.indy.promote.validate.model.ValidationRequest;
import org.commonjava.indy.util.LocationUtils;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.model.ConcreteResource;
import org.commonjava.maven.galley.model.Transfer;
import org.commonjava.maven.galley.model.TransferOperation;
import org.commonjava.maven.galley.spi.nfc.NotFoundCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
import static org.commonjava.indy.change.EventUtils.fireEvent;
import static org.commonjava.indy.core.ctl.PoolUtils.detectOverload;
import static org.commonjava.indy.core.ctl.PoolUtils.detectOverloadVoid;
import static org.commonjava.indy.model.core.StoreType.hosted;

/**
 * Component responsible for orchestrating the transfer of artifacts from one store to another, according to the given {@link PathsPromoteRequest} or
 * {@link PathsPromoteResult}. Currently provides promotePaths, resumePathsPromote, and rollbackPathsPromote (the latter two for dealing with failed promotePaths calls).
 *
 * @author jdcasey
 */
@ApplicationScoped
public class PromotionManager {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Inject
    private PromoteConfig config;

    @Inject
    private ContentManager contentManager;

    @Inject
    private DownloadManager downloadManager;

    @Inject
    private StoreDataManager storeManager;

    @Inject
    private PromotionValidator validator;

    @Inject
    private Event<PromoteCompleteEvent> promoteCompleteEvent;

    @StoreContentLocks
    @Inject
    private Locker<StoreKey> byPathTargetLocks;

    @GroupMembershipLocks
    @Inject
    private Locker<StoreKey> byGroupTargetLocks;

    private Map<String, StoreKey> targetGroupKeyMap = new ConcurrentHashMap<>(1);

    @WeftManaged
    @Inject
    @ExecutorConfig(named = "promotion", threads = 8, priority = 8, loadSensitive = ExecutorConfig.BooleanLiteral.TRUE)
    private WeftExecutorService asyncPromotionService;

    @WeftManaged
    @Inject
    @ExecutorConfig(named = "promotion-transfers", threads = 40, priority = 6, loadSensitive = ExecutorConfig.BooleanLiteral.TRUE, maxLoadFactor = 100)
    private WeftExecutorService transferService;

    @Inject
    private PromotionCallbackHelper callbackHelper;

    @Inject
    private NotFoundCache nfc;

    protected PromotionManager() {
    }

    public PromotionManager(PromotionValidator validator, final ContentManager contentManager,
            final DownloadManager downloadManager, final StoreDataManager storeManager,
            Locker<StoreKey> byPathTargetLocks, Locker<StoreKey> byGroupTargetLocks, PromoteConfig config,
            NotFoundCache nfc, WeftExecutorService asyncPromotionService, WeftExecutorService transferService) {
        this.validator = validator;
        this.contentManager = contentManager;
        this.downloadManager = downloadManager;
        this.storeManager = storeManager;
        this.byPathTargetLocks = byPathTargetLocks;
        this.byGroupTargetLocks = byGroupTargetLocks;
        this.config = config;
        this.nfc = nfc;
        this.asyncPromotionService = asyncPromotionService;
        this.transferService = transferService;
    }

    @Measure
    public GroupPromoteResult promoteToGroup(GroupPromoteRequest request, String user, String baseUrl)
            throws PromotionException, IndyWorkflowException {
        if (!storeManager.hasArtifactStore(request.getSource())) {
            String error = String.format("Cannot promote from missing source: %s", request.getSource());
            logger.warn(error);

            return new GroupPromoteResult(request, error);
        }

        final StoreKey targetKey = getTargetKey(request.getTargetGroup());

        if (!storeManager.hasArtifactStore(targetKey)) {
            String error = String.format("No such target group: %s.", request.getTargetGroup());
            logger.warn(error);

            return new GroupPromoteResult(request, error);
        }

        Future<GroupPromoteResult> future = submitGroupPromoteRequest(request, user, baseUrl);
        if (request.isAsync()) {
            return new GroupPromoteResult(request).accepted();
        } else {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                logger.error("Group prromotion failed: " + request.getSource() + " -> " + request.getTargetKey(),
                        e);
                throw new PromotionException("Execution of group promotion failed.", e);
            }
        }
    }

    private ValidationResult doValidationAndPromote(GroupPromoteRequest request, AtomicReference<Exception> error,
            String user, String baseUrl) {
        ValidationResult validation = new ValidationResult();
        logger.info("Running validations for promotion of: {} to group: {}", request.getSource(),
                request.getTargetGroup());

        final StoreKey targetKey = getTargetKey(request.getTargetGroup());
        byGroupTargetLocks.lockAnd(targetKey, config.getLockTimeoutSeconds(), k -> {
            Group target;
            try {
                target = (Group) storeManager.getArtifactStore(request.getTargetKey());
            } catch (IndyDataException e) {
                error.set(new PromotionException("Cannot retrieve target group: %s. Reason: %s", e,
                        request.getTargetGroup(), e.getMessage()));
                return null;
            }

            try {
                ValidationRequest validationRequest = validator.validate(request, validation, baseUrl);

                if (validation.isValid()) {
                    if (!request.isDryRun() && !target.getConstituents().contains(request.getSource())) {
                        // give the preUpdate event a different object to compare vs. the original group.
                        target = target.copyOf();
                        target.addConstituent(request.getSource());
                        try {
                            final ChangeSummary changeSummary = new ChangeSummary(user, "Promoting "
                                    + request.getSource() + " into membership of group: " + target.getKey());

                            storeManager.storeArtifactStore(target, changeSummary, false, true,
                                    new EventMetadata());
                            clearStoreNFC(validationRequest.getSourcePaths(), target);

                            if (hosted == request.getSource().getType() && config.isAutoLockHostedRepos()) {
                                HostedRepository source = (HostedRepository) storeManager
                                        .getArtifactStore(request.getSource());

                                source.setReadonly(true);

                                final ChangeSummary readOnlySummary = new ChangeSummary(user, "Promoting "
                                        + request.getSource() + " into membership of group: " + target.getKey());

                                storeManager.storeArtifactStore(source, readOnlySummary, false, true,
                                        new EventMetadata());
                            }
                        } catch (IndyDataException e) {
                            error.set(new PromotionException(
                                    "Failed to store group: %s with additional member: %s. Reason: %s", e,
                                    target.getKey(), request.getSource(), e.getMessage()));
                        }
                    }
                }
            } catch (PromotionValidationException | IndyWorkflowException e) {
                error.set(e);
            }

            return null;
        }, (k, lock) -> {
            //FIXME: should we consider to repeat the promote process several times when lock failed?
            String errorMsg = String.format(
                    "Failed to acquire group promotion lock on target when promote: %s in %d seconds.", targetKey,
                    config.getLockTimeoutSeconds());
            logger.error(errorMsg);
            error.set(new PromotionException(errorMsg));

            return Boolean.FALSE;
        });

        return validation;
    }

    /**
     * Provides target group store key for a given group name. This is meant to provide the same instance of the key
     * for a name to be able to synchronize promotion requests based on this instance.
     *
     * @param targetName the target group name
     * @return the group store key
     */
    private StoreKey getTargetKey(final String targetName) {
        return targetGroupKeyMap.computeIfAbsent(targetName,
                k -> new StoreKey(MavenPackageTypeDescriptor.MAVEN_PKG_KEY, StoreType.group, targetName));
    }

    public GroupPromoteResult rollbackGroupPromote(GroupPromoteResult result, String user)
            throws PromotionException, IndyWorkflowException {
        GroupPromoteRequest request = result.getRequest();

        if (!storeManager.hasArtifactStore(request.getSource())) {
            String error = String.format("No such source/member store: %s", request.getSource());
            logger.warn(error);

            return new GroupPromoteResult(request, error);
        }

        Group target;
        try {
            target = (Group) storeManager.getArtifactStore(request.getTargetKey());
        } catch (IndyDataException e) {
            throw new PromotionException("Cannot retrieve target group: %s. Reason: %s", e,
                    request.getTargetGroup(), e.getMessage());
        }

        if (target == null) {
            String error = String.format("No such target group: %s.", request.getTargetGroup());
            logger.warn(error);

            return new GroupPromoteResult(request, error);
        }

        Future<GroupPromoteResult> future = submitGroupPromoteRollback(result, target, user);
        if (request.isAsync()) {
            return new GroupPromoteResult(request).accepted();
        } else {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                logger.error(
                        "Group promotion rollback failed: " + request.getSource() + " -> " + request.getTargetKey(),
                        e);
                throw new PromotionException("Execution of group promotion rollback failed.", e);
            }
        }
    }

    private GroupPromoteResult doGroupPromoteRollback(GroupPromoteResult result, Group target, String user)
            throws PromotionException {
        GroupPromoteResult ret;

        GroupPromoteRequest request = result.getRequest();

        if (target.getConstituents().contains(request.getSource())) {
            // give the preUpdate event a different object to compare vs. the original group.
            target = target.copyOf();

            target.removeConstituent(request.getSource());
            try {
                final ChangeSummary changeSummary = new ChangeSummary(user,
                        "Removing " + request.getSource() + " from membership of group: " + target.getKey());

                storeManager.storeArtifactStore(target, changeSummary, false, true, new EventMetadata());
            } catch (IndyDataException e) {
                throw new PromotionException("Failed to store group: %s with additional member: %s. Reason: %s", e,
                        target.getKey(), request.getSource(), e.getMessage());
            }
            ret = new GroupPromoteResult(request);
        } else {
            ret = new GroupPromoteResult(request,
                    "Group: " + target.getKey() + " does not contain member: " + request.getSource());
        }

        return ret.withPromotionId(result.getPromotionId());
    }

    private Future<GroupPromoteResult> submitGroupPromoteRollback(final GroupPromoteResult result,
            final Group target, final String user) throws IndyWorkflowException {
        return detectOverload(() -> asyncPromotionService.submit(() -> {
            GroupPromoteResult ret;
            try {
                ret = doGroupPromoteRollback(result, target, user);
            } catch (Exception ex) {
                GroupPromoteRequest request = result.getRequest();
                String msg = "Group promotion rollback failed. Target: " + target.getKey() + ", Source: "
                        + request.getSource() + ", Reason: " + getStackTrace(ex);
                logger.warn(msg);
                ret = new GroupPromoteResult(request, msg).withPromotionId(result.getPromotionId());
            }

            if (ret.getRequest().getCallback() != null) {
                return callbackHelper.callback(ret.getRequest().getCallback(), ret);
            }

            return ret;
        }));
    }

    private Future<GroupPromoteResult> submitGroupPromoteRequest(final GroupPromoteRequest request,
            final String user, final String baseUrl) throws IndyWorkflowException {
        return detectOverload(() -> asyncPromotionService.submit(() -> {
            AtomicReference<Exception> error = new AtomicReference<>();
            ValidationResult validation = doValidationAndPromote(request, error, user, baseUrl);

            GroupPromoteResult ret;
            Exception ex = error.get();
            if (ex != null) {
                String msg = "Group promotion failed. Target: " + request.getTargetGroup() + ", Source: "
                        + request.getSource() + ", Reason: " + getStackTrace(ex);
                logger.warn(msg);
                ret = new GroupPromoteResult(request, msg);
            } else {
                ret = new GroupPromoteResult(request, validation);
            }

            if (request.getCallback() != null) {
                return callbackHelper.callback(ret.getRequest().getCallback(), ret);
            }

            return ret;
        }));
    }

    /**
     * Promote artifacts from the source to the target given in the {@link PathsPromoteRequest}. If a set of paths are given, only try to promotePaths those.
     * Otherwise, build a recursive list of available artifacts in the source store, and try to promotePaths them all.
     *
     * @param request The request containing source and target store keys, and an optional list of paths to promotePaths
     *
     * @return The result, including the source and target store keys used, the paths completed (promoted successfully), the pending paths (those that
     * weren't processed due to some error...or null), and a nullable error explaining what (if anything) went wrong with the promotion.
     *
     * @throws PromotionException
     * @throws IndyWorkflowException
     */
    @Measure
    public PathsPromoteResult promotePaths(final PathsPromoteRequest request, final String baseUrl)
            throws PromotionException, IndyWorkflowException {
        Future<PathsPromoteResult> future = submitPathsPromoteRequest(request, baseUrl);
        if (request.isAsync()) {
            return new PathsPromoteResult(request).accepted();
        } else {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                logger.error("Path prromotion failed: " + request.getSource() + " -> " + request.getTargetKey(), e);
                throw new PromotionException("Execution of path promotion failed.", e);
            }
        }
    }

    private PathsPromoteResult doPathsPromotion(PathsPromoteRequest request, String baseUrl)
            throws IndyWorkflowException, PromotionValidationException {
        final Set<String> paths = request.getPaths();
        final StoreKey source = request.getSource();

        List<Transfer> contents;
        if (paths == null || paths.isEmpty()) {
            contents = downloadManager.listRecursively(source, DownloadManager.ROOT_PATH);
        } else {
            contents = getTransfersForPaths(source, paths);
        }

        final Set<String> pending = new HashSet<>();

        for (final Transfer transfer : contents) {
            pending.add(transfer.getPath());
        }

        ValidationResult validation = new ValidationResult();
        ValidationRequest validationRequest = validator.validate(request, validation, baseUrl);
        if (request.isDryRun()) {
            return new PathsPromoteResult(request, pending, Collections.emptySet(), Collections.emptySet(),
                    validation);
        } else if (validation.isValid()) {
            return runPathPromotions(request, pending, Collections.emptySet(), Collections.emptySet(), contents,
                    validation, validationRequest);
        } else {
            return new PathsPromoteResult(request, pending, Collections.emptySet(), Collections.emptySet(),
                    validation);
        }
    }

    private Future<PathsPromoteResult> submitPathsPromoteRequest(PathsPromoteRequest request, final String baseUrl)
            throws IndyWorkflowException {
        return detectOverload(() -> asyncPromotionService.submit(() -> {
            PathsPromoteResult ret;
            try {
                ret = doPathsPromotion(request, baseUrl);
            } catch (Exception ex) {
                String msg = "Path promotion failed. Target: " + request.getTarget() + ", Source: "
                        + request.getSource() + ", Reason: " + getStackTrace(ex);
                logger.warn(msg);
                ret = new PathsPromoteResult(request, msg);
            }

            if (ret.getRequest().getCallback() != null) {
                return callbackHelper.callback(ret.getRequest().getCallback(), ret);
            }

            return ret;
        }));
    }

    /**
     * Attempt to resumePathsPromote from a previously failing {@link PathsPromoteResult}. This is meant to handle cases where a transient (or correctable) error
     * occurs on the server side, and promotion can proceed afterward. It works much like the {@link #promotePaths(PathsPromoteRequest, String)} call, using the pending
     * paths list from the input result as the list of paths to process. The output {@link PathsPromoteResult} contains all previous completed paths PLUS
     * any additional completed transfers when it is returned, thus providing a cumulative result to the user.
     *
     * @param result The result to resumePathsPromote
     *
     * @return The same result, with any successful path promotions moved from the pending to completed paths list, and the error cleared (or set to a
     * new error)
     *
     * @throws PromotionException
     * @throws IndyWorkflowException
     */
    public PathsPromoteResult resumePathsPromote(final PathsPromoteResult result, final String baseUrl)
            throws PromotionException, IndyWorkflowException {
        final PathsPromoteRequest request = result.getRequest();

        Future<PathsPromoteResult> future = submitResumePathsPromote(result, baseUrl);
        if (request.isAsync()) {
            return new PathsPromoteResult(request).accepted();
        } else {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                logger.error(
                        "Path promotion resume failed: " + request.getSource() + " -> " + request.getTargetKey(),
                        e);
                throw new PromotionException("Execution of path promotion resume failed.", e);
            }
        }
    }

    private Future<PathsPromoteResult> submitResumePathsPromote(PathsPromoteResult result, String baseUrl)
            throws IndyWorkflowException {
        return detectOverload(() -> asyncPromotionService.submit(() -> {
            PathsPromoteResult ret;
            try {
                ret = doResumePathsPromote(result, baseUrl);
            } catch (Exception ex) {
                final PathsPromoteRequest request = result.getRequest();
                String msg = "Path promotion failed. Target: " + request.getTarget() + ", Source: "
                        + request.getSource() + ", Reason: " + getStackTrace(ex);
                logger.warn(msg);
                ret = new PathsPromoteResult(request, msg).withPromotionId(result.getPromotionId());
            }

            if (ret.getRequest().getCallback() != null) {
                return callbackHelper.callback(ret.getRequest().getCallback(), ret);
            }

            return ret;
        }));
    }

    private PathsPromoteResult doResumePathsPromote(PathsPromoteResult result, String baseUrl)
            throws IndyWorkflowException, PromotionValidationException {
        final PathsPromoteRequest request = result.getRequest();

        final List<Transfer> contents = getTransfersForPaths(request.getSource(), result.getPendingPaths());

        ValidationResult validation = new ValidationResult();
        ValidationRequest validationRequest = validator.validate(request, validation, baseUrl);
        PathsPromoteResult ret = runPathPromotions(request, result.getPendingPaths(), result.getCompletedPaths(),
                result.getSkippedPaths(), contents, result.getValidations(), validationRequest);

        return ret.withPromotionId(result.getPromotionId());
    }

    /**
     * Attempt to rollbackPathsPromote a previously failing {@link PathsPromoteResult}. This is meant to handle cases where an unrecoverable error
     * occurs on the server side, and promotion can NOT proceed afterward. All paths in the completed paths set are deleted from the target, if
     * possible. The output {@link PathsPromoteResult} contains the previous content, with any successfully removed target paths moved back from the
     * completed-paths list to the pending-paths list. If an error occurs during rollbackPathsPromote, the error field will be set...otherwise, it will be null.
     *
     * @param result The result to rollbackPathsPromote
     *
     * @return The same result, with any successful deletions moved from the completed to pending paths list, and the error cleared (or set to a
     * new error)
     *
     * @throws PromotionException
     * @throws IndyWorkflowException
     */
    public PathsPromoteResult rollbackPathsPromote(final PathsPromoteResult result)
            throws PromotionException, IndyWorkflowException {
        final PathsPromoteRequest request = result.getRequest();

        Future<PathsPromoteResult> future = submitRollbackPathsPromote(result);
        if (request.isAsync()) {
            return new PathsPromoteResult(request).accepted();
        } else {
            try {
                return future.get();
            } catch (InterruptedException | ExecutionException e) {
                logger.error(
                        "Path promotion rollback failed: " + request.getSource() + " -> " + request.getTargetKey(),
                        e);
                throw new PromotionException("Execution of path promotion rollback failed.", e);
            }
        }
    }

    private Future<PathsPromoteResult> submitRollbackPathsPromote(PathsPromoteResult result)
            throws IndyWorkflowException {
        return detectOverload(() -> asyncPromotionService.submit(() -> {
            PathsPromoteResult ret;
            try {
                ret = doRollbackPathsPromote(result);
            } catch (Exception ex) {
                final PathsPromoteRequest request = result.getRequest();
                String msg = "Rollback path promotion failed. Target: " + request.getTarget() + ", Source: "
                        + request.getSource() + ", Reason: " + getStackTrace(ex);
                logger.warn(msg);
                ret = new PathsPromoteResult(request, msg).withPromotionId(result.getPromotionId());
            }

            if (ret.getRequest().getCallback() != null) {
                return callbackHelper.callback(ret.getRequest().getCallback(), ret);
            }

            return ret;
        }));
    }

    private PathsPromoteResult doRollbackPathsPromote(PathsPromoteResult result) throws IndyWorkflowException {
        StoreKey targetKey = result.getRequest().getTarget();

        final List<Transfer> contents = getTransfersForPaths(targetKey, result.getCompletedPaths());
        final Set<String> completed = result.getCompletedPaths();
        final Set<String> skipped = result.getSkippedPaths();

        if (completed == null || completed.isEmpty()) {
            result.setError(null);
            return result;
        }

        Set<String> pending = result.getPendingPaths() == null ? new HashSet<>()
                : new HashSet<>(result.getPendingPaths());

        final AtomicReference<String> error = new AtomicReference<>();
        final boolean copyToSource = result.getRequest().isPurgeSource();

        ArtifactStore source = null;
        try {
            source = storeManager.getArtifactStore(result.getRequest().getSource());
        } catch (final IndyDataException e) {
            String msg = String.format("Failed to retrieve artifact store: %s. Reason: %s",
                    result.getRequest().getSource(), e.getMessage());

            logger.error(msg, e);
            error.set(msg);
        }

        AtomicReference<IndyWorkflowException> wfEx = new AtomicReference<>();
        if (error.get() == null) {
            ArtifactStore src = source;
            byPathTargetLocks.lockAnd(targetKey, config.getLockTimeoutSeconds(), k -> {
                for (final Transfer transfer : contents) {
                    if (transfer != null && transfer.exists()) {
                        try {
                            if (copyToSource) {
                                final String path = transfer.getPath();
                                try (InputStream stream = transfer.openInputStream(true)) {
                                    contentManager.store(src, path, stream, TransferOperation.UPLOAD,
                                            new EventMetadata());
                                } catch (IndyWorkflowException e) {
                                    wfEx.set(e);
                                    return null;
                                }
                            }

                            transfer.delete(true);
                            completed.remove(transfer.getPath());
                            pending.add(transfer.getPath());
                        } catch (final IOException e) {
                            String msg = String.format(
                                    "Failed to rollback path promotion of: %s from: %s. Reason: %s", transfer,
                                    result.getRequest().getSource(), e.getMessage());
                            logger.error(msg, e);
                            error.set(msg);
                        }
                    }
                }

                return null;
            }, (k, lock) -> {
                String msg = String.format("Failed to acquire promotion lock on target: %s in %d seconds.",
                        targetKey, config.getLockTimeoutSeconds());
                logger.warn(msg);
                error.set(msg);

                return false;
            });

            IndyWorkflowException wfException = wfEx.get();
            if (wfException != null) {
                throw wfException;
            }

        }

        PathsPromoteResult ret = new PathsPromoteResult(result.getRequest(), pending, completed, skipped,
                error.get(), new ValidationResult());
        return ret.withPromotionId(result.getPromotionId());
    }

    private PathsPromoteResult runPathPromotions(final PathsPromoteRequest request, final Set<String> pending,
            final Set<String> prevComplete, final Set<String> prevSkipped, final List<Transfer> contents,
            ValidationResult validation, final ValidationRequest validationRequest) throws IndyWorkflowException {
        if (pending == null || pending.isEmpty()) {
            return new PathsPromoteResult(request, pending, prevComplete, prevSkipped, validation);
        }

        StoreKey targetKey = request.getTarget();

        final Set<String> complete = prevComplete == null ? new HashSet<>() : new HashSet<>(prevComplete);
        final Set<String> skipped = prevSkipped == null ? new HashSet<>() : new HashSet<>(prevSkipped);

        List<String> errors = new ArrayList<>();

        ArtifactStore sourceStore = null;
        try {
            sourceStore = storeManager.getArtifactStore(request.getSource());
        } catch (IndyDataException e) {
            String msg = String.format("Failed to retrieve artifact store: %s. Reason: %s", request.getSource(),
                    e.getMessage());
            errors.add(msg);
            logger.error(msg, e);
        }

        ArtifactStore targetStore = null;
        try {
            targetStore = storeManager.getArtifactStore(request.getTarget());
        } catch (IndyDataException e) {
            String msg = String.format("Failed to retrieve artifact store: %s. Reason: %s", request.getTarget(),
                    e.getMessage());
            errors.add(msg);
            logger.error(msg, e);
        }

        if (targetStore == null) {
            String msg = String.format("Failed to retrieve artifact store: %s", request.getTarget());
            errors.add(msg);
            logger.error(msg);
        }

        if (errors.isEmpty()) {
            ArtifactStore src = sourceStore;
            ArtifactStore tgt = targetStore;

            AtomicReference<IndyWorkflowException> wfError = new AtomicReference<>();

            Set<PathTransferResult> results = byPathTargetLocks.lockAnd(targetKey, config.getLockTimeoutSeconds(),
                    k -> {
                        logger.info("Running promotions from: {} (key: {})\n  to: {} (key: {})", src,
                                request.getSource(), tgt, request.getTarget());

                        DrainingExecutorCompletionService<PathTransferResult> svc = new DrainingExecutorCompletionService<>(
                                transferService);

                        try {
                            detectOverloadVoid(() -> contents.forEach((transfer) -> svc
                                    .submit(newPathsPromoteTransfer(transfer, src, tgt, request))));

                            Set<PathTransferResult> pathResults = new HashSet<>();
                            try {
                                svc.drain(ptr -> pathResults.add(ptr));
                            } catch (InterruptedException | ExecutionException e) {
                                Set<String> paths;
                                try {
                                    paths = validationRequest.getSourcePaths();
                                } catch (PromotionValidationException e1) {
                                    paths = contents.stream().map(txfr -> txfr.getPath())
                                            .collect(Collectors.toSet());
                                }

                                logger.error(
                                        String.format("Error waiting for promotion of: %s to: %s\nPaths:\n\n%s\n\n",
                                                request.getSource(), targetKey, paths),
                                        e);
                            }

                            try {
                                clearStoreNFC(validationRequest.getSourcePaths(), tgt);
                            } catch (IndyDataException | PromotionValidationException e) {
                                String msg = String.format("Failed to promote to: %s. Reason: %s", tgt,
                                        e.getMessage());
                                errors.add(msg);
                                logger.error(msg, e);
                            }
                            return pathResults;
                        } catch (IndyWorkflowException e) {
                            wfError.set(e);
                            return null;
                        }
                    }, (k, lock) -> {
                        String error = String.format(
                                "Failed to acquire promotion lock on target: %s in %d seconds.", targetKey,
                                config.getLockTimeoutSeconds());

                        errors.add(error);
                        logger.warn(error);

                        return false;
                    });

            if (wfError.get() != null) {
                throw wfError.get();
            }

            if (results != null) {
                results.forEach(pathResult -> {
                    if (pathResult.traversed) {
                        pending.remove(pathResult.path);
                    }

                    if (pathResult.skipped) {
                        skipped.add(pathResult.path);
                    }

                    if (pathResult.completed) {
                        complete.add(pathResult.path);
                    }

                    if (pathResult.error != null) {
                        errors.add(pathResult.error);
                    }
                });
            }
        }

        String error = null;

        if (!errors.isEmpty()) {
            error = StringUtils.join(errors, "\n");
        }

        PathsPromoteResult result = new PathsPromoteResult(request, pending, complete, skipped, error, validation);

        if (request.isFireEvents()) {
            PathsPromoteCompleteEvent evt = new PathsPromoteCompleteEvent(result);
            fireEvent(promoteCompleteEvent, evt);
        }
        return result;
    }

    private static final class PathTransferResult {
        public String error;

        public boolean traversed = false;

        public boolean skipped = false;

        public boolean completed = false;

        public final String path;

        public PathTransferResult(final String path) {
            this.path = path;
        }
    }

    private Callable<PathTransferResult> newPathsPromoteTransfer(final Transfer transfer, final ArtifactStore src,
            final ArtifactStore tgt, final PathsPromoteRequest request) {
        return () -> {
            PathTransferResult result = new PathTransferResult(transfer.getPath());
            final String path = transfer.getPath();

            if (!transfer.exists()) {
                result.traversed = true;
                result.skipped = true;
            } else {
                try {
                    Transfer target = contentManager.getTransfer(tgt, path, TransferOperation.UPLOAD);
                    //                        synchronized ( target )
                    //                        {
                    // TODO: Should the request object have an overwrite attribute? Is that something the user is qualified to decide?
                    if (target != null && target.exists()) {
                        logger.warn("NOT promoting: {} from: {} to: {}. Target file already exists.", path,
                                request.getSource(), request.getTarget());

                        // TODO: There's no guarantee that the pre-existing content is the same!
                        result.traversed = true;
                        result.skipped = true;
                    } else {
                        try (InputStream stream = transfer.openInputStream(true)) {
                            contentManager.store(tgt, path, stream, TransferOperation.UPLOAD, new EventMetadata());

                            result.traversed = true;
                            result.completed = true;

                            stream.close();

                            if (request.isPurgeSource()) {
                                contentManager.delete(src, path, new EventMetadata());
                            }
                        } catch (final IOException e) {
                            String msg = String.format("Failed to open input stream for: %s. Reason: %s", transfer,
                                    e.getMessage());
                            result.error = msg;
                            logger.error(msg, e);
                        }
                    }

                } catch (final IndyWorkflowException e) {
                    String msg = String.format("Failed to promote path: %s to: %s. Reason: %s", transfer, tgt,
                            e.getMessage());
                    result.error = msg;
                    logger.error(msg, e);
                }
            }

            return result;
        };
    }

    private List<Transfer> getTransfersForPaths(final StoreKey source, final Set<String> paths)
            throws IndyWorkflowException {
        final List<Transfer> contents = new ArrayList<>();
        for (final String path : paths) {
            final Transfer txfr = downloadManager.getStorageReference(source, path);
            if (txfr == null || !txfr.exists()) {
                logger.warn("Cannot promote path: '{}' from source: '{}'. It does not exist!", path, source);
                // TODO: Fail??
                continue;
            }

            contents.add(txfr);
        }

        return contents;
    }

    /**
     * NOTE: Adding sourcePaths parameter here to cut down on number of paths for clearing from NFC.
     *
     * @param sourcePaths The set of paths that need to be cleared from the NFC.
     * @param store The store whose affected groups should have their NFC entries cleared
     * @throws IndyDataException
     */
    private void clearStoreNFC(final Set<String> sourcePaths, ArtifactStore store) throws IndyDataException {
        Set<String> paths = sourcePaths.stream()
                .map(sp -> sp.startsWith("/") && sp.length() > 1 ? sp.substring(1) : sp)
                .collect(Collectors.toSet());

        paths.forEach(path -> {
            ConcreteResource resource = new ConcreteResource(LocationUtils.toLocation(store), path);

            logger.debug("Clearing NFC path: {} from: {}\n\tResource: {}", path, store.getKey(), resource);
            nfc.clearMissing(resource);
        });

        Set<Group> groups = storeManager.query().getGroupsAffectedBy(store.getKey());
        if (groups != null) {
            groups.forEach(group -> paths.forEach(path -> {
                ConcreteResource resource = new ConcreteResource(LocationUtils.toLocation(group), path);

                logger.debug("Clearing NFC path: {} from: {}\n\tResource: {}", path, group.getKey(), resource);
                nfc.clearMissing(resource);
            }));
        }
    }

}