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

org.btrplace.scheduler.choco.view.CShareableResource Maven / Gradle / Ivy

/*
 * Copyright  2022 The BtrPlace Authors. All rights reserved.
 * Use of this source code is governed by a LGPL-style
 * license that can be found in the LICENSE.txt file.
 */

package org.btrplace.scheduler.choco.view;

import gnu.trove.list.TDoubleList;
import gnu.trove.list.TIntList;
import gnu.trove.list.array.TDoubleArrayList;
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.map.TObjectDoubleMap;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectDoubleHashMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import org.btrplace.model.*;
import org.btrplace.model.constraint.Overbook;
import org.btrplace.model.constraint.Preserve;
import org.btrplace.model.constraint.ResourceCapacity;
import org.btrplace.model.constraint.SatConstraint;
import org.btrplace.model.view.ResourceRelated;
import org.btrplace.model.view.ShareableResource;
import org.btrplace.plan.ReconfigurationPlan;
import org.btrplace.plan.event.*;
import org.btrplace.scheduler.SchedulerException;
import org.btrplace.scheduler.SchedulerModelingException;
import org.btrplace.scheduler.choco.Parameters;
import org.btrplace.scheduler.choco.ReconfigurationProblem;
import org.btrplace.scheduler.choco.Slice;
import org.btrplace.scheduler.choco.extensions.RoundedUpDivision;
import org.btrplace.scheduler.choco.transition.VMTransition;
import org.btrplace.util.IntMap;
import org.chocosolver.solver.Cause;
import org.chocosolver.solver.Solution;
import org.chocosolver.solver.exception.ContradictionException;
import org.chocosolver.solver.variables.IntVar;

import java.util.*;

/**
 * Specify, for a given resource, the physical resource usage associated to each server,
 * and the virtual resource usage consumed by each of the VMs they host.
 *
 * @author Fabien Hermenier
 */
public class CShareableResource implements ChocoView {

    private final ShareableResource rc;

    private List phyRcUsage;

    private List virtRcUsage;

    /**
     * Next VM resource usage.
     */
    private TIntList vmAllocation;

    /**
     * Next node capacity.
     */
    private TIntList nodeCapacity;

    private TDoubleList ratios;

    private ReconfigurationProblem rp;

    private org.chocosolver.solver.Model csp;

    private String id;

    private Model source;

    private Map references;
    private Map clones;


    /**
     * VMs have changing demands.
     */
    private Set changing;

    /**
     * The default value of ratio is not logical to detect an unchanged value
     */
    public static final double UNCHECKED_RATIO = Double.MAX_VALUE / 100;

    /**
     * Make a new mapping.
     *
     * @param r the resource to consider
     */
    public CShareableResource(ShareableResource r) throws SchedulerException {
        this.rc = r;
        this.id = r.getIdentifier();
    }

    @Override
    public boolean inject(Parameters ps, ReconfigurationProblem p) throws SchedulerException {
        this.rp = p;
        this.references = new HashMap<>();
        this.clones = new HashMap<>();
        csp = p.getModel();
        this.source = p.getSourceModel();
        List nodes = p.getNodes();
        phyRcUsage = new ArrayList<>(nodes.size());
        virtRcUsage = new ArrayList<>(nodes.size());
        this.ratios = new TDoubleArrayList(nodes.size());
        id = ShareableResource.VIEW_ID_BASE + rc.getResourceIdentifier();
        for (Node nId : p.getNodes()) {
            final IntVar phyU;
            final IntVar virtU;
            if (rp.labelVariables()) {
                phyU = csp.intVar(p.makeVarLabel("phyRcUsage('", rc.getResourceIdentifier(), "', '", nId, "')"), 0, rc.getCapacity(nId), true);
                virtU = csp.intVar(p.makeVarLabel("virtRcUsage('", rc.getResourceIdentifier(), "', '", nId, "')"), 0, Integer.MAX_VALUE / 100, true);
            } else {
                phyU = csp.intVar("", 0, rc.getCapacity(nId), true);
                virtU = csp.intVar("", 0, Integer.MAX_VALUE / 100, true);
            }
            phyRcUsage.add(phyU);
            virtRcUsage.add(virtU);
            ratios.add(UNCHECKED_RATIO);
        }
        phyRcUsage = Collections.unmodifiableList(phyRcUsage);
        virtRcUsage = Collections.unmodifiableList(virtRcUsage);

        //Bin packing for the node vmAllocation

        vmAllocation = new TIntArrayList();
        for (VM vmId : p.getVMs()) {
            VMTransition a = p.getVMAction(vmId);
            Slice slice = a.getDSlice();
            if (slice == null) {
                //The VMs will not be running, so its consumption is set to 0
                vmAllocation.add(0);
            } else {
                vmAllocation.add(-1);
            }
        }

        //We don't know about the next node capacity for the moment, -1
        // is used by default to detect an non-updated value.
        nodeCapacity = new TIntArrayList(p.getNodes().size());
        nodeCapacity.fill(0, p.getNodes().size(), -1);
        return true;

    }

    /**
     * Get the resource identifier.
     *
     * @return an identifier
     */
    public String getResourceIdentifier() {
        return rc.getResourceIdentifier();
    }

    /**
     * Get the original resource node physical capacity and VM consumption.
     *
     * @return an {@link ShareableResource}
     */
    public ShareableResource getSourceResource() {
        return rc;
    }

    /**
     * Get the physical resource usage of each node.
     *
     * @return an array of variable denoting the resource usage for each node.
     */
    public List getPhysicalUsage() {
        return phyRcUsage;
    }

    /**
     * Get the physical resource usage of a given node
     *
     * @param nIdx the node identifier
     * @return the variable denoting the resource usage for the node.
     */
    public IntVar getPhysicalUsage(int nIdx) {
        return phyRcUsage.get(nIdx);
    }

    /**
     * Get the virtual resource usage  that is made by the VMs on the nodes.
     * Warning: the only possible approach to restrict these value is to increase their
     * upper bound using the associated {@code setSup()} method
     *
     * @return an immutable list of variables denoting each node virtual resource usage.
     */
    public List getVirtualUsage() {
        return virtRcUsage;
    }

    /**
     * Change the minimal VM resource allocation.
     * For example, with a first request of 7 and a second request of 4, then
     * the retained minimal allocation will be 7.
     *
     * @param vmIdx the VM identifier
     * @param v     the amount to ask.
     * @return the retained value. May be bigger than {@code v} if a previous call asks for more
     */
    public int minVMAllocation(int vmIdx, int v) {
        int vv = Math.max(v, vmAllocation.get(vmIdx));
        vmAllocation.set(vmIdx, vv);
        return vv;
    }

    /**
     * Change the minimal node resource capacity to provide.
     * For example, with a first request of 7 and a second request of 4, then
     * the retained minimal capacity will be 7.
     * @param nodeIdx the node identifier
     * @param v       the expected capacity.
     * @return the retained value. May be bigger than {@code v} if a previous call asks for more
     */
    public int minNodeCapacity(int nodeIdx, int v) {
        int vv = Math.max(v, nodeCapacity.get(nodeIdx));
        nodeCapacity.set(nodeIdx, vv);
        return vv;
    }

    /**
     * Get the future node capacity.
     *
     * @param nodeIdx the node identifier.
     * @return the capacity.
     */
    public int getFutureNodeCapacity(int nodeIdx) {
        final int next = nodeCapacity.get(nodeIdx);
        if (next == -1) {
            // The value has not been changed by a ResourceCapacity constraint.
            // Thus we consider that the capacity equals the current one.
            return rc.getCapacity(rp.getNode(nodeIdx));
        }
        return next;
    }

    /**
     * Get the future amount of resource to allocate to a VM.
     *
     * @param vmIdx the VM identifier
     * @return the variable denoting the virtual resources to allocate to the VM
     */
    public int getFutureVMAllocation(int vmIdx) {
        final int next = vmAllocation.get(vmIdx);
        if (next == -1) {
            // The value has not been changed by a Preserve constraint.
            // Thus we consider that the demands equals the current one.
            return rc.getConsumption(rp.getVM(vmIdx));
        }
        return next;
    }

    /**
     * Get the overbooking ratio for a node.
     *
     * @param nId the node identifier
     * @return a ratio >= 1
     */
    public double getOverbookRatio(int nId) {
        return ratios.get(nId);
    }

    /**
     * Cap the overbooking ratio for a given node.
     * @param nIdx the node
     * @param d the new ratio. {@code >= 1}
     * @return the resulting ratio. Will be lower than {@code d} if a previous cap stated a lower value
     */
    public double capOverbookRatio(int nIdx, double d) {
        if (d < 1) {
            return ratios.get(nIdx);
        }
        double v = Math.min(ratios.get(nIdx), d);
        ratios.set(nIdx, v);
        return v;
    }

    @Override
    public String getIdentifier() {
        return id;
    }

    @Override
    public String toString() {
        return id;
    }

    /**
     * Set the resource usage for each of the VM.
     * If the LB is < 0 , the previous consumption is used to maintain the resource usage.
     * Otherwise, the usage is set to the variable lower bound.
     *
     * @return false if an operation leads to a problem without solution
     */
    @Override
    public boolean beforeSolve(ReconfigurationProblem p) throws SchedulerException {

        ChocoView v = rp.getRequiredView(Packing.VIEW_ID);
        changing = new HashSet<>();
        IntVar[] host = new IntVar[p.getFutureRunningVMs().size()];
        int[] demand = new int[host.length];
        int i = 0;
        for (VM vm : p.getFutureRunningVMs()) {
            host[i] = rp.getVMAction(vm).getDSlice().getHoster();
            demand[i] = getFutureVMAllocation(p.getVM(vm));
            if (demand[i] != rc.getConsumption(vm)) {
                // The VM is asking for a change.
                changing.add(vm);
            }
            i++;
        }
        ((Packing) v).addDim(rc.getResourceIdentifier(),
            virtRcUsage, demand, host);

        return linkVirtualToPhysicalUsage();
    }

    private VM destVM(VM vm) {
        return clones.containsKey(vm) ? clones.get(vm) : vm;
    }

    @Override
    public boolean insertActions(ReconfigurationProblem r, Solution s, ReconfigurationPlan p) {
        Mapping srcMapping = r.getSourceModel().getMapping();

        // Encache the VM -> Action to ease the event injection. Focus only on the VMs that are changing (possibly
        // indentified by a clone).
        Map actions = new HashMap<>();
        p.getActions().stream().filter(RunningVMPlacement.class::isInstance)
                .map(a -> (RunningVMPlacement) a)
                .filter(a -> changing.contains(a.getVM()) || clones.containsValue(a.getVM()))
                .forEach(a -> actions.put(destVM(a.getVM()), (Action) a));
        for (VM vm : changing) {
            Slice dSlice = r.getVMAction(vm).getDSlice();
            Node destNode = r.getNode(s.getIntVal(dSlice.getHoster()));

            if (srcMapping.isRunning(vm) && destNode.equals(srcMapping.getVMLocation(vm))) {
                //Was running and stay on the same node
                //Check if the VM has been cloned
                //TODO: might be too late depending on the symmetry breaking on the actions schedule
                insertAllocateAction(p, vm, destNode, s.getIntVal(dSlice.getStart()));
            } else {
                VM dVM = destVM(vm);
                Action a = actions.get(dVM);
                if (a == null) {
                    // This is explained by a VM being clones. Accordingly, the action is assigned to its clone.
                    a = actions.get(clones.get(dVM));
                }
                if (a instanceof MigrateVM) {
                    //For a migrated VM, we allocate once the migration over
                    insertAllocateEvent(a, Action.Hook.POST, dVM);
                } else {
                    //Resume or Boot VM
                    //As the VM was not running, we pre-allocate
                    insertAllocateEvent(a, Action.Hook.PRE, dVM);
                }
            }
        }
        return true;
    }

    private void insertAllocateEvent(Action a, Action.Hook h, VM vm) {
        int prev = 0;
        VM sVM = references.containsKey(vm) ? references.get(vm) : vm;
        if (rc.consumptionDefined(sVM)) {
            prev = rc.getConsumption(sVM);
        }
        int now = getFutureVMAllocation(rp.getVM(sVM));
        if (prev != now) {
            AllocateEvent ev = new AllocateEvent(vm, getResourceIdentifier(), now);
            a.addEvent(h, ev);
        }
    }

    private boolean insertAllocateAction(ReconfigurationPlan p, VM vm, Node destNode, int st) {
        String rcId = getResourceIdentifier();
        int prev = rc.getConsumption(vm);
        int now = getFutureVMAllocation(rp.getVM(vm));
        if (prev != now) {
            Allocate a = new Allocate(vm, destNode, rcId, now, st, st);
            return p.add(a);
        }
        return false;
    }

    /**
     * Reduce the cardinality wrt. the worst case scenario.
     *
     * @param nIdx the node index
     * @param min  the min (but > 0 ) consumption for a VM
     * @param nbZeroes the number of VMs consuming 0
     * @return {@code false} if the problem no longer has a solution
     */
    private boolean capHosting(int nIdx, int min, int nbZeroes) {
        Node n = rp.getNode(nIdx);
        double capa = getSourceResource().getCapacity(n) * getOverbookRatio(nIdx)/*.getLB()*/;
      int card = (int) (capa / min) + nbZeroes + 1;
      if (card < source.getMapping().getRunningVMs(n).size()) {
        // This shortcut is required to prevent a filtering issue in the scheduling phase:
        // At time 0, LocalTaskScheduler will complain and start to backtrack.
        // TODO: revise the notion of continuous constraint for the cardinality issue.
        return true;
      }
        try {
            //Restrict the hosting capacity.
            rp.getNbRunningVMs().get(nIdx).updateUpperBound(card, Cause.Null);
        } catch (ContradictionException ex) {
            rp.getLogger().debug("Unable to cap the hosting capacity of '" + n + " ' to " + card, ex);
            return false;
        }
        return true;
    }

    private boolean linkVirtualToPhysicalUsage() throws SchedulerException {
        int min = Integer.MAX_VALUE;

        //Number of VMs with a 0 usage
        int nbZeroes = 0;

        for (int vId = 0; vId < vmAllocation.size(); vId++) {
            int alloc = vmAllocation.get(vId);
            if (alloc > 0) {
                min = Math.min(alloc, min);
            } else {
                nbZeroes++;
            }
        }
        for (int nIdx = 0; nIdx < ratios.size(); nIdx++) {
            if (!linkVirtualToPhysicalUsage(nIdx)) {
                return false;
            }
            if (!capHosting(nIdx, min, nbZeroes)) {
                return false;
            }
        }

        //The slice scheduling constraint that is necessary
        TIntArrayList cUse = new TIntArrayList();
        List dUse = new ArrayList<>();

        for (VMTransition a : rp.getVMActions()) {
            VM vm = a.getVM();
            Slice c = a.getCSlice();
            Slice d = a.getDSlice();
            if (c != null) {
                cUse.add(getSourceResource().getConsumption(vm));
            }
            if (d != null) {
                int m = getFutureVMAllocation(rp.getVM(vm));
                final IntVar var;
                if (rp.labelVariables()) {
                    var = rp.fixed(m, "vmAllocation('", getResourceIdentifier(), "', '", vm, "'");
                } else {
                    var = csp.intVar(m);
                }
                dUse.add(var);
            }
        }

        Cumulatives v = (Cumulatives) rp.getRequiredView(Cumulatives.VIEW_ID);
        v.addDim(virtRcUsage, cUse.toArray(), dUse.toArray(new IntVar[dUse.size()]));

        checkInitialSatisfaction();
        return true;
    }

    /**
     * Check if the initial capacity > sum current consumption
     * The ratio is instantiated now so the computation is correct
     */
    private void checkInitialSatisfaction() {
        //Seems to me we don't support ratio change
        for (Node n : rp.getSourceModel().getMapping().getOnlineNodes()) {
            int nIdx = rp.getNode(n);
            double ratio = getOverbookRatio(nIdx)/*.getLB()*/;
            double capa = getSourceResource().getCapacity(n) * ratio;
            int usage = 0;
            for (VM vm : rp.getSourceModel().getMapping().getRunningVMs(n)) {
                usage += getSourceResource().getConsumption(vm);
                if (usage > capa) {
                    //Here, the problem is not feasible but we consider an exception
                    //because such a situation does not physically makes sense (one cannot run at 110%)
                    throw new SchedulerModelingException(rp.getSourceModel(), "Usage of virtual resource " + getResourceIdentifier() + " on node " + n + " (" + usage + ") exceeds its capacity (" + capa + ")");
                }
            }
        }
    }

    private boolean linkVirtualToPhysicalUsage(int nIdx) {
        double r = ratios.get(nIdx);
        if (r == UNCHECKED_RATIO) {
            //Default overbooking ratio is 1.
            r = 1;
            capOverbookRatio(nIdx, r);
            return noOverbook(nIdx);
        }
        return overbook(nIdx, r);
    }

    private boolean overbook(int nIdx, double r) {
        Node n = rp.getNode(nIdx);
        int maxPhy = getSourceResource().getCapacity(n);
        int maxVirt = (int) (maxPhy * r);
        if (maxVirt != 0) {
            csp.post(new RoundedUpDivision(phyRcUsage.get(nIdx), virtRcUsage.get(nIdx), r));
            return true;
        }

        try {
            phyRcUsage.get(nIdx).instantiateTo(0, Cause.Null);
        } catch (ContradictionException ex) {
            rp.getLogger().debug("Unable to restrict the physical '" + getResourceIdentifier() + "' capacity of " + n + " to " + maxPhy, ex);
            return false;
        }
        return true;
    }

    private boolean noOverbook(int nIdx) {
        csp.post(csp.arithm(phyRcUsage.get(nIdx), "=", virtRcUsage.get(nIdx)));
        try {
            virtRcUsage.get(nIdx).updateUpperBound(phyRcUsage.get(nIdx).getUB(), Cause.Null);
        } catch (ContradictionException ex) {
            rp.getLogger().debug("Unable to restrict the virtual '" + getResourceIdentifier() + "' capacity of " + rp.getNode(nIdx) + " to " + phyRcUsage.get(nIdx).getUB(), ex);
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * @param i the model to use to inspect the VMs.
     * @return the set of VMs that cannot have their associated {@link Preserve} constraint satisfy with regards
     * to a possible {@link Overbook} and single-node {@link ResourceCapacity} constraint.
     */
    @Override
    public Set getMisPlacedVMs(Instance i) {

        final TObjectDoubleMap wantedRatios =
                new TObjectDoubleHashMap<>();
        final int nbVMs = i.getModel().getMapping().getNbVMs();
        final int nbNodes = i.getModel().getMapping().getNbNodes();
        final IntMap wantedAmount = new IntMap(0, nbVMs);
        final IntMap wantedCapacity = new IntMap(0, nbNodes);

        for (SatConstraint c : i.getSatConstraints()) {
            if (!(c instanceof ResourceRelated && ((ResourceRelated) c).getResource().equals(rc.getResourceIdentifier()))) {
                continue;
            }
            if (c instanceof Preserve) {
                // We guarantee the highest request so far.
                VM v = c.getInvolvedVMs().iterator().next();
                int qty = ((Preserve) c).getAmount();
                wantedAmount.put(v.id(), Math.max(wantedAmount.get(v.id()), qty));
            } else if (c instanceof Overbook) {
                Node n = c.getInvolvedNodes().iterator().next();
                double min = ((Overbook) c).getRatio();
                if (wantedRatios.containsKey(n)) {
                    min = Math.min(min, wantedRatios.get(n));
                }
                wantedRatios.put(n, min);
            } else if (c instanceof ResourceCapacity && c.getInvolvedNodes().size() == 1) {
                Node n = c.getInvolvedNodes().iterator().next();
                int qty = ((ResourceCapacity) c).getAmount();
                wantedCapacity.put(n.id(), Math.max(qty, wantedCapacity.get(n.id())));
            }
        }
        Mapping m = i.getModel().getMapping();
        Set candidates = new HashSet<>();
        for (Node n : m.getOnlineNodes()) {
            Set running = m.getRunningVMs(n);
            if (overloaded(wantedRatios, wantedAmount, wantedCapacity, running, n)) {
                candidates.addAll(running);
            }
        }
        return candidates;
    }

    /**
     * State if a given node is overloaded.
     *
     * @param wantedRatios   the desired overbooking ratio if modified.
     * @param wantedAmount   the desired allocation per VM if modified.
     * @param wantedCapacity the desired node capacity if modified.
     * @param vms            the running VMs.
     * @param n              the hosting node.
     * @return {@code true} iff the node is overloaded.
     */
    private boolean overloaded(final TObjectDoubleMap wantedRatios,
                               final IntMap wantedAmount,
                               final IntMap wantedCapacity,
                               final Collection vms, Node n) {

        // By default, free is the current capacity.
        int free = rc.getCapacity(n);
        if (wantedCapacity.has(n.id())) {
            free = wantedCapacity.get(n.id());
        }
        if (wantedRatios.containsKey(n)) {
            free *= (int) (wantedRatios.get(n));
        }
        for (VM vm : vms) {
            if (wantedAmount.has(vm.id())) {
                free -= wantedAmount.get(vm.id());
            } else {
                free -= rc.getConsumption(vm);
            }
        }
        return free < 0;
    }

    @Override
    public boolean cloneVM(VM vm, VM clone) {
        this.references.put(clone, vm);
        this.clones.put(vm, clone);
        return true;
    }

    @Override
    public List getDependencies(ReconfigurationProblem rp) {
        return Arrays.asList(Packing.VIEW_ID, Cumulatives.VIEW_ID);
    }

    /**
     * Estimate the weight of each VMs with regards to multiple dimensions.
     * In practice, it sums the normalised size of each VM against the total capacity
     *
     * @param rp  the problem to solve
     * @param rcs the resources to consider
     * @return a weight per VM
     */
    public static TObjectIntMap getWeights(ReconfigurationProblem rp, List rcs) {
        Model mo = rp.getSourceModel();

        int[] capa = new int[rcs.size()];
        int[] cons = new int[rcs.size()];
        TObjectIntMap cost = new TObjectIntHashMap<>(mo.getMapping().getNbVMs());
        for (Node n : mo.getMapping().getAllNodes()) {
            for (int i = 0; i < rcs.size(); i++) {
                capa[i] += (int) (rcs.get(i).virtRcUsage.get(rp.getNode(n)).getUB() * rcs.get(i).ratios.get(rp.getNode(n)));
            }
        }

        for (VM v : mo.getMapping().getAllVMs()) {
            for (int i = 0; i < rcs.size(); i++) {
                cons[i] += rcs.get(i).getFutureVMAllocation(rp.getVM(v));
            }
        }

        for (VM v : mo.getMapping().getAllVMs()) {
            double sum = 0;
            for (int i = 0; i < rcs.size(); i++) {
                double ratio = 0;
                if (cons[i] > 0) {
                    ratio = 1.0 * rcs.get(i).getFutureVMAllocation(rp.getVM(v)) / capa[i];
                }
                sum += ratio;
            }
            cost.put(v, (int) (sum * 10000));
        }
        return cost;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy