org.apache.aurora.scheduler.thrift.ReadOnlySchedulerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.aurora.scheduler.thrift.ReadOnlySchedulerImpl.java

Source

/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.aurora.scheduler.thrift;

import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;

import org.apache.aurora.GuavaUtils;
import org.apache.aurora.common.base.MorePreconditions;
import org.apache.aurora.gen.ConfigGroup;
import org.apache.aurora.gen.ConfigSummary;
import org.apache.aurora.gen.ConfigSummaryResult;
import org.apache.aurora.gen.GetJobUpdateDetailsResult;
import org.apache.aurora.gen.GetJobUpdateDiffResult;
import org.apache.aurora.gen.GetJobUpdateSummariesResult;
import org.apache.aurora.gen.GetJobsResult;
import org.apache.aurora.gen.GetPendingReasonResult;
import org.apache.aurora.gen.GetQuotaResult;
import org.apache.aurora.gen.GetTierConfigResult;
import org.apache.aurora.gen.JobConfiguration;
import org.apache.aurora.gen.JobKey;
import org.apache.aurora.gen.JobSummary;
import org.apache.aurora.gen.JobSummaryResult;
import org.apache.aurora.gen.JobUpdateKey;
import org.apache.aurora.gen.JobUpdateQuery;
import org.apache.aurora.gen.JobUpdateRequest;
import org.apache.aurora.gen.PendingReason;
import org.apache.aurora.gen.PopulateJobResult;
import org.apache.aurora.gen.ReadOnlyScheduler;
import org.apache.aurora.gen.Response;
import org.apache.aurora.gen.Result;
import org.apache.aurora.gen.RoleSummary;
import org.apache.aurora.gen.RoleSummaryResult;
import org.apache.aurora.gen.ScheduleStatus;
import org.apache.aurora.gen.ScheduleStatusResult;
import org.apache.aurora.gen.ScheduledTask;
import org.apache.aurora.gen.TaskConfig;
import org.apache.aurora.gen.TaskQuery;
import org.apache.aurora.gen.TierConfig;
import org.apache.aurora.scheduler.TierManager;
import org.apache.aurora.scheduler.base.JobKeys;
import org.apache.aurora.scheduler.base.Jobs;
import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.base.TaskGroupKey;
import org.apache.aurora.scheduler.base.Tasks;
import org.apache.aurora.scheduler.configuration.ConfigurationManager;
import org.apache.aurora.scheduler.configuration.ConfigurationManager.TaskDescriptionException;
import org.apache.aurora.scheduler.configuration.SanitizedConfiguration;
import org.apache.aurora.scheduler.cron.CronPredictor;
import org.apache.aurora.scheduler.cron.CrontabEntry;
import org.apache.aurora.scheduler.filter.SchedulingFilter.Veto;
import org.apache.aurora.scheduler.metadata.NearestFit;
import org.apache.aurora.scheduler.quota.QuotaInfo;
import org.apache.aurora.scheduler.quota.QuotaManager;
import org.apache.aurora.scheduler.storage.Storage;
import org.apache.aurora.scheduler.storage.entities.IAssignedTask;
import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
import org.apache.aurora.scheduler.storage.entities.IJobKey;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateDetails;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateKey;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateQuery;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateRequest;
import org.apache.aurora.scheduler.storage.entities.IJobUpdateSummary;
import org.apache.aurora.scheduler.storage.entities.IRange;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
import org.apache.aurora.scheduler.updater.JobDiff;
import org.apache.thrift.TException;

import static java.util.Objects.requireNonNull;

import static org.apache.aurora.gen.ResponseCode.INVALID_REQUEST;
import static org.apache.aurora.scheduler.base.Numbers.convertRanges;
import static org.apache.aurora.scheduler.base.Numbers.toRanges;
import static org.apache.aurora.scheduler.resources.ResourceManager.aggregateFromBag;
import static org.apache.aurora.scheduler.thrift.Responses.addMessage;
import static org.apache.aurora.scheduler.thrift.Responses.error;
import static org.apache.aurora.scheduler.thrift.Responses.invalidRequest;
import static org.apache.aurora.scheduler.thrift.Responses.ok;

class ReadOnlySchedulerImpl implements ReadOnlyScheduler.Iface {
    private static final Function<Entry<ITaskConfig, Collection<Integer>>, ConfigGroup> TO_GROUP = input -> new ConfigGroup()
            .setConfig(input.getKey().newBuilder())
            .setInstances(IRange.toBuildersSet(convertRanges(toRanges(input.getValue()))));

    private final ConfigurationManager configurationManager;
    private final Storage storage;
    private final NearestFit nearestFit;
    private final CronPredictor cronPredictor;
    private final QuotaManager quotaManager;
    private final TierManager tierManager;

    @Inject
    ReadOnlySchedulerImpl(ConfigurationManager configurationManager, Storage storage, NearestFit nearestFit,
            CronPredictor cronPredictor, QuotaManager quotaManager, TierManager tierManager) {

        this.configurationManager = requireNonNull(configurationManager);
        this.storage = requireNonNull(storage);
        this.nearestFit = requireNonNull(nearestFit);
        this.cronPredictor = requireNonNull(cronPredictor);
        this.quotaManager = requireNonNull(quotaManager);
        this.tierManager = requireNonNull(tierManager);
    }

    @Override
    public Response populateJobConfig(JobConfiguration description) {
        requireNonNull(description);

        try {
            ITaskConfig populatedTaskConfig = SanitizedConfiguration
                    .fromUnsanitized(configurationManager, IJobConfiguration.build(description)).getJobConfig()
                    .getTaskConfig();
            return ok(Result
                    .populateJobResult(new PopulateJobResult().setTaskConfig(populatedTaskConfig.newBuilder())));
        } catch (TaskDescriptionException e) {
            return invalidRequest("Invalid configuration: " + e.getMessage());
        }
    }

    // TODO(William Farner): Provide status information about cron jobs here.
    @Override
    public Response getTasksStatus(TaskQuery query) {
        return ok(Result.scheduleStatusResult(new ScheduleStatusResult().setTasks(getTasks(query))));
    }

    @Override
    public Response getTasksWithoutConfigs(TaskQuery query) {
        List<ScheduledTask> tasks = Lists.transform(getTasks(query), task -> {
            task.getAssignedTask().getTask().unsetExecutorConfig();
            return task;
        });

        return ok(Result.scheduleStatusResult(new ScheduleStatusResult().setTasks(tasks)));
    }

    @Override
    public Response getPendingReason(TaskQuery query) throws TException {
        requireNonNull(query);

        if (query.isSetSlaveHosts() && !query.getSlaveHosts().isEmpty()) {
            return invalidRequest("SlaveHosts are not supported in " + query.toString());
        }
        if (query.isSetStatuses() && !query.getStatuses().isEmpty()) {
            return invalidRequest("Statuses is not supported in " + query.toString());
        }

        // Only PENDING tasks should be considered.
        query.setStatuses(ImmutableSet.of(ScheduleStatus.PENDING));

        Set<PendingReason> reasons = FluentIterable.from(getTasks(query)).transform(scheduledTask -> {
            TaskGroupKey groupKey = TaskGroupKey.from(ITaskConfig.build(scheduledTask.getAssignedTask().getTask()));

            String reason = Joiner.on(',')
                    .join(Iterables.transform(nearestFit.getNearestFit(groupKey), Veto::getReason));

            return new PendingReason().setTaskId(scheduledTask.getAssignedTask().getTaskId()).setReason(reason);
        }).toSet();

        return ok(Result.getPendingReasonResult(new GetPendingReasonResult(reasons)));
    }

    @Override
    public Response getConfigSummary(JobKey job) throws TException {
        IJobKey jobKey = JobKeys.assertValid(IJobKey.build(job));

        Iterable<IAssignedTask> assignedTasks = Iterables.transform(
                Storage.Util.fetchTasks(storage, Query.jobScoped(jobKey).active()),
                IScheduledTask::getAssignedTask);
        Map<Integer, ITaskConfig> tasksByInstance = Maps.transformValues(
                Maps.uniqueIndex(assignedTasks, IAssignedTask::getInstanceId), IAssignedTask::getTask);
        Set<ConfigGroup> groups = instancesToConfigGroups(tasksByInstance);

        return ok(Result.configSummaryResult(new ConfigSummaryResult().setSummary(new ConfigSummary(job, groups))));
    }

    @Override
    public Response getRoleSummary() {
        Multimap<String, IJobKey> jobsByRole = storage.read(
                storeProvider -> Multimaps.index(storeProvider.getTaskStore().getJobKeys(), IJobKey::getRole));

        Multimap<String, IJobKey> cronJobsByRole = Multimaps.index(
                Iterables.transform(Storage.Util.fetchCronJobs(storage), IJobConfiguration::getKey),
                IJobKey::getRole);

        Set<RoleSummary> summaries = FluentIterable.from(Sets.union(jobsByRole.keySet(), cronJobsByRole.keySet()))
                .transform(
                        role -> new RoleSummary(role, jobsByRole.get(role).size(), cronJobsByRole.get(role).size()))
                .toSet();

        return ok(Result.roleSummaryResult(new RoleSummaryResult(summaries)));
    }

    @Override
    public Response getJobSummary(@Nullable String maybeNullRole) {
        Optional<String> ownerRole = Optional.fromNullable(maybeNullRole);

        Multimap<IJobKey, IScheduledTask> tasks = getTasks(maybeRoleScoped(ownerRole));
        Map<IJobKey, IJobConfiguration> jobs = getJobs(ownerRole, tasks);

        Function<IJobKey, JobSummary> makeJobSummary = jobKey -> {
            IJobConfiguration job = jobs.get(jobKey);
            JobSummary summary = new JobSummary().setJob(job.newBuilder())
                    .setStats(Jobs.getJobStats(tasks.get(jobKey)).newBuilder());

            if (job.isSetCronSchedule()) {
                CrontabEntry crontabEntry = CrontabEntry.parse(job.getCronSchedule());
                Optional<Date> nextRun = cronPredictor.predictNextRun(crontabEntry);
                return nextRun.transform(date -> summary.setNextCronRunMs(date.getTime())).or(summary);
            } else {
                return summary;
            }
        };

        ImmutableSet<JobSummary> jobSummaries = FluentIterable.from(jobs.keySet()).transform(makeJobSummary)
                .toSet();

        return ok(Result.jobSummaryResult(new JobSummaryResult().setSummaries(jobSummaries)));
    }

    @Override
    public Response getJobs(@Nullable String maybeNullRole) {
        Optional<String> ownerRole = Optional.fromNullable(maybeNullRole);

        return ok(Result.getJobsResult(new GetJobsResult().setConfigs(IJobConfiguration
                .toBuildersSet(getJobs(ownerRole, getTasks(maybeRoleScoped(ownerRole).active())).values()))));
    }

    @Override
    public Response getQuota(String ownerRole) {
        MorePreconditions.checkNotBlank(ownerRole);
        return storage.read(storeProvider -> {
            QuotaInfo quotaInfo = quotaManager.getQuotaInfo(ownerRole, storeProvider);
            GetQuotaResult result = new GetQuotaResult()
                    .setQuota(aggregateFromBag(quotaInfo.getQuota()).newBuilder())
                    .setProdSharedConsumption(aggregateFromBag(quotaInfo.getProdSharedConsumption()).newBuilder())
                    .setProdDedicatedConsumption(
                            aggregateFromBag(quotaInfo.getProdDedicatedConsumption()).newBuilder())
                    .setNonProdSharedConsumption(
                            aggregateFromBag(quotaInfo.getNonProdSharedConsumption()).newBuilder())
                    .setNonProdDedicatedConsumption(
                            aggregateFromBag(quotaInfo.getNonProdDedicatedConsumption()).newBuilder());

            return ok(Result.getQuotaResult(result));
        });
    }

    @Override
    public Response getJobUpdateSummaries(JobUpdateQuery mutableQuery) {
        IJobUpdateQuery query = IJobUpdateQuery.build(requireNonNull(mutableQuery));
        return ok(Result.getJobUpdateSummariesResult(
                new GetJobUpdateSummariesResult().setUpdateSummaries(IJobUpdateSummary.toBuildersList(storage.read(
                        storeProvider -> storeProvider.getJobUpdateStore().fetchJobUpdateSummaries(query))))));
    }

    @Override
    public Response getJobUpdateDetails(JobUpdateKey mutableKey, JobUpdateQuery mutableQuery) {
        if (mutableKey == null && mutableQuery == null) {
            return error("Either key or query must be set.");
        }

        if (mutableQuery != null) {
            IJobUpdateQuery query = IJobUpdateQuery.build(mutableQuery);

            List<IJobUpdateDetails> details = storage
                    .read(storeProvider -> storeProvider.getJobUpdateStore().fetchJobUpdateDetails(query));

            return ok(Result.getJobUpdateDetailsResult(
                    new GetJobUpdateDetailsResult().setDetailsList(IJobUpdateDetails.toBuildersList(details))));
        }

        // TODO(zmanji): Remove this code once `mutableKey` is removed in AURORA-1765
        IJobUpdateKey key = IJobUpdateKey.build(mutableKey);
        Optional<IJobUpdateDetails> details = storage
                .read(storeProvider -> storeProvider.getJobUpdateStore().fetchJobUpdateDetails(key));

        if (details.isPresent()) {
            return addMessage(
                    ok(Result.getJobUpdateDetailsResult(
                            new GetJobUpdateDetailsResult().setDetails(details.get().newBuilder()))),
                    "The key argument is deprecated, use the query argument instead");
        } else {
            return invalidRequest("Invalid update: " + key);
        }
    }

    @Override
    public Response getJobUpdateDiff(JobUpdateRequest mutableRequest) {
        IJobUpdateRequest request;
        try {
            request = IJobUpdateRequest
                    .build(new JobUpdateRequest(mutableRequest).setTaskConfig(configurationManager
                            .validateAndPopulate(ITaskConfig.build(mutableRequest.getTaskConfig())).newBuilder()));
        } catch (TaskDescriptionException e) {
            return error(INVALID_REQUEST, e);
        }

        IJobKey job = request.getTaskConfig().getJob();

        return storage.read(storeProvider -> {
            if (storeProvider.getCronJobStore().fetchJob(job).isPresent()) {
                return invalidRequest(NO_CRON);
            }

            JobDiff diff = JobDiff.compute(storeProvider.getTaskStore(), job,
                    JobDiff.asMap(request.getTaskConfig(), request.getInstanceCount()),
                    request.getSettings().getUpdateOnlyTheseInstances());

            Map<Integer, ITaskConfig> replaced = diff.getReplacedInstances();
            Map<Integer, ITaskConfig> replacements = Maps.asMap(diff.getReplacementInstances(),
                    Functions.constant(request.getTaskConfig()));

            Map<Integer, ITaskConfig> add = Maps.filterKeys(replacements,
                    Predicates.in(Sets.difference(replacements.keySet(), replaced.keySet())));
            Map<Integer, ITaskConfig> remove = Maps.filterKeys(replaced,
                    Predicates.in(Sets.difference(replaced.keySet(), replacements.keySet())));
            Map<Integer, ITaskConfig> update = Maps.filterKeys(replaced,
                    Predicates.in(Sets.intersection(replaced.keySet(), replacements.keySet())));

            return ok(
                    Result.getJobUpdateDiffResult(new GetJobUpdateDiffResult().setAdd(instancesToConfigGroups(add))
                            .setRemove(instancesToConfigGroups(remove)).setUpdate(instancesToConfigGroups(update))
                            .setUnchanged(instancesToConfigGroups(diff.getUnchangedInstances()))));
        });
    }

    @Override
    public Response getTierConfigs() throws TException {
        return ok(Result.getTierConfigResult(new GetTierConfigResult(tierManager.getDefaultTierName(),
                tierManager.getTiers().entrySet().stream()
                        .map(entry -> new TierConfig(entry.getKey(), entry.getValue().toMap()))
                        .collect(GuavaUtils.toImmutableSet()))));
    }

    private static Set<ConfigGroup> instancesToConfigGroups(Map<Integer, ITaskConfig> tasks) {
        Multimap<ITaskConfig, Integer> instancesByDetails = Multimaps.invertFrom(Multimaps.forMap(tasks),
                HashMultimap.create());
        return ImmutableSet.copyOf(Iterables.transform(instancesByDetails.asMap().entrySet(), TO_GROUP));
    }

    private List<ScheduledTask> getTasks(TaskQuery query) {
        requireNonNull(query);

        Iterable<IScheduledTask> tasks = Storage.Util.fetchTasks(storage, Query.arbitrary(query));
        if (query.getOffset() > 0) {
            tasks = Iterables.skip(tasks, query.getOffset());
        }
        if (query.getLimit() > 0) {
            tasks = Iterables.limit(tasks, query.getLimit());
        }

        return IScheduledTask.toBuildersList(tasks);
    }

    private Query.Builder maybeRoleScoped(Optional<String> ownerRole) {
        return ownerRole.isPresent() ? Query.roleScoped(ownerRole.get()) : Query.unscoped();
    }

    private Map<IJobKey, IJobConfiguration> getJobs(Optional<String> ownerRole,
            Multimap<IJobKey, IScheduledTask> tasks) {

        // We need to synthesize the JobConfiguration from the the current tasks because the
        // ImmediateJobManager doesn't store jobs directly and ImmediateJobManager#getJobs always
        // returns an empty Collection.
        Map<IJobKey, IJobConfiguration> jobs = Maps.newHashMap();

        jobs.putAll(Maps.transformEntries(tasks.asMap(), (jobKey, tasks1) -> {

            // Pick the latest transitioned task for each immediate job since the job can be in the
            // middle of an update or some shards have been selectively created.
            TaskConfig mostRecentTaskConfig = Tasks.getLatestActiveTask(tasks1).getAssignedTask().getTask()
                    .newBuilder();

            return IJobConfiguration.build(
                    new JobConfiguration().setKey(jobKey.newBuilder()).setOwner(mostRecentTaskConfig.getOwner())
                            .setTaskConfig(mostRecentTaskConfig).setInstanceCount(tasks1.size()));
        }));

        // Get cron jobs directly from the manager. Do this after querying the task store so the real
        // template JobConfiguration for a cron job will overwrite the synthesized one that could have
        // been created above.
        Predicate<IJobConfiguration> configFilter = ownerRole.isPresent()
                ? Predicates.compose(Predicates.equalTo(ownerRole.get()), JobKeys::getRole)
                : Predicates.alwaysTrue();
        jobs.putAll(Maps.uniqueIndex(FluentIterable.from(Storage.Util.fetchCronJobs(storage)).filter(configFilter),
                IJobConfiguration::getKey));

        return jobs;
    }

    private Multimap<IJobKey, IScheduledTask> getTasks(Query.Builder query) {
        return Tasks.byJobKey(Storage.Util.fetchTasks(storage, query));
    }

    @VisibleForTesting
    static final String NO_CRON = "Cron jobs are not supported.";
}