From 27a2eaecd30e813e6465636791fb41b0a2783b6a Mon Sep 17 00:00:00 2001 From: Peter Wagner Date: Mon, 19 Dec 2016 14:28:01 -0500 Subject: [PATCH 1/4] Scale executors by LaunchSpecification weight This works nicely with a SpotFleet weighted by vCPU count (easy in the management console). --- .../jenkins/ec2fleet/EC2FleetCloud.java | 24 +++++++++++--- .../jenkins/ec2fleet/FleetStateStats.java | 33 +++++++++++++++++-- .../jenkins/ec2fleet/cloud/FleetNode.java | 2 +- .../ec2fleet/EC2FleetCloud/config.jelly | 4 +++ .../jenkins/ec2fleet/EC2FleetCloudTest.java | 20 +++++------ .../ec2fleet/ProvisionIntegrationTest.java | 8 ++--- .../ec2fleet/RealEc2ApiIntegrationTest.java | 4 +-- .../jenkins/ec2fleet/UiIntegrationTest.java | 22 ++++++------- 8 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java b/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java index 976402d4..c5db472a 100644 --- a/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java +++ b/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java @@ -97,6 +97,7 @@ public class EC2FleetCloud extends Cloud { private final Integer numExecutors; private final boolean addNodeOnlyIfRunning; private final boolean restrictUsage; + private final boolean scaleExecutorsByWeight; private transient Set plannedNodesCache; // fleetInstancesCache contains all Jenkins nodes known to be in the fleet, not in dyingFleetInstancesCache @@ -120,7 +121,8 @@ public EC2FleetCloud(final String name, final Integer maxSize, final Integer numExecutors, final boolean addNodeOnlyIfRunning, - final boolean restrictUsage) { + final boolean restrictUsage, + final boolean scaleExecutorsByWeight) { super(StringUtils.isBlank(name) ? FLEET_CLOUD_ID : name); initCaches(); this.credentialsId = credentialsId; @@ -138,6 +140,7 @@ public EC2FleetCloud(final String name, this.numExecutors = numExecutors; this.addNodeOnlyIfRunning = addNodeOnlyIfRunning; this.restrictUsage = restrictUsage; + this.scaleExecutorsByWeight = scaleExecutorsByWeight; } /** @@ -198,6 +201,10 @@ public Integer getNumExecutors() { return numExecutors; } + public boolean isScaleExecutorsByWeight() { + return scaleExecutorsByWeight; + } + public String getJvmSettings() { return ""; } @@ -365,7 +372,7 @@ public synchronized FleetStateStats updateStatus() { StringUtils.join(newFleetInstances, ", ") + "]"); } for (final String instanceId : newFleetInstances) { - addNewSlave(ec2, instanceId); + addNewSlave(ec2, instanceId, stats); } } catch (final Exception ex) { LOGGER.log(Level.WARNING, "Unable to add a new instance.", ex); @@ -465,7 +472,7 @@ private synchronized void removeNode(String instanceId) { } } - private void addNewSlave(final AmazonEC2 ec2, final String instanceId) throws Exception { + private void addNewSlave(final AmazonEC2 ec2, final String instanceId, FleetStateStats stats) throws Exception { // Generate a random FS root if one isn't specified String effectiveFsRoot = fsRoot; if (StringUtils.isBlank(effectiveFsRoot)) { @@ -487,9 +494,18 @@ private void addNewSlave(final AmazonEC2 ec2, final String instanceId) throws Ex // Check if we have the address to use. Nodes don't get it immediately. if (address == null) return; // Wait some more... + int numExecutors; + if (scaleExecutorsByWeight) { + Double instanceTypeWeight = stats.getInstanceTypeWeight(instance.getInstanceType()); + Double instanceWeight = Math.ceil(this.numExecutors * instanceTypeWeight); + numExecutors = instanceWeight.intValue(); + } else { + numExecutors = this.numExecutors; + } + final Node.Mode nodeMode = restrictUsage ? Node.Mode.EXCLUSIVE : Node.Mode.NORMAL; final FleetNode slave = new FleetNode(instanceId, "Fleet slave for " + instanceId, - effectiveFsRoot, numExecutors.toString(), nodeMode, labelString, new ArrayList>(), + effectiveFsRoot, numExecutors, nodeMode, labelString, new ArrayList>(), name, computerConnector.launch(address, TaskListener.NULL)); // Initialize our retention strategy diff --git a/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java b/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java index f44dc7f0..13b1ecc4 100644 --- a/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java +++ b/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java @@ -6,12 +6,16 @@ import com.amazonaws.services.ec2.model.DescribeSpotFleetInstancesResult; import com.amazonaws.services.ec2.model.DescribeSpotFleetRequestsRequest; import com.amazonaws.services.ec2.model.DescribeSpotFleetRequestsResult; +import com.amazonaws.services.ec2.model.SpotFleetLaunchSpecification; import com.amazonaws.services.ec2.model.SpotFleetRequestConfig; +import com.amazonaws.services.ec2.model.SpotFleetRequestConfigData; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; /** @@ -19,7 +23,8 @@ */ @SuppressWarnings("unused") public final class FleetStateStats { - + private static + final double DEFAULT_WEIGHT = 1.0; private @Nonnull final String fleetId; private @Nonnegative @@ -31,17 +36,21 @@ public final class FleetStateStats { private @Nonnull final Set instances; private @Nonnull + final Map instanceTypeWeights; + private @Nonnull final String label; public FleetStateStats(final @Nonnull String fleetId, final int numDesired, final @Nonnull String state, final @Nonnull Set instances, + final @Nonnull Map instanceTypeWeights, final @Nonnull String label) { this.fleetId = fleetId; this.numActive = instances.size(); this.numDesired = numDesired; this.state = state; this.instances = instances; + this.instanceTypeWeights=instanceTypeWeights; this.label = label; } @@ -68,6 +77,12 @@ public Set getInstances() { return instances; } + @Nonnull + public Double getInstanceTypeWeight(String instanceType) { + Double instanceTypeWeight = instanceTypeWeights.get(instanceType); + return instanceTypeWeight == null ? DEFAULT_WEIGHT : instanceTypeWeight; + } + @Nonnull public String getLabel() { return label; @@ -95,10 +110,24 @@ public static FleetStateStats readClusterState(final AmazonEC2 ec2, final String throw new IllegalStateException("Fleet " + fleetId + " can't be described"); final SpotFleetRequestConfig fleetConfig = fleet.getSpotFleetRequestConfigs().get(0); + final SpotFleetRequestConfigData fleetRequestConfig = fleetConfig.getSpotFleetRequestConfig(); + + // Index configured instance types by weight: + final Map instanceTypeWeight = new HashMap<>(); + for (SpotFleetLaunchSpecification launchSpecification : fleetRequestConfig.getLaunchSpecifications()) { + final String instanceType = launchSpecification.getInstanceType(); + final Double instanceWeight = launchSpecification.getWeightedCapacity(); + final Double existingWeight = instanceTypeWeight.get(instanceType); + if (instanceWeight == null || (existingWeight != null && existingWeight > instanceWeight)) { + continue; + } + instanceTypeWeight.put(instanceType, instanceWeight); + } return new FleetStateStats(fleetId, - fleetConfig.getSpotFleetRequestConfig().getTargetCapacity(), + fleetRequestConfig.getTargetCapacity(), fleetConfig.getSpotFleetRequestState(), instances, + Collections.unmodifiableMap(instanceTypeWeight), label); } } diff --git a/src/main/java/com/amazon/jenkins/ec2fleet/cloud/FleetNode.java b/src/main/java/com/amazon/jenkins/ec2fleet/cloud/FleetNode.java index 2996eab4..fc723dbd 100644 --- a/src/main/java/com/amazon/jenkins/ec2fleet/cloud/FleetNode.java +++ b/src/main/java/com/amazon/jenkins/ec2fleet/cloud/FleetNode.java @@ -17,7 +17,7 @@ public class FleetNode extends Slave implements EphemeralNode { private final String cloudName; - public FleetNode(final String name, final String nodeDescription, final String remoteFS, final String numExecutors, final Mode mode, final String label, + public FleetNode(final String name, final String nodeDescription, final String remoteFS, final int numExecutors, final Mode mode, final String label, final List> nodeProperties, final String cloudName, ComputerLauncher launcher) throws IOException, Descriptor.FormException { super(name, nodeDescription, remoteFS, numExecutors, mode, label, launcher, RetentionStrategy.NOOP, nodeProperties); diff --git a/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/config.jelly b/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/config.jelly index 1ef35628..edf4203e 100644 --- a/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/config.jelly +++ b/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/config.jelly @@ -64,6 +64,10 @@ + + + + diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java index bc5deee4..49dc54ae 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java @@ -109,7 +109,7 @@ public void provision_fleetIsEmpty() { EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", null, null, null, null, false, - false, 0, 0, 1, 1, false, false); + false, 0, 0, 1, 1, false, false, false); // when Collection r = fleetCloud.provision(null, 1); @@ -146,7 +146,7 @@ public void updateStatus_doNothingWhenFleetIsEmpty() { EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "fleetId", null, null, null, false, - false, 0, 0, 1, 1, false, false); + false, 0, 0, 1, 1, false, false, false); // when FleetStateStats stats = fleetCloud.updateStatus(); @@ -207,7 +207,7 @@ public void updateStatus_shouldAddNodeIfAnyNewDescribed() throws IOException { EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, - false, 0, 0, 1, 1, false, false); + false, 0, 0, 1, 1, false, false, false); ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); Mockito.doNothing().when(jenkins).addNode(nodeCaptor.capture()); @@ -275,7 +275,7 @@ public void updateStatus_shouldAddNodeIfAnyNewDescribed_restrictUsage() throws I EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, - false, 0, 0, 1, 1, false, true); + false, 0, 0, 1, 1, false, true, false); ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); Mockito.doNothing().when(jenkins).addNode(nodeCaptor.capture()); @@ -498,7 +498,7 @@ public void getDisplayName_returnDefaultWhenNull() { null, null, null, null, null, null, null, null, false, false, null, null, null, - null, false, false); + null, false, false, false); Assert.assertEquals(ec2FleetCloud.getDisplayName(), EC2FleetCloud.FLEET_CLOUD_ID); } @@ -508,7 +508,7 @@ public void getDisplayName_returnDisplayName() { "CloudName", null, null, null, null, null, null, null, false, false, null, null, null, - null, false, false); + null, false, false, false); Assert.assertEquals(ec2FleetCloud.getDisplayName(), "CloudName"); } @@ -518,7 +518,7 @@ public void getAwsCredentialsId_returnNull_whenNoCredentialsIdOrAwsCredentialsId null, null, null, null, null, null, null, null, false, false, null, null, null, - null, false, false); + null, false, false, false); Assert.assertNull(ec2FleetCloud.getAwsCredentialsId()); } @@ -528,7 +528,7 @@ public void getAwsCredentialsId_returnValue_whenCredentialsIdPresent() { null, null, "Opa", null, null, null, null, null, false, false, null, null, null, - null, false, false); + null, false, false, false); Assert.assertEquals("Opa", ec2FleetCloud.getAwsCredentialsId()); } @@ -538,7 +538,7 @@ public void getAwsCredentialsId_returnValue_whenAwsCredentialsIdPresent() { null, "Opa", null, null, null, null, null, null, false, false, null, null, null, - null, false, false); + null, false, false, false); Assert.assertEquals("Opa", ec2FleetCloud.getAwsCredentialsId()); } @@ -548,7 +548,7 @@ public void getAwsCredentialsId_returnAwsCredentialsId_whenAwsCredentialsIdAndCr null, "A", "B", null, null, null, null, null, false, false, null, null, null, - null, false, false); + null, false, false, false); Assert.assertEquals("A", ec2FleetCloud.getAwsCredentialsId()); } diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java index 1bf44630..e4be2c48 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java @@ -123,7 +123,7 @@ public void should_not_do_anything_if_fleet_is_empty_and_max_size_isreached() th EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", "fId", "momo", null, computerConnector, false, false, - 0, 0, 0, 1, false, false); + 0, 0, 0, 1, false, false, false); j.jenkins.clouds.add(cloud); EC2Api ec2Api = mock(EC2Api.class); @@ -163,7 +163,7 @@ public void should_add_planned_if_capacity_required_but_not_described_yet() thro EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", "fId", "momo", null, computerConnector, false, false, - 0, 0, 10, 1, false, false); + 0, 0, 10, 1, false, false, false); j.jenkins.clouds.add(cloud); EC2Api ec2Api = mock(EC2Api.class); @@ -232,7 +232,7 @@ public void should_convert_planed_to_node_if_describe_instance() throws Exceptio EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", "fId", "momo", null, computerConnector, false, false, - 0, 0, 10, 1, false, false); + 0, 0, 10, 1, false, false, false); j.jenkins.clouds.add(cloud); EC2Api ec2Api = mock(EC2Api.class); @@ -314,7 +314,7 @@ public void should_not_convert_planned_to_node_if_state_is_not_running_and_check EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", "fId", "momo", null, computerConnector, false, false, - 0, 0, 10, 1, true, false); + 0, 0, 10, 1, true, false, false); j.jenkins.clouds.add(cloud); EC2Api ec2Api = mock(EC2Api.class); diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java index f9011463..d0ff845f 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java @@ -50,7 +50,7 @@ public void shouldSuccessfullyUpdatePluginWithFleetStatus() throws Exception { public void run(AmazonEC2 amazonEC2, String fleetId) throws Exception { EC2FleetCloud cloud = new EC2FleetCloud("", "credId", null, null, fleetId, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud); // 10 sec refresh time so wait @@ -82,7 +82,7 @@ public void shouldSuccessfullyUpdateBigFleetPluginWithFleetStatus() throws Excep public void run(AmazonEC2 amazonEC2, String fleetId) throws Exception { EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, null, fleetId, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud); final long start = System.currentTimeMillis(); diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java index 44f0945a..c14b84f3 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java @@ -44,7 +44,7 @@ public void shouldFindThePluginByShortName() { public void shouldShowInConfigurationClouds() throws IOException, SAXException { Cloud cloud = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud); HtmlPage page = j.createWebClient().goTo("configure"); @@ -57,12 +57,12 @@ public void shouldShowInConfigurationClouds() throws IOException, SAXException { public void shouldShowMultipleClouds() throws IOException, SAXException { Cloud cloud1 = new EC2FleetCloud("a", null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud1); Cloud cloud2 = new EC2FleetCloud("b", null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud2); HtmlPage page = j.createWebClient().goTo("configure"); @@ -78,12 +78,12 @@ public void shouldShowMultipleClouds() throws IOException, SAXException { public void shouldShowMultipleCloudsWithDefaultName() throws IOException, SAXException { Cloud cloud1 = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud1); Cloud cloud2 = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud2); HtmlPage page = j.createWebClient().goTo("configure"); @@ -99,12 +99,12 @@ public void shouldShowMultipleCloudsWithDefaultName() throws IOException, SAXExc public void shouldUpdateProperCloudWhenMultiple() throws IOException, SAXException { EC2FleetCloud cloud1 = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud1); EC2FleetCloud cloud2 = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud2); HtmlPage page = j.createWebClient().goTo("configure"); @@ -122,12 +122,12 @@ public void shouldUpdateProperCloudWhenMultiple() throws IOException, SAXExcepti public void shouldGetFirstWhenMultipleCloudWithSameName() { EC2FleetCloud cloud1 = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud1); EC2FleetCloud cloud2 = new EC2FleetCloud(null, null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud2); assertSame(cloud1, j.jenkins.getCloud("FleetCloud")); @@ -137,12 +137,12 @@ public void shouldGetFirstWhenMultipleCloudWithSameName() { public void shouldGetProperWhenMultipleWithDiffName() { EC2FleetCloud cloud1 = new EC2FleetCloud("a", null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud1); EC2FleetCloud cloud2 = new EC2FleetCloud("b", null, null, null, null, null, null, null, false, false, - 0, 0, 0, 0, false, false); + 0, 0, 0, 0, false, false, false); j.jenkins.clouds.add(cloud2); assertSame(cloud1, j.jenkins.getCloud("a")); From e8ffd2ef60387c9f3692607a4071fa90a60685f7 Mon Sep 17 00:00:00 2001 From: Artem Stasiuk Date: Wed, 3 Jul 2019 18:04:43 -0700 Subject: [PATCH 2/4] Rebase of #32 allow to scale number of executors by weight --- .../jenkins/ec2fleet/EC2FleetCloud.java | 19 +- .../amazon/jenkins/ec2fleet/EC2FleetNode.java | 5 +- .../jenkins/ec2fleet/FleetStateStats.java | 62 ++- .../help-scaleExecutorsByWeight.html | 64 +++ .../ec2fleet/AutoResubmitIntegrationTest.java | 6 +- .../jenkins/ec2fleet/EC2FleetCloudTest.java | 382 ++++++++++++++++-- .../jenkins/ec2fleet/FleetStateStatsTest.java | 159 ++++++++ .../ec2fleet/ProvisionIntegrationTest.java | 13 +- .../ec2fleet/RealEc2ApiIntegrationTest.java | 4 +- .../jenkins/ec2fleet/UiIntegrationTest.java | 22 +- 10 files changed, 631 insertions(+), 105 deletions(-) create mode 100644 src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/help-scaleExecutorsByWeight.html create mode 100644 src/test/java/com/amazon/jenkins/ec2fleet/FleetStateStatsTest.java diff --git a/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java b/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java index b4f4ab1c..0683f06a 100644 --- a/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java +++ b/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetCloud.java @@ -507,25 +507,26 @@ private void addNewSlave(final AmazonEC2 ec2, final String instanceId, FleetStat if (address == null) return; // Wait some more... // Generate a random FS root if one isn't specified - String effectiveFsRoot = fsRoot; - if (StringUtils.isBlank(effectiveFsRoot)) { + final String effectiveFsRoot; + if (StringUtils.isBlank(fsRoot)) { effectiveFsRoot = "/tmp/jenkins-" + UUID.randomUUID().toString().substring(0, 8); + } else { + effectiveFsRoot = fsRoot; } - int numExecutors; - if (scaleExecutorsByWeight) { - Double instanceTypeWeight = stats.getInstanceTypeWeight(instance.getInstanceType()); - Double instanceWeight = Math.ceil(this.numExecutors * instanceTypeWeight); - numExecutors = instanceWeight.intValue(); + final Double instanceTypeWeight = stats.getInstanceTypeWeights().get(instance.getInstanceType()); + final int effectiveNumExecutors; + if (scaleExecutorsByWeight && instanceTypeWeight != null) { + effectiveNumExecutors = (int) Math.max(Math.round(numExecutors * instanceTypeWeight), 1); } else { - numExecutors = this.numExecutors; + effectiveNumExecutors = numExecutors; } final EC2FleetAutoResubmitComputerLauncher computerLauncher = new EC2FleetAutoResubmitComputerLauncher( computerConnector.launch(address, TaskListener.NULL), disableTaskResubmit); final Node.Mode nodeMode = restrictUsage ? Node.Mode.EXCLUSIVE : Node.Mode.NORMAL; final EC2FleetNode node = new EC2FleetNode(instanceId, "Fleet slave for " + instanceId, - effectiveFsRoot, numExecutors.toString(), nodeMode, labelString, new ArrayList>(), + effectiveFsRoot, effectiveNumExecutors, nodeMode, labelString, new ArrayList>(), name, computerLauncher); // Initialize our retention strategy diff --git a/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetNode.java b/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetNode.java index 51958a48..ec2e211b 100644 --- a/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetNode.java +++ b/src/main/java/com/amazon/jenkins/ec2fleet/EC2FleetNode.java @@ -17,8 +17,9 @@ public class EC2FleetNode extends Slave implements EphemeralNode { private final String cloudName; - public FleetNode(final String name, final String nodeDescription, final String remoteFS, final int numExecutors, final Mode mode, final String label, - final List> nodeProperties, final String cloudName, ComputerLauncher launcher) throws IOException, Descriptor.FormException { + @SuppressWarnings("WeakerAccess") + public EC2FleetNode(final String name, final String nodeDescription, final String remoteFS, final int numExecutors, final Mode mode, final String label, + final List> nodeProperties, final String cloudName, ComputerLauncher launcher) throws IOException, Descriptor.FormException { super(name, nodeDescription, remoteFS, numExecutors, mode, label, launcher, RetentionStrategy.NOOP, nodeProperties); this.cloudName = cloudName; diff --git a/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java b/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java index 13b1ecc4..ab52c4e2 100644 --- a/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java +++ b/src/main/java/com/amazon/jenkins/ec2fleet/FleetStateStats.java @@ -12,6 +12,7 @@ import javax.annotation.Nonnegative; import javax.annotation.Nonnull; +import javax.annotation.concurrent.ThreadSafe; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -21,37 +22,33 @@ /** * @see EC2FleetCloud */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess"}) +@ThreadSafe public final class FleetStateStats { - private static - final double DEFAULT_WEIGHT = 1.0; - private @Nonnull - final String fleetId; - private @Nonnegative - final int numActive; - private @Nonnegative - final int numDesired; - private @Nonnull - final String state; - private @Nonnull - final Set instances; - private @Nonnull - final Map instanceTypeWeights; - private @Nonnull - final String label; + + @Nonnull + private final String fleetId; + @Nonnegative + private final int numActive; + @Nonnegative + private final int numDesired; + @Nonnull + private final String state; + @Nonnull + private final Set instances; + @Nonnull + private final Map instanceTypeWeights; public FleetStateStats(final @Nonnull String fleetId, final int numDesired, final @Nonnull String state, final @Nonnull Set instances, - final @Nonnull Map instanceTypeWeights, - final @Nonnull String label) { + final @Nonnull Map instanceTypeWeights) { this.fleetId = fleetId; this.numActive = instances.size(); this.numDesired = numDesired; this.state = state; this.instances = instances; - this.instanceTypeWeights=instanceTypeWeights; - this.label = label; + this.instanceTypeWeights = instanceTypeWeights; } @Nonnull @@ -78,14 +75,8 @@ public Set getInstances() { } @Nonnull - public Double getInstanceTypeWeight(String instanceType) { - Double instanceTypeWeight = instanceTypeWeights.get(instanceType); - return instanceTypeWeight == null ? DEFAULT_WEIGHT : instanceTypeWeight; - } - - @Nonnull - public String getLabel() { - return label; + public Map getInstanceTypeWeights() { + return instanceTypeWeights; } public static FleetStateStats readClusterState(final AmazonEC2 ec2, final String fleetId, final String label) { @@ -113,21 +104,22 @@ public static FleetStateStats readClusterState(final AmazonEC2 ec2, final String final SpotFleetRequestConfigData fleetRequestConfig = fleetConfig.getSpotFleetRequestConfig(); // Index configured instance types by weight: - final Map instanceTypeWeight = new HashMap<>(); + final Map instanceTypeWeights = new HashMap<>(); for (SpotFleetLaunchSpecification launchSpecification : fleetRequestConfig.getLaunchSpecifications()) { final String instanceType = launchSpecification.getInstanceType(); + if (instanceType == null) continue; + final Double instanceWeight = launchSpecification.getWeightedCapacity(); - final Double existingWeight = instanceTypeWeight.get(instanceType); - if (instanceWeight == null || (existingWeight != null && existingWeight > instanceWeight)) { + final Double existingWeight = instanceTypeWeights.get(instanceType); + if (instanceWeight == null || (existingWeight != null && existingWeight > instanceWeight)) { continue; } - instanceTypeWeight.put(instanceType, instanceWeight); + instanceTypeWeights.put(instanceType, instanceWeight); } return new FleetStateStats(fleetId, fleetRequestConfig.getTargetCapacity(), fleetConfig.getSpotFleetRequestState(), instances, - Collections.unmodifiableMap(instanceTypeWeight), - label); + instanceTypeWeights); } } diff --git a/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/help-scaleExecutorsByWeight.html b/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/help-scaleExecutorsByWeight.html new file mode 100644 index 00000000..22b6a472 --- /dev/null +++ b/src/main/resources/com/amazon/jenkins/ec2fleet/EC2FleetCloud/help-scaleExecutorsByWeight.html @@ -0,0 +1,64 @@ +If unchecked which is default, plugin will not use +instance + weight +information to scale number of executors per node, and just set number of executors defined in +configuration field Number of Executors + +

+ When it's checked, plugin consumes instance weight information provided by Launch Specification + and uses it to scale node number of executors from configuration field Number of Executors + Note current implementation doesn't support Launch Template only Launch Specification +

+ +

+ Example (here instance type from launch specification match with + launched instance type): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Number of ExecutorsInstance WeightEffective
Number of Executors
111
10.51
10.11
100.11
11.52
11.441
+

+ +

+ Plugin always set number of executors at least one. + If launched instance type doesn't match any weight in launch specification + regular number of executors will be used without any scale. +

diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/AutoResubmitIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/AutoResubmitIntegrationTest.java index 55cc45e6..153c0789 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/AutoResubmitIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/AutoResubmitIntegrationTest.java @@ -79,7 +79,7 @@ public void should_successfully_resubmit_freestyle_task() throws Exception { EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, new SingleLocalComputerConnector(j), false, false, 0, 0, 10, 1, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud); List rs = getQueueTaskFutures(1); @@ -115,7 +115,7 @@ public void should_successfully_resubmit_parametrized_task() throws Exception { EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, new SingleLocalComputerConnector(j), false, false, 0, 0, 10, 1, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud); List rs = new ArrayList<>(); @@ -171,7 +171,7 @@ public void should_not_resubmit_if_disabled() throws Exception { EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, new SingleLocalComputerConnector(j), false, false, 0, 0, 10, 1, false, false, - true, 0, 0); + true, 0, 0, false); j.jenkins.clouds.add(cloud); List rs = getQueueTaskFutures(1); diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java index 4c0843b6..0157efcf 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/EC2FleetCloudTest.java @@ -15,6 +15,7 @@ import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.Region; import com.amazonaws.services.ec2.model.Reservation; +import com.amazonaws.services.ec2.model.SpotFleetLaunchSpecification; import com.amazonaws.services.ec2.model.SpotFleetRequestConfig; import com.amazonaws.services.ec2.model.SpotFleetRequestConfigData; import hudson.ExtensionList; @@ -70,6 +71,9 @@ public class EC2FleetCloudTest { @Mock private EC2Api ec2Api; + @Mock + private AmazonEC2 amazonEC2; + @Before public void before() { spotFleetRequestConfig1 = new SpotFleetRequestConfig(); @@ -89,6 +93,8 @@ public void before() { Registry.setEc2Api(ec2Api); + PowerMockito.mockStatic(LabelFinder.class); + PowerMockito.mockStatic(Jenkins.class); PowerMockito.when(Jenkins.getInstance()).thenReturn(jenkins); } @@ -101,7 +107,6 @@ public void after() { @Test public void provision_fleetIsEmpty() { // given - AmazonEC2 amazonEC2 = mock(AmazonEC2.class); when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); @@ -120,7 +125,7 @@ public void provision_fleetIsEmpty() { EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "", null, null, null, null, false, false, 0, 0, 1, 1, false, - false, false, 0, 0); + false, false, 0, 0, false); // when Collection r = fleetCloud.provision(null, 1); @@ -132,7 +137,6 @@ public void provision_fleetIsEmpty() { @Test public void updateStatus_doNothingWhenFleetIsEmpty() { // given - AmazonEC2 amazonEC2 = mock(AmazonEC2.class); when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); @@ -151,7 +155,8 @@ public void updateStatus_doNothingWhenFleetIsEmpty() { EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "", "fleetId", null, null, null, false, false, 0, 0, 1, 1, - false, false, false, 0, 0); + false, false, false, 0, + 0, false); // when FleetStateStats stats = fleetCloud.updateStatus(); @@ -165,9 +170,6 @@ public void updateStatus_doNothingWhenFleetIsEmpty() { @Test public void updateStatus_shouldAddNodeIfAnyNewDescribed() throws IOException { // given - PowerMockito.mockStatic(LabelFinder.class); - - AmazonEC2 amazonEC2 = mock(AmazonEC2.class); when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); @@ -196,20 +198,13 @@ public void updateStatus_shouldAddNodeIfAnyNewDescribed() throws IOException { when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) .thenReturn(describeSpotFleetRequestsResult); - when(jenkins.getNodesObject()).thenReturn(mock(Nodes.class)); - - // mock - ExtensionList labelFinder = mock(ExtensionList.class); - when(labelFinder.iterator()).thenReturn(Collections.emptyIterator()); - PowerMockito.when(LabelFinder.all()).thenReturn(labelFinder); - - // mocking part of node creation process Jenkins.getInstance().getLabelAtom(l) - when(jenkins.getLabelAtom(anyString())).thenReturn(new LabelAtom("mock-label")); + mockNodeCreatingPart(); EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, false, 0, 0, 1, 1, - false, false, false, 0, 0); + false, false, false, + 0, 0, false); ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); doNothing().when(jenkins).addNode(nodeCaptor.capture()); @@ -230,9 +225,6 @@ public void updateStatus_shouldAddNodeIfAnyNewDescribed() throws IOException { @Test public void updateStatus_shouldAddNodeIfAnyNewDescribed_restrictUsage() throws IOException { // given - PowerMockito.mockStatic(LabelFinder.class); - - AmazonEC2 amazonEC2 = mock(AmazonEC2.class); when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); @@ -261,20 +253,13 @@ public void updateStatus_shouldAddNodeIfAnyNewDescribed_restrictUsage() throws I when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) .thenReturn(describeSpotFleetRequestsResult); - when(jenkins.getNodesObject()).thenReturn(mock(Nodes.class)); - - // mock - ExtensionList labelFinder = mock(ExtensionList.class); - when(labelFinder.iterator()).thenReturn(Collections.emptyIterator()); - PowerMockito.when(LabelFinder.all()).thenReturn(labelFinder); - - // mocking part of node creation process Jenkins.getInstance().getLabelAtom(l) - when(jenkins.getLabelAtom(anyString())).thenReturn(new LabelAtom("mock-label")); + mockNodeCreatingPart(); EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, false, 0, 0, 1, 1, false, - true, false, 0, 0); + true, false, + 0, 0, false); ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); doNothing().when(jenkins).addNode(nodeCaptor.capture()); @@ -292,6 +277,308 @@ public void updateStatus_shouldAddNodeIfAnyNewDescribed_restrictUsage() throws I assertEquals(Node.Mode.EXCLUSIVE, actualFleetNode.getMode()); } + @Test + public void updateStatus_shouldAddNodeWithNumExecutors_whenWeightProvidedButNotEnabled() throws IOException { + // given + when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); + + DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); + final String instanceType = "t"; + describeInstancesResult.withReservations( + new Reservation().withInstances(new Instance() + .withPublicIpAddress("p-ip") + .withInstanceType(instanceType) + .withInstanceId("i-0"))); + + when(amazonEC2.describeInstances(any(DescribeInstancesRequest.class))) + .thenReturn(describeInstancesResult); + + DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); + describeSpotFleetInstancesResult.setActiveInstances(Arrays.asList( + new ActiveInstance().withInstanceId("i-0") + )); + + when(amazonEC2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(describeSpotFleetInstancesResult); + + DescribeSpotFleetRequestsResult describeSpotFleetRequestsResult = new DescribeSpotFleetRequestsResult(); + describeSpotFleetRequestsResult.setSpotFleetRequestConfigs(Arrays.asList( + new SpotFleetRequestConfig() + .withSpotFleetRequestState("active") + .withSpotFleetRequestConfig( + new SpotFleetRequestConfigData() + .withLaunchSpecifications( + new SpotFleetLaunchSpecification() + .withInstanceType(instanceType) + .withWeightedCapacity(1.1)) + .withTargetCapacity(0)))); + when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(describeSpotFleetRequestsResult); + + mockNodeCreatingPart(); + + EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", + "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, + false, 0, 0, 1, 1, false, + true, false, + 0, 0, false); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); + doNothing().when(jenkins).addNode(nodeCaptor.capture()); + + // when + fleetCloud.updateStatus(); + + // then + Node actualFleetNode = nodeCaptor.getValue(); + assertEquals(1, actualFleetNode.getNumExecutors()); + } + + @Test + public void updateStatus_shouldAddNodeWithScaledNumExecutors_whenWeightPresentAndEnabled() throws IOException { + // given + when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); + + final String instanceType = "t"; + final String instanceId = "i-0"; + mockDescribeInstances(instanceId, instanceType); + + DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); + describeSpotFleetInstancesResult.setActiveInstances(Arrays.asList( + new ActiveInstance().withInstanceId(instanceId) + )); + + when(amazonEC2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(describeSpotFleetInstancesResult); + + DescribeSpotFleetRequestsResult describeSpotFleetRequestsResult = new DescribeSpotFleetRequestsResult(); + describeSpotFleetRequestsResult.setSpotFleetRequestConfigs(Arrays.asList( + new SpotFleetRequestConfig() + .withSpotFleetRequestState("active") + .withSpotFleetRequestConfig( + new SpotFleetRequestConfigData() + .withLaunchSpecifications( + new SpotFleetLaunchSpecification() + .withInstanceType(instanceType) + .withWeightedCapacity(2.0)) + .withTargetCapacity(0)))); + when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(describeSpotFleetRequestsResult); + + mockNodeCreatingPart(); + + EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", + "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, + false, 0, 0, 1, 1, false, + true, false, + 0, 0, true); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); + doNothing().when(jenkins).addNode(nodeCaptor.capture()); + + // when + fleetCloud.updateStatus(); + + // then + Node actualFleetNode = nodeCaptor.getValue(); + assertEquals(2, actualFleetNode.getNumExecutors()); + } + + @Test + public void updateStatus_shouldAddNodeWithNumExecutors_whenWeightPresentAndEnabledButForDiffType() throws IOException { + // given + when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); + + mockDescribeInstances("i-0", "t"); + + DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); + describeSpotFleetInstancesResult.setActiveInstances(Arrays.asList( + new ActiveInstance().withInstanceId("i-0") + )); + + when(amazonEC2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(describeSpotFleetInstancesResult); + + DescribeSpotFleetRequestsResult describeSpotFleetRequestsResult = new DescribeSpotFleetRequestsResult(); + describeSpotFleetRequestsResult.setSpotFleetRequestConfigs(Arrays.asList( + new SpotFleetRequestConfig() + .withSpotFleetRequestState("active") + .withSpotFleetRequestConfig( + new SpotFleetRequestConfigData() + .withLaunchSpecifications( + new SpotFleetLaunchSpecification() + .withInstanceType("non-t") + .withWeightedCapacity(2.0)) + .withTargetCapacity(0)))); + when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(describeSpotFleetRequestsResult); + + mockNodeCreatingPart(); + + EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", + "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, + false, 0, 0, 1, 1, false, + true, false, + 0, 0, true); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); + doNothing().when(jenkins).addNode(nodeCaptor.capture()); + + // when + fleetCloud.updateStatus(); + + // then + Node actualFleetNode = nodeCaptor.getValue(); + assertEquals(1, actualFleetNode.getNumExecutors()); + } + + @Test + public void updateStatus_shouldAddNodeWithRoundToLowScaledNumExecutors_whenWeightPresentAndEnabled() throws IOException { + // given + when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); + + final String instanceId = "i-0"; + final String instanceType = "t"; + mockDescribeInstances(instanceId, instanceType); + + DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); + describeSpotFleetInstancesResult.setActiveInstances(Arrays.asList( + new ActiveInstance().withInstanceId(instanceId) + )); + + when(amazonEC2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(describeSpotFleetInstancesResult); + + DescribeSpotFleetRequestsResult describeSpotFleetRequestsResult = new DescribeSpotFleetRequestsResult(); + describeSpotFleetRequestsResult.setSpotFleetRequestConfigs(Arrays.asList( + new SpotFleetRequestConfig() + .withSpotFleetRequestState("active") + .withSpotFleetRequestConfig(new SpotFleetRequestConfigData() + .withLaunchSpecifications( + new SpotFleetLaunchSpecification() + .withInstanceType(instanceType) + .withWeightedCapacity(1.44)) + .withTargetCapacity(0)))); + when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(describeSpotFleetRequestsResult); + + mockNodeCreatingPart(); + + EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", + "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, + false, 0, 0, 1, 1, false, + true, false, + 0, 0, true); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); + doNothing().when(jenkins).addNode(nodeCaptor.capture()); + + // when + fleetCloud.updateStatus(); + + // then + Node actualFleetNode = nodeCaptor.getValue(); + assertEquals(1, actualFleetNode.getNumExecutors()); + } + + @Test + public void updateStatus_shouldAddNodeWithRoundToLowScaledNumExecutors_whenWeightPresentAndEnabled1() throws IOException { + // given + when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); + + final String instanceId = "i-0"; + final String instanceType = "t"; + mockDescribeInstances(instanceId, instanceType); + + DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); + describeSpotFleetInstancesResult.setActiveInstances(Arrays.asList( + new ActiveInstance().withInstanceId(instanceId) + )); + + when(amazonEC2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(describeSpotFleetInstancesResult); + + DescribeSpotFleetRequestsResult describeSpotFleetRequestsResult = new DescribeSpotFleetRequestsResult(); + describeSpotFleetRequestsResult.setSpotFleetRequestConfigs(Arrays.asList( + new SpotFleetRequestConfig() + .withSpotFleetRequestState("active") + .withSpotFleetRequestConfig(new SpotFleetRequestConfigData() + .withLaunchSpecifications( + new SpotFleetLaunchSpecification() + .withInstanceType("t") + .withWeightedCapacity(1.5)) + .withTargetCapacity(0)))); + when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(describeSpotFleetRequestsResult); + + mockNodeCreatingPart(); + + EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", + "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, + false, 0, 0, 1, 1, false, + true, false, + 0, 0, true); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); + doNothing().when(jenkins).addNode(nodeCaptor.capture()); + + // when + fleetCloud.updateStatus(); + + // then + Node actualFleetNode = nodeCaptor.getValue(); + assertEquals(2, actualFleetNode.getNumExecutors()); + } + + @Test + public void updateStatus_shouldAddNodeWithScaledToOneNumExecutors_whenWeightPresentButLessOneAndEnabled() throws IOException { + // given + when(ec2Api.connect(any(String.class), any(String.class), anyString())).thenReturn(amazonEC2); + + final String instanceId = "i-0"; + final String instanceType = "t"; + mockDescribeInstances(instanceId, instanceType); + + DescribeSpotFleetInstancesResult describeSpotFleetInstancesResult = new DescribeSpotFleetInstancesResult(); + describeSpotFleetInstancesResult.setActiveInstances(Arrays.asList( + new ActiveInstance().withInstanceId(instanceId) + )); + + when(amazonEC2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(describeSpotFleetInstancesResult); + + DescribeSpotFleetRequestsResult describeSpotFleetRequestsResult = new DescribeSpotFleetRequestsResult(); + describeSpotFleetRequestsResult.setSpotFleetRequestConfigs(Arrays.asList( + new SpotFleetRequestConfig() + .withSpotFleetRequestState("active") + .withSpotFleetRequestConfig(new SpotFleetRequestConfigData() + .withLaunchSpecifications( + new SpotFleetLaunchSpecification() + .withInstanceType("t") + .withWeightedCapacity(0.1)) + .withTargetCapacity(0)))); + when(amazonEC2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(describeSpotFleetRequestsResult); + + mockNodeCreatingPart(); + + EC2FleetCloud fleetCloud = new EC2FleetCloud(null, "credId", null, "region", + "", "fleetId", null, null, PowerMockito.mock(ComputerConnector.class), false, + false, 0, 0, 1, 1, false, + true, false, + 0, 0, true); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(Node.class); + doNothing().when(jenkins).addNode(nodeCaptor.capture()); + + // when + fleetCloud.updateStatus(); + + // then + Node actualFleetNode = nodeCaptor.getValue(); + assertEquals(1, actualFleetNode.getNumExecutors()); + } + @Test public void descriptorImpl_doFillRegionItems_returnStaticRegionsIfApiCallFailed() { AmazonEC2Client amazonEC2Client = mock(AmazonEC2Client.class); @@ -489,7 +776,7 @@ public void getDisplayName_returnDefaultWhenNull() { null, null, null, false, false, null, null, null, null, false, false, false - , 0, 0); + , 0, 0, false); assertEquals(ec2FleetCloud.getDisplayName(), EC2FleetCloud.FLEET_CLOUD_ID); } @@ -500,7 +787,7 @@ public void getDisplayName_returnDisplayName() { null, null, null, false, false, null, null, null, null, false, false, false - , 0, 0); + , 0, 0, false); assertEquals(ec2FleetCloud.getDisplayName(), "CloudName"); } @@ -511,7 +798,7 @@ public void getAwsCredentialsId_returnNull_whenNoCredentialsIdOrAwsCredentialsId null, null, null, false, false, null, null, null, null, false, false, false - , 0, 0); + , 0, 0, false); Assert.assertNull(ec2FleetCloud.getAwsCredentialsId()); } @@ -522,7 +809,7 @@ public void getAwsCredentialsId_returnValue_whenCredentialsIdPresent() { null, null, null, false, false, null, null, null, null, false, false, false - , 0, 0); + , 0, 0, false); assertEquals("Opa", ec2FleetCloud.getAwsCredentialsId()); } @@ -533,7 +820,7 @@ public void getAwsCredentialsId_returnValue_whenAwsCredentialsIdPresent() { null, null, null, false, false, null, null, null, null, false, false, false - , 0, 0); + , 0, 0, false); assertEquals("Opa", ec2FleetCloud.getAwsCredentialsId()); } @@ -544,8 +831,31 @@ public void getAwsCredentialsId_returnAwsCredentialsId_whenAwsCredentialsIdAndCr null, null, null, false, false, null, null, null, null, false, false, false - , 0, 0); + , 0, 0, false); assertEquals("A", ec2FleetCloud.getAwsCredentialsId()); } + private void mockNodeCreatingPart() { + when(jenkins.getNodesObject()).thenReturn(mock(Nodes.class)); + + ExtensionList labelFinder = mock(ExtensionList.class); + when(labelFinder.iterator()).thenReturn(Collections.emptyIterator()); + PowerMockito.when(LabelFinder.all()).thenReturn(labelFinder); + + // mocking part of node creation process Jenkins.getInstance().getLabelAtom(l) + when(jenkins.getLabelAtom(anyString())).thenReturn(new LabelAtom("mock-label")); + } + + private void mockDescribeInstances(String instanceId, String instanceType) { + DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); + describeInstancesResult.withReservations( + new Reservation().withInstances(new Instance() + .withPublicIpAddress("p-ip") + .withInstanceType(instanceType) + .withInstanceId(instanceId))); + + when(amazonEC2.describeInstances(any(DescribeInstancesRequest.class))) + .thenReturn(describeInstancesResult); + } + } diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/FleetStateStatsTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/FleetStateStatsTest.java new file mode 100644 index 00000000..943e74fc --- /dev/null +++ b/src/test/java/com/amazon/jenkins/ec2fleet/FleetStateStatsTest.java @@ -0,0 +1,159 @@ +package com.amazon.jenkins.ec2fleet; + +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.ActiveInstance; +import com.amazonaws.services.ec2.model.BatchState; +import com.amazonaws.services.ec2.model.DescribeSpotFleetInstancesRequest; +import com.amazonaws.services.ec2.model.DescribeSpotFleetInstancesResult; +import com.amazonaws.services.ec2.model.DescribeSpotFleetRequestsRequest; +import com.amazonaws.services.ec2.model.DescribeSpotFleetRequestsResult; +import com.amazonaws.services.ec2.model.LaunchTemplateConfig; +import com.amazonaws.services.ec2.model.SpotFleetLaunchSpecification; +import com.amazonaws.services.ec2.model.SpotFleetRequestConfig; +import com.amazonaws.services.ec2.model.SpotFleetRequestConfigData; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class FleetStateStatsTest { + + @Mock + private AmazonEC2 ec2; + + @Before + public void before() { + when(ec2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(new DescribeSpotFleetInstancesResult()); + + when(ec2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(new DescribeSpotFleetRequestsResult() + .withSpotFleetRequestConfigs( + new SpotFleetRequestConfig() + .withSpotFleetRequestConfig( + new SpotFleetRequestConfigData() + .withTargetCapacity(0)))); + } + + @Test(expected = IllegalStateException.class) + public void readClusterState_failIfNoFleet() { + when(ec2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(new DescribeSpotFleetRequestsResult()); + + FleetStateStats.readClusterState(ec2, "f", ""); + } + + @Test + public void readClusterState_returnFleetInfo() { + when(ec2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(new DescribeSpotFleetRequestsResult() + .withSpotFleetRequestConfigs( + new SpotFleetRequestConfig() + .withSpotFleetRequestState(BatchState.Active) + .withSpotFleetRequestConfig( + new SpotFleetRequestConfigData() + .withTargetCapacity(12)))); + + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f-id", ""); + + Assert.assertEquals("f-id", stats.getFleetId()); + Assert.assertEquals("active", stats.getState()); + Assert.assertEquals(12, stats.getNumDesired()); + } + + @Test + public void readClusterState_returnEmptyIfNoInstancesForFleet() { + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f", ""); + + Assert.assertEquals(Collections.emptySet(), stats.getInstances()); + Assert.assertEquals(0, stats.getNumActive()); + } + + @Test + public void readClusterState_returnAllDescribedInstancesForFleet() { + when(ec2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(new DescribeSpotFleetInstancesResult() + .withActiveInstances( + new ActiveInstance().withInstanceId("i-1"), + new ActiveInstance().withInstanceId("i-2"))); + + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f", ""); + + Assert.assertEquals(ImmutableSet.of("i-1", "i-2"), stats.getInstances()); + Assert.assertEquals(2, stats.getNumActive()); + verify(ec2).describeSpotFleetInstances(new DescribeSpotFleetInstancesRequest() + .withSpotFleetRequestId("f")); + } + + @Test + public void readClusterState_returnAllPagesDescribedInstancesForFleet() { + when(ec2.describeSpotFleetInstances(any(DescribeSpotFleetInstancesRequest.class))) + .thenReturn(new DescribeSpotFleetInstancesResult() + .withNextToken("p1") + .withActiveInstances(new ActiveInstance().withInstanceId("i-1"))) + .thenReturn(new DescribeSpotFleetInstancesResult() + .withActiveInstances(new ActiveInstance().withInstanceId("i-2"))); + + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f", ""); + + Assert.assertEquals(ImmutableSet.of("i-1", "i-2"), stats.getInstances()); + Assert.assertEquals(2, stats.getNumActive()); + verify(ec2).describeSpotFleetInstances(new DescribeSpotFleetInstancesRequest() + .withSpotFleetRequestId("f").withNextToken("p1")); + verify(ec2).describeSpotFleetInstances(new DescribeSpotFleetInstancesRequest() + .withSpotFleetRequestId("f")); + } + + @Test + public void readClusterState_returnEmptyInstanceTypeWeightsIfNoInformation() { + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f", ""); + + Assert.assertEquals(Collections.emptyMap(), stats.getInstanceTypeWeights()); + } + + @Test + public void readClusterState_returnInstanceTypeWeightsFromLaunchSpecification() { + when(ec2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(new DescribeSpotFleetRequestsResult() + .withSpotFleetRequestConfigs(new SpotFleetRequestConfig() + .withSpotFleetRequestState(BatchState.Active) + .withSpotFleetRequestConfig(new SpotFleetRequestConfigData() + .withTargetCapacity(1) + .withLaunchSpecifications( + new SpotFleetLaunchSpecification().withInstanceType("t1").withWeightedCapacity(0.1), + new SpotFleetLaunchSpecification().withInstanceType("t2").withWeightedCapacity(12.0))))); + + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f", ""); + + Assert.assertEquals(ImmutableMap.of("t1", 0.1, "t2", 12.0), stats.getInstanceTypeWeights()); + } + + @Test + public void readClusterState_returnInstanceTypeWeightsForLaunchSpecificationIfItHasIt() { + when(ec2.describeSpotFleetRequests(any(DescribeSpotFleetRequestsRequest.class))) + .thenReturn(new DescribeSpotFleetRequestsResult() + .withSpotFleetRequestConfigs(new SpotFleetRequestConfig() + .withSpotFleetRequestState(BatchState.Active) + .withSpotFleetRequestConfig(new SpotFleetRequestConfigData() + .withTargetCapacity(1) + .withLaunchSpecifications( + new SpotFleetLaunchSpecification().withInstanceType("t1"), + new SpotFleetLaunchSpecification().withWeightedCapacity(12.0))))); + + FleetStateStats stats = FleetStateStats.readClusterState(ec2, "f", ""); + + Assert.assertEquals(Collections.emptyMap(), stats.getInstanceTypeWeights()); + } + +} diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java index 99ed0e94..55371912 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/ProvisionIntegrationTest.java @@ -20,7 +20,6 @@ import hudson.model.queue.QueueTaskFuture; import hudson.slaves.ComputerConnector; import hudson.slaves.ComputerLauncher; -import jenkins.model.Jenkins; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -50,7 +49,7 @@ public void dont_provide_any_planned_if_empty_and_reached_max_capacity() throws EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, computerConnector, false, false, 0, 0, 0, 1, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud); EC2Api ec2Api = mock(EC2Api.class); @@ -93,7 +92,7 @@ public void should_add_planned_if_capacity_required_but_not_described_yet() thro EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, computerConnector, false, false, 0, 0, 10, 1, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud); mockEc2ApiToDescribeFleetNotInstanceWhenModified(); @@ -125,7 +124,7 @@ public void should_keep_planned_node_until_node_will_not_be_online_so_jenkins_wi EC2FleetCloud cloud = spy(new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, computerConnector, false, false, 0, 0, 10, 1, false, false, - false, 300, 15)); + false, 300, 15, false)); j.jenkins.clouds.add(cloud); mockEc2ApiToDescribeInstancesWhenModified(InstanceStateName.Running); @@ -151,7 +150,7 @@ public void should_not_keep_planned_node_if_configured_so_jenkins_will_overprovi final EC2FleetCloud cloud = spy(new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, computerConnector, false, false, 0, 0, 10, 1, false, false, - false, 0, 0)); + false, 0, 0, false)); j.jenkins.clouds.add(cloud); mockEc2ApiToDescribeInstancesWhenModified(InstanceStateName.Running); @@ -176,7 +175,7 @@ public void should_not_allow_jenkins_to_provision_if_address_not_available() thr EC2FleetCloud cloud = spy(new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, computerConnector, false, false, 0, 0, 10, 1, false, false, - false, 0, 0)); + false, 0, 0, false)); j.jenkins.clouds.add(cloud); EC2Api ec2Api = mock(EC2Api.class); @@ -228,7 +227,7 @@ public void should_not_convert_planned_to_node_if_state_is_not_running_and_check EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, "region", null, "fId", "momo", null, computerConnector, false, false, 0, 0, 10, 1, true, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud); mockEc2ApiToDescribeInstancesWhenModified(InstanceStateName.Pending); diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java index 8456e2b1..fb051f15 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/RealEc2ApiIntegrationTest.java @@ -51,7 +51,7 @@ public void run(AmazonEC2 amazonEC2, String fleetId) throws Exception { EC2FleetCloud cloud = new EC2FleetCloud("", "credId", null, null, null, fleetId, null, null, null, false, false, 0, 0, 0, 0, false, false, - false,0, 0); + false,0, 0, false); j.jenkins.clouds.add(cloud); // 10 sec refresh time so wait @@ -84,7 +84,7 @@ public void run(AmazonEC2 amazonEC2, String fleetId) throws Exception { EC2FleetCloud cloud = new EC2FleetCloud(null, "credId", null, null, null, fleetId, null, null, null, false, false, 0, 0, 0, 0, false, false, - false,0, 0); + false,0, 0, false); j.jenkins.clouds.add(cloud); final long start = System.currentTimeMillis(); diff --git a/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java b/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java index 091f0794..7f7357ca 100644 --- a/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java +++ b/src/test/java/com/amazon/jenkins/ec2fleet/UiIntegrationTest.java @@ -45,7 +45,7 @@ public void shouldShowInConfigurationClouds() throws IOException, SAXException { Cloud cloud = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud); HtmlPage page = j.createWebClient().goTo("configure"); @@ -59,13 +59,13 @@ public void shouldShowMultipleClouds() throws IOException, SAXException { Cloud cloud1 = new EC2FleetCloud("a", null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud1); Cloud cloud2 = new EC2FleetCloud("b", null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud2); HtmlPage page = j.createWebClient().goTo("configure"); @@ -82,13 +82,13 @@ public void shouldShowMultipleCloudsWithDefaultName() throws IOException, SAXExc Cloud cloud1 = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud1); Cloud cloud2 = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud2); HtmlPage page = j.createWebClient().goTo("configure"); @@ -105,13 +105,13 @@ public void shouldUpdateProperCloudWhenMultiple() throws IOException, SAXExcepti EC2FleetCloud cloud1 = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud1); EC2FleetCloud cloud2 = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud2); HtmlPage page = j.createWebClient().goTo("configure"); @@ -130,13 +130,13 @@ public void shouldGetFirstWhenMultipleCloudWithSameName() { EC2FleetCloud cloud1 = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud1); EC2FleetCloud cloud2 = new EC2FleetCloud(null, null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud2); assertSame(cloud1, j.jenkins.getCloud("FleetCloud")); @@ -147,13 +147,13 @@ public void shouldGetProperWhenMultipleWithDiffName() { EC2FleetCloud cloud1 = new EC2FleetCloud("a", null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud1); EC2FleetCloud cloud2 = new EC2FleetCloud("b", null, null, null, null, null, null, null, null, false, false, 0, 0, 0, 0, false, false, - false, 0, 0); + false, 0, 0, false); j.jenkins.clouds.add(cloud2); assertSame(cloud1, j.jenkins.getCloud("a")); From 78a5f5da2b0cfe5bac3c0862141f1c9f3fb52903 Mon Sep 17 00:00:00 2001 From: Artem Stasiuk Date: Wed, 3 Jul 2019 18:34:24 -0700 Subject: [PATCH 3/4] Trigger build --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6f2cb36a..e23d79b0 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ jenkins.clouds.add(ec2FleetCloud) jenkins.save() ``` + # Development Plugin usage statistic per Jenkins version [here](https://stats.jenkins.io/pluginversions/ec2-fleet.html) From 5f2791f3bf83462b4e139d51c16edc12bc51dad6 Mon Sep 17 00:00:00 2001 From: Artem Stasiuk Date: Wed, 3 Jul 2019 18:43:41 -0700 Subject: [PATCH 4/4] Trigger build --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e23d79b0..6f2cb36a 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,6 @@ jenkins.clouds.add(ec2FleetCloud) jenkins.save() ``` - # Development Plugin usage statistic per Jenkins version [here](https://stats.jenkins.io/pluginversions/ec2-fleet.html)