Skip to content

Commit 3a18fc3

Browse files
pjdartontimja
andauthored
When limiting VMs per template, count template VMs not all cloud VMs (#367)
Co-authored-by: Tim Jacomb <timjacomb1+github@gmail.com>
1 parent ae634e6 commit 3a18fc3

12 files changed

Lines changed: 574 additions & 152 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/bin
22
/target
33
/work
4+
/.checkstyle
45

56
*.idea
67
*.settings

src/main/java/com/microsoft/azure/vmagent/AzureVMAgent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ public synchronized void deprovision(Localizable reason) throws Exception {
661661
// Adjust estimated virtual machine count.
662662
AzureVMCloud parentCloud = getCloud();
663663
if (parentCloud != null) {
664-
parentCloud.adjustVirtualMachineCount(-1, template.getMaxVirtualMachinesLimit());
664+
parentCloud.adjustApproximateVirtualMachineCount(-1, template);
665665
}
666666

667667
Jenkins.get().removeNode(this);

src/main/java/com/microsoft/azure/vmagent/AzureVMCloud.java

Lines changed: 122 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.microsoft.azure.vmagent.util.FailureStage;
3535
import com.microsoft.azure.vmagent.util.PoolLock;
3636
import com.microsoft.jenkins.credentials.AzureResourceManagerCache;
37+
import edu.umd.cs.findbugs.annotations.CheckForNull;
3738
import edu.umd.cs.findbugs.annotations.NonNull;
3839
import hudson.Extension;
3940
import hudson.init.Initializer;
@@ -56,6 +57,8 @@
5657
import org.jenkinsci.plugins.cloudstats.CloudStatistics;
5758
import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
5859
import org.jenkinsci.plugins.cloudstats.TrackedPlannedNode;
60+
import org.kohsuke.accmod.Restricted;
61+
import org.kohsuke.accmod.restrictions.NoExternalUse;
5962
import org.kohsuke.stapler.AncestorInPath;
6063
import org.kohsuke.stapler.DataBoundConstructor;
6164
import org.kohsuke.stapler.DataBoundSetter;
@@ -71,6 +74,7 @@
7174
import java.util.HashMap;
7275
import java.util.List;
7376
import java.util.Map;
77+
import java.util.TreeMap;
7478
import java.util.concurrent.Callable;
7579
import java.util.concurrent.CopyOnWriteArrayList;
7680
import java.util.concurrent.ExecutionException;
@@ -122,7 +126,8 @@ public class AzureVMCloud extends Cloud {
122126
private transient String configurationStatus;
123127

124128
// Approximate virtual machine count. Updated periodically.
125-
private int approximateVirtualMachineCount;
129+
@CheckForNull
130+
private transient Map<String, Integer> approximateVirtualMachineCountsByTemplate;
126131

127132
private transient AzureResourceManager azureClient;
128133

@@ -358,74 +363,94 @@ public void setConfigurationStatus(String status) {
358363
}
359364

360365
/**
361-
* Retrieves the current approximate virtual machine count.
366+
* Retrieves the current approximate virtual machine count,
367+
* counting only VMs we've made.
362368
*
363369
* @return The approximate count
364370
*/
365371
public int getApproximateVirtualMachineCount() {
366372
synchronized (this) {
367-
return approximateVirtualMachineCount;
373+
int count = 0;
374+
if (approximateVirtualMachineCountsByTemplate != null) {
375+
for (final Integer templateCount : approximateVirtualMachineCountsByTemplate.values()) {
376+
count += templateCount;
377+
}
378+
}
379+
return count;
368380
}
369381
}
370382

371383
/**
372-
* Given the number of VMs that are desired, returns the number of VMs that
373-
* can be allocated and adjusts the number of VMs we believe exist.
374-
* If the number desired is less than 0, this subtracts from the total number
375-
* of virtual machines we currently have available.
384+
* Retrieves the current count of the number of virtual machines for a specific
385+
* template.
376386
*
377-
* @param delta Number that are desired, or if less than 0, the number we are 'returning' to the pool
378-
* @param templateLimit max agents for the template in use
379-
* @return Number that can be allocated, up to the number desired. 0 if the number desired was < 0
387+
* @param template the template VMs are being counted for.
388+
* @return The number of VMs of that template.
380389
*/
381-
public int adjustVirtualMachineCount(int delta, int templateLimit) {
390+
@Restricted(NoExternalUse.class)
391+
int getApproximateVirtualMachineCountForTemplate(AzureVMAgentTemplate template) {
392+
final String templateName = template.getTemplateName();
382393
synchronized (this) {
383-
if (delta < 0) {
384-
LOGGER.log(Level.FINE, "Current estimated VM count: {0}, reducing by {1}",
385-
new Object[]{approximateVirtualMachineCount, delta});
386-
approximateVirtualMachineCount = Math.max(0, approximateVirtualMachineCount + delta);
387-
return 0;
388-
} else {
389-
LOGGER.log(Level.FINE, "Current estimated VM count: {0}, quantity desired {1}",
390-
new Object[]{approximateVirtualMachineCount, delta});
391-
int limit = determineLimit(templateLimit);
392-
if (approximateVirtualMachineCount + delta <= limit) {
393-
// Enough available, return the desired quantity, and update the number we think we
394-
// have laying around.
395-
approximateVirtualMachineCount += delta;
396-
return delta;
397-
} else {
398-
// Not enough available, return what we have. Remember we could
399-
// go negative (if for instance another Jenkins instance had
400-
// a higher limit.
401-
int quantityAvailable = Math.max(0, limit - approximateVirtualMachineCount);
402-
approximateVirtualMachineCount += quantityAvailable;
403-
return quantityAvailable;
394+
if (approximateVirtualMachineCountsByTemplate != null) {
395+
final Integer templateCount = approximateVirtualMachineCountsByTemplate.get(templateName);
396+
if (templateCount != null) {
397+
return templateCount;
404398
}
405399
}
400+
return 0;
406401
}
407402
}
408403

409-
private int determineLimit(int templateLimit) {
410-
int cloudLimit = getMaxVirtualMachinesLimit();
411-
if (templateLimit > cloudLimit) {
412-
return cloudLimit;
413-
}
414-
if (templateLimit == 0) {
415-
return cloudLimit;
404+
/**
405+
* Adjusts our estimate of the current number of VMs we have for a specific
406+
* template.
407+
*
408+
* @param delta The change to make. Positive means we think we've got more
409+
* VMs, negative means we think we've got fewer.
410+
* @param template the template we're talking about.
411+
*/
412+
@Restricted(NoExternalUse.class)
413+
void adjustApproximateVirtualMachineCount(int delta, AzureVMAgentTemplate template) {
414+
final String templateName = template.getTemplateName();
415+
synchronized (this) {
416+
final int currentCount = getApproximateVirtualMachineCountForTemplate(template);
417+
final int newCount = currentCount + delta;
418+
if (approximateVirtualMachineCountsByTemplate == null) {
419+
if (newCount != 0) {
420+
setCurrentVirtualMachineCount(Collections.singletonMap(templateName, newCount));
421+
}
422+
} else {
423+
if (newCount == 0) {
424+
approximateVirtualMachineCountsByTemplate.remove(templateName);
425+
if (approximateVirtualMachineCountsByTemplate.isEmpty()) {
426+
approximateVirtualMachineCountsByTemplate = null;
427+
}
428+
} else {
429+
approximateVirtualMachineCountsByTemplate.put(templateName, newCount);
430+
}
431+
}
416432
}
417-
return templateLimit;
418433
}
419434

420435
/**
421-
* Sets the new approximate virtual machine count.
436+
* Sets the new approximate virtual machine counts.
422437
* This is run by the verification task to update the VM count periodically.
423438
*
424-
* @param newCount New approximate count
439+
* @param countsIndexedByTemplateName New approximate counts
425440
*/
426-
public void setVirtualMachineCount(int newCount) {
441+
@Restricted(NoExternalUse.class)
442+
void setCurrentVirtualMachineCount(Map<String, Integer> countsIndexedByTemplateName) {
427443
synchronized (this) {
428-
approximateVirtualMachineCount = newCount;
444+
if (countsIndexedByTemplateName == null || countsIndexedByTemplateName.isEmpty()) {
445+
approximateVirtualMachineCountsByTemplate = null;
446+
} else {
447+
if (approximateVirtualMachineCountsByTemplate == null) {
448+
approximateVirtualMachineCountsByTemplate = new TreeMap<>(countsIndexedByTemplateName);
449+
} else {
450+
approximateVirtualMachineCountsByTemplate.clear();
451+
approximateVirtualMachineCountsByTemplate.putAll(countsIndexedByTemplateName);
452+
}
453+
}
429454
}
430455
}
431456

@@ -675,26 +700,22 @@ public Collection<PlannedNode> provision(CloudState cloudState, int workLoad) {
675700
if (numberOfAgents > 0) {
676701
if (template.getMaximumDeploymentSize() > 0 && numberOfAgents > template.getMaximumDeploymentSize()) {
677702
LOGGER.log(Level.FINE,
678-
"Setting size of deployment from {0} to {1} nodes, according to template's maximum deployment size",
679-
new Object[]{numberOfAgents, template.getMaximumDeploymentSize()});
703+
"Reduced template {0} deployment from {1} to {2} nodes, due to its maximumDeploymentSize",
704+
new Object[] {template.getTemplateName(), numberOfAgents,
705+
template.getMaximumDeploymentSize() });
680706
numberOfAgents = template.getMaximumDeploymentSize();
681707
}
682708

683709
try {
684710
// Determine how many agents we can actually provision from here and
685711
// adjust our count (before deployment to avoid races)
686-
int adjustedNumberOfAgents = adjustVirtualMachineCount(numberOfAgents,
687-
template.getMaxVirtualMachinesLimit());
712+
final int adjustedNumberOfAgents;
713+
synchronized (this) {
714+
adjustedNumberOfAgents = calculateNumberOfAgentsToRequest(template, numberOfAgents);
715+
adjustApproximateVirtualMachineCount(adjustedNumberOfAgents, template);
716+
}
688717
if (adjustedNumberOfAgents == 0) {
689-
LOGGER.log(Level.INFO,
690-
"Not able to create {0} nodes, at or above maximum VM count of {1} and already {2} VM(s)",
691-
new Object[]{numberOfAgents, determineLimit(template.getMaxVirtualMachinesLimit()),
692-
getApproximateVirtualMachineCount()});
693718
return plannedNodes;
694-
} else if (adjustedNumberOfAgents < numberOfAgents) {
695-
LOGGER.log(Level.INFO,
696-
"Able to create new nodes, but can only create {0} (desired {1})",
697-
new Object[]{adjustedNumberOfAgents, numberOfAgents});
698719
}
699720
doProvision(adjustedNumberOfAgents, plannedNodes, template);
700721
// wait for deployment completion and then check for created nodes
@@ -710,6 +731,49 @@ public Collection<PlannedNode> provision(CloudState cloudState, int workLoad) {
710731
return plannedNodes;
711732
}
712733

734+
/**
735+
* Works out how many VMs our limits allow us to request.
736+
*
737+
* @param template The template in question
738+
* @param desiredNumberOfAgents The number of VMs we'd like to have if there
739+
* were no limits.
740+
* @return The number we can request before exceeding our cloud and template
741+
* limits.
742+
*/
743+
@Restricted(NoExternalUse.class) // Package access for tests only
744+
int calculateNumberOfAgentsToRequest(final AzureVMAgentTemplate template, int desiredNumberOfAgents) {
745+
final int currentVMsForTemplate = Math.max(0, getApproximateVirtualMachineCountForTemplate(template));
746+
final int maxVMsForTemplate = template.getMaxVirtualMachinesLimit() > 0
747+
? template.getMaxVirtualMachinesLimit()
748+
: Integer.MAX_VALUE;
749+
final int currentVMsForCloud = Math.max(0, getApproximateVirtualMachineCount());
750+
final int maxVMsForCloud = getMaxVirtualMachinesLimit() > 0 ? getMaxVirtualMachinesLimit()
751+
: Integer.MAX_VALUE;
752+
final int maxBeforeTemplateLimit = Math.max(0, maxVMsForTemplate - currentVMsForTemplate);
753+
final int maxBeforeCloudLimit = Math.max(0, maxVMsForCloud - currentVMsForCloud);
754+
final int adjustedNumberOfAgents = Math.min(Math.min(maxBeforeTemplateLimit, maxBeforeCloudLimit),
755+
desiredNumberOfAgents);
756+
final String templateMsg = desiredNumberOfAgents > maxBeforeTemplateLimit
757+
? ", have template limit of {3} but have {4} VMs already so we can have {5} more"
758+
: ", currently have {4} VMs of this template";
759+
final String cloudMsg = desiredNumberOfAgents > maxBeforeCloudLimit
760+
? ", have cloud limit of {6} but have {7} VMs already so we can only have {8} more"
761+
: ", currently have {7} VMs in cloud";
762+
final Object[] logParams = new Object[] {template.getTemplateName(), desiredNumberOfAgents,
763+
adjustedNumberOfAgents, maxVMsForTemplate, currentVMsForTemplate, maxBeforeTemplateLimit,
764+
maxVMsForCloud, currentVMsForCloud, maxBeforeCloudLimit };
765+
if (adjustedNumberOfAgents == 0) {
766+
LOGGER.log(Level.INFO, "Wanted to create {1} nodes from template {0} but cannot create any"
767+
+ templateMsg + cloudMsg, logParams);
768+
} else if (adjustedNumberOfAgents < desiredNumberOfAgents) {
769+
LOGGER.log(Level.INFO, "Wanted to create {1} nodes from template {0} but can only create {2}"
770+
+ templateMsg + cloudMsg, logParams);
771+
} else {
772+
LOGGER.log(Level.INFO, "Creating {1} nodes from template {0}" + templateMsg + cloudMsg, logParams);
773+
}
774+
return adjustedNumberOfAgents;
775+
}
776+
713777
public void doProvision(final int numberOfNewAgents,
714778
List<PlannedNode> plannedNodes,
715779
final AzureVMAgentTemplate template) {
@@ -858,8 +922,8 @@ private void handleFailure(
858922
// Do not throw to avoid it being recorded
859923
}
860924
}
861-
template.retrieveAzureCloudReference().adjustVirtualMachineCount(-1,
862-
template.getMaxVirtualMachinesLimit());
925+
template.retrieveAzureCloudReference().adjustApproximateVirtualMachineCount(-1,
926+
template);
863927
// Update the template status given this new issue.
864928
template.handleTemplateProvisioningFailure(e.getMessage(), stage);
865929
}

src/main/java/com/microsoft/azure/vmagent/AzureVMCloudVerificationTask.java

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.commons.lang.StringUtils;
2525

2626
import java.util.List;
27+
import java.util.Map;
2728
import java.util.logging.Level;
2829
import java.util.logging.Logger;
2930

@@ -132,7 +133,7 @@ private static void verifyCloud(String cloudName) {
132133
"AzureVMCloudVerificationTask: verify: cloud {0} already verified pass",
133134
cloudName);
134135
// Update the count.
135-
cloud.setVirtualMachineCount(getVirtualMachineCount(cloud));
136+
updateCloudVirtualMachineCounts(cloud);
136137
return;
137138
}
138139

@@ -141,7 +142,7 @@ private static void verifyCloud(String cloudName) {
141142
LOGGER.log(getStaticNormalLoggingLevel(), "AzureVMCloudVerificationTask: validate: {0} "
142143
+ "verified pass", cloudName);
143144
// Update the count
144-
cloud.setVirtualMachineCount(getVirtualMachineCount(cloud));
145+
updateCloudVirtualMachineCounts(cloud);
145146
// We grab the current VM count and
146147
cloud.setConfigurationStatus(Constants.VERIFIED_PASS);
147148
return;
@@ -181,28 +182,35 @@ public static boolean verifyConfiguration(AzureVMCloud cloud) {
181182
}
182183

183184
/**
184-
* Retrieve the current VM count.
185+
* Retrieves the current VM count for the cloud and caches the result in the
186+
* cloud instance.
185187
*
186-
* @param cloud The Azure VM Cloud
187-
* @return The current VM count
188+
* @param cloud The cloud to update.
189+
* @return true if we were successful, false if we failed (the reason will be in
190+
* the log).
188191
*/
189-
public static int getVirtualMachineCount(AzureVMCloud cloud) {
190-
LOGGER.log(getStaticNormalLoggingLevel(), "AzureVMCloudVerificationTask: getVirtualMachineCount: start");
191-
try {
192-
int vmCount = cloud.getServiceDelegate().getVirtualMachineCount(cloud.getCloudName(),
193-
cloud.getResourceGroupName());
194-
LOGGER.log(getStaticNormalLoggingLevel(),
195-
"AzureVMCloudVerificationTask: getVirtualMachineCount: end, cloud {0} has currently {1} vms",
196-
new Object[]{cloud.getCloudName(), vmCount});
197-
return vmCount;
198-
} catch (Exception e) {
192+
private static boolean updateCloudVirtualMachineCounts(AzureVMCloud cloud) {
193+
synchronized (cloud) {
199194
LOGGER.log(getStaticNormalLoggingLevel(),
200-
"AzureVMCloudVerificationTask: getVirtualMachineCount: failed to retrieve vm count:\n{0}",
201-
e.toString());
202-
// We could have failed for any number of reasons. Just return the current
203-
// number of virtual machines.
204-
return cloud.getApproximateVirtualMachineCount();
195+
"AzureVMCloudVerificationTask: updateCloudVirtualMachineCounts({0},{1}): start",
196+
new Object[]{cloud.getCloudName(), cloud.getResourceGroupName()});
197+
try {
198+
final AzureVMManagementServiceDelegate sd = cloud.getServiceDelegate();
199+
final Map<String, Integer> counts = sd.getVirtualMachineCountsByTemplate(cloud.getCloudName(),
200+
cloud.getResourceGroupName());
201+
cloud.setCurrentVirtualMachineCount(counts);
202+
LOGGER.log(getStaticNormalLoggingLevel(),
203+
"AzureVMCloudVerificationTask: updateCloudVirtualMachineCounts({0},{1}): end",
204+
new Object[]{cloud.getCloudName(), cloud.getResourceGroupName()});
205+
return true;
206+
} catch (Exception e) {
207+
LOGGER.log(getStaticNormalLoggingLevel(),
208+
"AzureVMCloudVerificationTask: updateCloudVirtualMachineCounts({0},{1}): failed\n{2}",
209+
new Object[]{cloud.getCloudName(), cloud.getResourceGroupName(), e});
210+
return false;
211+
}
205212
}
213+
206214
}
207215

208216

@@ -212,12 +220,13 @@ public static AzureVMCloud getCloud(String cloudName) {
212220

213221
@Override
214222
public void execute(TaskListener arg0) {
215-
for (Cloud cloud : Jenkins.get().clouds) {
216-
if (cloud instanceof AzureVMCloud) {
217-
AzureVMCloud azureVMCloud = (AzureVMCloud) cloud;
218-
synchronized (azureVMCloud) {
219-
azureVMCloud.setVirtualMachineCount(getVirtualMachineCount(azureVMCloud));
220-
}
223+
for (final Cloud anyTypeOfCloud : Jenkins.get().clouds) {
224+
if (!(anyTypeOfCloud instanceof AzureVMCloud)) {
225+
continue; // not one of ours; ignore.
226+
}
227+
final AzureVMCloud cloud = (AzureVMCloud) anyTypeOfCloud;
228+
synchronized (cloud) {
229+
updateCloudVirtualMachineCounts(cloud);
221230
}
222231
}
223232
}

0 commit comments

Comments
 (0)