diff --git a/src/main/java/com/microsoft/azure/vmagent/AzureVMNoDelayProvisionerStrategy.java b/src/main/java/com/microsoft/azure/vmagent/AzureVMNoDelayProvisionerStrategy.java new file mode 100644 index 00000000..6a593412 --- /dev/null +++ b/src/main/java/com/microsoft/azure/vmagent/AzureVMNoDelayProvisionerStrategy.java @@ -0,0 +1,151 @@ +package com.microsoft.azure.vmagent; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Label; +import hudson.model.LoadStatistics; +import hudson.model.Queue; +import hudson.model.queue.QueueListener; +import hudson.slaves.Cloud; +import hudson.slaves.CloudProvisioningListener; +import hudson.slaves.NodeProvisioner; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import jenkins.util.Timer; + +/** + * Implementation of {@link NodeProvisioner.Strategy} which will provision a new node immediately as + * a task enter the queue. + * In Azure, we don't really need to wait before provisioning a new node, + * because Azure agents can be started and destroyed quickly + * + * @author runzexia + */ +@Extension(ordinal = AzureVMNoDelayProvisionerStrategy.ORDER) +public class AzureVMNoDelayProvisionerStrategy extends NodeProvisioner.Strategy { + + private static final Logger LOGGER = Logger.getLogger(AzureVMNoDelayProvisionerStrategy.class.getName()); + private static final boolean DISABLE_NO_DELAY_PROVISIONING = + SystemProperties.getBoolean( + AzureVMNoDelayProvisionerStrategy.class.getName() + ".disableNoDelayProvisioning"); + private static final boolean DISABLE_CLOUD_SHUFFLE = + SystemProperties.getBoolean( + AzureVMNoDelayProvisionerStrategy.class.getName() + ".disableCloudShuffle"); + public static final int ORDER = 101; + + @NonNull + @Override + public NodeProvisioner.StrategyDecision apply(@NonNull NodeProvisioner.StrategyState strategyState) { + if (DISABLE_NO_DELAY_PROVISIONING) { + LOGGER.log(Level.FINE, "Provisioning not complete, NoDelayProvisionerStrategy is disabled"); + return NodeProvisioner.StrategyDecision.CONSULT_REMAINING_STRATEGIES; + } + + final Label label = strategyState.getLabel(); + + LoadStatistics.LoadStatisticsSnapshot snapshot = strategyState.getSnapshot(); + int availableCapacity = snapshot.getAvailableExecutors() // live executors + + snapshot.getConnectingExecutors() // executors present but not yet connected + + strategyState + .getPlannedCapacitySnapshot() // capacity added by previous strategies from previous rounds + + strategyState.getAdditionalPlannedCapacity(); // capacity added by previous strategies _this round_ + int previousCapacity = availableCapacity; + int currentDemand = snapshot.getQueueLength(); + LOGGER.log( + Level.FINE, "Available capacity={0}, currentDemand={1}", new Object[] {availableCapacity, currentDemand + }); + if (availableCapacity < currentDemand) { + List jenkinsClouds = new ArrayList<>(Jenkins.get().clouds); + if (!DISABLE_CLOUD_SHUFFLE) { + Collections.shuffle(jenkinsClouds); + } + + Cloud.CloudState cloudState = new Cloud.CloudState(label, strategyState.getAdditionalPlannedCapacity()); + + searchClouds: + for (Cloud cloud : jenkinsClouds) { + int workloadToProvision = currentDemand - availableCapacity; + if (!(cloud instanceof AzureVMCloud)) { + continue; + } + if (!cloud.canProvision(cloudState)) { + continue; + } + for (CloudProvisioningListener cl : CloudProvisioningListener.all()) { + if (cl.canProvision(cloud, cloudState, workloadToProvision) != null) { + continue searchClouds; + } + } + + Collection plannedNodes = cloud.provision(cloudState, workloadToProvision); + LOGGER.log(Level.FINE, "Planned {0} new nodes", plannedNodes.size()); + fireOnStarted(cloud, strategyState.getLabel(), plannedNodes); + strategyState.recordPendingLaunches(plannedNodes); + availableCapacity += plannedNodes.size(); + LOGGER.log(Level.FINE, "After provisioning, available capacity={0}, currentDemand={1}", new Object[] { + availableCapacity, currentDemand + }); + break; + } + } + if (availableCapacity > previousCapacity && label != null) { + LOGGER.log(Level.FINE, "Suggesting NodeProvisioner review"); + Timer.get().schedule(label.nodeProvisioner::suggestReviewNow, 1L, TimeUnit.SECONDS); + } + if (availableCapacity >= currentDemand) { + LOGGER.log(Level.FINE, "Provisioning completed"); + return NodeProvisioner.StrategyDecision.PROVISIONING_COMPLETED; + } else { + LOGGER.log(Level.FINE, "Provisioning not complete, consulting remaining strategies"); + return NodeProvisioner.StrategyDecision.CONSULT_REMAINING_STRATEGIES; + } + } + + private static void fireOnStarted( + final Cloud cloud, final Label label, final Collection plannedNodes) { + for (CloudProvisioningListener cl : CloudProvisioningListener.all()) { + try { + cl.onStarted(cloud, label, plannedNodes); + } catch (Error e) { + throw e; + } catch (Throwable e) { + LOGGER.log( + Level.SEVERE, + "Unexpected uncaught exception encountered while " + + "processing onStarted() listener call in " + cl + " for label " + + label.toString(), + e); + } + } + } + + /** + * Ping the nodeProvisioner as a new task enters the queue. + */ + @Extension + public static class AzureVMFastProvisioning extends QueueListener { + + @Override + public void onEnterBuildable(Queue.BuildableItem item) { + if (DISABLE_NO_DELAY_PROVISIONING) { + return; + } + final Jenkins jenkins = Jenkins.get(); + final Label label = item.getAssignedLabel(); + for (Cloud cloud : jenkins.clouds) { + if (cloud instanceof AzureVMCloud && cloud.canProvision(new Cloud.CloudState(label, 0))) { + final NodeProvisioner provisioner = + (label == null ? jenkins.unlabeledNodeProvisioner : label.nodeProvisioner); + provisioner.suggestReviewNow(); + } + } + } + } +}