package io.vivarium.server.workloadmanagement; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.collections4.ListUtils; import com.google.common.collect.Maps; import io.vivarium.persistence.JobModel; import io.vivarium.persistence.JobStatus; import io.vivarium.persistence.PersistenceModule; import io.vivarium.persistence.WorkerModel; import io.vivarium.util.UUID; import io.vivarium.util.concurrency.VoidFunction; public class WorkloadEnforcer implements VoidFunction { // static configs public final static long DEFAULT_ENFORCE_TIME_GAP_IN_MS = 60_000; // Dependencies private final PersistenceModule _persistenceModule; private final JobAssignmentThreadFactory _jobAssignmentThreadFactory; private List<JobAssignmentThread> _jobAssignmentThreads; public WorkloadEnforcer(PersistenceModule persistenceModule, JobAssignmentThreadFactory jobAssignmentThreadFactory) { _persistenceModule = persistenceModule; _jobAssignmentThreadFactory = jobAssignmentThreadFactory; } @Override public void execute() { // Update the job statuses _persistenceModule.updateJobStatuses(); // Get the current worker models List<WorkerModel> workers = _persistenceModule.fetchAllWorkers(); List<WorkerModel> activeWorkers = workers.stream().filter(w -> w.isActive()).collect(Collectors.toList()); // Get the top priority unblocked jobs, the currently assigned jobs List<JobModel> waitingJobs = _persistenceModule.fetchJobsWithStatus(JobStatus.WAITING); List<JobModel> assignedJobs = _persistenceModule.fetchJobsWithStatus(JobStatus.PROCESSING); List<JobModel> prioritySortedJobs = ListUtils.union(waitingJobs, assignedJobs); prioritySortedJobs.sort(new JobPriorityComparator()); // Determine optimal greedy allocation of jobs and the current real allocation JobAssignments idealAssingments = buildDesiredJobAssingments(activeWorkers, prioritySortedJobs); JobAssignments actualAssingments = buildActualJobAssingments(activeWorkers, assignedJobs); // Determine how many jobs of each priority need to give/take from each worker. JobAssignments jobsAssignmentsToGive = JobAssignments.subtract(idealAssingments, actualAssingments); JobAssignments jobsAssignmentsToTake = JobAssignments.subtract(actualAssingments, idealAssingments); // Determine which jobs will be taken back Map<Integer, List<JobModel>> assignedJobsByPriority = assignedJobs .stream() .collect(Collectors.groupingBy(JobModel::getPriority)); List<JobAssignmentOperation> takeJobOperations = buildListOfJobsToTake(jobsAssignmentsToTake, assignedJobsByPriority); // Determine which jobs are available (or will be available) for assignment Set<UUID> jobIDsToTake = takeJobOperations .stream() .map(jobOperation -> jobOperation.getWorkerID()) .collect(Collectors.toSet()); List<JobModel> jobsToTake = assignedJobs .stream() .filter(job -> jobIDsToTake.contains(job.getJobID())) .collect(Collectors.toList()); List<JobModel> availableJobs = ListUtils.union(waitingJobs, jobsToTake); Map<Integer, List<JobModel>> availableJobsByPriority = availableJobs .stream() .collect(Collectors.groupingBy(JobModel::getPriority)); // Determine which jobs will be given out List<JobAssignmentOperation> giveJobOperations = buildListOfJobsToGive(jobsAssignmentsToGive, availableJobsByPriority); // Because we have to wait for worker acknowledgments before we can finish taking a job back that we want to // re-assign, we want to create a thread for every individual assign and take operation. Each assign operation // will have an job future. As soon as the job future is available, we can talk to that worker and give it the // job. Jobs which are already unassigned will get their future fulfilled immediately, but any jobs that the // network will need to wait for will only get fulfilled when the worker who currently has that job has returned // it. List<JobAssignmentOperation> allJobOperations = ListUtils.union(takeJobOperations, giveJobOperations); _jobAssignmentThreads = allJobOperations .stream() .map(jobOperation -> _jobAssignmentThreadFactory.make(jobOperation)) .collect(Collectors.toList()); _jobAssignmentThreads.stream().forEach(jobThread -> jobThread.start()); _jobAssignmentThreads.stream().forEach(jobThread -> jobThread.join()); } private JobAssignments buildDesiredJobAssingments(Collection<WorkerModel> workers, List<JobModel> prioritySortedJobs) { JobAssignments desiredAssingments = new JobAssignments(workers); for (JobModel job : prioritySortedJobs) { long maxScoreImprovement = 0; WorkerModel assignToWorker = null; for (WorkerModel worker : workers) { if (desiredAssingments.isWorkerFull(worker)) { continue; } long workerScoreImprovment = desiredAssingments.getScoreChangeForJob(worker, job.getPriority()); if (workerScoreImprovment > maxScoreImprovement) { maxScoreImprovement = workerScoreImprovment; assignToWorker = worker; } } if (assignToWorker != null) { desiredAssingments.addWorkerJob(assignToWorker, job.getPriority()); } } return desiredAssingments; } private JobAssignments buildActualJobAssingments(Collection<WorkerModel> workers, List<JobModel> assignedJobs) { JobAssignments actualAssingments = new JobAssignments(workers); Map<UUID, WorkerModel> workersByID = Maps.uniqueIndex(workers, WorkerModel::getWorkerID); for (JobModel job : assignedJobs) { UUID workerID = job.getCheckedOutByWorkerID().get(); actualAssingments.addWorkerJob(workersByID.get(workerID), job.getPriority()); } return actualAssingments; } private List<JobAssignmentOperation> buildListOfJobsToTake(JobAssignments jobsAssignmentsToTake, Map<Integer, List<JobModel>> assignedJobsByPriority) { return convertJobAssignmentsIntoJobAssignmentOperations(JobAssignmentAction.TAKE, jobsAssignmentsToTake, assignedJobsByPriority); } private List<JobAssignmentOperation> buildListOfJobsToGive(JobAssignments jobsAssignmentsToGive, Map<Integer, List<JobModel>> availableJobsByPriority) { return convertJobAssignmentsIntoJobAssignmentOperations(JobAssignmentAction.GIVE, jobsAssignmentsToGive, availableJobsByPriority); } private List<JobAssignmentOperation> convertJobAssignmentsIntoJobAssignmentOperations(JobAssignmentAction action, JobAssignments jobsAssignments, Map<Integer, List<JobModel>> jobsByPriority) { List<JobAssignmentOperation> operations = new LinkedList<>(); Set<WorkerModel> workers = jobsAssignments.getWorkers(); for (WorkerModel worker : workers) { Map<Integer, Integer> priorityCounts = jobsAssignments.getJobPriorityCounts(worker); for (int priority : priorityCounts.keySet()) { while (priorityCounts.get(priority) > 0) { JobModel job = jobsByPriority.get(priority).remove(0); JobAssignmentOperation jobOperation = new JobAssignmentOperation(action, worker.getWorkerID(), job.getJobID()); operations.add(jobOperation); priorityCounts.put(priority, priorityCounts.get(priority) - 1); } } } return operations; } }