aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From wfar...@apache.org
Subject [36/51] [partial] Rename twitter* and com.twitter to apache and org.apache directories to preserve all file history before the refactor.
Date Tue, 31 Dec 2013 21:20:29 GMT
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/base/Numbers.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/base/Numbers.java b/src/main/java/org/apache/aurora/scheduler/base/Numbers.java
new file mode 100644
index 0000000..74b5e0b
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/base/Numbers.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.base;
+
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.PeekingIterator;
+import com.google.common.collect.Range;
+import com.google.common.collect.Sets;
+
+/**
+ * Utility class for working with numbers.
+ */
+public final class Numbers {
+
+  private Numbers() {
+    // Utility class.
+  }
+
+  /**
+   * Converts a set of integers into a set of contiguous closed ranges that equally represent the
+   * input integers.
+   * <p>
+   * The resulting ranges will be in ascending order.
+   *
+   * @param values Values to transform to ranges.
+   * @return Closed ranges with identical members to the input set.
+   */
+  public static Set<Range<Integer>> toRanges(Iterable<Integer> values) {
+    ImmutableSet.Builder<Range<Integer>> builder = ImmutableSet.builder();
+
+    PeekingIterator<Integer> iterator =
+        Iterators.peekingIterator(Sets.newTreeSet(values).iterator());
+
+    // Build ranges until there are no numbers left.
+    while (iterator.hasNext()) {
+      // Start a new range.
+      int start = iterator.next();
+      int end = start;
+      // Increment the end until the range is non-contiguous.
+      while (iterator.hasNext() && (iterator.peek() == (end + 1))) {
+        end++;
+        iterator.next();
+      }
+
+      builder.add(Range.closed(start, end));
+    }
+
+    return builder.build();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/base/Query.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/base/Query.java b/src/main/java/org/apache/aurora/scheduler/base/Query.java
new file mode 100644
index 0000000..d02ef87
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/base/Query.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.base;
+
+import java.util.EnumSet;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Ints;
+
+import com.twitter.aurora.gen.Identity;
+import com.twitter.aurora.gen.InstanceKey;
+import com.twitter.aurora.gen.ScheduleStatus;
+import com.twitter.aurora.gen.TaskQuery;
+import com.twitter.aurora.scheduler.storage.entities.IJobKey;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import static org.apache.commons.lang.StringUtils.isEmpty;
+
+/**
+ * A utility class to construct storage queries.
+ * TODO(Sathya): Add some basic unit tests for isJobScoped and isOnlyJobScoped.
+ */
+public final class Query {
+
+  private Query() {
+    // Utility.
+  }
+
+  /**
+   * Checks whether a query is scoped to a specific job.
+   * A query scoped to a job specifies a role and job name.
+   *
+   * @param taskQuery Query to test.
+   * @return {@code true} if the query specifies at least a role and job name,
+   *         otherwise {@code false}.
+   */
+  public static boolean isJobScoped(Builder taskQuery) {
+    TaskQuery query = taskQuery.get();
+    return (query.getOwner() != null)
+        && !isEmpty(query.getOwner().getRole())
+        && !isEmpty(query.getEnvironment())
+        && !isEmpty(query.getJobName());
+  }
+
+  /**
+   * Checks whether a query is strictly scoped to a specific job. A query is strictly job scoped,
+   * iff it has the role, environment and jobName set.
+   *
+   * @param query Query to test.
+   * @return {@code true} if the query is strictly job scoped, otherwise {@code false}.
+   */
+  public static boolean isOnlyJobScoped(Builder query) {
+    Optional<IJobKey> jobKey = JobKeys.from(query);
+    return jobKey.isPresent() && Query.jobScoped(jobKey.get()).equals(query);
+  }
+
+  public static Builder arbitrary(TaskQuery query) {
+    return new Builder(query.deepCopy());
+  }
+
+  public static Builder unscoped() {
+    return new Builder();
+  }
+
+  public static Builder roleScoped(String role) {
+    return unscoped().byRole(role);
+  }
+
+  public static Builder envScoped(String role, String environment) {
+    return unscoped().byEnv(role, environment);
+  }
+
+  public static Builder jobScoped(IJobKey jobKey) {
+    return unscoped().byJob(jobKey);
+  }
+
+  public static Builder instanceScoped(InstanceKey instanceKey) {
+    return instanceScoped(IJobKey.build(instanceKey.getJobKey()), instanceKey.getInstanceId());
+  }
+
+  public static Builder instanceScoped(IJobKey jobKey, int instanceId, int... instanceIds) {
+    return unscoped().byInstances(jobKey, instanceId, instanceIds);
+  }
+
+  public static Builder instanceScoped(IJobKey jobKey, Iterable<Integer> instanceIds) {
+    return unscoped().byInstances(jobKey, instanceIds);
+  }
+
+  public static Builder taskScoped(String taskId, String... taskIds) {
+    return unscoped().byId(taskId, taskIds);
+  }
+
+  public static Builder taskScoped(Iterable<String> taskIds) {
+    return unscoped().byId(taskIds);
+  }
+
+  public static Builder slaveScoped(String slaveHost) {
+    return unscoped().bySlave(slaveHost);
+  }
+
+  public static Builder statusScoped(ScheduleStatus status, ScheduleStatus... statuses) {
+    return unscoped().byStatus(status, statuses);
+  }
+
+  public static Builder statusScoped(Iterable<ScheduleStatus> statuses) {
+    return unscoped().byStatus(statuses);
+  }
+
+  /**
+   * A Builder of TaskQueries. Builders are immutable and provide access to a set of convenience
+   * methods to return a new builder of another scope. Available scope filters include slave,
+   * taskId, role, jobs of a role, and instances of a job.
+   *
+   * <p>
+   * This class does not expose the full functionality of TaskQuery but rather subsets of it that
+   * can be efficiently executed and make sense in the context of the scheduler datastores. This
+   * builder should be preferred over constructing TaskQueries directly.
+   * </p>
+   *
+   * TODO(ksweeney): Add an environment scope.
+   */
+  public static final class Builder implements Supplier<TaskQuery> {
+    private final TaskQuery query;
+
+    private Builder() {
+      this.query = new TaskQuery();
+    }
+
+    private Builder(final TaskQuery query) {
+      this.query = checkNotNull(query); // It is expected that the caller calls deepCopy.
+    }
+
+    /**
+     * Build a query that is the combination of all the filters applied to a Builder. Mutating the
+     * returned object will not affect the state of the builder. Can be called any number of times
+     * and will return a new {@code TaskQuery} each time.
+     *
+     * @return A new TaskQuery satisfying this builder's constraints.
+     */
+    @Override
+    public TaskQuery get() {
+      return query.deepCopy();
+    }
+
+    @Override
+    public boolean equals(Object that) {
+      return that != null
+          && that instanceof Builder
+          && Objects.equal(query, ((Builder) that).query);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(query);
+    }
+
+    @Override
+    public String toString() {
+      return Objects.toStringHelper(this)
+          .add("query", query)
+          .toString();
+    }
+
+    /**
+     * Create a builder scoped to tasks.
+     *
+     * @param taskId An ID of a task to scope the builder to.
+     * @param taskIds Additional IDs of tasks to scope the builder to (they are ORed together).
+     * @return A new Builder scoped to the given tasks.
+     */
+    public Builder byId(String taskId, String... taskIds) {
+      checkNotNull(taskId);
+
+      return new Builder(
+          query.deepCopy()
+              .setTaskIds(ImmutableSet.<String>builder().add(taskId).add(taskIds).build()));
+    }
+
+    /**
+     * Create a builder scoped to tasks.
+     *
+     * @see #byId(String, String...)
+     *
+     * @param taskIds The IDs of the tasks to scope the query to (ORed together).
+     * @return A new Builder scoped to the given tasks.
+     */
+    public Builder byId(Iterable<String> taskIds) {
+      checkNotNull(taskIds);
+
+      return new Builder(
+          query.deepCopy().setTaskIds(ImmutableSet.copyOf(taskIds)));
+    }
+
+    /**
+     * Create a builder scoped to a role. A role scope conflicts with job and instance scopes.
+     *
+     * @param role The role to scope the query to.
+     * @return A new Builder scoped to the given role.
+     */
+    public Builder byRole(String role) {
+      checkNotNull(role);
+
+      return new Builder(
+          query.deepCopy().setOwner(new Identity().setRole(role)));
+    }
+
+    /**
+     * Create a builder scoped to an environment. An environment scope conflicts with role, job,
+     * and instance scopes.
+     *
+     * @param role The role to scope the query to.
+     * @param environment The environment to scope the query to.
+     * @return A new Builder scoped to the given environment.
+     */
+    public Builder byEnv(String role, String environment) {
+      checkNotNull(role);
+      checkNotNull(environment);
+
+      return new Builder(
+          query.deepCopy()
+              .setOwner(new Identity().setRole(role))
+              .setEnvironment(environment));
+    }
+
+    /**
+     * Returns a new builder scoped to the job uniquely identified by the given key. A job scope
+     * conflicts with role and instance scopes.
+     *
+     * @param jobKey The key of the job to scope the query to.
+     * @return A new Builder scoped to the given jobKey.
+     */
+    public Builder byJob(IJobKey jobKey) {
+      JobKeys.assertValid(jobKey);
+
+      return new Builder(
+          query.deepCopy()
+              .setOwner(new Identity().setRole(jobKey.getRole()))
+              .setEnvironment(jobKey.getEnvironment())
+              .setJobName(jobKey.getName()));
+    }
+
+    /**
+     * Returns a new builder scoped to the slave uniquely identified by the given slaveHost. A
+     * builder can only be scoped to slaves once.
+     *
+     * @param slaveHost The hostname of the slave to scope the query to.
+     * @return A new Builder scoped to the given slave.
+     */
+    public Builder bySlave(String slaveHost) {
+      checkNotNull(slaveHost);
+
+      return new Builder(query.deepCopy().setSlaveHost(slaveHost));
+    }
+
+    /**
+     * Returns a new builder scoped to the given statuses. A builder can only be scoped to statuses
+     * once.
+     *
+     * @param status The status to scope this Builder to.
+     * @param statuses Additional statuses to scope this Builder to (they are ORed together).
+     * @return A new Builder scoped to the given statuses.
+     */
+    public Builder byStatus(ScheduleStatus status, ScheduleStatus... statuses) {
+      checkNotNull(status);
+
+      return new Builder(
+          query.deepCopy().setStatuses(EnumSet.of(status, statuses)));
+    }
+
+    /**
+     * Create a new Builder scoped to statuses.
+     *
+     * @see Builder#byStatus(ScheduleStatus, ScheduleStatus...)
+     *
+     * @param statuses The statuses to scope this Builder to.
+     * @return A new Builder scoped to the given statuses.
+     */
+    public Builder byStatus(Iterable<ScheduleStatus> statuses) {
+      checkNotNull(statuses);
+
+      return new Builder(
+          query.deepCopy().setStatuses(EnumSet.copyOf(ImmutableSet.copyOf(statuses))));
+    }
+
+    /**
+     * Returns a new Builder scoped to the given instances of the given job. A builder can only
+     * be scoped to a set of instances, a job, or a role once.
+     *
+     * @param jobKey The key identifying the job.
+     * @param instanceId An instance id of the target job.
+     * @param instanceIds Additional instance ids of the target job.
+     * @return A new Builder scoped to the given instance ids.
+     */
+    public Builder byInstances(IJobKey jobKey, int instanceId, int... instanceIds) {
+      JobKeys.assertValid(jobKey);
+
+      return new Builder(
+          query.deepCopy()
+              .setOwner(new Identity().setRole(jobKey.getRole()))
+              .setEnvironment(jobKey.getEnvironment())
+              .setJobName(jobKey.getName())
+              .setInstanceIds(ImmutableSet.<Integer>builder()
+                  .add(instanceId)
+                  .addAll(Ints.asList(instanceIds))
+                  .build()));
+    }
+
+    /**
+     * Create a new Builder scoped to instances.
+     *
+     * @see Builder#byInstances
+     *
+     * @param jobKey The key identifying the job.
+     * @param instanceIds Instances of the target job.
+     * @return A new Builder scoped to the given instance ids.
+     */
+    public Builder byInstances(IJobKey jobKey, Iterable<Integer> instanceIds) {
+      JobKeys.assertValid(jobKey);
+      checkNotNull(instanceIds);
+
+      return new Builder(
+          query.deepCopy()
+              .setOwner(new Identity().setRole(jobKey.getRole()))
+              .setEnvironment(jobKey.getEnvironment())
+              .setJobName(jobKey.getName())
+              .setInstanceIds(ImmutableSet.copyOf(instanceIds)));
+    }
+
+    /**
+     * A convenience method to scope this builder to {@link Tasks#ACTIVE_STATES}.
+     *
+     * @return A new Builder scoped to Tasks#ACTIVE_STATES.
+     */
+    public Builder active() {
+      return byStatus(Tasks.ACTIVE_STATES);
+    }
+
+    /**
+     * A convenience method to scope this builder to {@link Tasks#TERMINAL_STATES}.
+     *
+     * @return A new Builder scoped to Tasks#TERMINAL_STATES.
+     */
+    public Builder terminal() {
+      return byStatus(Tasks.TERMINAL_STATES);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/base/ScheduleException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/base/ScheduleException.java b/src/main/java/org/apache/aurora/scheduler/base/ScheduleException.java
new file mode 100644
index 0000000..0420ee9
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/base/ScheduleException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.base;
+
+/**
+ * Exception class to signal a failure to schedule a task or job.
+ */
+public class ScheduleException extends Exception {
+  public ScheduleException(String msg) {
+    super(msg);
+  }
+
+  public ScheduleException(String msg, Throwable t) {
+    super(msg, t);
+  }
+
+  public ScheduleException(Throwable t) {
+    super(t);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/base/SchedulerException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/base/SchedulerException.java b/src/main/java/org/apache/aurora/scheduler/base/SchedulerException.java
new file mode 100644
index 0000000..a51c4e0
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/base/SchedulerException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.base;
+
+/**
+ * Indicates some form of unexpected scheduler exception.
+ */
+public class SchedulerException extends RuntimeException {
+  public SchedulerException(String message) {
+    super(message);
+  }
+  public SchedulerException(String message, Throwable cause) {
+    super(message, cause);
+  }
+  public SchedulerException(Throwable cause) {
+    super(cause);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/base/Tasks.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/base/Tasks.java b/src/main/java/org/apache/aurora/scheduler/base/Tasks.java
new file mode 100644
index 0000000..d98da3f
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/base/Tasks.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.base;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Ordering;
+
+import com.twitter.aurora.gen.ScheduleStatus;
+import com.twitter.aurora.gen.ScheduledTask;
+import com.twitter.aurora.gen.apiConstants;
+import com.twitter.aurora.scheduler.storage.entities.IAssignedTask;
+import com.twitter.aurora.scheduler.storage.entities.IJobKey;
+import com.twitter.aurora.scheduler.storage.entities.IScheduledTask;
+import com.twitter.aurora.scheduler.storage.entities.ITaskConfig;
+
+/**
+ * Utility class providing convenience functions relating to tasks.
+ */
+public final class Tasks {
+
+  public static final Function<IScheduledTask, IAssignedTask> SCHEDULED_TO_ASSIGNED =
+      new Function<IScheduledTask, IAssignedTask>() {
+        @Override public IAssignedTask apply(IScheduledTask task) {
+          return task.getAssignedTask();
+        }
+      };
+
+  public static final Function<IAssignedTask, ITaskConfig> ASSIGNED_TO_INFO =
+      new Function<IAssignedTask, ITaskConfig>() {
+        @Override public ITaskConfig apply(IAssignedTask task) {
+          return task.getTask();
+        }
+      };
+
+  public static final Function<IScheduledTask, ITaskConfig> SCHEDULED_TO_INFO =
+      Functions.compose(ASSIGNED_TO_INFO, SCHEDULED_TO_ASSIGNED);
+
+  public static final Function<IAssignedTask, String> ASSIGNED_TO_ID =
+      new Function<IAssignedTask, String>() {
+        @Override public String apply(IAssignedTask task) {
+          return task.getTaskId();
+        }
+      };
+
+  public static final Function<IScheduledTask, String> SCHEDULED_TO_ID =
+      Functions.compose(ASSIGNED_TO_ID, SCHEDULED_TO_ASSIGNED);
+
+  public static final Function<IAssignedTask, Integer> ASSIGNED_TO_INSTANCE_ID =
+      new Function<IAssignedTask, Integer>() {
+        @Override public Integer apply(IAssignedTask task) {
+          return task.getInstanceId();
+        }
+      };
+
+  public static final Function<IScheduledTask, Integer> SCHEDULED_TO_INSTANCE_ID =
+      Functions.compose(ASSIGNED_TO_INSTANCE_ID, SCHEDULED_TO_ASSIGNED);
+
+  public static final Function<ITaskConfig, IJobKey> INFO_TO_JOB_KEY =
+      new Function<ITaskConfig, IJobKey>() {
+        @Override public IJobKey apply(ITaskConfig task) {
+          return JobKeys.from(task);
+        }
+      };
+
+  public static final Function<IAssignedTask, IJobKey> ASSIGNED_TO_JOB_KEY =
+      Functions.compose(INFO_TO_JOB_KEY, ASSIGNED_TO_INFO);
+
+  public static final Function<IScheduledTask, IJobKey> SCHEDULED_TO_JOB_KEY =
+      Functions.compose(ASSIGNED_TO_JOB_KEY, SCHEDULED_TO_ASSIGNED);
+
+  /**
+   * Different states that an active task may be in.
+   */
+  public static final EnumSet<ScheduleStatus> ACTIVE_STATES =
+      EnumSet.copyOf(apiConstants.ACTIVE_STATES);
+
+  /**
+   * Terminal states, which a task should not move from.
+   */
+  public static final Set<ScheduleStatus> TERMINAL_STATES =
+      EnumSet.copyOf(apiConstants.TERMINAL_STATES);
+
+  public static final Predicate<ITaskConfig> IS_PRODUCTION =
+      new Predicate<ITaskConfig>() {
+        @Override public boolean apply(ITaskConfig task) {
+          return task.isProduction();
+        }
+      };
+
+  public static final Function<IScheduledTask, ScheduleStatus> GET_STATUS =
+      new Function<IScheduledTask, ScheduleStatus>() {
+        @Override public ScheduleStatus apply(IScheduledTask task) {
+          return task.getStatus();
+        }
+      };
+
+  /**
+   * Order by production flag (true, then false), subsorting by task ID.
+   */
+  public static final Ordering<IAssignedTask> SCHEDULING_ORDER =
+      Ordering.explicit(true, false)
+          .onResultOf(Functions.compose(Functions.forPredicate(IS_PRODUCTION), ASSIGNED_TO_INFO))
+          .compound(Ordering.natural().onResultOf(ASSIGNED_TO_ID));
+
+  private Tasks() {
+    // Utility class.
+  }
+
+  /**
+   * A utility method that returns a multi-map of tasks keyed by IJobKey.
+   * @param tasks A list of tasks to be keyed by map
+   * @return A multi-map of tasks keyed by job key.
+   */
+  public static Multimap<IJobKey, IScheduledTask> byJobKey(Iterable<IScheduledTask> tasks) {
+    return Multimaps.index(tasks, Tasks.SCHEDULED_TO_JOB_KEY);
+  }
+
+  public static boolean isActive(ScheduleStatus status) {
+    return ACTIVE_STATES.contains(status);
+  }
+
+  public static boolean isTerminated(ScheduleStatus status) {
+    return TERMINAL_STATES.contains(status);
+  }
+
+  public static String id(IScheduledTask task) {
+    return task.getAssignedTask().getTaskId();
+  }
+
+  // TODO(William Farner: Remove this once the code base is switched to IScheduledTask.
+  public static String id(ScheduledTask task) {
+    return task.getAssignedTask().getTaskId();
+  }
+
+  public static Set<String> ids(Iterable<IScheduledTask> tasks) {
+    return ImmutableSet.copyOf(Iterables.transform(tasks, SCHEDULED_TO_ID));
+  }
+
+  public static Set<String> ids(IScheduledTask... tasks) {
+    return ids(ImmutableList.copyOf(tasks));
+  }
+
+  public static Map<String, IScheduledTask> mapById(Iterable<IScheduledTask> tasks) {
+    return Maps.uniqueIndex(tasks, SCHEDULED_TO_ID);
+  }
+
+  public static String getRole(IScheduledTask task) {
+    return task.getAssignedTask().getTask().getOwner().getRole();
+  }
+
+  public static String getJob(IScheduledTask task) {
+    return task.getAssignedTask().getTask().getJobName();
+  }
+
+  public static final Ordering<IScheduledTask> LATEST_ACTIVITY = Ordering.natural()
+      .onResultOf(new Function<IScheduledTask, Long>() {
+        @Override public Long apply(IScheduledTask task) {
+          return Iterables.getLast(task.getTaskEvents()).getTimestamp();
+        }
+      });
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java b/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
new file mode 100644
index 0000000..4839d0f
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/configuration/ConfigurationManager.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.configuration;
+
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.apache.commons.lang.StringUtils;
+
+import com.twitter.aurora.gen.Constraint;
+import com.twitter.aurora.gen.JobConfiguration;
+import com.twitter.aurora.gen.LimitConstraint;
+import com.twitter.aurora.gen.TaskConfig;
+import com.twitter.aurora.gen.TaskConfig._Fields;
+import com.twitter.aurora.gen.TaskConstraint;
+import com.twitter.aurora.scheduler.base.JobKeys;
+import com.twitter.aurora.scheduler.storage.entities.IConstraint;
+import com.twitter.aurora.scheduler.storage.entities.IIdentity;
+import com.twitter.aurora.scheduler.storage.entities.IJobConfiguration;
+import com.twitter.aurora.scheduler.storage.entities.ITaskConfig;
+import com.twitter.aurora.scheduler.storage.entities.ITaskConstraint;
+import com.twitter.aurora.scheduler.storage.entities.IValueConstraint;
+import com.twitter.common.base.Closure;
+import com.twitter.common.base.MorePreconditions;
+
+import static com.twitter.aurora.gen.apiConstants.DEFAULT_ENVIRONMENT;
+import static com.twitter.aurora.gen.apiConstants.GOOD_IDENTIFIER_PATTERN_JVM;
+
+/**
+ * Manages translation from a string-mapped configuration to a concrete configuration type, and
+ * defaults for optional values.
+ *
+ * TODO(William Farner): Add input validation to all fields (strings not empty, positive ints, etc).
+ */
+public final class ConfigurationManager {
+
+  public static final String DEDICATED_ATTRIBUTE = "dedicated";
+
+  @VisibleForTesting public static final String HOST_CONSTRAINT = "host";
+  @VisibleForTesting public static final String RACK_CONSTRAINT = "rack";
+
+  private static final Pattern GOOD_IDENTIFIER = Pattern.compile(GOOD_IDENTIFIER_PATTERN_JVM);
+
+  private static final int MAX_IDENTIFIER_LENGTH = 255;
+
+  private static class DefaultField implements Closure<TaskConfig> {
+    private final _Fields field;
+    private final Object defaultValue;
+
+    DefaultField(_Fields field, Object defaultValue) {
+      this.field = field;
+      this.defaultValue = defaultValue;
+    }
+
+    @Override public void execute(TaskConfig task) {
+      if (!task.isSet(field)) {
+        task.setFieldValue(field, defaultValue);
+      }
+    }
+  }
+
+  private interface Validator<T> {
+    void validate(T value) throws TaskDescriptionException;
+  }
+
+  private static class GreaterThan implements Validator<Number> {
+    private final double min;
+    private final String label;
+
+    GreaterThan(double min, String label) {
+      this.min = min;
+      this.label = label;
+    }
+
+    @Override public void validate(Number value) throws TaskDescriptionException {
+      if (this.min >= value.doubleValue()) {
+        throw new TaskDescriptionException(label + " must be greater than " + this.min);
+      }
+    }
+  }
+
+  private static class RequiredFieldValidator<T> implements Validator<TaskConfig> {
+    private final _Fields field;
+    private final Validator<T> validator;
+
+    RequiredFieldValidator(_Fields field, Validator<T> validator) {
+      this.field = field;
+      this.validator = validator;
+    }
+
+    public void validate(TaskConfig task) throws TaskDescriptionException {
+      if (!task.isSet(field)) {
+        throw new TaskDescriptionException("Field " + field.getFieldName() + " is required.");
+      }
+      @SuppressWarnings("unchecked")
+      T value = (T) task.getFieldValue(field);
+      validator.validate(value);
+    }
+  }
+
+  private static final Iterable<Closure<TaskConfig>> DEFAULT_FIELD_POPULATORS =
+      ImmutableList.of(
+          new DefaultField(_Fields.IS_SERVICE, false),
+          new DefaultField(_Fields.PRIORITY, 0),
+          new DefaultField(_Fields.PRODUCTION, false),
+          new DefaultField(_Fields.MAX_TASK_FAILURES, 1),
+          new DefaultField(_Fields.TASK_LINKS, Maps.<String, String>newHashMap()),
+          new DefaultField(_Fields.REQUESTED_PORTS, Sets.<String>newHashSet()),
+          new DefaultField(_Fields.CONSTRAINTS, Sets.<Constraint>newHashSet()),
+          new DefaultField(_Fields.ENVIRONMENT, DEFAULT_ENVIRONMENT),
+          new Closure<TaskConfig>() {
+            @Override public void execute(TaskConfig task) {
+              if (!Iterables.any(task.getConstraints(), hasName(HOST_CONSTRAINT))) {
+                task.addToConstraints(hostLimitConstraint(1));
+              }
+            }
+          },
+          new Closure<TaskConfig>() {
+            @Override public void execute(TaskConfig task) {
+              if (!isDedicated(ITaskConfig.build(task))
+                  && task.isProduction()
+                  && task.isIsService()
+                  && !Iterables.any(task.getConstraints(), hasName(RACK_CONSTRAINT))) {
+
+                task.addToConstraints(rackLimitConstraint(1));
+              }
+            }
+          });
+
+  private static final Iterable<RequiredFieldValidator<?>> REQUIRED_FIELDS_VALIDATORS =
+      ImmutableList.<RequiredFieldValidator<?>>of(
+          new RequiredFieldValidator<>(_Fields.NUM_CPUS, new GreaterThan(0.0, "num_cpus")),
+          new RequiredFieldValidator<>(_Fields.RAM_MB, new GreaterThan(0.0, "ram_mb")),
+          new RequiredFieldValidator<>(_Fields.DISK_MB, new GreaterThan(0.0, "disk_mb")));
+
+  private ConfigurationManager() {
+    // Utility class.
+  }
+
+  @VisibleForTesting
+  static boolean isGoodIdentifier(String identifier) {
+    return GOOD_IDENTIFIER.matcher(identifier).matches()
+        && (identifier.length() <= MAX_IDENTIFIER_LENGTH);
+  }
+
+  private static void checkNotNull(Object value, String error) throws TaskDescriptionException {
+    if (value == null) {
+      throw new TaskDescriptionException(error);
+    }
+  }
+
+  private static void assertOwnerValidity(IIdentity jobOwner) throws TaskDescriptionException {
+    checkNotNull(jobOwner, "No job owner specified!");
+    checkNotNull(jobOwner.getRole(), "No job role specified!");
+    checkNotNull(jobOwner.getUser(), "No job user specified!");
+
+    if (!isGoodIdentifier(jobOwner.getRole())) {
+      throw new TaskDescriptionException(
+          "Job role contains illegal characters: " + jobOwner.getRole());
+    }
+
+    if (!isGoodIdentifier(jobOwner.getUser())) {
+      throw new TaskDescriptionException(
+          "Job user contains illegal characters: " + jobOwner.getUser());
+    }
+  }
+
+  private static String getRole(IValueConstraint constraint) {
+    return Iterables.getOnlyElement(constraint.getValues()).split("/")[0];
+  }
+
+  private static boolean isValueConstraint(ITaskConstraint taskConstraint) {
+    return taskConstraint.getSetField() == TaskConstraint._Fields.VALUE;
+  }
+
+  public static boolean isDedicated(ITaskConfig task) {
+    return Iterables.any(task.getConstraints(), getConstraintByName(DEDICATED_ATTRIBUTE));
+  }
+
+  @Nullable
+  private static IConstraint getDedicatedConstraint(ITaskConfig task) {
+    return Iterables.find(task.getConstraints(), getConstraintByName(DEDICATED_ATTRIBUTE), null);
+  }
+
+  /**
+   * Check validity of and populates defaults in a job configuration.  This will return a deep copy
+   * of the provided job configuration with default configuration values applied, and configuration
+   * map values sanitized and applied to their respective struct fields.
+   *
+   * @param job Job to validate and populate.
+   * @return A deep copy of {@code job} that has been populated.
+   * @throws TaskDescriptionException If the job configuration is invalid.
+   */
+  public static IJobConfiguration validateAndPopulate(IJobConfiguration job)
+      throws TaskDescriptionException {
+
+    Preconditions.checkNotNull(job);
+
+    if (!job.isSetTaskConfig()) {
+      throw new TaskDescriptionException("Job configuration must have taskConfig set.");
+    }
+
+    if (!job.isSetInstanceCount()) {
+      throw new TaskDescriptionException("Job configuration does not have shardCount set.");
+    }
+
+    if (job.getInstanceCount() <= 0) {
+      throw new TaskDescriptionException("Shard count must be positive.");
+    }
+
+    JobConfiguration builder = job.newBuilder();
+
+    assertOwnerValidity(job.getOwner());
+
+    if (!JobKeys.isValid(job.getKey())) {
+      throw new TaskDescriptionException("Job key " + job.getKey() + " is invalid.");
+    }
+    if (!job.getKey().getRole().equals(job.getOwner().getRole())) {
+      throw new TaskDescriptionException("Role in job key must match job owner.");
+    }
+    if (!isGoodIdentifier(job.getKey().getRole())) {
+      throw new TaskDescriptionException(
+          "Job role contains illegal characters: " + job.getKey().getRole());
+    }
+    if (!isGoodIdentifier(job.getKey().getEnvironment())) {
+      throw new TaskDescriptionException(
+          "Job environment contains illegal characters: " + job.getKey().getEnvironment());
+    }
+    if (!isGoodIdentifier(job.getKey().getName())) {
+      throw new TaskDescriptionException(
+          "Job name contains illegal characters: " + job.getKey().getName());
+    }
+
+    builder.setTaskConfig(
+        validateAndPopulate(ITaskConfig.build(builder.getTaskConfig())).newBuilder());
+
+    // Only one of [service=true, cron_schedule] may be set.
+    if (!StringUtils.isEmpty(job.getCronSchedule()) && builder.getTaskConfig().isIsService()) {
+      throw new TaskDescriptionException(
+          "A service task may not be run on a cron schedule: " + builder);
+    }
+
+    return IJobConfiguration.build(builder);
+  }
+
+  /**
+   * Check validity of and populates defaults in a task configuration.  This will return a deep copy
+   * of the provided task configuration with default configuration values applied, and configuration
+   * map values sanitized and applied to their respective struct fields.
+   *
+   *
+   * @param config Task config to validate and populate.
+   * @return A reference to the modified {@code config} (for chaining).
+   * @throws TaskDescriptionException If the task is invalid.
+   */
+  public static ITaskConfig validateAndPopulate(ITaskConfig config)
+      throws TaskDescriptionException {
+
+    TaskConfig builder = config.newBuilder();
+
+    if (!builder.isSetRequestedPorts()) {
+      builder.setRequestedPorts(ImmutableSet.<String>of());
+    }
+
+    maybeFillLinks(builder);
+
+    assertOwnerValidity(config.getOwner());
+
+    if (!isGoodIdentifier(config.getJobName())) {
+      throw new TaskDescriptionException(
+          "Job name contains illegal characters: " + config.getJobName());
+    }
+
+    if (!isGoodIdentifier(config.getEnvironment())) {
+      throw new TaskDescriptionException(
+          "Environment contains illegal characters: " + config.getEnvironment());
+    }
+
+    if (!builder.isSetExecutorConfig()) {
+      throw new TaskDescriptionException("Configuration may not be null");
+    }
+
+    // Maximize the usefulness of any thrown error message by checking required fields first.
+    for (RequiredFieldValidator<?> validator : REQUIRED_FIELDS_VALIDATORS) {
+      validator.validate(builder);
+    }
+
+    IConstraint constraint = getDedicatedConstraint(config);
+    if (constraint != null) {
+      if (!isValueConstraint(constraint.getConstraint())) {
+        throw new TaskDescriptionException("A dedicated constraint must be of value type.");
+      }
+
+      IValueConstraint valueConstraint = constraint.getConstraint().getValue();
+
+      if (!(valueConstraint.getValues().size() == 1)) {
+        throw new TaskDescriptionException("A dedicated constraint must have exactly one value");
+      }
+
+      String dedicatedRole = getRole(valueConstraint);
+      if (!config.getOwner().getRole().equals(dedicatedRole)) {
+        throw new TaskDescriptionException(
+            "Only " + dedicatedRole + " may use hosts dedicated for that role.");
+      }
+    }
+
+    return ITaskConfig.build(applyDefaultsIfUnset(builder));
+  }
+
+  /**
+   * Provides a filter for the given constraint name.
+   *
+   * @param name The name of the constraint.
+   * @return A filter that matches the constraint.
+   */
+  public static Predicate<IConstraint> getConstraintByName(final String name) {
+    return new Predicate<IConstraint>() {
+      @Override public boolean apply(IConstraint constraint) {
+        return constraint.getName().equals(name);
+      }
+    };
+  }
+
+  @VisibleForTesting
+  public static Constraint hostLimitConstraint(int limit) {
+    return new Constraint(HOST_CONSTRAINT, TaskConstraint.limit(new LimitConstraint(limit)));
+  }
+
+  @VisibleForTesting
+  public static Constraint rackLimitConstraint(int limit) {
+    return new Constraint(RACK_CONSTRAINT, TaskConstraint.limit(new LimitConstraint(limit)));
+  }
+
+  private static Predicate<Constraint> hasName(final String name) {
+    MorePreconditions.checkNotBlank(name);
+    return new Predicate<Constraint>() {
+      @Override public boolean apply(Constraint constraint) {
+        return name.equals(constraint.getName());
+      }
+    };
+  }
+
+  /**
+   * Applies defaults to unset values in a task.
+   *
+   * @param task Task to apply defaults to.
+   * @return A reference to the (modified) {@code task}.
+   */
+  @VisibleForTesting
+  public static TaskConfig applyDefaultsIfUnset(TaskConfig task) {
+    for (Closure<TaskConfig> populator : DEFAULT_FIELD_POPULATORS) {
+      populator.execute(task);
+    }
+
+    return task;
+  }
+
+  /**
+   * Applies defaults to unset values in a job and its tasks.
+   *
+   * @param job Job to apply defaults to.
+   */
+  @VisibleForTesting
+  public static void applyDefaultsIfUnset(JobConfiguration job) {
+    ConfigurationManager.applyDefaultsIfUnset(job.getTaskConfig());
+  }
+
+  private static void maybeFillLinks(TaskConfig task) {
+    if (task.getTaskLinksSize() == 0) {
+      ImmutableMap.Builder<String, String> links = ImmutableMap.builder();
+      if (task.getRequestedPorts().contains("health")) {
+        links.put("health", "http://%host%:%port:health%");
+      }
+      if (task.getRequestedPorts().contains("http")) {
+        links.put("http", "http://%host%:%port:http%");
+      }
+      task.setTaskLinks(links.build());
+    }
+  }
+
+  /**
+   * Thrown when an invalid task or job configuration is encountered.
+   */
+  public static class TaskDescriptionException extends Exception {
+    public TaskDescriptionException(String msg) {
+      super(msg);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/configuration/Resources.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/configuration/Resources.java b/src/main/java/org/apache/aurora/scheduler/configuration/Resources.java
new file mode 100644
index 0000000..51e9973
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/configuration/Resources.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.configuration;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.mesos.Protos.Offer;
+import org.apache.mesos.Protos.Resource;
+import org.apache.mesos.Protos.Value.Range;
+import org.apache.mesos.Protos.Value.Ranges;
+import org.apache.mesos.Protos.Value.Scalar;
+import org.apache.mesos.Protos.Value.Type;
+
+import com.twitter.aurora.scheduler.base.Numbers;
+import com.twitter.aurora.scheduler.storage.entities.ITaskConfig;
+import com.twitter.common.quantity.Amount;
+import com.twitter.common.quantity.Data;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A container for multiple resource vectors.
+ * TODO(wfarner): Collapse this in with Quotas.
+ */
+public class Resources {
+
+  public static final String CPUS = "cpus";
+  public static final String RAM_MB = "mem";
+  public static final String DISK_MB = "disk";
+  public static final String PORTS = "ports";
+
+  private static final Function<Range, Set<Integer>> RANGE_TO_MEMBERS =
+      new Function<Range, Set<Integer>>() {
+        @Override public Set<Integer> apply(Range range) {
+          return ContiguousSet.create(
+              com.google.common.collect.Range.closed((int) range.getBegin(), (int) range.getEnd()),
+              DiscreteDomain.integers());
+        }
+      };
+
+  private final double numCpus;
+  private final Amount<Long, Data> disk;
+  private final Amount<Long, Data> ram;
+  private final int numPorts;
+
+  /**
+   * Creates a new resources object.
+   *
+   * @param numCpus Number of CPUs.
+   * @param ram Amount of RAM.
+   * @param disk Amount of disk.
+   * @param numPorts Number of ports.
+   */
+  public Resources(double numCpus, Amount<Long, Data> ram, Amount<Long, Data> disk, int numPorts) {
+    this.numCpus = numCpus;
+    this.ram = checkNotNull(ram);
+    this.disk = checkNotNull(disk);
+    this.numPorts = numPorts;
+  }
+
+  /**
+   * Tests whether this bundle of resources is greater than or equal to another bundle of resources.
+   *
+   * @param other Resources being compared to.
+   * @return {@code true} if all resources in this bundle are greater than or equal to the
+   *    equivalents from {@code other}, otherwise {@code false}.
+   */
+  public boolean greaterThanOrEqual(Resources other) {
+    return (numCpus >= other.numCpus)
+        && (disk.as(Data.MB) >= other.disk.as(Data.MB))
+        && (ram.as(Data.MB) >= other.ram.as(Data.MB))
+        && (numPorts >= other.numPorts);
+  }
+
+  /**
+   * Adapts this resources object to a list of mesos resources.
+   *
+   * @param selectedPorts The ports selected, to be applied as concrete task ranges.
+   * @return Mesos resources.
+   */
+  public List<Resource> toResourceList(Set<Integer> selectedPorts) {
+    ImmutableList.Builder<Resource> resourceBuilder =
+      ImmutableList.<Resource>builder()
+          .add(Resources.makeMesosResource(CPUS, numCpus))
+          .add(Resources.makeMesosResource(DISK_MB, disk.as(Data.MB)))
+          .add(Resources.makeMesosResource(RAM_MB, ram.as(Data.MB)));
+    if (selectedPorts.size() > 0) {
+        resourceBuilder.add(Resources.makeMesosRangeResource(Resources.PORTS, selectedPorts));
+    }
+
+    return resourceBuilder.build();
+  }
+
+  /**
+   * Convenience method for adapting to mesos resources without applying a port range.
+   *
+   * @see {@link #toResourceList(java.util.Set)}
+   * @return Mesos resources.
+   */
+  public List<Resource> toResourceList() {
+    return toResourceList(ImmutableSet.<Integer>of());
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof Resources)) {
+      return false;
+    }
+
+    Resources other = (Resources) o;
+    return new EqualsBuilder()
+        .append(numCpus, other.numCpus)
+        .append(ram, other.ram)
+        .append(disk, other.disk)
+        .append(numPorts, other.numPorts)
+        .isEquals();
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(numCpus, ram, disk, numPorts);
+  }
+
+  /**
+   * Extracts the resources required from a task.
+   *
+   * @param task Task to get resources from.
+   * @return The resources required by the task.
+   */
+  public static Resources from(ITaskConfig task) {
+    checkNotNull(task);
+    return new Resources(
+        task.getNumCpus(),
+        Amount.of(task.getRamMb(), Data.MB),
+        Amount.of(task.getDiskMb(), Data.MB),
+        task.getRequestedPorts().size());
+  }
+
+  /**
+   * Extracts the resources specified in a list of resource objects.
+   *
+   * @param resources Resources to translate.
+   * @return The canonical resources.
+   */
+  public static Resources from(List<Resource> resources) {
+    checkNotNull(resources);
+    return new Resources(
+        getScalarValue(resources, CPUS),
+        Amount.of((long) getScalarValue(resources, RAM_MB), Data.MB),
+        Amount.of((long) getScalarValue(resources, DISK_MB), Data.MB),
+        getNumAvailablePorts(resources)
+    );
+  }
+
+  /**
+   * Extracts the resources available in a slave offer.
+   *
+   * @param offer Offer to get resources from.
+   * @return The resources available in the offer.
+   */
+  public static Resources from(Offer offer) {
+    checkNotNull(offer);
+    return new Resources(
+        getScalarValue(offer, CPUS),
+        Amount.of((long) getScalarValue(offer, RAM_MB), Data.MB),
+        Amount.of((long) getScalarValue(offer, DISK_MB), Data.MB),
+        getNumAvailablePorts(offer.getResourcesList()));
+  }
+
+  private static final Resources NO_RESOURCES =
+      new Resources(0, Amount.of(0L, Data.BITS), Amount.of(0L, Data.BITS), 0);
+
+  private static Resources none() {
+    return NO_RESOURCES;
+  }
+
+  /**
+   * a - b
+   */
+  public static Resources subtract(Resources a, Resources b) {
+    return new Resources(
+        a.getNumCpus() - b.getNumCpus(),
+        Amount.of(a.getRam().as(Data.MB) - b.getRam().as(Data.MB), Data.MB),
+        Amount.of(a.getDisk().as(Data.MB) - b.getDisk().as(Data.MB), Data.MB),
+        a.getNumPorts() - b.getNumPorts());
+  }
+
+  /**
+   * sum(a, b)
+   */
+  public static Resources sum(Resources a, Resources b) {
+    return sum(ImmutableList.of(a, b));
+  }
+
+  /**
+   * sum(rs)
+   */
+  public static Resources sum(Iterable<Resources> rs) {
+    Resources sum = none();
+
+    for (Resources r : rs) {
+      double numCpus = sum.getNumCpus() + r.getNumCpus();
+      Amount<Long, Data> disk =
+          Amount.of(sum.getDisk().as(Data.BYTES) + r.getDisk().as(Data.BYTES), Data.BYTES);
+      Amount<Long, Data> ram =
+          Amount.of(sum.getRam().as(Data.BYTES) + r.getRam().as(Data.BYTES), Data.BYTES);
+      int ports = sum.getNumPorts() + r.getNumPorts();
+      sum =  new Resources(numCpus, ram, disk, ports);
+    }
+
+    return sum;
+  }
+
+  private static int getNumAvailablePorts(List<Resource> resource) {
+    int offeredPorts = 0;
+    for (Range range : getPortRanges(resource)) {
+      offeredPorts += 1 + (range.getEnd() - range.getBegin());
+    }
+    return offeredPorts;
+  }
+
+  private static double getScalarValue(Offer offer, String key) {
+    return getScalarValue(offer.getResourcesList(), key);
+  }
+
+  private static double getScalarValue(List<Resource> resources, String key) {
+    Resource resource = getResource(resources, key);
+    if (resource == null) {
+      return 0;
+    }
+
+    return resource.getScalar().getValue();
+  }
+
+  private static Resource getResource(List<Resource> resource, String key) {
+      return Iterables.find(resource, withName(key), null);
+  }
+
+  private static Predicate<Resource> withName(final String name) {
+    return new Predicate<Resource>() {
+      @Override public boolean apply(Resource resource) {
+        return resource.getName().equals(name);
+      }
+    };
+  }
+
+  private static Iterable<Range> getPortRanges(List<Resource> resources) {
+    Resource resource = getResource(resources, Resources.PORTS);
+    if (resource == null) {
+      return ImmutableList.of();
+    }
+
+    return resource.getRanges().getRangeList();
+  }
+
+  /**
+   * Creates a scalar mesos resource.
+   *
+   * @param name Name of the resource.
+   * @param value Value for the resource.
+   * @return A mesos resource.
+   */
+  public static Resource makeMesosResource(String name, double value) {
+    return Resource.newBuilder().setName(name).setType(Type.SCALAR)
+        .setScalar(Scalar.newBuilder().setValue(value)).build();
+  }
+
+  private static final Function<com.google.common.collect.Range<Integer>, Range> RANGE_TRANSFORM =
+      new Function<com.google.common.collect.Range<Integer>, Range>() {
+        @Override public Range apply(com.google.common.collect.Range<Integer> input) {
+          return Range.newBuilder()
+              .setBegin(input.lowerEndpoint())
+              .setEnd(input.upperEndpoint())
+              .build();
+        }
+      };
+
+  /**
+   * Creates a mesos resource of integer ranges.
+   *
+   * @param name Name of the resource
+   * @param values Values to translate into ranges.
+   * @return A mesos ranges resource.
+   */
+  static Resource makeMesosRangeResource(String name, Set<Integer> values) {
+    return Resource.newBuilder()
+        .setName(name)
+        .setType(Type.RANGES)
+        .setRanges(Ranges.newBuilder()
+            .addAllRange(Iterables.transform(Numbers.toRanges(values), RANGE_TRANSFORM)))
+        .build();
+  }
+
+  /**
+   * Number of CPUs.
+   *
+   * @return CPUs.
+   */
+  public double getNumCpus() {
+    return numCpus;
+  }
+
+  /**
+   * Disk amount.
+   *
+   * @return Disk.
+   */
+  public Amount<Long, Data> getDisk() {
+    return disk;
+  }
+
+  /**
+   * RAM amount.
+   *
+   * @return RAM.
+   */
+  public Amount<Long, Data> getRam() {
+    return ram;
+  }
+
+  /**
+   * Number of ports.
+   *
+   * @return Port count.
+   */
+  public int getNumPorts() {
+    return numPorts;
+  }
+
+  /**
+   * Thrown when there are insufficient resources to satisfy a request.
+   */
+  static class InsufficientResourcesException extends RuntimeException {
+    public InsufficientResourcesException(String message) {
+      super(message);
+    }
+  }
+
+  /**
+   * Attempts to grab {@code numPorts} from the given resource {@code offer}.
+   *
+   * @param offer The offer to grab ports from.
+   * @param numPorts The number of ports to grab.
+   * @return The set of ports grabbed.
+   * @throws InsufficientResourcesException if not enough ports were available.
+   */
+  public static Set<Integer> getPorts(Offer offer, int numPorts)
+      throws InsufficientResourcesException {
+
+    checkNotNull(offer);
+
+    if (numPorts == 0) {
+      return ImmutableSet.of();
+    }
+
+    List<Integer> availablePorts = Lists.newArrayList(Sets.newHashSet(
+        Iterables.concat(
+            Iterables.transform(getPortRanges(offer.getResourcesList()), RANGE_TO_MEMBERS))));
+
+    if (availablePorts.size() < numPorts) {
+      throw new InsufficientResourcesException(
+          String.format("Could not get %d ports from %s", numPorts, offer));
+    }
+
+    Collections.shuffle(availablePorts);
+    return ImmutableSet.copyOf(availablePorts.subList(0, numPorts));
+  }
+
+  /**
+   * A Resources object is greater than another iff _all_ of its resource components are greater
+   * or equal. A Resources object compares as equal if some but not all components are greater than
+   * or equal to the other.
+   */
+  public static final Ordering<Resources> RESOURCE_ORDER = new Ordering<Resources>() {
+    @Override public int compare(Resources left, Resources right) {
+      int diskC = left.getDisk().compareTo(right.getDisk());
+      int ramC = left.getRam().compareTo(right.getRam());
+      int portC = Integer.compare(left.getNumPorts(), right.getNumPorts());
+      int cpuC = Double.compare(left.getNumCpus(), right.getNumCpus());
+
+      FluentIterable<Integer> vector =
+          FluentIterable.from(ImmutableList.of(diskC, ramC, portC, cpuC));
+
+      if (vector.allMatch(IS_ZERO))  {
+        return 0;
+      }
+
+      if (vector.filter(Predicates.not(IS_ZERO)).allMatch(IS_POSITIVE)) {
+        return 1;
+      }
+
+      if (vector.filter(Predicates.not(IS_ZERO)).allMatch(IS_NEGATIVE)) {
+        return -1;
+      }
+
+      return 0;
+    }
+  };
+
+  private static final Predicate<Integer> IS_POSITIVE = new Predicate<Integer>() {
+    @Override public boolean apply(Integer input) {
+      return input > 0;
+    }
+  };
+
+  private static final Predicate<Integer> IS_NEGATIVE = new Predicate<Integer>() {
+    @Override public boolean apply(Integer input) {
+      return input < 0;
+    }
+  };
+
+  private static final Predicate<Integer> IS_ZERO = new Predicate<Integer>() {
+    @Override public boolean apply(Integer input) {
+      return input == 0;
+    }
+  };
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/configuration/SanitizedConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/configuration/SanitizedConfiguration.java b/src/main/java/org/apache/aurora/scheduler/configuration/SanitizedConfiguration.java
new file mode 100644
index 0000000..890acbb
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/configuration/SanitizedConfiguration.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.configuration;
+
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Functions;
+import com.google.common.base.Objects;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Range;
+
+import com.twitter.aurora.scheduler.configuration.ConfigurationManager.TaskDescriptionException;
+import com.twitter.aurora.scheduler.storage.entities.IJobConfiguration;
+import com.twitter.aurora.scheduler.storage.entities.ITaskConfig;
+
+/**
+ * Wrapper for a configuration that has been fully-sanitized and populated with defaults.
+ */
+public final class SanitizedConfiguration {
+
+  private final IJobConfiguration sanitized;
+  private final Map<Integer, ITaskConfig> tasks;
+
+  /**
+   * Constructs a SanitizedConfiguration object and populates the set of {@link ITaskConfig}s for
+   * the provided config.
+   *
+   * @param sanitized A sanitized configuration.
+   */
+  @VisibleForTesting
+  public SanitizedConfiguration(IJobConfiguration sanitized) {
+    this.sanitized = sanitized;
+    this.tasks = Maps.toMap(
+        ContiguousSet.create(
+            Range.closedOpen(0, sanitized.getInstanceCount()),
+            DiscreteDomain.integers()),
+        Functions.constant(sanitized.getTaskConfig()));
+  }
+
+  /**
+   * Wraps an unsanitized job configuration.
+   *
+   * @param unsanitized Unsanitized configuration to sanitize/populate and wrap.
+   * @return A wrapper containing the sanitized configuration.
+   * @throws TaskDescriptionException If the configuration is invalid.
+   */
+  public static SanitizedConfiguration fromUnsanitized(IJobConfiguration unsanitized)
+      throws TaskDescriptionException {
+
+    return new SanitizedConfiguration(ConfigurationManager.validateAndPopulate(unsanitized));
+  }
+
+  public IJobConfiguration getJobConfig() {
+    return sanitized;
+  }
+
+  // TODO(William Farner): Rework this API now that all configs are identical.
+  public Map<Integer, ITaskConfig> getTaskConfigs() {
+    return tasks;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof SanitizedConfiguration)) {
+      return false;
+    }
+
+    SanitizedConfiguration other = (SanitizedConfiguration) o;
+
+    return Objects.equal(sanitized, other.sanitized);
+  }
+
+  @Override
+  public int hashCode() {
+    return sanitized.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return sanitized.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/CronException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CronException.java b/src/main/java/org/apache/aurora/scheduler/cron/CronException.java
new file mode 100644
index 0000000..c29a578
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/CronException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron;
+
+/**
+ * Exception class to signal a failure in the underlying cron implementation.
+ */
+public class CronException extends Exception {
+  public CronException(String msg) {
+    super(msg);
+  }
+
+  public CronException(String msg, Throwable t) {
+    super(msg, t);
+  }
+
+  public CronException(Throwable t) {
+    super(t);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java b/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
new file mode 100644
index 0000000..d01e2f8
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/CronPredictor.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron;
+
+import java.util.Date;
+
+/**
+ * A utility function that predicts a cron run given a schedule.
+ */
+public interface CronPredictor {
+  /**
+   * Predicts the next date at which a cron schedule will trigger.
+   *
+   * @param schedule Cron schedule to predict the next time for.
+   * @return A prediction for the next time a cron will run.
+   */
+  Date predictNextRun(String schedule);
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java b/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
new file mode 100644
index 0000000..0ea9b65
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/CronScheduler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Optional;
+
+/**
+ * An execution manager that executes work on a cron schedule.
+ */
+public interface CronScheduler {
+  /**
+   * Schedules a task on a cron schedule.
+   *
+   * @param schedule Cron-style schedule.
+   * @param task Work to run when on the cron schedule.
+   * @return A unique ID to identify the scheduled cron task.
+   * @throws CronException when there was a failure to schedule, for example if {@code schedule}
+   *         is not a valid input.
+   * @throws IllegalStateException If the cron scheduler is not currently running.
+   */
+  String schedule(String schedule, Runnable task) throws CronException, IllegalStateException;
+
+  /**
+   * Removes a scheduled cron item.
+   *
+   * @param key Key previously returned from {@link #schedule(String, Runnable)}.
+   * @throws IllegalStateException If the cron scheduler is not currently running.
+   */
+  void deschedule(String key) throws IllegalStateException;
+
+  /**
+   * Gets the cron schedule associated with a scheduling key.
+   *
+   * @param key Key previously returned from {@link #schedule(String, Runnable)}.
+   * @return The task's cron schedule, if a matching task was found.
+   * @throws IllegalStateException If the cron scheduler is not currently running.
+   */
+  Optional<String> getSchedule(String key) throws IllegalStateException;
+
+  /**
+   * Block until fully initialized. It is an error to call start twice. Prior to calling start,
+   * all other methods of this interface may throw {@link IllegalStateException}. The underlying
+   * implementation should not spawn threads or connect to databases prior to invocation of
+   * {@link #start()}.
+   *
+   * @throws IllegalStateException If called twice.
+   */
+  void start() throws IllegalStateException;
+
+  /**
+   * Block until stopped. Generally this means that underlying resources are freed, threads are
+   * terminated, and any bookkeeping state is persisted. If {@link #stop()} has already been called
+   * by another thread, {@link #stop()} either blocks until completion or returns immediately.
+   *
+   * @throws CronException If there was a problem stopping the scheduler, for example if it was not
+   *                       started.
+   */
+  void stop() throws CronException;
+
+  /**
+   * Checks to see if the scheduler would be accepted by the underlying scheduler.
+   *
+   * @param schedule Cron scheduler to validate.
+   * @return {@code true} if the schedule is valid.
+   */
+  boolean isValidSchedule(@Nullable String schedule);
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java
new file mode 100644
index 0000000..d1c5419
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronModule.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron.noop;
+
+import javax.inject.Singleton;
+
+import com.google.inject.AbstractModule;
+
+import com.twitter.aurora.scheduler.cron.CronPredictor;
+import com.twitter.aurora.scheduler.cron.CronScheduler;
+
+/**
+ * A Module to wire up a cron scheduler that does not actually schedule cron jobs.
+ *
+ * This class exists as a short term hack to get around a license compatibility issue - Real
+ * Implementation (TM) coming soon.
+ */
+public class NoopCronModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(CronScheduler.class).to(NoopCronScheduler.class);
+    bind(NoopCronScheduler.class).in(Singleton.class);
+
+    bind(CronPredictor.class).to(NoopCronPredictor.class);
+    bind(NoopCronPredictor.class).in(Singleton.class);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java
new file mode 100644
index 0000000..a779d2b
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronPredictor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron.noop;
+
+import java.util.Date;
+
+import com.twitter.aurora.scheduler.cron.CronPredictor;
+
+/**
+ * A cron predictor that always suggests that the next run is Unix epoch time.
+ *
+ * This class exists as a short term hack to get around a license compatibility issue - Real
+ * Implementation (TM) coming soon.
+ */
+class NoopCronPredictor implements CronPredictor {
+  @Override
+  public Date predictNextRun(String schedule) {
+    return new Date(0);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java
new file mode 100644
index 0000000..2893a6a
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/noop/NoopCronScheduler.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron.noop;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+
+import com.twitter.aurora.scheduler.cron.CronException;
+import com.twitter.aurora.scheduler.cron.CronScheduler;
+
+/**
+ * A cron scheduler that accepts cron jobs but never runs them. Useful if you want to hook up an
+ * external triggering mechanism (e.g. a system cron job that calls the startCronJob RPC manually
+ * on an interval).
+ *
+ * This class exists as a short term hack to get around a license compatibility issue - Real
+ * Implementation (TM) coming soon.
+ */
+class NoopCronScheduler implements CronScheduler {
+  private static final Logger LOG = Logger.getLogger(NoopCronScheduler.class.getName());
+
+  // Keep a list of schedules we've seen.
+  private final Set<String> schedules = Collections.synchronizedSet(Sets.<String>newHashSet());
+
+  @Override
+  public String schedule(String schedule, Runnable task) {
+    schedules.add(schedule);
+
+    LOG.warning(String.format(
+        "NO-OP cron scheduler is in use! %s with schedule %s WILL NOT be automatically triggered!",
+        task,
+        schedule));
+
+    return schedule;
+  }
+
+  @Override
+  public void deschedule(String key) throws IllegalStateException {
+    schedules.remove(key);
+  }
+
+  @Override
+  public Optional<String> getSchedule(String key) throws IllegalStateException {
+    return schedules.contains(key)
+        ? Optional.of(key)
+        : Optional.<String>absent();
+  }
+
+  @Override
+  public void start() throws IllegalStateException {
+    LOG.warning("NO-OP cron scheduler is in use. Cron jobs submitted will not be triggered!");
+  }
+
+  @Override
+  public void stop() throws CronException {
+    // No-op.
+  }
+
+  @Override
+  public boolean isValidSchedule(@Nullable String schedule) {
+    // Accept everything.
+    return schedule != null;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java b/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java
new file mode 100644
index 0000000..6bfc909
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/cron/testing/AbstractCronIT.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.cron.testing;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.Test;
+
+import com.twitter.aurora.scheduler.cron.CronPredictor;
+import com.twitter.aurora.scheduler.cron.CronScheduler;
+import com.twitter.common.testing.easymock.EasyMockTest;
+
+import static org.junit.Assert.assertTrue;
+
+import static com.twitter.aurora.gen.test.testConstants.VALID_CRON_SCHEDULES;
+
+/**
+ * Abstract test to verify conformance with the {@link CronScheduler} interface.
+ */
+public abstract class AbstractCronIT extends EasyMockTest {
+  /**
+   * Child should return an instance of the {@link CronScheduler} test under test here.
+   */
+  protected abstract CronScheduler makeCronScheduler() throws Exception;
+
+  /**
+   * Child should configure expectations for a scheduler start.
+   */
+  protected abstract void expectStartCronScheduler();
+
+  /**
+   * Child should configure expectations for a scheduler stop.
+   */
+  protected abstract void expectStopCronScheduler();
+
+  /**
+   * Child should return an instance of the {@link CronPredictor} under test here.
+   */
+  protected abstract CronPredictor makeCronPredictor() throws Exception;
+
+  @Test
+  public void testCronSchedulerLifecycle() throws Exception {
+    CronScheduler scheduler = makeCronScheduler();
+
+    expectStartCronScheduler();
+    expectStopCronScheduler();
+
+    control.replay();
+
+    scheduler.start();
+    final CountDownLatch cronRan = new CountDownLatch(1);
+    scheduler.schedule("* * * * *", new Runnable() {
+      @Override public void run() {
+        cronRan.countDown();
+      }
+    });
+    cronRan.await();
+    scheduler.stop();
+  }
+
+  @Test
+  public void testCronPredictorAcceptsValidSchedules() throws Exception {
+    control.replay();
+
+    CronPredictor cronPredictor = makeCronPredictor();
+    for (String schedule : VALID_CRON_SCHEDULES) {
+      cronPredictor.predictNextRun(schedule);
+    }
+  }
+
+  @Test
+  public void testCronScheduleValidatorAcceptsValidSchedules() throws Exception {
+    CronScheduler cron = makeCronScheduler();
+
+    control.replay();
+
+    for (String schedule : VALID_CRON_SCHEDULES) {
+      assertTrue(String.format("Cron schedule %s should validate.", schedule),
+          cron.isValidSchedule(schedule));
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/events/NotifyingMethodInterceptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/events/NotifyingMethodInterceptor.java b/src/main/java/org/apache/aurora/scheduler/events/NotifyingMethodInterceptor.java
new file mode 100644
index 0000000..2656766
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/events/NotifyingMethodInterceptor.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.events;
+
+import java.lang.reflect.Method;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import com.google.common.base.Preconditions;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import com.twitter.aurora.scheduler.events.PubsubEvent.Interceptors.Event;
+import com.twitter.aurora.scheduler.events.PubsubEvent.Interceptors.SendNotification;
+import com.twitter.common.base.Closure;
+
+/**
+ * A method interceptor that sends pubsub notifications before and/or after a method annotated
+ * with {@link SendNotification}
+ * is invoked.
+ */
+class NotifyingMethodInterceptor implements MethodInterceptor {
+  private static final Logger LOG = Logger.getLogger(NotifyingMethodInterceptor.class.getName());
+
+  @Inject
+  private Closure<PubsubEvent> eventSink;
+
+  private void maybeFire(Event event) {
+    if (event != Event.None) {
+      eventSink.execute(event.getEvent());
+    }
+  }
+
+  @Override
+  public Object invoke(MethodInvocation invocation) throws Throwable {
+    Preconditions.checkNotNull(eventSink, "Event sink has not yet been set.");
+
+    Method method = invocation.getMethod();
+    SendNotification sendNotification = method.getAnnotation(SendNotification.class);
+    if (sendNotification == null) {
+      LOG.warning("Interceptor should not match methods without @"
+          + SendNotification.class.getSimpleName());
+      return invocation.proceed();
+    }
+
+    maybeFire(sendNotification.before());
+    Object result = invocation.proceed();
+    maybeFire(sendNotification.after());
+    return result;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/bc1635df/src/main/java/org/apache/aurora/scheduler/events/NotifyingSchedulingFilter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/events/NotifyingSchedulingFilter.java b/src/main/java/org/apache/aurora/scheduler/events/NotifyingSchedulingFilter.java
new file mode 100644
index 0000000..ffb952c
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/events/NotifyingSchedulingFilter.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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 com.twitter.aurora.scheduler.events;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import com.google.inject.BindingAnnotation;
+
+import com.twitter.aurora.scheduler.ResourceSlot;
+import com.twitter.aurora.scheduler.events.PubsubEvent.Vetoed;
+import com.twitter.aurora.scheduler.filter.SchedulingFilter;
+import com.twitter.aurora.scheduler.storage.entities.ITaskConfig;
+import com.twitter.common.base.Closure;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A decorating scheduling filter that sends an event when a scheduling assignment is vetoed.
+ */
+class NotifyingSchedulingFilter implements SchedulingFilter {
+
+  /**
+   * Binding annotation that the underlying {@link SchedulingFilter} must be bound with.
+   */
+  @BindingAnnotation
+  @Target({FIELD, PARAMETER, METHOD}) @Retention(RUNTIME)
+  public @interface NotifyDelegate { }
+
+  private final SchedulingFilter delegate;
+  private final Closure<PubsubEvent> eventSink;
+
+  @Inject
+  NotifyingSchedulingFilter(
+      @NotifyDelegate SchedulingFilter delegate,
+      Closure<PubsubEvent> eventSink) {
+
+    this.delegate = checkNotNull(delegate);
+    this.eventSink = checkNotNull(eventSink);
+  }
+
+  @Override
+  public Set<Veto> filter(ResourceSlot offer, String slaveHost, ITaskConfig task, String taskId) {
+    Set<Veto> vetoes = delegate.filter(offer, slaveHost, task, taskId);
+    if (!vetoes.isEmpty()) {
+      eventSink.execute(new Vetoed(taskId, vetoes));
+    }
+
+    return vetoes;
+  }
+}


Mime
View raw message