Skip to content

Commit 8059f1c

Browse files
committed
add integration tests for configuration change leading to lost state and rebuilding lost state to terminate instances previously marked for termination
1 parent 261964d commit 8059f1c

File tree

1 file changed

+129
-5
lines changed

1 file changed

+129
-5
lines changed

src/test/java/com/amazon/jenkins/ec2fleet/EC2RetentionStrategyIntegrationTest.java

+129-5
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,27 @@
1111
import com.amazonaws.services.ec2.model.Reservation;
1212
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
1313
import com.amazonaws.services.ec2.model.TerminateInstancesResult;
14-
import hudson.model.Executor;
14+
import com.gargoylesoftware.htmlunit.html.HtmlForm;
15+
import com.gargoylesoftware.htmlunit.html.HtmlFormUtil;
16+
import com.gargoylesoftware.htmlunit.html.HtmlPage;
17+
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
1518
import hudson.model.Node;
16-
import hudson.model.Queue;
1719
import hudson.model.queue.QueueTaskFuture;
18-
import hudson.security.AccessControlled;
19-
import jenkins.model.Jenkins;
2020
import org.junit.Before;
2121
import org.junit.Test;
2222
import org.mockito.ArgumentCaptor;
2323
import org.mockito.Mockito;
2424

25-
import java.io.IOException;
2625
import java.util.ArrayList;
2726
import java.util.Arrays;
2827
import java.util.Collections;
2928
import java.util.HashSet;
3029
import java.util.List;
30+
import java.util.concurrent.TimeUnit;
31+
import java.util.stream.Collectors;
3132

33+
import static org.junit.Assert.assertNotNull;
34+
import static org.junit.Assert.assertNotSame;
3235
import static org.junit.Assert.assertTrue;
3336
import static org.mockito.ArgumentMatchers.any;
3437
import static org.mockito.ArgumentMatchers.anyString;
@@ -72,6 +75,30 @@ public void before() {
7275
when(amazonEC2.terminateInstances(any(TerminateInstancesRequest.class))).thenReturn(new TerminateInstancesResult());
7376
}
7477

78+
@Test
79+
public void shouldTerminateNodeMarkedForDeletion() throws Exception {
80+
final EC2FleetCloud cloud = new EC2FleetCloud(null, null, "credId", null, "region",
81+
null, "fId", "momo", null, new LocalComputerConnector(j), false, false,
82+
1, 0, 0, 0, 1, false, true, "-1", false, 0, 0, false, 999, false);
83+
// Set initial jenkins nodes
84+
cloud.update();
85+
j.jenkins.clouds.add(cloud);
86+
87+
assertAtLeastOneNode();
88+
89+
EC2FleetNode node = (EC2FleetNode) j.jenkins.getNode("i-1");
90+
EC2FleetNodeComputer c = (EC2FleetNodeComputer) node.toComputer();
91+
c.doDoDelete(); // mark node for termination
92+
node.getRetentionStrategy().check(c);
93+
94+
// Make sure the scheduled for termination instances are terminated
95+
cloud.update();
96+
97+
final ArgumentCaptor<TerminateInstancesRequest> argument = ArgumentCaptor.forClass(TerminateInstancesRequest.class);
98+
verify(amazonEC2, times(1)).terminateInstances(argument.capture());
99+
assertTrue(argument.getAllValues().get(0).getInstanceIds().containsAll(Arrays.asList("i-1")));
100+
}
101+
75102
@Test
76103
public void shouldTerminateExcessCapacity() throws Exception {
77104
final EC2FleetCloud cloud = new EC2FleetCloud(null, null, "credId", null, "region",
@@ -222,4 +249,101 @@ public void shouldNotTerminateBelowMinSpareSize() throws Exception {
222249

223250
verify((amazonEC2), times(0)).terminateInstances(any());
224251
}
252+
253+
@Test
254+
public void shouldTerminateWhenMaxTotalUsesIsExhausted() throws Exception {
255+
final String label = "momo";
256+
final int numTasks = 4; // schedule a total of 4 tasks, 2 per instance
257+
final int maxTotalUses = 2;
258+
final int taskSleepTime = 1;
259+
260+
EC2FleetCloud cloud = spy(new EC2FleetCloud("testCloud", null, "credId", null, "region",
261+
null, "fId", label, null, new LocalComputerConnector(j), false, false,
262+
0, 0, 10, 0, 1, false, true,
263+
String.valueOf(maxTotalUses), true, 0, 0, false, 10, false));
264+
j.jenkins.clouds.add(cloud);
265+
cloud.update();
266+
assertAtLeastOneNode();
267+
268+
System.out.println("*** scheduling tasks ***");
269+
waitJobSuccessfulExecution(enqueTask(numTasks, taskSleepTime));
270+
Thread.sleep(3000); // sleep for a bit to make sure post job actions finish and the computers are idle
271+
272+
// make sure the instances scheduled for termination are terminated
273+
cloud.update();
274+
275+
final ArgumentCaptor<TerminateInstancesRequest> argument = ArgumentCaptor.forClass(TerminateInstancesRequest.class);
276+
verify((amazonEC2)).terminateInstances(argument.capture());
277+
assertTrue(argument.getAllValues().get(0).getInstanceIds().containsAll(Arrays.asList("i-1", "i-2")));
278+
}
279+
280+
@Test
281+
public void shouldTerminateNodeForMaxTotalUsesIsExhaustedAfterConfigChange() throws Exception {
282+
final String label = "momo";
283+
final int numTasks = 4; // schedule a total of 4 tasks, 2 per instance
284+
final int maxTotalUses = 2;
285+
final long scheduleInterval = 5;
286+
final int cloudStatusInternalSec = 60; // increase to trigger update manually
287+
final int taskSleepTime = 1;
288+
289+
EC2FleetCloud cloud = new EC2FleetCloud("testCloud", null, "credId", null, "region",
290+
null, "fId", label, null, new LocalComputerConnector(j), false, false,
291+
0, 0, 10, 0, 1, false, true,
292+
String.valueOf(maxTotalUses), true, 0, 0, false,
293+
cloudStatusInternalSec, false);
294+
j.jenkins.clouds.add(cloud);
295+
cloud.update();
296+
assertAtLeastOneNode();
297+
298+
// initiate a config change after a node exhausts maxTotalUses and is scheduled for termination
299+
EC2FleetCloud newCloud;
300+
String nodeToTerminate = null;
301+
int taskCount = 0;
302+
final List<QueueTaskFuture> tasks = new ArrayList<>();
303+
for (int i=numTasks; i > 0 ; i--) {
304+
// get first node that is about to get terminated, before scheduling more tasks
305+
List<String> nodesWithExhaustedMaxUses = j.jenkins.getNodes().stream()
306+
.filter(n -> ((EC2FleetNode)n).getUsesRemaining() == 0)
307+
.map(Node::getNodeName)
308+
.collect(Collectors.toList());
309+
if (nodesWithExhaustedMaxUses != null && !nodesWithExhaustedMaxUses.isEmpty()) {
310+
nodeToTerminate = nodesWithExhaustedMaxUses.get(0);
311+
break; // we have what we want, stop scheduling more tasks - exit loop and verify
312+
}
313+
314+
// schedule a task
315+
tasks.addAll(enqueTask(1, taskSleepTime));
316+
taskCount++;
317+
System.out.println("scheduled task " + taskCount + ", waiting " + scheduleInterval + " sec");
318+
Thread.sleep(TimeUnit.SECONDS.toMillis(scheduleInterval));
319+
}
320+
waitJobSuccessfulExecution(tasks); // wait for scheduleToTerminate to be called
321+
322+
assertNotNull(nodeToTerminate);
323+
324+
// make a config change after a node is scheduled to terminate
325+
HtmlPage page = j.createWebClient().goTo("configureClouds");
326+
HtmlForm form = page.getFormByName("config");
327+
System.out.println(form.toString());
328+
System.out.println(IntegrationTest.getElementsByNameWithoutJdk(page, "_.name"));
329+
((HtmlTextInput) IntegrationTest.getElementsByNameWithoutJdk(page, "_.name").get(0)).setText("new-name");
330+
HtmlFormUtil.submit(form);
331+
332+
// verify cloud object was re-created, leading to lost state (i.e. instanceIdsToTerminate)
333+
newCloud = (EC2FleetCloud) j.jenkins.clouds.get(0);
334+
assertNotSame(cloud, newCloud);
335+
assertTrue(cloud.getInstanceIdsToTerminate().containsKey(nodeToTerminate));
336+
assertTrue(newCloud.getInstanceIdsToTerminate().isEmpty());
337+
338+
// initiate check to schedule instance to terminate again
339+
EC2FleetNode node = (EC2FleetNode) j.jenkins.getNode(nodeToTerminate);
340+
node.getRetentionStrategy().check(node.toComputer());
341+
342+
// terminate scheduled instances
343+
cloud.update();
344+
345+
final ArgumentCaptor<TerminateInstancesRequest> argument = ArgumentCaptor.forClass(TerminateInstancesRequest.class);
346+
verify((amazonEC2)).terminateInstances(argument.capture());
347+
assertTrue(argument.getAllValues().get(0).getInstanceIds().contains(nodeToTerminate));
348+
}
225349
}

0 commit comments

Comments
 (0)