/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import com.netflix.fenzo.functions.Action1;
import com.netflix.fenzo.functions.Func1;
import com.netflix.fenzo.queues.QueuableTask;
import org.apache.mesos.Protos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class AutoScaler {

    private static final Logger logger = LoggerFactory.getLogger(AutoScaler.class);
    final VMCollection vmCollection;
    private final String mapHostnameAttributeName;
    private final String scaleDownBalancedByAttributeName;
    private final ActiveVmGroups activeVmGroups;
    private final AutoScaleRules autoScaleRules;
    private final boolean disableShortfallEvaluation;
    private final String attributeName;
    private final AssignableVMs assignableVMs;
    private final ExecutorService executor =
            new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10),
                    new ThreadPoolExecutor.DiscardPolicy());
    private final AtomicBoolean isShutdown = new AtomicBoolean();
    private final ConcurrentMap<String, ScalingActivity> scalingActivityMap = new ConcurrentHashMap<>();
    private final ScaleDownConstraintExecutor scaleDownConstraintExecutor;
    private volatile Action1<AutoScaleAction> callback = null;
    private ShortfallEvaluator shortfallEvaluator;
    private long delayScaleUpBySecs = 0L;
    private long delayScaleDownBySecs = 0L;
    private long disabledVmDurationInSecs = 0L;
    private volatile Func1<QueuableTask, List<String>> taskToClustersGetter = null;

    AutoScaler(final String attributeName, String mapHostnameAttributeName, String scaleDownBalancedByAttributeName,
               final List<AutoScaleRule> autoScaleRules, final AssignableVMs assignableVMs,
               final boolean disableShortfallEvaluation, ActiveVmGroups activeVmGroups, VMCollection vmCollection,
               ScaleDownConstraintExecutor scaleDownConstraintExecutor) {
        this.mapHostnameAttributeName = mapHostnameAttributeName;
        this.scaleDownBalancedByAttributeName = scaleDownBalancedByAttributeName;
        this.shortfallEvaluator = new NaiveShortfallEvaluator();
        this.attributeName = attributeName;
        this.autoScaleRules = new AutoScaleRules(autoScaleRules);
        this.assignableVMs = assignableVMs;
        this.disableShortfallEvaluation = disableShortfallEvaluation;
        this.activeVmGroups = activeVmGroups;
        this.vmCollection = vmCollection;
        this.scaleDownConstraintExecutor = scaleDownConstraintExecutor;
    }

    /* package */ void useOptimizingShortfallAnalyzer() {
        shortfallEvaluator = new OptimizingShortfallEvaluator();
    }

    /* package */ void setSchedulingService(TaskSchedulingService service) {
        shortfallEvaluator.setTaskSchedulingService(service);
    }

    Collection<AutoScaleRule> getRules() {
        return Collections.unmodifiableCollection(autoScaleRules.getRules());
    }

    void replaceRule(AutoScaleRule rule) {
        if (rule == null)
            throw new NullPointerException("Can't add null rule");
        autoScaleRules.replaceRule(rule);
    }

    AutoScaleRule getRule(String name) {
        return autoScaleRules.get(name);
    }

    void removeRule(String ruleName) {
        if (ruleName != null)
            autoScaleRules.remRule(ruleName);
    }

    void setDelayScaleUpBySecs(long secs) {
        delayScaleUpBySecs = secs;
    }

    void setDelayScaleDownBySecs(long secs) {
        delayScaleDownBySecs = secs;
    }

    void setDisabledVmDurationInSecs(long disabledVmDurationInSecs) {
        this.disabledVmDurationInSecs = disabledVmDurationInSecs;
    }

    void setTaskToClustersGetter(Func1<QueuableTask, List<String>> getter) {
        this.taskToClustersGetter = getter;
    }

    void doAutoscale(final AutoScalerInput autoScalerInput) {
        if (isShutdown.get()) {
            return;
        }

        try {
            shortfallEvaluator.setTaskToClustersGetter(taskToClustersGetter);
            autoScaleRules.prepare();
            Map<String, HostAttributeGroup> hostAttributeGroupMap = setupHostAttributeGroupMap(autoScaleRules, scalingActivityMap);
            if (!disableShortfallEvaluation) {
                Map<String, Integer> shortfall = shortfallEvaluator.getShortfall(hostAttributeGroupMap.keySet(), autoScalerInput.getFailures(), autoScaleRules);
                for (Map.Entry<String, Integer> entry : shortfall.entrySet()) {
                    hostAttributeGroupMap.get(entry.getKey()).shortFall = entry.getValue() == null ? 0 : entry.getValue();
                }
            }
            populateIdleResources(autoScalerInput.getIdleResourcesList(), autoScalerInput.getIdleInactiveResourceList(), hostAttributeGroupMap);
            List<Runnable> callbacks = new ArrayList<>();
            for (HostAttributeGroup hostAttributeGroup : hostAttributeGroupMap.values()) {
                callbacks.addAll(processScalingNeeds(hostAttributeGroup, scalingActivityMap, assignableVMs));
            }

            executor.submit(() -> {
                if (isShutdown.get()) {
                    return;
                }
                //Since the agents are disabled synchronously for each attribute group, the agents will become enabled again
                //if the sum duration of all callback calls takes longer than the cooldown period
                for (Runnable callback : callbacks) {
                    callback.run();
                }
            });
        } catch (Exception e) {
            logger.error("Autoscaler failure: ", e);
        }
    }

    private boolean shouldScaleNow(boolean scaleUp, long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
        return scaleUp ?
                now > (Math.max(activeVmGroups.getLastSetAt(), prevScalingActivity.scaleUpAt) + rule.getCoolDownSecs() * 1000) :
                now > (Math.max(activeVmGroups.getLastSetAt(), Math.max(prevScalingActivity.scaleDownAt, prevScalingActivity.scaleUpAt))
                        + rule.getCoolDownSecs() * 1000);
    }

    private boolean shouldScaleUp(long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
        return shouldScaleNow(true, now, prevScalingActivity, rule);
    }

    private boolean shouldScaleDown(long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
        return shouldScaleNow(false, now, prevScalingActivity, rule);
    }

    private boolean shouldScaleDownInactive(long now, ScalingActivity prevScalingActivity, AutoScaleRule rule) {
        return now > (Math.max(activeVmGroups.getLastSetAt(), prevScalingActivity.inactiveScaleDownAt) + rule.getCoolDownSecs() * 1000);
    }

    private List<Runnable> processScalingNeeds(HostAttributeGroup hostAttributeGroup, ConcurrentMap<String, ScalingActivity> scalingActivityMap, AssignableVMs assignableVMs) {
        List<Runnable> callbacks = new ArrayList<>();
        AutoScaleRule rule = hostAttributeGroup.rule;
        long now = System.currentTimeMillis();
        ScalingActivity prevScalingActivity = scalingActivityMap.get(rule.getRuleName());

        int excess = hostAttributeGroup.shortFall > 0 ? 0 : hostAttributeGroup.idleHosts.size() - rule.getMaxIdleHostsToKeep();
        int inactiveIdleCount = hostAttributeGroup.idleInactiveHosts.size();

        List<String> allHostsToTerminate = new ArrayList<>();
        if (inactiveIdleCount > 0 && shouldScaleDownInactive(now, prevScalingActivity, rule)) {
            ScalingActivity scalingActivity = scalingActivityMap.get(rule.getRuleName());
            long lastReqstAge = (now - scalingActivity.inactiveScaleDownRequestedAt) / 1000L;
            if (delayScaleDownBySecs > 0L && lastReqstAge > 2 * delayScaleDownBySecs) { // reset the request at time
                scalingActivity.inactiveScaleDownRequestedAt = now;
            } else if (delayScaleDownBySecs == 0L || lastReqstAge > delayScaleDownBySecs) {
                scalingActivity.inactiveScaleDownRequestedAt = 0L;
                scalingActivity.inactiveScaleDownAt = now;
                Map<String, String> hostsToTerminate = getInactiveHostsToTerminate(hostAttributeGroup.idleInactiveHosts);
                if (logger.isDebugEnabled()) {
                    logger.debug("{} has an excess of {} inactive hosts ({})", rule.getRuleName(), hostsToTerminate.size(),
                            String.join(", ", hostsToTerminate.keySet()));
                }
                allHostsToTerminate.addAll(hostsToTerminate.values());
            }
        }

        if (excess > 0 && shouldScaleDown(now, prevScalingActivity, rule)) {
            ScalingActivity scalingActivity = scalingActivityMap.get(rule.getRuleName());
            long lastReqstAge = (now - scalingActivity.scaleDownRequestedAt) / 1000L;
            if (delayScaleDownBySecs > 0L && lastReqstAge > 2 * delayScaleDownBySecs) { // reset the request at time
                scalingActivity.scaleDownRequestedAt = now;
            } else if (delayScaleDownBySecs == 0L || lastReqstAge > delayScaleDownBySecs) {
                final int size = vmCollection.size(rule.getRuleName());
                if (rule.getMinSize() > (size - excess))
                    excess = Math.max(0, size - rule.getMinSize());
                if (excess > 0) {
                    scalingActivity.scaleDownRequestedAt = 0L;
                    scalingActivity.scaleDownAt = now;
                    scalingActivity.shortfall = hostAttributeGroup.shortFall;
                    Map<String, String> hostsToTerminate = getHostsToTerminate(hostAttributeGroup.idleHosts, excess);
                    scalingActivity.scaledNumInstances = hostsToTerminate.size();
                    scalingActivity.type = AutoScaleAction.Type.Down;
                    for (String host : hostsToTerminate.keySet()) {
                        long disabledDurationInSecs = Math.max(disabledVmDurationInSecs, rule.getCoolDownSecs());
                        assignableVMs.disableUntil(host, now + disabledDurationInSecs * 1000);
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("{} has an excess of {} hosts ({})", rule.getRuleName(), hostsToTerminate.size(),
                                String.join(", ", hostsToTerminate.keySet()));
                    }
                    allHostsToTerminate.addAll(hostsToTerminate.values());
                }
            }
        } else if (hostAttributeGroup.shortFall > 0 || (excess <= 0 && shouldScaleUp(now, prevScalingActivity, rule))) {
            if (hostAttributeGroup.shortFall > 0 || rule.getMinIdleHostsToKeep() > hostAttributeGroup.idleHosts.size()) {
                // scale up to rule.getMaxIdleHostsToKeep() instead of just until rule.getMinIdleHostsToKeep()
                // but, if not shouldScaleUp(), then, scale up due to shortfall
                ScalingActivity scalingActivity = scalingActivityMap.get(rule.getRuleName());
                long lastReqstAge = (now - scalingActivity.scaleUpRequestedAt) / 1000L;
                if (delayScaleUpBySecs > 0L && lastReqstAge > 2 * delayScaleUpBySecs) { // reset the request at time
                    scalingActivity.scaleUpRequestedAt = now;
                } else if (delayScaleUpBySecs == 0L || lastReqstAge > delayScaleUpBySecs) {
                    int shortage = (excess <= 0 && shouldScaleUp(now, prevScalingActivity, rule)) ?
                            rule.getMaxIdleHostsToKeep() - hostAttributeGroup.idleHosts.size() : 0;
                    shortage = Math.max(shortage, hostAttributeGroup.shortFall);
                    final int size = vmCollection.size(rule.getRuleName());
                    if (shortage + size > rule.getMaxSize())
                        shortage = Math.max(0, rule.getMaxSize() - size);
                    if (shortage > 0) {
                        scalingActivity.scaleUpRequestedAt = 0L;
                        scalingActivity.scaleUpAt = now;
                        scalingActivity.shortfall = hostAttributeGroup.shortFall;
                        scalingActivity.scaledNumInstances = shortage;
                        scalingActivity.type = AutoScaleAction.Type.Up;
                        int finalShortage = shortage;
                        logger.debug("{} has a shortage of {} hosts", rule.getRuleName(), finalShortage);
                        callbacks.add(() -> {
                            logger.debug("Executing callback to scale up {} by {} hosts", rule.getRuleName(), finalShortage);
                            callback.call(new ScaleUpAction(rule.getRuleName(), finalShortage));
                        });
                    }
                }
            }
        }
        if (!allHostsToTerminate.isEmpty()) {
            callbacks.add(() -> {
                if (logger.isDebugEnabled()) {
                    logger.debug("Executing callback to scale down {} by {} hosts ({})", rule.getRuleName(), allHostsToTerminate.size(),
                            String.join(", ", allHostsToTerminate));
                }
                callback.call(new ScaleDownAction(rule.getRuleName(), allHostsToTerminate));
            });
        }

        return callbacks;
    }

    private void populateIdleResources(List<VirtualMachineLease> idleResources,
                                       List<VirtualMachineLease> idleInactiveResources,
                                       Map<String, HostAttributeGroup> leasesMap) {
        // Leases from active clusters
        for (VirtualMachineLease l : idleResources) {
            getAttribute(l).ifPresent(attrValue -> {
                if (leasesMap.containsKey(attrValue)) {
                    if (!leasesMap.get(attrValue).rule.idleMachineTooSmall(l))
                        leasesMap.get(attrValue).idleHosts.add(l);
                }
            });
        }

        // Leases from inactive clusters
        for (VirtualMachineLease l : idleInactiveResources) {
            getAttribute(l).ifPresent(attrValue -> {
                if (leasesMap.containsKey(attrValue)) {
                    leasesMap.get(attrValue).idleInactiveHosts.add(l);
                }
            });
        }
    }

    private Optional<String> getAttribute(VirtualMachineLease lease) {
        boolean hasValue = lease.getAttributeMap() != null
                && lease.getAttributeMap().get(attributeName) != null
                && lease.getAttributeMap().get(attributeName).getText().hasValue();
        return hasValue ? Optional.of(lease.getAttributeMap().get(attributeName).getText().getValue()) : Optional.empty();
    }

    private Map<String, HostAttributeGroup> setupHostAttributeGroupMap(AutoScaleRules autoScaleRules, ConcurrentMap<String, ScalingActivity> lastScalingAt) {
        Map<String, HostAttributeGroup> leasesMap = new HashMap<>();
        for (AutoScaleRule rule : autoScaleRules.getRules()) {
            leasesMap.put(rule.getRuleName(),
                    new HostAttributeGroup(rule.getRuleName(), rule));
            long initialCoolDown = getInitialCoolDown(rule.getCoolDownSecs());
            lastScalingAt.putIfAbsent(rule.getRuleName(), new ScalingActivity(initialCoolDown, initialCoolDown, 0, 0, null));
        }
        return leasesMap;
    }

    // make scaling activity happen after a fixed delayed time for the first time encountered (e.g., server start)
    private long getInitialCoolDown(long coolDownSecs) {
        long initialCoolDownInPastSecs = 120;
        initialCoolDownInPastSecs = Math.min(coolDownSecs, initialCoolDownInPastSecs);
        return System.currentTimeMillis() - coolDownSecs * 1000 + initialCoolDownInPastSecs * 1000;
    }

    private Map<String, String> getHostsToTerminate(List<VirtualMachineLease> idleHosts, int excess) {
        if (scaleDownConstraintExecutor == null) {
            return idleHosts.isEmpty() ? Collections.emptyMap() : getHostsToTerminateLegacy(idleHosts, excess);
        } else {
            return getHostsToTerminateUsingCriteria(idleHosts, excess);
        }
    }

    private Map<String, String> getInactiveHostsToTerminate(List<VirtualMachineLease> inactiveIdleHosts) {
        if (scaleDownConstraintExecutor == null) {
            return Collections.emptyMap();
        } else {
            Map<String, String> hostsMap = new HashMap<>();
            List<VirtualMachineLease> result = scaleDownConstraintExecutor.evaluate(inactiveIdleHosts);
            result.forEach(lease -> hostsMap.put(lease.hostname(), getMappedHostname(lease)));
            return hostsMap;
        }
    }

    private Map<String, String> getHostsToTerminateUsingCriteria(List<VirtualMachineLease> idleHosts, int excess) {
        Map<String, String> hostsMap = new HashMap<>();

        List<VirtualMachineLease> allIdle = new ArrayList<>(idleHosts);
        List<VirtualMachineLease> result = scaleDownConstraintExecutor.evaluate(allIdle);

        // The final result should contain only excess number of active hosts. Enforce this invariant.
        Set<String> activeIds = idleHosts.stream().map(VirtualMachineLease::hostname).collect(Collectors.toSet());
        int activeCounter = 0;
        Iterator<VirtualMachineLease> it = result.iterator();
        while (it.hasNext()) {
            VirtualMachineLease leaseToRemove = it.next();
            if (activeIds.contains(leaseToRemove.hostname())) {
                if (activeCounter < excess) {
                    activeCounter++;
                } else {
                    it.remove();
                }
            }
        }
        result.forEach(lease -> hostsMap.put(lease.hostname(), getMappedHostname(lease)));

        return hostsMap;
    }

    private Map<String, String> getHostsToTerminateLegacy(List<VirtualMachineLease> hosts, int excess) {
        Map<String, String> result = new HashMap<>();
        final Map<String, List<VirtualMachineLease>> hostsMap = new HashMap<>();
        final String defaultAttributeName = "default";
        for (VirtualMachineLease host : hosts) {
            final Protos.Attribute attribute = host.getAttributeMap().get(scaleDownBalancedByAttributeName);
            String val = (attribute != null && attribute.hasText()) ? attribute.getText().getValue() : defaultAttributeName;
            if (hostsMap.get(val) == null)
                hostsMap.put(val, new ArrayList<VirtualMachineLease>());
            hostsMap.get(val).add(host);
        }
        final List<List<VirtualMachineLease>> lists = new ArrayList<>();
        for (List<VirtualMachineLease> l : hostsMap.values())
            lists.add(l);
        int taken = 0;
        while (taken < excess) {
            List<VirtualMachineLease> takeFrom = null;
            int max = 0;
            for (List<VirtualMachineLease> l : lists) {
                if (l.size() > max) {
                    max = l.size();
                    takeFrom = l;
                }
            }
            final VirtualMachineLease removed = takeFrom.remove(0);
            result.put(removed.hostname(), getMappedHostname(removed));
            taken++;
        }
        return result;
    }

    private String getMappedHostname(VirtualMachineLease lease) {
        if (mapHostnameAttributeName == null || mapHostnameAttributeName.isEmpty())
            return lease.hostname();
        Protos.Attribute attribute = lease.getAttributeMap().get(mapHostnameAttributeName);
        if (attribute == null) {
            logger.error("Didn't find attribute " + mapHostnameAttributeName + " for host " + lease.hostname());
            return lease.hostname();
        }
        return attribute.getText().getValue();
    }

    public void setCallback(Action1<AutoScaleAction> callback) {
        this.callback = callback;
    }

    void shutdown() {
        if (isShutdown.compareAndSet(false, true))
            executor.shutdown();
    }

    private static class HostAttributeGroup {
        String name;
        List<VirtualMachineLease> idleHosts;
        List<VirtualMachineLease> idleInactiveHosts;
        int shortFall;
        AutoScaleRule rule;

        private HostAttributeGroup(String name, AutoScaleRule rule) {
            this.name = name;
            this.rule = rule;
            this.idleHosts = new ArrayList<>();
            this.idleInactiveHosts = new ArrayList<>();
            this.shortFall = 0;
        }
    }

    private static class ScalingActivity {
        private long scaleUpAt;
        private long scaleUpRequestedAt;
        private long scaleDownAt;
        private long scaleDownRequestedAt;
        private long inactiveScaleDownAt;
        private long inactiveScaleDownRequestedAt;
        private int shortfall;
        private int scaledNumInstances;
        private AutoScaleAction.Type type;

        private ScalingActivity(long scaleUpAt, long scaleDownAt, int shortfall, int scaledNumInstances, AutoScaleAction.Type type) {
            this.scaleUpAt = scaleUpAt;
            scaleUpRequestedAt = 0L;
            this.scaleDownAt = scaleDownAt;
            scaleDownRequestedAt = 0L;
            this.shortfall = shortfall;
            this.scaledNumInstances = scaledNumInstances;
            this.type = type;
        }
    }
}


/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.triggers;

import org.quartz.*;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Properties;

import static org.quartz.JobBuilder.newJob;

/**
 * Basically, a wrapper over Quartz scheduler. Not intended to be used by classes outside this package
 *
 */
class Scheduler {

    static final String DEFAULT_GROUP = "DEFAULT_GROUP";
    private org.quartz.Scheduler quartzScheduler;

    private static class SchedulerHolder {
        private static final Scheduler INSTANCE = new Scheduler();
    }

    /**
     * @warn method description missing
     *
     * @return
     */
    static Scheduler getInstance() {
        return SchedulerHolder.INSTANCE;
    }

    private Scheduler() {
    }

    /**
     * @warn method description missing
     * @warn parameter threadPoolSize description missing
     * @warn exception SchedulerException description missing
     *
     * @param threadPoolSize
     * @throws SchedulerException
     */
    synchronized void startScheduler(int threadPoolSize) throws SchedulerException {
        if (quartzScheduler == null) {
            Properties props = new Properties();
            props.setProperty("org.quartz.threadPool.threadCount", String.format("%d", threadPoolSize));
            StdSchedulerFactory stdSchedulerFactory = new StdSchedulerFactory(props);
            quartzScheduler = stdSchedulerFactory.getScheduler();
            quartzScheduler.start();
        }
    }

    /**
     * @warn method description missing
     * @warn exception SchedulerException description missing
     *
     * @throws SchedulerException
     */
    void stopScheduler() throws SchedulerException {
        quartzScheduler.shutdown();
    }

    /**
     * @warn method description missing
     * @warn parameter waitForJobsToComplete description missing
     * @warn exception SchedulerException description missing
     *
     * @param waitForJobsToComplete
     * @throws SchedulerException
     */
    void stopScheduler(boolean waitForJobsToComplete) throws SchedulerException {
        quartzScheduler.shutdown(waitForJobsToComplete);
    }

    /**
     * @warn method description missing
     * @warn parameter descriptions missing
     * @warn exception SchedulerException description missing
     *
     * @param jobId
     * @param jobGroup
     * @param jobClass
     * @param trigger
     * @throws SchedulerException
     */
    void scheduleQuartzJob(String jobId, String jobGroup, Class<? extends Job> jobClass, Trigger trigger) throws SchedulerException {
        JobDetail job = newJob(jobClass).withIdentity(jobId, jobGroup).build();
        quartzScheduler.scheduleJob(job, trigger);
    }

    /**
     * @warn method description missing
     * @warn parameter descriptions missing
     * @warn exception SchedulerException description missing
     *
     * @param jobId
     * @param jobGroup
     * @throws SchedulerException
     */
    void unscheduleQuartzJob(String jobId, String jobGroup) throws SchedulerException {
        quartzScheduler.unscheduleJob(TriggerKey.triggerKey(jobId, jobGroup));
    }

    /**
     * @warn method description missing
     * @warn parameter descriptions missing
     * @warn exception SchedulerException description missing
     *
     * @param jobId
     * @param jobGroup
     * @throws SchedulerException
     * @return
     */
    boolean isScheduled(String jobId, String jobGroup) throws SchedulerException {
        return quartzScheduler.checkExists(new JobKey(jobId, jobGroup));
    }

}


/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.plugins;

import com.netflix.fenzo.ConstraintEvaluator;
import com.netflix.fenzo.TaskAssignmentResult;
import com.netflix.fenzo.TaskRequest;
import com.netflix.fenzo.TaskTrackerState;
import com.netflix.fenzo.VirtualMachineCurrentState;

import java.util.Collection;

/**
 * This constraint ensures that the host is exclusively assigned to the given job. You may not extend this
 * class.
 * <p>
 * The task scheduler gives this constraint special treatment: While usually the scheduler only evaluates the
 * constraints of a new task, if an already-assigned task has an {@code ExclusiveHostRestraint} then the host on
 * which the task has been assigned fails this exclusive host constraint check as well.
 */
public final class ExclusiveHostConstraint implements ConstraintEvaluator {
    /**
     * Returns the name of this class as a String.
     *
     * @return the name of this class
     */
    @Override
    public String getName() {
        return ExclusiveHostConstraint.class.getName();
    }

    /**
     * Determines whether the prospective host already has tasks either running on it or assigned to be run on
     * it, and returns a false Result if either of those things is the case.
     *
     * @param taskRequest      describes the task to be considered for assignment to the host
     * @param targetVM         describes the host to be considered for accepting the task
     * @param taskTrackerState describes the state of tasks assigned to or running on hosts throughout the
     *                         system
     * @return an unsuccessful Result if the target already has running tasks or assigned tasks, or a successful
     *         Result otherwise
     */
    @Override
    public Result evaluate(TaskRequest taskRequest, VirtualMachineCurrentState targetVM, TaskTrackerState taskTrackerState) {
        Collection<TaskRequest> runningTasks = targetVM.getRunningTasks();
        if(runningTasks!=null && !runningTasks.isEmpty())
            return new Result(false, "Already has " + runningTasks.size() + " tasks running on it");
        Collection<TaskAssignmentResult> tasksCurrentlyAssigned = targetVM.getTasksCurrentlyAssigned();
        if(tasksCurrentlyAssigned!=null && !tasksCurrentlyAssigned.isEmpty())
            return new Result(false, "Already has " + tasksCurrentlyAssigned.size() + " assigned on it");
        return new Result(true, "");
    }
}


/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.plugins;

import com.netflix.fenzo.ConstraintEvaluator;
import com.netflix.fenzo.TaskRequest;
import com.netflix.fenzo.TaskTracker;
import com.netflix.fenzo.TaskTrackerState;
import com.netflix.fenzo.VMTaskFitnessCalculator;
import com.netflix.fenzo.VirtualMachineCurrentState;
import com.netflix.fenzo.functions.Func1;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * A balanced host attribute constraint attempts to distribute co-tasks evenly among host types, where their
 * types are determined by the values of particular host attributes.
 */
public class BalancedHostAttrConstraint implements ConstraintEvaluator {
    private final String name;
    private final Func1<String, Set<String>> coTasksGetter;
    private final String hostAttributeName;
    private final int expectedValues;

    /**
     * Create a constraint evaluator to balance tasks across hosts based on a given host attribute. For example,
     * if ten hosts have three distinct host attribute values between them of A, B, and C, when nine tasks are
     * scheduled, three tasks are assigned to hosts with attribute value A, another three tasks to hosts with
     * attribute B, and so on.
     *
     * @param coTasksGetter a one-argument function that, given a task ID being considered for assignment,
     *                      returns the set of Task IDs of the tasks that form the group of tasks which need to
     *                      be balanced
     * @param hostAttributeName the name of the host attribute whose values need to be balanced across the
     *                          co-tasks
     * @param expectedValues the number of distinct values to expect for {@code hostAttributeName}
     */
    public BalancedHostAttrConstraint(Func1<String, Set<String>> coTasksGetter, String hostAttributeName, int expectedValues) {
        this.coTasksGetter = coTasksGetter;
        this.hostAttributeName = hostAttributeName;
        this.name = BalancedHostAttrConstraint.class.getName()+"-"+hostAttributeName;
        this.expectedValues = expectedValues;
    }

    /**
     * Returns the name of the balanced host attribute constraint, which takes the form of the name of the
     * class (or subclass) followed by a dash followed by the value of {@code hostAttributeName} as it was set
     * when the constraint object was created.
     *
     * @return the name of the balanced host attribute constraint
     */
    @Override
    public String getName() {
        return name;
    }

    @Override
    public Result evaluate(TaskRequest taskRequest, VirtualMachineCurrentState targetVM, TaskTrackerState taskTrackerState) {
        Set<String> coTasks = coTasksGetter.call(taskRequest.getId());
        String targetHostAttrVal = AttributeUtilities.getAttrValue(targetVM.getCurrAvailableResources(), hostAttributeName);
        if(targetHostAttrVal==null || targetHostAttrVal.isEmpty()) {
            return new Result(false, hostAttributeName + " attribute unavailable on host " + targetVM.getCurrAvailableResources().hostname());
        }
        Map<String, Integer> usedAttribsMap = null;
        try {
            usedAttribsMap = getUsedAttributesMap(coTasks, taskTrackerState);
        } catch (Exception e) {
            return new Result(false, e.getMessage());
        }
        final Integer integer = usedAttribsMap.get(targetHostAttrVal);
        if(integer==null || usedAttribsMap.isEmpty())
            return new Result(true, "");
        int min=Integer.MAX_VALUE;
        int max=Integer.MIN_VALUE;
        for(Integer i: usedAttribsMap.values()) {
            min = Math.min(min, i);
            max = Math.max(max, i);
        }
        min = expectedValues>usedAttribsMap.size()? 0 : min;
        if(min == max || integer<max)
            return new Result(true, "");
        return new Result(false, "Would further imbalance by host attribute " + hostAttributeName);
    }

    private Map<String, Integer> getUsedAttributesMap(Set<String> coTasks, TaskTrackerState taskTrackerState) throws Exception {
        Map<String, Integer> usedAttribsMap = new HashMap<>();
        for(String coTask: coTasks) {
            TaskTracker.ActiveTask activeTask = taskTrackerState.getAllRunningTasks().get(coTask);
            if(activeTask==null)
                activeTask = taskTrackerState.getAllCurrentlyAssignedTasks().get(coTask);
            if(activeTask!=null) {
                String usedAttrVal = AttributeUtilities.getAttrValue(activeTask.getTotalLease(), hostAttributeName);
                if(usedAttrVal==null || usedAttrVal.isEmpty())
                    throw new Exception(hostAttributeName+" attribute unavailable on host " + activeTask.getTotalLease().hostname() +
                            " running co-task " + coTask); // indicate missing attribute in host
                if(usedAttribsMap.get(usedAttrVal)==null)
                    usedAttribsMap.put(usedAttrVal, 1);
                else
                    usedAttribsMap.put(usedAttrVal, usedAttribsMap.get(usedAttrVal)+1);
            }
        }
        return usedAttribsMap;
    }

    /**
     * Converts this constraint into a "soft" constraint. By default, a balanced host attribute constraint is a
     * "hard" constraint, which is to say that Fenzo will guarantee that the constraint is applied and will fail
     * to place a task if the only way it can do so is to violate the constraint. This method returns a
     * {@link VMTaskFitnessCalculator} that represents this constraint as a "soft" constraint that will permit
     * Fenzo to place a task in violation of the constraint if it cannot do so otherwise.
     *
     * @return a task fitness calculator that represents the balanced host attribute constraint as a soft
     *         constraint
     */
    public VMTaskFitnessCalculator asSoftConstraint() {
        return new VMTaskFitnessCalculator() {
            @Override
            public String getName() {
                return name;
            }

            @Override
            public double calculateFitness(TaskRequest taskRequest, VirtualMachineCurrentState targetVM, TaskTrackerState taskTrackerState) {
                String targetHostAttrVal = AttributeUtilities.getAttrValue(targetVM.getCurrAvailableResources(), hostAttributeName);
                if(targetHostAttrVal==null || targetHostAttrVal.isEmpty()) {
                    return 0.0;
                }
                Set<String> coTasks = coTasksGetter.call(taskRequest.getId());
                Map<String, Integer> usedAttribsMap = null;
                try {
                    usedAttribsMap = getUsedAttributesMap(coTasks, taskTrackerState);
                } catch (Exception e) {
                    return 0.0;
                }
                final Integer integer = usedAttribsMap.get(targetHostAttrVal);
                if(integer==null)
                    return 1.0;
                if(usedAttribsMap.isEmpty())
                    return 1.0;
                double avg=0.0;
                for(Integer i: usedAttribsMap.values())
                    avg += i;
                avg = Math.ceil(avg+1 / Math.max(expectedValues, usedAttribsMap.size()));
                if(integer<=avg)
                    return (avg-(double)integer) / avg;
                return 0.0;
            }
        };
    }
}


/*
 * Copyright 2017 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.queues.tiered;

import com.netflix.fenzo.sla.ResAllocs;
import com.netflix.fenzo.sla.ResAllocsUtil;

import java.util.HashMap;
import java.util.Map;

/* package */ class TierSla {

    // small allocation for a bucket with no defiend allocation
    static final double eps = 0.001;

    private ResAllocs tierCapacity;

    private final Map<String, ResAllocs> allocsMap = new HashMap<>();
    private double totalCpu = 0.0;
    private double totalMem = 0.0;
    private double totalNetwork = 0.0;
    private double totalDisk = 0.0;

    void setTierCapacity(ResAllocs tierCapacity) {
        this.tierCapacity  = tierCapacity;
    }

    void setAlloc(String bucket, ResAllocs value) {
        final ResAllocs prev = allocsMap.put(bucket, value);
        if (prev != null)
            subtract(prev);
        add(value);
    }

    private void add(ResAllocs value) {
        totalCpu += value.getCores();
        totalMem += value.getMemory();
        totalNetwork += value.getNetworkMbps();
        totalDisk += value.getDisk();
    }

    private void subtract(ResAllocs value) {
        totalCpu -= value.getCores();
        totalMem -= value.getMemory();
        totalNetwork -= value.getNetworkMbps();
        totalDisk -= value.getDisk();
    }

    public ResAllocs getTierCapacity() {
        return tierCapacity;
    }

    ResAllocs getBucketAllocs(String bucketName) {
        return allocsMap.computeIfAbsent(bucketName, name -> ResAllocsUtil.emptyOf(bucketName));
    }

    Map<String, ResAllocs> getAllocsMap() {
        return allocsMap;
    }

    /**
     * Evaluate the allocation share of a bucket among all the buckets for which allocations are defined. If there are
     * no allocations setup, return 1.0, implying 100%. If no allocation is setup for the given bucket,
     * return a small value. Otherwise, calculate the share percentage of each resource cpu, memory, network, and disk
     * from the total and return the maximum of these shares.
     *
     * @param bucket Name of the bucket.
     * @return Allocation share for the bucket.
     */
    double evalAllocationShare(String bucket) {
        if (allocsMap.isEmpty()) {
            return 1.0; // special case if there are no allocations setup
        }
        final ResAllocs resAllocs = allocsMap.get(bucket);
        if (resAllocs == null)
            return totalCpu < (1.0 / eps) ? eps : 1.0 / totalCpu; // arbitrarily base it on cpus
        double val = totalCpu < 1.0 ? eps : resAllocs.getCores() / totalCpu;
        val = Math.max(val, totalMem < 1.0 ? eps : resAllocs.getMemory() / totalMem);
        val = Math.max(val, totalNetwork < 1.0 ? eps : resAllocs.getNetworkMbps() / totalNetwork);
        return Math.max(val, totalDisk < 1.0 ? eps : resAllocs.getDisk() / totalDisk);
    }
}


/*
 * Copyright 2017 Netflix, Inc.
 *
 * 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 com.netflix.fenzo;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.mesos.Protos;

/* package */ class InternalVMCloner {

    /**
     * Creates a VM lease object representing the max of resources from the given collection of VMs. Attributes
     * are created by adding all unique attributes across the collection.
     *
     * @param avms Collection of VMs to create the new lease object from.
     * @return VM lease object representing the max of resources from the given collection of VMs
     */
    /* package */  VirtualMachineLease getClonedMaxResourcesLease(Collection<AssignableVirtualMachine> avms) {
        double cpus = 0.0;
        double mem = 0.0;
        double disk = 0.0;
        double network = 0.0;
        double ports = 0.0;
        final Map<String, Double> scalars = new HashMap<>();
        final Map<String, Protos.Attribute> attributeMap = new HashMap<>();
        if (avms != null) {
            for (AssignableVirtualMachine avm : avms) {
                Map<VMResource, Double> maxResources = avm.getMaxResources();
                Double value = maxResources.get(VMResource.CPU);
                if (value != null) {
                    cpus = Math.max(cpus, value);
                }
                value = maxResources.get(VMResource.Memory);
                if (value != null) {
                    mem = Math.max(mem, value);
                }
                value = maxResources.get(VMResource.Disk);
                if (value != null) {
                    disk = Math.max(disk, value);
                }
                value = maxResources.get(VMResource.Network);
                if (value != null) {
                    network = Math.max(network, value);
                }
                value = maxResources.get(VMResource.Ports);
                if (value != null) {
                    ports = Math.max(ports, value);
                }
                final Map<String, Double> maxScalars = avm.getMaxScalars();
                if (maxScalars != null && !maxScalars.isEmpty()) {
                    for (String k : maxScalars.keySet())
                        scalars.compute(k, (s, oldVal) -> {
                            if (oldVal == null) {
                                oldVal = 0.0;
                            }
                            Double aDouble = maxScalars.get(k);
                            if (aDouble == null) {
                                aDouble = 0.0;
                            }
                            return oldVal + aDouble;
                        });
                }
                final Map<String, Protos.Attribute> attrs = avm.getCurrTotalLease().getAttributeMap();
                if (attrs != null && !attrs.isEmpty()) {
                    for (Map.Entry<String, Protos.Attribute> e : attrs.entrySet())
                        attributeMap.putIfAbsent(e.getKey(), e.getValue());
                }
            }
        }
        final double fcpus = cpus;
        final double fmem = mem;
        final double fdisk = disk;
        final double fnetwork = network;
        final List<VirtualMachineLease.Range> fports = Collections.singletonList(
                new VirtualMachineLease.Range(100, 100 + (int) ports));
        return new VirtualMachineLease() {
            @Override
            public String getId() {
                return "NoID";
            }

            @Override
            public long getOfferedTime() {
                return 0;
            }

            @Override
            public String hostname() {
                return "NoHostname";
            }

            @Override
            public String getVMID() {
                return "NoID";
            }

            @Override
            public double cpuCores() {
                return fcpus;
            }

            @Override
            public double memoryMB() {
                return fmem;
            }

            @Override
            public double networkMbps() {
                return fnetwork;
            }

            @Override
            public double diskMB() {
                return fdisk;
            }

            @Override
            public List<Range> portRanges() {
                return fports;
            }

            @Override
            public Protos.Offer getOffer() {
                return null;
            }

            @Override
            public Map<String, Protos.Attribute> getAttributeMap() {
                return attributeMap;
            }

            @Override
            public Double getScalarValue(String name) {
                return scalars.get(name);
            }

            @Override
            public Map<String, Double> getScalarValues() {
                return scalars;
            }
        };
    }

    /* package */ VirtualMachineLease cloneLease(VirtualMachineLease lease, final String hostname, long now) {
        final List<VirtualMachineLease.Range> ports = new LinkedList<>();
        final List<VirtualMachineLease.Range> ranges = lease.portRanges();
        if (ranges != null && !ranges.isEmpty()) {
            for (VirtualMachineLease.Range r : ranges)
                ports.add(new VirtualMachineLease.Range(r.getBeg(), r.getEnd()));
        }
        final double cpus = lease.cpuCores();
        final double memory = lease.memoryMB();
        final double network = lease.networkMbps();
        final double disk = lease.diskMB();
        final Map<String, Protos.Attribute> attributes = new HashMap<>(lease.getAttributeMap());
        final Map<String, Double> scalarValues = new HashMap<>(lease.getScalarValues());
        return new VirtualMachineLease() {
            @Override
            public String getId() {
                return hostname;
            }

            @Override
            public long getOfferedTime() {
                return now;
            }

            @Override
            public String hostname() {
                return hostname;
            }

            @Override
            public String getVMID() {
                return hostname;
            }

            @Override
            public double cpuCores() {
                return cpus;
            }

            @Override
            public double memoryMB() {
                return memory;
            }

            @Override
            public double networkMbps() {
                return network;
            }

            @Override
            public double diskMB() {
                return disk;
            }

            @Override
            public List<Range> portRanges() {
                return ports;
            }

            @Override
            public Protos.Offer getOffer() {
                return null;
            }

            @Override
            public Map<String, Protos.Attribute> getAttributeMap() {
                return attributes;
            }

            @Override
            public Double getScalarValue(String name) {
                return scalarValues.get(name);
            }

            @Override
            public Map<String, Double> getScalarValues() {
                return scalarValues;
            }
        };
    }

}


/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.plugins;

import com.netflix.fenzo.ConstraintEvaluator;
import com.netflix.fenzo.TaskRequest;
import com.netflix.fenzo.TaskTrackerState;
import com.netflix.fenzo.VirtualMachineCurrentState;
import com.netflix.fenzo.VirtualMachineLease;
import org.apache.mesos.Protos;
import com.netflix.fenzo.functions.Func1;

import java.util.Map;

/**
 * A constraint that ensures that a task gets a host with an attribute of a specified value.
 */
public class HostAttrValueConstraint implements ConstraintEvaluator {
    private static final String HOSTNAME="HOSTNAME";
    private final String hostAttributeName;
    private final Func1<String, String> hostAttributeValueGetter;

    public HostAttrValueConstraint(String hostAttributeName, Func1<String, String> hostAttributeValueGetter) {
        this.hostAttributeName = hostAttributeName==null? HOSTNAME:hostAttributeName;
        this.hostAttributeValueGetter = hostAttributeValueGetter;
    }

    /**
     * Returns the name of this constraint as a String, in the form of the class name followed by a dash
     * followed by the value of {@code hostAttributeName} as it was set when this object was constructed.
     *
     * @return the name of this constraint
     */
    @Override
    public String getName() {
        return HostAttrValueConstraint.class.getName()+"-"+hostAttributeName;
    }

    /**
     * Tests a host to determine whether it has an attribute of the required value for this task request.
     *
     * @param taskRequest      describes the task being evaluated for assignment to the host
     * @param targetVM         describes the host being evaluated as a target for the task
     * @param taskTrackerState describes the state of tasks already assigned or running on hosts throughout the
     *                         system
     * @return a successful Result if the host has an attribute with the required value, or an unsuccessful
     *         Result otherwise
     */
    @Override
    public Result evaluate(TaskRequest taskRequest, VirtualMachineCurrentState targetVM, TaskTrackerState taskTrackerState) {
        String targetHostAttrVal = getAttrValue(targetVM.getCurrAvailableResources());
        if(targetHostAttrVal==null || targetHostAttrVal.isEmpty()) {
            return new Result(false, hostAttributeName + " attribute unavailable on host " + targetVM.getCurrAvailableResources().hostname());
        }
        String requiredAttrVal = hostAttributeValueGetter.call(taskRequest.getId());
        return targetHostAttrVal.equals(requiredAttrVal)?
                new Result(true, "") :
                new Result(false, "Host attribute " + hostAttributeName + ": required=" + requiredAttrVal + ", got=" + targetHostAttrVal);
    }

    private String getAttrValue(VirtualMachineLease lease) {
        switch (hostAttributeName) {
            case HOSTNAME:
                return lease.hostname();
            default:
                Map<String,Protos.Attribute> attributeMap = lease.getAttributeMap();
                if(attributeMap==null)
                    return null;
                Protos.Attribute attribute = attributeMap.get(hostAttributeName);
                if(attribute==null)
                    return null;
                return attribute.getText().getValue();
        }
    }
}


/*
 * Copyright 2016 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.samples;

import com.netflix.fenzo.*;
import com.netflix.fenzo.functions.Action0;
import com.netflix.fenzo.functions.Action1;
import com.netflix.fenzo.plugins.BinPackingFitnessCalculators;
import com.netflix.fenzo.plugins.VMLeaseObject;
import com.netflix.fenzo.queues.*;
import org.apache.mesos.MesosSchedulerDriver;
import org.apache.mesos.Protos;
import org.apache.mesos.Scheduler;
import org.apache.mesos.SchedulerDriver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class SampleQbasedScheduling {

    private static class MesosScheduler implements Scheduler {

        private final AtomicInteger numTasksCompleted;
        private final AtomicReference<TaskSchedulingService> schedSvcGetter;
        private Action1<List<Protos.Offer>> leaseAction = null;

        MesosScheduler(AtomicInteger numTasksCompleted, AtomicReference<TaskSchedulingService> schedSvcGetter) {
            this.numTasksCompleted = numTasksCompleted;
            this.schedSvcGetter = schedSvcGetter;
        }

        @Override
        public void registered(SchedulerDriver driver, Protos.FrameworkID frameworkId, Protos.MasterInfo masterInfo) {
            System.out.println("Mesos scheduler registered");
        }

        @Override
        public void reregistered(SchedulerDriver driver, Protos.MasterInfo masterInfo) {
            System.out.println("Mesos scheduler re-registered");
        }

        @Override
        public void resourceOffers(SchedulerDriver driver, List<Protos.Offer> offers) {
            leaseAction.call(offers);
        }

        @Override
        public void offerRescinded(SchedulerDriver driver, Protos.OfferID offerId) {
            System.out.println("Unexpected offers Rescinded");
        }

        @Override
        public void statusUpdate(SchedulerDriver driver, Protos.TaskStatus status) {
            switch (status.getState()) {
                case TASK_FAILED:
                case TASK_LOST:
                case TASK_FINISHED:
                    System.out.println("Task status for " + status.getTaskId().getValue() + ": " + status.getState());
                    schedSvcGetter.get().removeTask(status.getTaskId().getValue(),
                            allTasks.get(status.getTaskId().getValue()).getQAttributes(),
                            tasksToHostnameMap.get(status.getTaskId().getValue()));
                    numTasksCompleted.incrementAndGet();
            }
        }

        @Override
        public void frameworkMessage(SchedulerDriver driver, Protos.ExecutorID executorId, Protos.SlaveID slaveId, byte[] data) {

        }

        @Override
        public void disconnected(SchedulerDriver driver) {
            System.out.println("Mesos driver disconnected");
        }

        @Override
        public void slaveLost(SchedulerDriver driver, Protos.SlaveID slaveId) {
            System.out.println("Mesos agent lost");
        }

        @Override
        public void executorLost(SchedulerDriver driver, Protos.ExecutorID executorId, Protos.SlaveID slaveId, int status) {
            System.out.println("Mesos executor lost");
        }

        @Override
        public void error(SchedulerDriver driver, String message) {
            System.out.println("Unexpected mesos scheduler error: " + message);
        }
    }

    private final static QAttributes qAttribs = new QAttributes.QAttributesAdaptor(0, "onlyBucket");

    private final static ConcurrentMap<String, QueuableTask> allTasks = new ConcurrentHashMap<>();
    private final static ConcurrentMap<String, String> tasksToHostnameMap = new ConcurrentHashMap<>();

    /**
     * This is the main method of this sample framework. It showcases how to use Fenzo queues for scheduling. It creates
     * some number of tasks and launches them into Mesos using the Mesos built-in command executor. The tasks launched
     * are simple sleep commands that sleep for 3 seconds each.
     * @param args Requires one argument for the location of Mesos master to connect to.
     * @throws Exception Upon catching any exceptions within the program.
     */
    public static void main(String[] args) throws Exception {
        if(args.length!=1) {
            System.err.println("Must provide one argument - Mesos master location string");
            System.exit(1);
        }
        int numTasks=10;
        final AtomicInteger numTasksCompleted = new AtomicInteger();

        // create Fenzo TaskScheduler object.
        final TaskScheduler taskScheduler = new TaskScheduler.Builder()
                .withFitnessCalculator(BinPackingFitnessCalculators.cpuMemBinPacker)
                .withLeaseOfferExpirySecs(1000000)
                .withLeaseRejectAction(new Action1<VirtualMachineLease>() {
                    @Override
                    public void call(VirtualMachineLease v) {
                        System.out.println("Unexpected to reject lease on " + v.hostname());
                    }
                })
                .build();

        // Create a queue from Fenzo provided queue implementations.
        final TaskQueue queue = TaskQueues.createTieredQueue(2);

        // Create our Mesos scheduler callback implementation
        AtomicReference<TaskSchedulingService> schedSvcGetter = new AtomicReference<>();
        final MesosScheduler mesosSchedulerCallback = new MesosScheduler(numTasksCompleted, schedSvcGetter);

        // create Mesos driver
        Protos.FrameworkInfo framework = Protos.FrameworkInfo.newBuilder()
                .setName("Sample Fenzo Framework")
                .setUser("")
                .build();
        final MesosSchedulerDriver driver = new MesosSchedulerDriver(mesosSchedulerCallback, framework, args[0]);

        // Build Fenzo task scheduling service using the TaskScheduler and TaskQueue objects created above
        final AtomicInteger schedCounter = new AtomicInteger();
        final TaskSchedulingService schedulingService = new TaskSchedulingService.Builder()
                .withLoopIntervalMillis(1000)
                .withMaxDelayMillis(1500)
                .withPreSchedulingLoopHook(new Action0() {
                    @Override
                    public void call() {
                        System.out.println("Starting scheduling iteration " + schedCounter.incrementAndGet());
                    }
                })
                .withTaskQueue(queue)
                .withTaskScheduler(taskScheduler)
                // TaskSchedulingService will call us back when there are task assignments. Handle them by launching
                // tasks using MesosDriver
                .withSchedulingResultCallback(new Action1<SchedulingResult>() {
                    @Override
                    public void call(SchedulingResult schedulingResult) {
                        final List<Exception> exceptions = schedulingResult.getExceptions();
                        if (exceptions != null && !exceptions.isEmpty()) {
                            System.out.println("Exceptions from scheduling iteration:");
                            for (Exception e: exceptions)
                                e.printStackTrace();
                        }
                        else {
                            for (Map.Entry<String, VMAssignmentResult> e: schedulingResult.getResultMap().entrySet()) {
                                List<Protos.OfferID> offers = new ArrayList<Protos.OfferID>();
                                for (VirtualMachineLease l: e.getValue().getLeasesUsed())
                                    offers.add(l.getOffer().getId());
                                List<Protos.TaskInfo> taskInfos = new ArrayList<Protos.TaskInfo>();
                                for (TaskAssignmentResult r: e.getValue().getTasksAssigned()) {
                                    taskInfos.add(SampleFramework.getTaskInfo(
                                            e.getValue().getLeasesUsed().iterator().next().getOffer().getSlaveId(),
                                            r.getTaskId(),
                                            "sleep 2"
                                    ));
                                    tasksToHostnameMap.put(r.getTaskId(), r.getHostname());
                                }
                                driver.launchTasks(
                                        offers,
                                        taskInfos
                                );
                            }
                        }
                    }
                })
                .build();
        schedSvcGetter.set(schedulingService);

        // set up action in our scheduler callback to send resource offers into our scheduling service
        mesosSchedulerCallback.leaseAction = new Action1<List<Protos.Offer>>() {
            @Override
            public void call(List<Protos.Offer> offers) {
                List<VirtualMachineLease> leases = new ArrayList<>();
                for (Protos.Offer o: offers)
                    leases.add(new VMLeaseObject(o)); // Fenzo lease object adapter for Mesos Offer object
                schedulingService.addLeases(leases);
            }
        };
        schedulingService.start();

        // start Mesos driver
        new Thread() {
            @Override
            public void run() {
                driver.run();
            }
        }.start();

        // submit some tasks
        for (int i=0; i<numTasks; i++) {
            final QueuableTask task = getTask(i);
            allTasks.put(task.getId(), task);
            queue.queueTask(task);
        }

        // wait for tasks to complete
        while (numTasksCompleted.get() < numTasks) {
            Thread.sleep(1000);
            System.out.println("        #tasks completed: " + numTasksCompleted.get() + " of " + numTasks);
        }

        // verify that Fenzo has no tasks in its queue
        final CountDownLatch latch = new CountDownLatch(1);
        schedulingService.requestAllTasks(new Action1<Map<TaskQueue.TaskState, Collection<QueuableTask>>>() {
            @Override
            public void call(Map<TaskQueue.TaskState, Collection<QueuableTask>> taskStateCollectionMap) {
                System.out.println("Fenzo queue has " + taskStateCollectionMap.size() + " items");
                latch.countDown();
            }
        });
        if (!latch.await(5, TimeUnit.SECONDS))
            System.err.println("Timeout waiting for listing all tasks in Fenzo queues");

        System.out.println("ALL DONE");
        System.exit(0);
    }

    private static QueuableTask getTask(final int i) {
        return new QueuableTask() {
            @Override
            public QAttributes getQAttributes() {
                return qAttribs;
            }

            @Override
            public String getId() {
                return "Task-" + i;
            }

            @Override
            public String taskGroupName() {
                return "groupA";
            }

            @Override
            public double getCPUs() {
                return 1;
            }

            @Override
            public double getMemory() {
                return 100;
            }

            @Override
            public double getNetworkMbps() {
                return 0;
            }

            @Override
            public double getDisk() {
                return 0;
            }

            @Override
            public int getPorts() {
                return 0;
            }

            @Override
            public Map<String, Double> getScalarRequests() {
                return null;
            }

            @Override
            public Map<String, NamedResourceSetRequest> getCustomNamedResources() {
                return null;
            }

            @Override
            public List<? extends ConstraintEvaluator> getHardConstraints() {
                return null;
            }

            @Override
            public List<? extends VMTaskFitnessCalculator> getSoftConstraints() {
                return null;
            }

            @Override
            public void setAssignedResources(AssignedResources assignedResources) {
                // no-op, unexpected for this sample
            }

            @Override
            public AssignedResources getAssignedResources() {
                return null;
            }
        };
    }
}


/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.plugins;

import com.netflix.fenzo.ConstraintEvaluator;
import com.netflix.fenzo.TaskRequest;
import com.netflix.fenzo.TaskTracker;
import com.netflix.fenzo.TaskTrackerState;
import com.netflix.fenzo.VirtualMachineCurrentState;
import com.netflix.fenzo.functions.Func1;

import java.util.Set;

/**
 * A unique constraint evaluator that constrains tasks according to a given host attribute. Use this evaluator
 * to constrain a given set of tasks (co-tasks) by assigning them to hosts such that each task is assigned to a
 * host that has a different value for the host attribute you specify.
 *

 * For example, if you specify the host attribute {@code ZoneAttribute}, the evaluator will place the co-tasks
 * such that each task is in a different zone. Note that because of this, if more tasks are submitted than there
 * are zones available for them, the excess tasks will not be assigned to any hosts. If instead you want to
 * implement a load balancing strategy, use {@link BalancedHostAttrConstraint} instead.
 *

 * If you construct this evaluator without passing in a host attribute name, it will use the host name as the
 * host attribute by which it uniquely identifies hosts.
 */
public class UniqueHostAttrConstraint implements ConstraintEvaluator {
    private final Func1<String, Set<String>> coTasksGetter;
    private final String hostAttributeName;
    private final String name;

    /**
     * Create this constraint evaluator with the given co-tasks of a group. This is equivalent to
     * {@code UniqueHostAttrConstraint(coTasksGetter, null)}.
     *
     * @param coTasksGetter a single-argument function that, given a task ID, returns the set of task IDs of its
     *                      co-tasks
     */
    public UniqueHostAttrConstraint(Func1<String, Set<String>> coTasksGetter) {
        this(coTasksGetter, AttributeUtilities.DEFAULT_ATTRIBUTE);
    }

    /**
     * Create this constraint evaluator with the given co-tasks of a group and a given host attribute name.
     *
     * @param coTasksGetter a single-argument function that, given a task ID, returns the set of task IDs of its
     *                      co-tasks
     * @param hostAttributeName the name of the host attribute whose value is to be unique for each co-task's
     *                          host assignment. If this is {@code null}, this indicates that the host name is
     *                          to be unique for each co-task's host assignment.
     */
    public UniqueHostAttrConstraint(Func1<String, Set<String>> coTasksGetter, String hostAttributeName) {
        this.coTasksGetter = coTasksGetter;
        this.hostAttributeName = hostAttributeName;
        this.name = UniqueHostAttrConstraint.class.getName()+"-"+hostAttributeName;
    }

    /**
     * Returns the name of this constraint evaluator as a String in the form of the name of this class followed
     * by a dash followed by the host attribute name that this evaluator uses to uniquely identify the host.
     *
     * @return the name of this constraint evaluator
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Determines whether a particular target host is appropriate for a particular task request by rejecting any
     * host that has the same value for the unique constraint attribute as another host that is already assigned
     * a co-task of the specified task request.
     *
     * @param taskRequest      describes the task being considered for assignment to the host
     * @param targetVM         describes the host being considered as a target for the task
     * @param taskTrackerState describes the state of tasks previously assigned or already running throughout
     *                         the system
     * @return a successful Result if the target does not have the same value for its unique constraint
     *         attribute as another host that has already been assigned a co-task of {@code taskRequest}, or an
     *         unsuccessful Result otherwise
     */
    @Override
    public Result evaluate(TaskRequest taskRequest, VirtualMachineCurrentState targetVM, TaskTrackerState taskTrackerState) {
        Set<String> coTasks = coTasksGetter.call(taskRequest.getId());
        String targetHostAttrVal = AttributeUtilities.getAttrValue(targetVM.getCurrAvailableResources(), hostAttributeName);
        if(targetHostAttrVal==null || targetHostAttrVal.isEmpty()) {
            return new Result(false, hostAttributeName + " attribute unavailable on host " + targetVM.getCurrAvailableResources().hostname());
        }
        for(String coTask: coTasks) {
            TaskTracker.ActiveTask activeTask = taskTrackerState.getAllRunningTasks().get(coTask);
            if(activeTask==null)
                activeTask = taskTrackerState.getAllCurrentlyAssignedTasks().get(coTask);
            if(activeTask!=null) {
                String usedAttrVal = AttributeUtilities.getAttrValue(activeTask.getTotalLease(), hostAttributeName);
                if(usedAttrVal==null || usedAttrVal.isEmpty())
                    return new Result(false, hostAttributeName+" attribute unavailable on host " + activeTask.getTotalLease().hostname() +
                            " running co-task " + coTask);
                if(usedAttrVal.equals(targetHostAttrVal)) {
                    return new Result(false, hostAttributeName+" " + targetHostAttrVal + " already used for another co-task " + coTask);
                }
            }
        }
        return new Result(true, "");
    }

}


/*
 * Copyright 2015 Netflix, Inc.
 *
 * 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 com.netflix.fenzo.triggers;

import com.netflix.fenzo.triggers.exceptions.SchedulerException;
import com.netflix.fenzo.triggers.exceptions.TriggerNotFoundException;
import com.netflix.fenzo.triggers.persistence.InMemoryTriggerDao;
import com.netflix.fenzo.triggers.persistence.TriggerDao;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.functions.Action1;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.TriggerKey.triggerKey;

/**
 * @warn class description missing
 */
public class TriggerOperator {

    public static final String TRIGGER_KEY = "trigger";
    public static final String TRIGGER_OPERATOR_KEY = "triggerOperator";

    private static final Logger logger = LoggerFactory.getLogger(TriggerOperator.class);

    private final Scheduler scheduler;
    private final TriggerDao triggerDao;
    private final int threadPoolSize;
    private final AtomicBoolean initialized = new AtomicBoolean(false);

    /**
     * Constructor
     * @param threadPoolSize the thread pool size for the scheduler
     */
    public TriggerOperator(int threadPoolSize) {
        this(new InMemoryTriggerDao(), threadPoolSize);
    }

    /**
     * Constructor
     * @param triggerDao dao implementation for {@code Trigger}
     * @param threadPoolSize the thread pool size for the scheduler
     */
    public TriggerOperator(TriggerDao triggerDao, int threadPoolSize) {
        this.triggerDao = triggerDao;
        this.scheduler = Scheduler.getInstance();
        this.threadPoolSize = threadPoolSize;
    }

    /**
     * Users of this class must call {@code initialize()} before they use this class.
     * @warn exception SchedulerException description missing
     *
     * @throws SchedulerException
     */
    @PostConstruct
    public void initialize() throws SchedulerException {
        if (initialized.compareAndSet(false, true)) {
            try {
                this.scheduler.startScheduler(threadPoolSize);
            } catch (org.quartz.SchedulerException se) {
                throw new SchedulerException("Exception occurred while initializing TriggerOperator", se);
            }
            for (Iterator<Trigger> iterator = getTriggers().iterator(); iterator.hasNext(); ) {
                Trigger trigger = iterator.next();
                if (!trigger.isDisabled() && trigger instanceof ScheduledTrigger) {
                    scheduleTrigger((ScheduledTrigger) trigger);
                }
            }
        }
    }

    /**
     * @warn method description missing
     * @warn exception SchedulerException description missing
     */
    @PreDestroy
    public void destroy() throws SchedulerException {
        if (initialized.get() && this.scheduler != null) {
            try {
                this.scheduler.stopScheduler(true);
            } catch (org.quartz.SchedulerException se) {
                throw new SchedulerException("Exception occurred while destroying TriggerOperator", se);
            }
        }
    }

    /**
     * Returns a default instance of {@code TriggerOperator} with sensible default values.
     * Uses an in-memory implementation of Dao.
     *
     * @return
     */
    public static TriggerOperator getInstance() {
        return new TriggerOperator(new InMemoryTriggerDao(), 20);
    }

    /**
     * @warn method description missing
     * @warn parameter threadPoolSize description missing
     * @param threadPoolSize
     * @return
     */
    public static TriggerOperator getInstance(int threadPoolSize) {
        return new TriggerOperator(new InMemoryTriggerDao(), threadPoolSize);
    }

    /**
     * @warn method description missing
     * @warn parameter descriptions missing
     * @param triggerDao
     * @param threadPoolSize
     * @return
     */
    public static TriggerOperator getInstance(TriggerDao triggerDao, int threadPoolSize) {
        return new TriggerOperator(triggerDao, threadPoolSize);
    }

    /**
     * Returns the {@code Trigger} based on the unique trigger id.
     *
     * @param triggerId the string that uniquely identifies the {@code Trigger}
     * @return the {@code Trigger} that matches {@code triggerId}
     */
    public Trigger getTrigger(String triggerId) {
        return triggerDao.getTrigger(triggerId);
    }

    /**
     * Registers a {@code Trigger} with trigger service.
     * @warn parameter descriptions missing
     * @warn exception SchedulerException description missing
     *
     * @param triggerGroup
     * @param trigger
     * @throws SchedulerException
     * @return
     */
    public String registerTrigger(String triggerGroup, Trigger trigger) throws SchedulerException {
        String triggerId = triggerDao.createTrigger(triggerGroup, trigger);
        if (trigger instanceof ScheduledTrigger) {
            scheduleTrigger((ScheduledTrigger) trigger);
        }
        return triggerId;
    }

    /**
     * Disables the {@code Trigger}. If the {@code Trigger} is disabled it will not execute.
     * @warn exception SchedulerException description missing
     *
     * @param triggerId the string that uniquely identifies the {@code Trigger} to be disabled
     * @throws TriggerNotFoundException if there is no {@code Trigger} that matches {@code triggerId}
     * @throws SchedulerException
     */
    public void disableTrigger(String triggerId) throws TriggerNotFoundException, SchedulerException {
        Trigger trigger = getTrigger(triggerId);
        if (trigger != null) {
            disableTrigger(trigger);
        } else {
            throw new TriggerNotFoundException("No trigger found with trigger id: " + triggerId);
        }
    }

    /**
     * Disables the {@code Trigger}. If the {@code Trigger} is disabled it will not execute.
     * @warn exception SchedulerException description missing
     *
     * @param trigger the {@code Trigger} to be disabled
     * @throws SchedulerException
     */
    public void disableTrigger(Trigger trigger) throws SchedulerException {
        trigger.setDisabled(true);
        triggerDao.updateTrigger(trigger);
        if (trigger instanceof ScheduledTrigger) {
            unscheduleTrigger((ScheduledTrigger) trigger);
        }
    }

    /**
     * Enables the {@code Trigger}.
     * @warn exception SchedulerException description missing
     *
     * @param triggerId the string that uniquely identifies the {@code Trigger} to be enabled
     * @throws TriggerNotFoundException if there is no {@code Trigger} that matches {@code triggerId}
     * @throws SchedulerException
     */
    public void enableTrigger(String triggerId) throws TriggerNotFoundException, SchedulerException {
        Trigger trigger = getTrigger(triggerId);
        if (trigger != null) {
            enableTrigger(trigger);
        } else {
            throw new TriggerNotFoundException("No trigger found with trigger id: " + triggerId);
        }
    }

    /**
     * Enables the {@code Trigger}
     * @warn exception SchedulerException description missing
     *
     * @param trigger the {@code Trigger} to be enabled
     * @throws SchedulerException
     */
    public void enableTrigger(Trigger trigger) throws SchedulerException {
        trigger.setDisabled(false);
        triggerDao.updateTrigger(trigger);
        if (trigger instanceof ScheduledTrigger) {
            scheduleTrigger((ScheduledTrigger) trigger);
        }
    }

    /**
     * Deletes/Removes the {@code Trigger}. If it is a {@code ScheduledTrigger} then it is also un-scheduled from
     * scheduler.
     *
     * @param triggerId the string that uniquely identifies the {@code Trigger} to be removed
     * @throws TriggerNotFoundException if there is no {@code Trigger} that matches {@code triggerId}
     * @throws SchedulerException
     */
    public void deleteTrigger(String triggerGroup, String triggerId) throws TriggerNotFoundException, SchedulerException {
        Trigger trigger = getTrigger(triggerId);
        if (trigger != null) {
            deleteTrigger(triggerGroup, trigger);
        } else {
            throw new TriggerNotFoundException("No trigger found for trigger id: " + triggerId);
        }
    }

    /**
     * Deletes/Removes the {@code Trigger}. If it is a {@code ScheduledTrigger} then it is also un-scheduled from
     * scheduler.
     * @warn exception SchedulerException description missing
     *
     * @param trigger the {@code Trigger} to be removed
     */
    public void deleteTrigger(String triggerGroup, Trigger trigger) throws SchedulerException {
        triggerDao.deleteTrigger(triggerGroup, trigger);
        if (trigger instanceof ScheduledTrigger) {
            unscheduleTrigger((ScheduledTrigger) trigger);
        }
    }

    /**
     * Schedules the {@code Trigger} using the scheduler.
     * @warn exception SchedulerException description missing
     * @warn parameter scheduledTrigger description missing
     *
     * @param scheduledTrigger
     * @throws SchedulerException
     */
    public void scheduleTrigger(ScheduledTrigger scheduledTrigger) throws SchedulerException {
        if (!initialized.get())
            throw new SchedulerException("Trigger service is not initialized. initialize() must be called before calling scheduleTrigger() method");
        Map jobDataMap = new HashMap();
        jobDataMap.put(TRIGGER_OPERATOR_KEY, this);
        jobDataMap.put(TRIGGER_KEY, scheduledTrigger);
        try {
            org.quartz.Trigger quartzTrigger = newTrigger()
                    .withIdentity(triggerKey(scheduledTrigger.getId(), Scheduler.DEFAULT_GROUP))
                    .withSchedule(scheduledTrigger.getScheduleBuilder())
                    .usingJobData(new JobDataMap(jobDataMap))
                    .startAt(scheduledTrigger.getStartAt())
                    .endAt(scheduledTrigger.getEndAt())
                    .build();
            scheduler.scheduleQuartzJob(scheduledTrigger.getId(), Scheduler.DEFAULT_GROUP, ScheduledTriggerJob.class, quartzTrigger);
            scheduledTrigger.setQuartzTrigger(quartzTrigger);
            logger.info("Successfully scheduled {}", scheduledTrigger);
        } catch (org.quartz.SchedulerException e) {
            throw new SchedulerException("Exception occurred while scheduling trigger: " + scheduledTrigger, e);
        }
    }

    /**
     * A quartz job that is executed every time a {@code Trigger} is invoked.
     */
    public static class ScheduledTriggerJob implements org.quartz.Job {
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            TriggerOperator triggerOperator = (TriggerOperator) context.getMergedJobDataMap().get(TRIGGER_OPERATOR_KEY);
            ScheduledTrigger scheduledTrigger = (ScheduledTrigger) context.getMergedJobDataMap().get(TRIGGER_KEY);
            try {
                logger.info("Executing scheduledTrigger: {}, Previous fire time: {}, Next fire time: {}",
                    scheduledTrigger, scheduledTrigger.getPreviousFireTime(), scheduledTrigger.getNextFireTime());
                triggerOperator.execute(scheduledTrigger);
            } catch (Exception e) {
                throw new JobExecutionException(e);
            }
        }
    }

    /**
     * Checks if a {@code ScheduledTrigger} is scheduled in the scheduler or not.
     * @warn exception SchedulerException description missing
     * @warn parameter scheduledTrigger description missing
     *
     * @param scheduledTrigger
     * @return
     * @throws SchedulerException
     */
    public boolean isScheduled(ScheduledTrigger scheduledTrigger) throws SchedulerException {
        try {
            return scheduler.isScheduled(scheduledTrigger.getId(), Scheduler.DEFAULT_GROUP);
        } catch (org.quartz.SchedulerException e) {
            throw new SchedulerException("Exception occurred while checking isScheduled() for: " + scheduledTrigger, e);
        }
    }

    /**
     * Un-schedules the {@code Trigger}.
     * @warn exception SchedulerException description missing
     * @warn parameter scheduledTrigger description missing
     *
     * @param scheduledTrigger
     * @throws SchedulerException
     */
    public void unscheduleTrigger(ScheduledTrigger scheduledTrigger) throws SchedulerException {
        if (!initialized.get())
            throw new SchedulerException("Trigger service is not initialized. initialize() must be called before calling unscheduleTrigger() method");
        try {
            scheduler.unscheduleQuartzJob(scheduledTrigger.getId(), Scheduler.DEFAULT_GROUP);
            scheduledTrigger.setQuartzTrigger(null);
            logger.info("Successfully unscheduled {}", scheduledTrigger);
        } catch (org.quartz.SchedulerException e) {
            throw new SchedulerException("Exception occurred while unscheduling trigger: " + scheduledTrigger, e);
        }
    }

    /**
     * Returns a list of {@code Trigger}s registered with the trigger service for the given triggerGroup.
     * @warn parameter triggerGroup description missing
     *
     * @param triggerGroup
     * @return
     */
    public List<Trigger> getTriggers(String triggerGroup) {
        return triggerDao.getTriggers(triggerGroup);
    }

    /**
     * Returns a list of all the {@code Trigger}s registered with the trigger service.
     *
     * @return a list of the {@code Trigger}s that are registered with the trigger service
     */
    public List<Trigger> getTriggers() {
        return triggerDao.getTriggers();
    }

    /**
     * Executes the {@code Trigger}.
     *
     * @param triggerId the string that uniquely identifies the {@code Trigger} to be executed
     * @throws TriggerNotFoundException if there is no {@code Trigger} that matches {@code triggerId}
     * @throws Exception if an exception occurred during the execution of the {@code Trigger}
     */
    public void execute(String triggerId) throws Exception {
        Trigger trigger = getTrigger(triggerId);
        if (trigger != null) {
            execute(trigger);
        } else {
            throw new TriggerNotFoundException(String.format("No trigger found with id: %s", triggerId));
        }
    }

    /**
     * Executes the {@code Trigger}.
     *
     * @param trigger the {@code Trigger} to be executed
     * @throws Exception if an exception occurred during the execution of the {@code Trigger}
     */
    public void execute(Trigger trigger) throws Exception {
        if (trigger.isDisabled()) return;
        try {
            ((Action1) trigger.getAction().newInstance()).call(trigger.getData());
        } catch (Exception e) {
            throw new Exception(String.format("Exception occurred while executing trigger '%s'", trigger), e);
        }
    }

}