Skip to content

Commit

Permalink
Merge pull request #57 from pjdarton/enhance_jenkins-20743
Browse files Browse the repository at this point in the history
JENKINS-20743 part two
  • Loading branch information
jswager committed Sep 23, 2016
2 parents 666a5c4 + d27c9da commit e712d53
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 16 deletions.
22 changes: 10 additions & 12 deletions src/main/java/org/jenkinsci/plugins/vSphereCloud.java
Expand Up @@ -25,7 +25,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
Expand Down Expand Up @@ -345,7 +344,8 @@ public Collection<PlannedNode> provision(final Label label, int excessWorkload)
if (whatWeShouldSpinUp==null) {
break; // out of capacity due to template instance cap
}
final PlannedNode plannedNode = VSpherePlannedNode.createInstance(templateState, whatWeShouldSpinUp);
final String nodeName = CloudProvisioningAlgorithm.findUnusedName(whatWeShouldSpinUp);
final PlannedNode plannedNode = VSpherePlannedNode.createInstance(templateState, nodeName, whatWeShouldSpinUp);
plannedNodes.add(plannedNode);
excessWorkloadSoFar -= plannedNode.numExecutors;
}
Expand Down Expand Up @@ -396,32 +396,30 @@ private VSpherePlannedNode(String displayName, Future<Node> future, int numExecu
super(displayName, future, numExecutors);
}

public static VSpherePlannedNode createInstance(final CloudProvisioningState algorithm,
public static VSpherePlannedNode createInstance(final CloudProvisioningState templateState,
final String nodeName,
final CloudProvisioningRecord whatWeShouldSpinUp) {
final vSphereCloudSlaveTemplate template = whatWeShouldSpinUp.getTemplate();
final String cloneNamePrefix = template.getCloneNamePrefix();
final int numberOfExecutors = template.getNumberOfExecutors();
final UUID cloneUUID = UUID.randomUUID();
final String nodeName = cloneNamePrefix + "_" + cloneUUID;
final Callable<Node> provisionNodeCallable = new Callable<Node>() {
public Node call() throws Exception {
try {
final Node newNode = provisionNewNode(algorithm, whatWeShouldSpinUp, nodeName);
final Node newNode = provisionNewNode(templateState, whatWeShouldSpinUp, nodeName);
VSLOG.log(Level.INFO, "Provisioned new slave " + nodeName);
synchronized (algorithm) {
algorithm.provisionedSlaveNowActive(whatWeShouldSpinUp, nodeName);
synchronized (templateState) {
templateState.provisionedSlaveNowActive(whatWeShouldSpinUp, nodeName);
}
return newNode;
} catch (Exception ex) {
VSLOG.log(Level.WARNING, "Failed to provision new slave " + nodeName, ex);
synchronized (algorithm) {
algorithm.provisioningEndedInError(whatWeShouldSpinUp, nodeName);
synchronized (templateState) {
templateState.provisioningEndedInError(whatWeShouldSpinUp, nodeName);
}
throw ex;
}
}
};
algorithm.provisioningStarted(whatWeShouldSpinUp, nodeName);
templateState.provisioningStarted(whatWeShouldSpinUp, nodeName);
final Future<Node> provisionNodeTask = Computer.threadPoolForRemoting.submit(provisionNodeCallable);
final VSpherePlannedNode result = new VSpherePlannedNode(nodeName, provisionNodeTask, numberOfExecutors);
return result;
Expand Down
@@ -1,13 +1,16 @@
package org.jenkinsci.plugins.vsphere.tools;

import java.math.BigInteger;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;

import org.jenkinsci.plugins.vSphereCloudSlaveTemplate;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* How we decide what template to create the next slave on.
Expand Down Expand Up @@ -39,4 +42,98 @@ public static CloudProvisioningRecord findTemplateWithMostFreeCapacity(
}
return null;
}

/**
* Chooses a name for a new node.
* <ul>
* <li>If the template has a limited number of instances available then the
* name will be of the form "prefix_number" where "number" is a number that
* should be between 1 and the number of permitted instances.</li>
* <li>If the template has an unlimited number of instances available then
* the name will be of the form "prefix_random" where "random" is a random
* UUID's 32-byte number (rendered using a high radix to keep the string
* short).</li>
* </ul>
*
* @param record
* Our record regarding the template the slave will be created
* from.
* @return A name for the new node. This will start with the
* {@link vSphereCloudSlaveTemplate#getCloneNamePrefix()}.
*/
public static String findUnusedName(CloudProvisioningRecord record) {
final vSphereCloudSlaveTemplate template = record.getTemplate();
final String cloneNamePrefix = template.getCloneNamePrefix();
final Set<String> existingNames = new TreeSet<String>();
existingNames.addAll(record.getCurrentlyPlanned());
existingNames.addAll(record.getCurrentlyProvisioned());
final int templateInstanceCap = template.getTemplateInstanceCap();
final boolean hasCap = templateInstanceCap > 0 && templateInstanceCap < Integer.MAX_VALUE;
final int maxAttempts = hasCap ? (templateInstanceCap + 1) : 100;
for (int attempt = 0; attempt < maxAttempts; attempt++) {
final String suffix = hasCap ? calcSequentialSuffix(attempt) : calcRandomSuffix(attempt);
final String nodeName = cloneNamePrefix + "_" + suffix;
if (!existingNames.contains(nodeName)) {
return nodeName;
}
}
throw new IllegalStateException("Unable to find unused name for slave for record " + record.toString()
+ ", even after " + maxAttempts + " attempts.");
}

private static String calcSequentialSuffix(final int attempt) {
final int slaveNumber = attempt + 1;
final String suffix = Integer.toString(slaveNumber);
return suffix;
}

private static String calcRandomSuffix(int attempt) {
// get "unique" UUID
final UUID uuid = UUID.randomUUID();
// put both "long"s into a BigInteger.
final long lsb = uuid.getLeastSignificantBits();
final long msb = uuid.getMostSignificantBits();
final BigInteger bigNumber = toBigInteger(msb, lsb);
// turn into a string
final String suffix = bigNumber.toString(Character.MAX_RADIX);
return suffix;
}

/**
* Turns two 64-bit long numbers into a positive 128-bit {@link BigInteger}
* that's in the range 0 to 2<sup>128</sup>-1.
* <p>
* <b>Note:</b> This is only package-level access for unit-testing.
* </p>
*
* @param msb
* The most-significant 64 bits.
* @param lsb
* The least-significant 64 bits.
* @return A {@link BigInteger}.
*/
@Restricted(NoExternalUse.class)
static BigInteger toBigInteger(final long msb, final long lsb) {
final byte[] bytes = new byte[17];
int b = 0;
bytes[b++] = (byte) 0; // ensure we're all positive
bytes[b++] = (byte) (msb >> 56);
bytes[b++] = (byte) (msb >> 48);
bytes[b++] = (byte) (msb >> 40);
bytes[b++] = (byte) (msb >> 32);
bytes[b++] = (byte) (msb >> 24);
bytes[b++] = (byte) (msb >> 16);
bytes[b++] = (byte) (msb >> 8);
bytes[b++] = (byte) (msb);
bytes[b++] = (byte) (lsb >> 56);
bytes[b++] = (byte) (lsb >> 48);
bytes[b++] = (byte) (lsb >> 40);
bytes[b++] = (byte) (lsb >> 32);
bytes[b++] = (byte) (lsb >> 24);
bytes[b++] = (byte) (lsb >> 16);
bytes[b++] = (byte) (lsb >> 8);
bytes[b++] = (byte) (lsb);
final BigInteger bigNumber = new BigInteger(bytes);
return bigNumber;
}
}
@@ -1,6 +1,6 @@
<div>
Controls the name by which the virtual machine will be known to vSphere.
The plug-in will append an underscore and a random UUID to this prefix in order to generate unique names.<p>
The plug-in will append an underscore and a unique ID to this prefix in order to generate unique names.<p>
Note: You must ensure that no virtual machines (other than those created by this template) have names starting with this prefix.
</p>
</div>
Expand Up @@ -5,12 +5,13 @@
import hudson.slaves.JNLPLauncher;
import hudson.slaves.RetentionStrategy;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;

import org.hamcrest.core.IsSame;
import org.jenkinsci.plugins.vSphereCloudSlaveTemplate;
import org.jenkinsci.plugins.vsphere.tools.CloudProvisioningRecord;
import org.junit.Before;
Expand Down Expand Up @@ -149,6 +150,111 @@ private static void testScenario(List<CloudProvisioningRecord> records, CloudPro
}
}

@Test
public void toBigIntegerGivenTwoPow128MinusOneThenReturnsTwoPow128MinusOne() {
testToBigInteger(-1, -1, "340282366920938463463374607431768211455");
}

@Test
public void toBigIntegerGivenTwoPow64PlusOneThenReturnsTwoPow64PlusOne() {
testToBigInteger(1, 1, "18446744073709551617");
}

@Test
public void toBigIntegerGivenZeroThenReturnsZero() {
testToBigInteger(0, 0, "0");
}

@Test
public void toBigIntegerGivenPowersOfTwoThenReturnsPowersOfTwo() {
long lsb = 1;
long msb = 0;
BigInteger big = new BigInteger("1");
for (int bit = 0; bit < 64; bit++) {
testToBigInteger(msb, lsb, big.toString());
lsb <<= 1;
big = big.add(big);
}
msb = 1;
for (int bit = 0; bit < 64; bit++) {
testToBigInteger(msb, lsb, big.toString());
msb <<= 1;
big = big.add(big);
}
}

private void testToBigInteger(long msb, long lsb, String expected) {
// Given
final BigInteger expectedValue = new BigInteger(expected);
final byte[] expectedBytes = expectedValue.toByteArray();
// When
final BigInteger actual = CloudProvisioningAlgorithm.toBigInteger(msb, lsb);
final byte[] actualBytes = actual.toByteArray();
final String scenario = "toBigInteger(" + msb + "," + lsb + ") == " + expected + "\ni.e. "
+ toHexString(actualBytes) + " ~= " + toHexString(expectedBytes);
// Then
assertThat(scenario, actual, equalTo(expectedValue));
}

@Test
public void findUnusedNameGivenZeroOfTwoExistsThenReturnsOneThenTwo() {
// Given
final CloudProvisioningRecord record = createInstance(2, 0, 0);
final String prefix = record.getTemplate().getCloneNamePrefix();
final String expected1 = prefix + "_1";
final String expected2 = prefix + "_2";

// When
final String actual1 = CloudProvisioningAlgorithm.findUnusedName(record);
record.addCurrentlyActive(actual1);
final String actual2 = CloudProvisioningAlgorithm.findUnusedName(record);
record.addCurrentlyActive(actual2);

// Then
assertThat(actual1, equalTo(expected1));
assertThat(actual2, equalTo(expected2));
}

@Test
public void findUnusedNameGivenOneOfTwoHasEndedThenReturnsOne() {
// Given
final CloudProvisioningRecord record = createInstance(2, 0, 0);
final String prefix = record.getTemplate().getCloneNamePrefix();
final String expected = prefix + "_1";

// When
final String actual1 = CloudProvisioningAlgorithm.findUnusedName(record);
record.addCurrentlyActive(actual1);
final String actual2 = CloudProvisioningAlgorithm.findUnusedName(record);
record.addCurrentlyActive(actual2);
record.removeCurrentlyActive(actual1);
final String actual = CloudProvisioningAlgorithm.findUnusedName(record);
record.addCurrentlyActive(actual);

// Then
assertThat(actual, equalTo(expected));
}

@Test
public void findUnusedNameGivenUncappedInstancesThenReturnsUniqueNames() {
// Given
final CloudProvisioningRecord record = createInstance(0, 5, 6);
final String prefix = record.getTemplate().getCloneNamePrefix();
final List<String> actuals = new ArrayList<String>();

// When
for (int i = 0; i < 100; i++) {
final String actual = CloudProvisioningAlgorithm.findUnusedName(record);
record.addCurrentlyActive(actual);
actuals.add(actual);
}

// Then
final List<String> uniques = new ArrayList<String>(new LinkedHashSet<String>(actuals));
assertThat(actuals, equalTo(uniques));
assertThat(actuals, everyItem(startsWith(prefix + "_")));
}

private CloudProvisioningRecord createInstance(int capacity, int provisioned, int planned) {
final int iNum = ++instanceNumber;
final vSphereCloudSlaveTemplate template = stubTemplate(iNum + "cap" + capacity, capacity);
Expand All @@ -166,6 +272,18 @@ private CloudProvisioningRecord createInstance(int capacity, int provisioned, in

private static vSphereCloudSlaveTemplate stubTemplate(String prefix, int templateInstanceCap) {
return new vSphereCloudSlaveTemplate(prefix, "", null, false, null, null, null, null, templateInstanceCap, 1,
null, null, null, false, false, 0, 0, false, null, null, null, new JNLPLauncher(), RetentionStrategy.NOOP, null, null);
null, null, null, false, false, 0, 0, false, null, null, null, new JNLPLauncher(),
RetentionStrategy.NOOP, null, null);
}

private static String toHexString(byte[] bytes) {
final StringBuilder s = new StringBuilder("0x");
for (final byte b : bytes) {
final int highDigit = (((int) b) >> 8) & 15;
final int lowDigit = ((int) b) & 15;
s.append(Integer.toString(highDigit, 16));
s.append(Integer.toString(lowDigit, 16));
}
return s.toString();
}
}

0 comments on commit e712d53

Please sign in to comment.