/******************************************************************************* * Copyright 2013 Michael Marconi * * 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 oncue.scheduler; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import oncue.backingstore.BackingStore; import oncue.common.messages.JVMCapacityWorkRequest; import oncue.common.messages.Job; import oncue.common.messages.SimpleMessages.SimpleMessage; import scala.concurrent.duration.Duration; import scala.concurrent.duration.FiniteDuration; import akka.actor.ActorRef; import akka.actor.Cancellable; /** * A JVM capacity-aware scheduler will collect work requests for the specified * amount of time (giving a collection of agents time to respond to a work * broadcast) and then schedule jobs according to the memory capacity reported * by each agent. */ public class JVMCapacityScheduler extends AbstractScheduler<JVMCapacityWorkRequest> { // The list of work requests in this time window List<JVMCapacityWorkRequest> workRequests = new ArrayList<>(); // A scheduled request to schedule jobs private Cancellable scheduleJobs; // The size parameter on the job public static final String JOB_SIZE = "size"; public JVMCapacityScheduler(Class<? extends BackingStore> backingStore) { super(backingStore); } @Override public void onReceive(Object message) throws Exception { if (message.equals(SimpleMessage.SCHEDULE_JOBS)) { scheduleJobs(); } else super.onReceive(message); } /** * <p> * The real brains of this scheduler: using the memory capacity reported in * each work request, determine how to spread the load of unscheduled jobs * efficiently across the requesting agents. * </p> * * <p> * The algorithm works by taking the collection of work requests and sorting * them from smallest to largest free memory capacity. * * The scheduler then iterates over each job, handing it to the first agent * that has enough capacity to complete it. In this sense, each agent is * "greedy", grabbing as many jobs as it can manage. * * If a job is too big to fit into the remaining capacity of any of the * agents, it will remain unscheduled. * </p> * * <p> * <b>WARNING:</b> If a job is too big for any of the agents, it will sit on * the backlog forever! * <p> */ private void scheduleJobs() { Map<ActorRef, List<Job>> agentJobs = new HashMap<ActorRef, List<Job>>(); List<Job> scheduledJobs = new ArrayList<>(); Iterator<Job> iterator = unscheduledJobs.iterator(); while (iterator.hasNext()) { Job job = iterator.next(); long jobSize = new Long(job.getParams().get(JVMCapacityScheduler.JOB_SIZE)); sortWorkRequestsByFreeMemory(); for (JVMCapacityWorkRequest workRequest : workRequests) { long freeMemory = workRequest.getFreeMemory(); ActorRef agent = workRequest.getAgent(); if (freeMemory >= jobSize) { // Add the job to the agent List<Job> jobs = agentJobs.get(agent); if (jobs == null) { jobs = new ArrayList<Job>(); agentJobs.put(agent, jobs); } jobs.add(job); scheduledJobs.add(job); // Decrease agent's available memory workRequest.setFreeMemory(freeMemory - jobSize); break; } } } // Create a schedule Schedule schedule = new Schedule(); for (ActorRef agent : agentJobs.keySet()) { schedule.setJobs(agent, agentJobs.get(agent)); } // Dispatch the schedule dispatchJobs(schedule); // Clear old work requests workRequests.clear(); // Cancel the scheduling scheduleJobs.cancel(); } @Override protected void scheduleJobs(JVMCapacityWorkRequest workRequest) { // Add to the map of unserviced work requests workRequests.add(workRequest); /* * Give agents the specified amount of time to react to a jobs broadcast * by scheduling a request to create a job schedule in the future */ // TODO move duration into config if (scheduleJobs == null || scheduleJobs.isCancelled()) { scheduleJobs = getContext().system().scheduler() .scheduleOnce((FiniteDuration) Duration.create("1 second"), new Runnable() { @Override public void run() { getSelf().tell(SimpleMessage.SCHEDULE_JOBS, getSelf()); } }, getContext().dispatcher()); } } private void sortWorkRequestsByFreeMemory() { Collections.sort(workRequests, new Comparator<JVMCapacityWorkRequest>() { @Override public int compare(JVMCapacityWorkRequest workRequest1, JVMCapacityWorkRequest workRequest2) { return new Long(workRequest1.getFreeMemory()).compareTo(new Long(workRequest2.getFreeMemory())); } }); } }