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

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

/*
 * Copyright (c) 2016 University Nice Sophia Antipolis
 *
 * This file is part of btrplace.
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 */

package org.btrplace.scheduler.choco.view;

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.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.chocosolver.solver.Cause;
import org.chocosolver.solver.Solver;
import org.chocosolver.solver.constraints.IntConstraintFactory;
import org.chocosolver.solver.exception.ContradictionException;
import org.chocosolver.solver.search.solution.Solution;
import org.chocosolver.solver.variables.IntVar;
import org.chocosolver.solver.variables.RealVar;
import org.chocosolver.solver.variables.VariableFactory;

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 ShareableResource rc;

    private List phyRcUsage;

    private List virtRcUsage;

    private List vmAllocation;

    private List ratios;

    private ReconfigurationProblem rp;

    private Solver solver;

    private String id;

    private Model source;

    private Map references;
    private Map clones;

    private TObjectDoubleMap wantedRatios;
    private TObjectIntMap wantedAmount;
    private TObjectIntMap wantedCapacity;
    /**
     * 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();
        wantedCapacity = new TObjectIntHashMap<>();
        wantedAmount = new TObjectIntHashMap<>();
        wantedRatios = new TObjectDoubleHashMap<>();
    }

    @Override
    public boolean inject(Parameters ps, ReconfigurationProblem p) throws SchedulerException {
        this.rp = p;
        this.references = new HashMap<>();
        this.clones = new HashMap<>();
        solver = p.getSolver();
        this.source = p.getSourceModel();
        List nodes = p.getNodes();
        phyRcUsage = new ArrayList<>(nodes.size());
        virtRcUsage = new ArrayList<>(nodes.size());
        this.ratios = new ArrayList<>(nodes.size());
        id = ShareableResource.VIEW_ID_BASE + rc.getResourceIdentifier();
        for (Node nId : p.getNodes()) {
            phyRcUsage.add(VariableFactory.bounded(p.makeVarLabel("phyRcUsage('", rc.getResourceIdentifier(), "', '", nId, "')"), 0, rc.getCapacity(nId), p.getSolver()));
            virtRcUsage.add(VariableFactory.bounded(p.makeVarLabel("virtRcUsage('", rc.getResourceIdentifier(), "', '", nId, "')"), 0, Integer.MAX_VALUE / 100, p.getSolver()));
            ratios.add(VariableFactory.real(p.makeVarLabel("overbook('", rc.getResourceIdentifier(), "', '", nId, "')"), 1, UNCHECKED_RATIO, 0.01, p.getSolver()));
        }
        phyRcUsage = Collections.unmodifiableList(phyRcUsage);
        virtRcUsage = Collections.unmodifiableList(virtRcUsage);
        ratios = Collections.unmodifiableList(ratios);

        //Bin packing for the node vmAllocation
        Solver s = p.getSolver();
        List notNullUsage = new ArrayList<>();
        List hosts = new ArrayList<>();

        vmAllocation = new ArrayList<>();
        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(VariableFactory.fixed(p.makeVarLabel("cste -- " + "vmAllocation('", rc.getResourceIdentifier(), "', '", vmId, "'"), 0, s));
            } else {
                //We don't know about the next VM usage for the moment, -1 is used by default to allow to detect an
                //non-updated value.
                IntVar v = VariableFactory.bounded(p.makeVarLabel("vmAllocation('", rc.getResourceIdentifier(), "', '", vmId, "')"), -1, Integer.MAX_VALUE / 1000, s);
                vmAllocation.add(v);
                notNullUsage.add(v);
                hosts.add(slice.getHoster());
            }
        }
        vmAllocation = Collections.unmodifiableList(vmAllocation);
        //We create a BP with only the VMs requiring a not null amount of resources
        ChocoView v = rp.getView(Packing.VIEW_ID);
        if (v == null) {
            throw new SchedulerException(rp.getSourceModel(), "View '" + Cumulatives.VIEW_ID + "' is required but missing");
        }
        ((Packing) v).addDim(rc.getResourceIdentifier(),
                virtRcUsage,
                notNullUsage.toArray(new IntVar[notNullUsage.size()]),
                hosts.toArray(new IntVar[hosts.size()]));

        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;
    }

    /**
     * Get the virtual resource usage of a given node.
     * Warning: the only possible approach to restrict the value is to increase their
     * upper bound using the associated {@code setSup()} method
     *
     * @param nIdx the node identifier
     * @return the variable denoting the resource usage for the node.
     */
    public IntVar getVirtualUsage(int nIdx) {
        return virtRcUsage.get(nIdx);
    }

    /**
     * Get the amount of virtual resource to allocate to each VM.
     * Warning: the only possible approach to restrict these value is to increase their
     * lower bound using the associated {@code setInf()} method
     *
     * @return an array of variables denoting each VM vmAllocation
     */
    public List getVMsAllocation() {
        return vmAllocation;
    }

    /**
     * Get the amount of virtual resource to allocate a given VM.
     * Warning: the only possible approach to restrict this value is to increase their
     * lower bound using the associated {@code setInf()} method
     *
     * @param vmIdx the VM identifier
     * @return the variable denoting the virtual resources to allocate to the VM
     */
    public IntVar getVMsAllocation(int vmIdx) {
        return vmAllocation.get(vmIdx);
    }

    /**
     * Get the overbooking ratio for a node.
     * WARNING: it is only allowed to reduce the upper-bound of the ratio using {@code #setSup(x)} methods
     *
     * @param nId the node identifier
     * @return an array of ratios.
     */
    public RealVar getOverbookRatio(int nId) {
        return ratios.get(nId);
    }

    /**
     * Get the overbooking ratios for every nodes.
     * WARNING: it is only allowed to reduce the upper-bound of the ratio using {@code #setSup(x)} methods
     *
     * @return an array of ratios.
     */
    public List getOverbookRatios() {
        return ratios;
    }

    /**
     * Generate and addDim an {@link org.btrplace.plan.event.Allocate} action if the amount of
     * resources allocated to a VM has changed.
     * The action schedule must be known.
     *
     * @param e    the VM identifier
     * @param node the identifier of the node that is currently hosting the VM
     * @param st   the moment that action starts
     * @param ed   the moment the action ends
     * @return {@code true} if the action has been added to the plan,{@code false} otherwise
     */
    public boolean addAllocateAction(ReconfigurationPlan plan, VM e, Node node, int st, int ed) {
        int use = vmAllocation.get(rp.getVM(e)).getLB();
        if (rc.getConsumption(e) != use) {
            //The allocation has changed
            Allocate a = new Allocate(e, node, rc.getIdentifier(), use, st, ed);
            return plan.add(a);
        }
        return false;
    }

    @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 {
        for (VM vm : source.getMapping().getAllVMs()) {
            int vmId = p.getVM(vm);
            IntVar v = vmAllocation.get(vmId);
            if (v.getLB() < 0) {
                int prevUsage = rc.getConsumption(vm);
                try {
                    v.updateLowerBound(prevUsage, Cause.Null);
                } catch (ContradictionException e) {
                    p.getLogger().error("Unable to set the minimal '" + rc.getResourceIdentifier() + "' usage for " + vm + " to its current usage (" + prevUsage + ")", e);
                    return false;
                }
            } else {
                try {
                    v.updateLowerBound(v.getLB(), Cause.Null);
                } catch (ContradictionException e) {
                    p.getLogger().error("Unable to set the VM '" + rc.getResourceIdentifier() + "' consumption to '" + v.getLB() + "'", e);
                    return false;
                }
            }
        }
        return linkVirtualToPhysicalUsage();
    }

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

        for (VM vm : r.getFutureRunningVMs()) {
            Slice dSlice = r.getVMAction(vm).getDSlice();
            Node destNode = r.getNode(s.getIntVal(dSlice.getHoster()));

            if (srcMapping.isRunning(vm) && destNode == 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(s, p, vm, destNode, s.getIntVal(dSlice.getStart()));
            } else {
                //TODO: not constant time operation. Maybe a big failure
                VM dVM = clones.containsKey(vm) ? clones.get(vm) : vm;
                for (Action a : p.getActions()) {
                    if (a instanceof RunningVMPlacement) {
                        RunningVMPlacement tmp = (RunningVMPlacement) a;
                        if (tmp.getVM() == 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);
                            }
                            break;
                        }
                    }
                }
            }
        }
        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 = 0;
        IntVar nowI = getVMsAllocation(rp.getVM(sVM));
        if (nowI != null) {
            now = nowI.getLB();
        }
        if (prev != now) {
            AllocateEvent ev = new AllocateEvent(vm, getResourceIdentifier(), now);
            a.addEvent(h, ev);
        }
    }

    private boolean insertAllocateAction(Solution s, ReconfigurationPlan p, VM vm, Node destNode, int st) {
        String rcId = getResourceIdentifier();
        int prev = rc.getConsumption(vm);
        int now = s.getIntVal(getVMsAllocation().get(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 ) consumptionfor 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 + 1) + nbZeroes;
        try {
            //Restrict the hosting capacity.
            rp.getNbRunningVMs().get(nIdx).updateUpperBound(card, Cause.Null);
        } catch (ContradictionException ex) {
            rp.getLogger().error("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 (IntVar v : vmAllocation) {
            if (v.getLB() > 0) {
                //Catch the minimum but > 0 usage
                min = Math.min(v.getLB(), 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) {
                dUse.add(vmAllocation.get(rp.getVM(vm)));
            }
        }

        Cumulatives v = (Cumulatives) rp.getView(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) {
                    throw new SchedulerException(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).getUB();
        if (r == UNCHECKED_RATIO) {
            //Default overbooking ratio is 1.
            r = 1;
        }

        try {
            ratios.get(nIdx).updateBounds(r, r, Cause.Null);
        } catch (ContradictionException ex) {
            rp.getLogger().error("Unable to set '" + ratios.get(nIdx) + " ' to " + r, ex);
            return false;
        }


        if (r == 1) {
            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) {
            solver.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().error("Unable to restrict the physical '" + getResourceIdentifier() + "' capacity of " + n + " to " + maxPhy, ex);
            return false;
        }
        return true;
    }

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

    private double ratio(Node n, double d) {
        if (wantedRatios.containsKey(n)) {
            return Math.min(d, wantedRatios.get(n));
        }
        return d;
    }

    private int consumption(VM v, int a) {
        if (wantedAmount.containsKey(v)) {
            return Math.max(a, wantedAmount.get(v));
        }
        return a;
    }

    private int capacity(Node n, int a) {
        if (wantedCapacity.containsKey(n)) {
            return Math.max(a, wantedCapacity.get(n));
        }
        return a;
    }

    /**
     * {@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) {
        for (SatConstraint c : i.getSatConstraints()) {
            if (!(c instanceof ResourceRelated && ((ResourceRelated) c).getResource().equals(rc.getResourceIdentifier()))) {
                continue;
            }
            if (c instanceof Preserve) {
                VM v = c.getInvolvedVMs().iterator().next();
                wantedAmount.put(v, consumption(v, ((Preserve) c).getAmount()));
            } else if (c instanceof Overbook) {
                Node n = c.getInvolvedNodes().iterator().next();
                wantedRatios.put(n, ratio(n, ((Overbook) c).getRatio()));
            } else if (c instanceof ResourceCapacity && c.getInvolvedNodes().size() == 1) {
                Node n = c.getInvolvedNodes().iterator().next();
                wantedCapacity.put(n, capacity(n, ((ResourceCapacity) c).getAmount()));
            }
        }
        Mapping m = i.getModel().getMapping();
        Set candidates = new HashSet<>();
        for (Node n : m.getOnlineNodes()) {
            if (overloaded(m, n)) {
                candidates.addAll(m.getRunningVMs(n));
            }
        }
        return candidates;
    }

    private boolean overloaded(Mapping m, Node n) {
        int free = rc.getCapacity(n);
        if (wantedCapacity.containsKey(n)) {
            free -= wantedCapacity.get(n);
        }
        if (wantedRatios.containsKey(n)) {
            free *= wantedRatios.get(n);
        }
        for (VM vm : m.getRunningVMs(n)) {
            if (wantedAmount.containsKey(vm)) {
                free -= wantedAmount.get(vm);
            } 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() {
        return Arrays.asList(Packing.VIEW_ID, Cumulatives.VIEW_ID);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy