You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by jc...@apache.org on 2016/11/28 21:06:11 UTC

aurora git commit: Added the 'reason' to the /pendingTasks endpoint

Repository: aurora
Updated Branches:
  refs/heads/master d56f8c644 -> 8e07b04bb


Added the 'reason' to the /pendingTasks endpoint

Bugs closed: AURORA-1762

Reviewed at https://reviews.apache.org/r/51993/


Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/8e07b04b
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/8e07b04b
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/8e07b04b

Branch: refs/heads/master
Commit: 8e07b04bbd4de23b8f492627da4a614d1e517cf1
Parents: d56f8c6
Author: Pradyumna Kaushik <pk...@binghamton.edu>
Authored: Mon Nov 28 15:05:56 2016 -0600
Committer: Joshua Cohen <jc...@apache.org>
Committed: Mon Nov 28 15:05:56 2016 -0600

----------------------------------------------------------------------
 config/legacy_untested_classes.txt              |   1 -
 .../aurora/scheduler/http/PendingTasks.java     |  33 +++-
 .../aurora/scheduler/metadata/NearestFit.java   |  24 ++-
 .../aurora/scheduler/scheduling/TaskGroup.java  |  12 +-
 .../aurora/scheduler/scheduling/TaskGroups.java |   3 +-
 .../aurora/scheduler/http/OffersTest.java       |   1 -
 .../aurora/scheduler/http/PendingTasksTest.java | 172 +++++++++++++++++++
 .../apache/aurora/scheduler/http/TestUtils.java |  56 ++++++
 .../scheduler/metadata/NearestFitTest.java      |  41 +++++
 9 files changed, 332 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/config/legacy_untested_classes.txt
----------------------------------------------------------------------
diff --git a/config/legacy_untested_classes.txt b/config/legacy_untested_classes.txt
index 8426506..ec3e934 100644
--- a/config/legacy_untested_classes.txt
+++ b/config/legacy_untested_classes.txt
@@ -25,7 +25,6 @@ org/apache/aurora/scheduler/http/Offers$2
 org/apache/aurora/scheduler/http/Offers$3
 org/apache/aurora/scheduler/http/Offers$4
 org/apache/aurora/scheduler/http/Offers$5
-org/apache/aurora/scheduler/http/PendingTasks
 org/apache/aurora/scheduler/http/Quotas
 org/apache/aurora/scheduler/http/Quotas$1
 org/apache/aurora/scheduler/http/Quotas$2

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/main/java/org/apache/aurora/scheduler/http/PendingTasks.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/PendingTasks.java b/src/main/java/org/apache/aurora/scheduler/http/PendingTasks.java
index c80e0c8..4571070 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/PendingTasks.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/PendingTasks.java
@@ -13,6 +13,10 @@
  */
 package org.apache.aurora.scheduler.http;
 
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 import javax.inject.Inject;
@@ -22,7 +26,11 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import org.apache.aurora.scheduler.metadata.NearestFit;
 import org.apache.aurora.scheduler.scheduling.TaskGroups;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.node.ObjectNode;
 
 /**
  * Servlet that exposes detailed information about tasks that are pending.
@@ -31,10 +39,12 @@ import org.apache.aurora.scheduler.scheduling.TaskGroups;
 public class PendingTasks {
 
   private final TaskGroups taskGroups;
+  private final NearestFit nearestFit;
 
   @Inject
-  PendingTasks(TaskGroups taskGroups) {
+  PendingTasks(TaskGroups taskGroups, NearestFit nearestFit) {
     this.taskGroups = Objects.requireNonNull(taskGroups);
+    this.nearestFit = Objects.requireNonNull(nearestFit);
   }
 
   /**
@@ -44,7 +54,24 @@ public class PendingTasks {
    */
   @GET
   @Produces(MediaType.APPLICATION_JSON)
-  public Response getOffers() {
-    return Response.ok(taskGroups.getGroups()).build();
+  public Response getOffers() throws IOException {
+    // Adding reason, received from NearestFit#getPendingReasons() to the JSON Object.
+    Map<String, List<String>> taskGroupReasonMap =
+        nearestFit.getPendingReasons(taskGroups.getGroups());
+    // Adding the attribute "reason" to each of the JSON Objects in the JsonNode.
+    ObjectMapper mapper = new ObjectMapper();
+    JsonNode jsonNode = mapper.valueToTree(taskGroups.getGroups());
+    Iterator<JsonNode> jsonNodeIterator = jsonNode.iterator();
+
+    while (jsonNodeIterator.hasNext()) {
+      JsonNode pendingTask = jsonNodeIterator.next();
+
+      // Retrieving the reasons corresponding to this pendingTask.
+      List<String> reasons = taskGroupReasonMap.get(pendingTask.get("name").asText());
+      // Adding the reasons corresponding to the pendingTask.
+      ((ObjectNode) pendingTask).put("reason", reasons.toString());
+    }
+    return Response.ok(jsonNode).build();
   }
+
 }

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/main/java/org/apache/aurora/scheduler/metadata/NearestFit.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/metadata/NearestFit.java b/src/main/java/org/apache/aurora/scheduler/metadata/NearestFit.java
index f783e7f..1c015a2 100644
--- a/src/main/java/org/apache/aurora/scheduler/metadata/NearestFit.java
+++ b/src/main/java/org/apache/aurora/scheduler/metadata/NearestFit.java
@@ -13,8 +13,13 @@
  */
 package org.apache.aurora.scheduler.metadata;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import javax.inject.Inject;
 
@@ -28,6 +33,7 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.eventbus.Subscribe;
 
+import org.apache.aurora.GuavaUtils;
 import org.apache.aurora.common.quantity.Amount;
 import org.apache.aurora.common.quantity.Time;
 import org.apache.aurora.gen.ScheduleStatus;
@@ -38,6 +44,7 @@ import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange;
 import org.apache.aurora.scheduler.events.PubsubEvent.TasksDeleted;
 import org.apache.aurora.scheduler.events.PubsubEvent.Vetoed;
 import org.apache.aurora.scheduler.filter.SchedulingFilter.Veto;
+import org.apache.aurora.scheduler.scheduling.TaskGroup;
 
 /**
  * Tracks vetoes against scheduling decisions and maintains the closest fit among all the vetoes
@@ -52,8 +59,7 @@ public class NearestFit implements EventSubscriber {
 
   private final LoadingCache<TaskGroupKey, Fit> fitByGroupKey;
 
-  @VisibleForTesting
-  NearestFit(Ticker ticker) {
+  public NearestFit(Ticker ticker) {
     fitByGroupKey = CacheBuilder.newBuilder()
         .expireAfterWrite(EXPIRATION.getValue(), EXPIRATION.getUnit().getTimeUnit())
         .ticker(ticker)
@@ -118,6 +124,20 @@ public class NearestFit implements EventSubscriber {
     fitByGroupKey.getUnchecked(vetoEvent.getGroupKey()).maybeUpdate(vetoEvent.getVetoes());
   }
 
+  /**
+   * Determine the pending reason, for each of the given tasks in taskGroups.
+   *
+   * @param taskGroups Group of pending tasks.
+   * @return A map with key=String (the taskgroup key) and value=List of reasons.
+   */
+  public synchronized Map<String, List<String>> getPendingReasons(Iterable<TaskGroup> taskGroups) {
+    return StreamSupport.stream(taskGroups.spliterator(), false).map(t -> {
+      List<String> reasons = getNearestFit(t.getKey()).stream()
+          .map(Veto::getReason).collect(Collectors.toList());
+      return new HashMap.SimpleEntry<>(t.getKey().toString(), reasons);
+    }).collect(GuavaUtils.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+  }
+
   private static class Fit {
     private ImmutableSet<Veto> vetoes;
 

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroup.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroup.java b/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroup.java
index b521620..b5c50bb 100644
--- a/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroup.java
+++ b/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroup.java
@@ -17,29 +17,34 @@ import java.util.Collection;
 import java.util.Queue;
 import java.util.Set;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 
 import org.apache.aurora.scheduler.base.TaskGroupKey;
+import org.codehaus.jackson.annotate.JsonIgnore;
 
 import static org.apache.aurora.GuavaUtils.toImmutableSet;
 
 /**
  * A group of task IDs that are eligible for scheduling, but may be waiting for a backoff to expire.
  */
-class TaskGroup {
+public class TaskGroup {
   private final TaskGroupKey key;
   private long penaltyMs;
   private final Queue<String> tasks;
 
-  TaskGroup(TaskGroupKey key, String initialTaskId) {
+  @VisibleForTesting
+  public TaskGroup(TaskGroupKey key, String initialTaskId) {
     this.key = key;
     this.penaltyMs = 0;
     this.tasks = Lists.newLinkedList();
     this.tasks.add(initialTaskId);
   }
 
-  synchronized TaskGroupKey getKey() {
+  // This class is serialized by the PendingTasks endpoint, but the key is exposed via getName().
+  @JsonIgnore
+  public synchronized TaskGroupKey getKey() {
     return key;
   }
 
@@ -76,4 +81,5 @@ class TaskGroup {
   public synchronized long getPenaltyMs() {
     return penaltyMs;
   }
+
 }

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroups.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroups.java b/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroups.java
index 2cd2105..2d548b0 100644
--- a/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroups.java
+++ b/src/main/java/org/apache/aurora/scheduler/scheduling/TaskGroups.java
@@ -126,8 +126,9 @@ public class TaskGroups implements EventSubscriber {
     }
   }
 
+  @VisibleForTesting
   @Inject
-  TaskGroups(
+  public TaskGroups(
       @AsyncExecutor DelayExecutor executor,
       TaskGroupsSettings settings,
       TaskScheduler taskScheduler,

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/test/java/org/apache/aurora/scheduler/http/OffersTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/OffersTest.java b/src/test/java/org/apache/aurora/scheduler/http/OffersTest.java
index 9e35732..add0eb8 100644
--- a/src/test/java/org/apache/aurora/scheduler/http/OffersTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/http/OffersTest.java
@@ -152,5 +152,4 @@ public class OffersTest extends EasyMockTest {
   private static List toList(String json) {
     return new Gson().fromJson(json, List.class);
   }
-
 }

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/test/java/org/apache/aurora/scheduler/http/PendingTasksTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/PendingTasksTest.java b/src/test/java/org/apache/aurora/scheduler/http/PendingTasksTest.java
new file mode 100644
index 0000000..b71669f
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/http/PendingTasksTest.java
@@ -0,0 +1,172 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.http;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.aurora.common.testing.easymock.EasyMockTest;
+import org.apache.aurora.common.util.testing.FakeTicker;
+import org.apache.aurora.gen.JobKey;
+import org.apache.aurora.gen.ScheduleStatus;
+import org.apache.aurora.scheduler.base.TaskGroupKey;
+import org.apache.aurora.scheduler.events.PubsubEvent;
+import org.apache.aurora.scheduler.filter.SchedulingFilter.Veto;
+import org.apache.aurora.scheduler.metadata.NearestFit;
+import org.apache.aurora.scheduler.scheduling.TaskGroup;
+import org.apache.aurora.scheduler.scheduling.TaskGroups;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.node.ArrayNode;
+import org.codehaus.jackson.node.ObjectNode;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+
+public class PendingTasksTest extends EasyMockTest {
+
+  private TaskGroups pendingTaskGroups;
+  private NearestFit nearestFit;
+
+  @Before
+  public void setUp() {
+    pendingTaskGroups = createMock(TaskGroups.class);
+    nearestFit = new NearestFit(new FakeTicker());
+  }
+
+  /**
+   * Create a {@link JsonNode} object to mimic the response.
+   *
+   * @param penaltyMs
+   * @param taskIds
+   * @param name
+   * @param reasons
+   * @return Json node for pending tasks whose values are initialized to the provided values.
+   * @throws IOException
+   */
+  private JsonNode getMimicResponseJson(
+      long penaltyMs, String[] taskIds, String name, List<String> reasons) throws IOException {
+    ObjectMapper mapper = new ObjectMapper();
+    ObjectNode mutablePendingTaskJson = mapper.createObjectNode();
+    // Adding the key=value pairs to mutablePendingTaskJson.
+    mutablePendingTaskJson.put("penaltyMs", penaltyMs);
+    mutablePendingTaskJson.putArray("taskIds");
+    for (String taskId : taskIds) {
+      ((ArrayNode) mutablePendingTaskJson.get("taskIds")).add(taskId);
+    }
+    mutablePendingTaskJson.put("name", name);
+    mutablePendingTaskJson.put("reason", reasons.toString());
+    return mutablePendingTaskJson;
+  }
+
+  @Test
+  public void testNoOffers() throws IOException {
+    // Making a task that is not in PENDING state.
+    IJobKey jobKey = IJobKey.build(new JobKey("role", "test", "nonPendingJob"));
+    IScheduledTask task = TestUtils.makeTask(jobKey, "task0", 0,
+        ScheduleStatus.ASSIGNED, 10, 10, 10);
+
+    PubsubEvent.TaskStateChange taskStateChange = PubsubEvent.TaskStateChange.transition(
+        task, ScheduleStatus.INIT);
+
+    pendingTaskGroups.taskChangedState(taskStateChange);
+    expectLastCall();
+
+    // Recording the return value of pendingTaskGroups.getGroups().
+    List<TaskGroup> taskGroupList = new ArrayList<>();
+    expect(pendingTaskGroups.getGroups()).andReturn(taskGroupList).anyTimes();
+
+    replay(pendingTaskGroups);
+
+    // Testing.
+    pendingTaskGroups.taskChangedState(taskStateChange);
+    PendingTasks pendingTasks = new PendingTasks(pendingTaskGroups, nearestFit);
+    JsonNode mimicResponseNoPendingTaskJson = new ObjectMapper().createArrayNode();
+    JsonNode actualResponseJson = new ObjectMapper().valueToTree(
+        pendingTasks.getOffers().getEntity());
+    assertEquals(mimicResponseNoPendingTaskJson, actualResponseJson);
+  }
+
+  @Test
+  public void testOffers() throws IOException {
+    // Making pending tasks.
+    IJobKey jobKey0 = IJobKey.build(new JobKey("role", "test", "jobA"));
+    IJobKey jobKey1 = IJobKey.build(new JobKey("role", "test", "jobB"));
+    IScheduledTask task0 = TestUtils.makeTask(jobKey0, "task0", 0,
+        ScheduleStatus.PENDING, 1000, 1000000, 10);
+    IScheduledTask task1 = TestUtils.makeTask(jobKey1, "task1", 0,
+        ScheduleStatus.PENDING, 1000, 10, 1000000);
+
+    PubsubEvent.TaskStateChange taskStateChange0 = PubsubEvent.TaskStateChange.transition(
+        task0, ScheduleStatus.INIT);
+    PubsubEvent.TaskStateChange taskStateChange1 = PubsubEvent.TaskStateChange.transition(
+        task1, ScheduleStatus.INIT);
+
+    pendingTaskGroups.taskChangedState(taskStateChange0);
+    pendingTaskGroups.taskChangedState(taskStateChange1);
+    expectLastCall();
+
+    // Recording the return value of pendingTaskGroups.getGroups().
+    TaskGroupKey taskGroupKey0 = TaskGroupKey.from(task0.getAssignedTask().getTask());
+    TaskGroupKey taskGroupKey1 = TaskGroupKey.from(task1.getAssignedTask().getTask());
+    TaskGroup taskGroup0 = new TaskGroup(taskGroupKey0, "task0");
+    TaskGroup taskGroup1 = new TaskGroup(taskGroupKey1, "task1");
+    List<TaskGroup> taskGroupList = new ArrayList<>();
+    taskGroupList.add(taskGroup0);
+    taskGroupList.add(taskGroup1);
+    expect(pendingTaskGroups.getGroups()).andReturn(taskGroupList).anyTimes();
+
+    // Creating vetoes for CPU and RAM, corresponding to task0.
+    ImmutableSet<Veto> vetoes = ImmutableSet.<Veto>builder()
+        .add(Veto.insufficientResources("CPU", 1))
+        .add(Veto.insufficientResources("RAM", 1)).build();
+    nearestFit.vetoed(new PubsubEvent.Vetoed(taskGroupKey0, vetoes));
+    // Creating vetoes for CPU and DISK, corresponding to task1.
+    ImmutableSet<Veto> vetoes1 = ImmutableSet.<Veto>builder()
+        .add(Veto.insufficientResources("CPU", 1))
+        .add(Veto.insufficientResources("DISK", 1)).build();
+    nearestFit.vetoed(new PubsubEvent.Vetoed(taskGroupKey1, vetoes1));
+    replay(pendingTaskGroups);
+
+    // Testing.
+    pendingTaskGroups.taskChangedState(taskStateChange0);
+    pendingTaskGroups.taskChangedState(taskStateChange1);
+    PendingTasks pendingTasks0 = new PendingTasks(pendingTaskGroups, nearestFit);
+    String[] taskIds0 = {"task0"};
+    String[] taskIds1 = {"task1"};
+    String[] reasonsArr0 = {"Insufficient: CPU", "Insufficient: RAM"};
+    String[] reasonsArr1 = {"Insufficient: CPU", "Insufficient: DISK"};
+    List<String> reasons0 = Arrays.stream(reasonsArr0).collect(Collectors.toList());
+    List<String> reasons1 = Arrays.stream(reasonsArr1).collect(Collectors.toList());
+    JsonNode mimicResponseTwoPendingTasksJson = new ObjectMapper().createArrayNode();
+    JsonNode mimicJson0 = getMimicResponseJson(0, taskIds0, "role/test/jobA", reasons0);
+    JsonNode mimicJson1 = getMimicResponseJson(0, taskIds1, "role/test/jobB", reasons1);
+    ((ArrayNode) mimicResponseTwoPendingTasksJson).add(mimicJson0);
+    ((ArrayNode) mimicResponseTwoPendingTasksJson).add(mimicJson1);
+    JsonNode actualResponseJson = new ObjectMapper().valueToTree(
+        pendingTasks0.getOffers().getEntity());
+    assertEquals(mimicResponseTwoPendingTasksJson, actualResponseJson);
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/test/java/org/apache/aurora/scheduler/http/TestUtils.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/TestUtils.java b/src/test/java/org/apache/aurora/scheduler/http/TestUtils.java
new file mode 100644
index 0000000..689482c
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/http/TestUtils.java
@@ -0,0 +1,56 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.aurora.scheduler.http;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.aurora.gen.AssignedTask;
+import org.apache.aurora.gen.ScheduleStatus;
+import org.apache.aurora.gen.ScheduledTask;
+import org.apache.aurora.gen.TaskConfig;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
+
+public final class TestUtils {
+
+  private TestUtils() { }
+
+  /**
+   * Make task and set status to the given {@link ScheduleStatus}.
+   *
+   * @param jobKey The task group key.
+   * @param id The task id.
+   * @param instanceId The id of the instance of the task.
+   * @param taskStatus The status of the task.
+   * @param numCPUs The number of CPUs required for the task.
+   * @param ramMB The amount of RAM (in MegaBytes) required for the task.
+   * @param diskMB The amount of disk space (in MegaBytes) required for the task.
+   * @return Task.
+   */
+  @VisibleForTesting
+  public static IScheduledTask makeTask(
+      IJobKey jobKey, String id, int instanceId,
+      ScheduleStatus taskStatus, double numCPUs, long ramMB, long diskMB) {
+    return IScheduledTask.build(new ScheduledTask()
+        .setStatus(taskStatus)
+        .setAssignedTask(new AssignedTask()
+            .setInstanceId(instanceId)
+            .setTaskId(id)
+            .setTask(new TaskConfig()
+                .setJob(jobKey.newBuilder())
+                .setNumCpus(numCPUs)
+                .setRamMb(ramMB)
+                .setDiskMb(diskMB))));
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8e07b04b/src/test/java/org/apache/aurora/scheduler/metadata/NearestFitTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/metadata/NearestFitTest.java b/src/test/java/org/apache/aurora/scheduler/metadata/NearestFitTest.java
index d9b3cc6..195d083 100644
--- a/src/test/java/org/apache/aurora/scheduler/metadata/NearestFitTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/metadata/NearestFitTest.java
@@ -13,7 +13,13 @@
  */
 package org.apache.aurora.scheduler.metadata;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import com.google.common.collect.ImmutableSet;
 
@@ -21,6 +27,7 @@ import org.apache.aurora.common.quantity.Amount;
 import org.apache.aurora.common.quantity.Time;
 import org.apache.aurora.common.util.testing.FakeTicker;
 import org.apache.aurora.gen.AssignedTask;
+import org.apache.aurora.gen.JobKey;
 import org.apache.aurora.gen.ScheduleStatus;
 import org.apache.aurora.gen.ScheduledTask;
 import org.apache.aurora.gen.TaskConfig;
@@ -29,6 +36,9 @@ import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange;
 import org.apache.aurora.scheduler.events.PubsubEvent.TasksDeleted;
 import org.apache.aurora.scheduler.events.PubsubEvent.Vetoed;
 import org.apache.aurora.scheduler.filter.SchedulingFilter.Veto;
+import org.apache.aurora.scheduler.http.TestUtils;
+import org.apache.aurora.scheduler.scheduling.TaskGroup;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
 import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
 import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
 import org.junit.Before;
@@ -111,6 +121,32 @@ public class NearestFitTest {
     assertNearest();
   }
 
+  @Test
+  public void testGetPendingReasons() {
+    // Making task that requires lot of CPUs and RAM.
+    IJobKey jobKey = IJobKey.build(new JobKey("role", "test", "jobA"));
+    IScheduledTask task = TestUtils.makeTask(jobKey, "task0", 0,
+        ScheduleStatus.ASSIGNED, 1000, 10000000, 10);
+    // Changing the state of the task to PENDING.
+    nearest.stateChanged(TaskStateChange.transition(task, ScheduleStatus.PENDING));
+    // Adding the task to a list of pending tasks.
+    TaskGroupKey taskGroupKey = TaskGroupKey.from(task.getAssignedTask().getTask());
+    TaskGroup taskGroup = new TaskGroup(taskGroupKey, "task0");
+    List<TaskGroup> pendingTaskGroups = new ArrayList<>();
+    pendingTaskGroups.add(taskGroup);
+
+    // Creating vetoes for CPU and RAM.
+    nearest.vetoed(new Vetoed(taskGroupKey, vetoes(SEVERITY_4_CPU, SEVERITY_4_RAM)));
+
+    // Testing.
+    Map<String, List<String>> mimicPendingReasons = new LinkedHashMap<>();
+    List<String> reasons = Arrays.stream(
+        new String[]{SEVERITY_4_CPU.getReason(), SEVERITY_4_RAM.getReason()})
+            .collect(Collectors.toList());
+    mimicPendingReasons.put("role/test/jobA",  reasons);
+    assertPendingReasons(nearest.getPendingReasons(pendingTaskGroups), mimicPendingReasons);
+  }
+
   private Set<Veto> vetoes(Veto... vetoes) {
     return ImmutableSet.copyOf(vetoes);
   }
@@ -122,4 +158,9 @@ public class NearestFitTest {
   private void assertNearest(Veto... vetoes) {
     assertEquals(vetoes(vetoes), nearest.getNearestFit(GROUP_KEY));
   }
+
+  private void assertPendingReasons(Map<String, List<String>> actualPendingReasons,
+      Map<String, List<String>> mimicPendingReasons) {
+    assertEquals(actualPendingReasons, mimicPendingReasons);
+  }
 }