All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.kylin.rest.service.JobService Maven / Gradle / Ivy

There is a newer version: 4.0.4
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.kylin.rest.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.apache.directory.api.util.Strings;
import org.apache.kylin.common.KylinConfig;
import org.apache.kylin.common.util.Pair;
import org.apache.kylin.cube.CubeInstance;
import org.apache.kylin.cube.CubeManager;
import org.apache.kylin.cube.CubeSegment;
import org.apache.kylin.cube.CubeUpdate;
import org.apache.kylin.cube.model.CubeBuildTypeEnum;
import org.apache.kylin.engine.EngineFactory;
import org.apache.kylin.engine.mr.BatchOptimizeJobCheckpointBuilder;
import org.apache.kylin.engine.mr.CubingJob;
import org.apache.kylin.engine.mr.LookupSnapshotBuildJob;
import org.apache.kylin.engine.mr.LookupSnapshotJobBuilder;
import org.apache.kylin.engine.mr.common.JobInfoConverter;
import org.apache.kylin.engine.mr.steps.CubingExecutableUtil;
import org.apache.kylin.job.JobInstance;
import org.apache.kylin.job.JobSearchResult;
import org.apache.kylin.job.Scheduler;
import org.apache.kylin.job.SchedulerFactory;
import org.apache.kylin.job.constant.JobStatusEnum;
import org.apache.kylin.job.constant.JobTimeFilterEnum;
import org.apache.kylin.job.dao.ExecutableOutputPO;
import org.apache.kylin.job.engine.JobEngineConfig;
import org.apache.kylin.job.exception.JobException;
import org.apache.kylin.job.exception.SchedulerException;
import org.apache.kylin.job.execution.AbstractExecutable;
import org.apache.kylin.job.execution.CheckpointExecutable;
import org.apache.kylin.job.execution.DefaultChainedExecutable;
import org.apache.kylin.job.execution.ExecutableState;
import org.apache.kylin.job.execution.Output;
import org.apache.kylin.metadata.model.SegmentRange;
import org.apache.kylin.metadata.model.SegmentRange.TSRange;
import org.apache.kylin.metadata.model.SegmentStatusEnum;
import org.apache.kylin.metadata.model.Segments;
import org.apache.kylin.metadata.model.TableDesc;
import org.apache.kylin.metadata.realization.RealizationStatusEnum;
import org.apache.kylin.rest.exception.BadRequestException;
import org.apache.kylin.rest.msg.Message;
import org.apache.kylin.rest.msg.MsgPicker;
import org.apache.kylin.rest.util.AclEvaluate;
import org.apache.kylin.source.ISource;
import org.apache.kylin.source.SourceManager;
import org.apache.kylin.source.SourcePartition;
import org.apache.kylin.storage.hbase.util.ZookeeperJobLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

/**
 * @author ysong1
 */

@EnableAspectJAutoProxy(proxyTargetClass = true)
@Component("jobService")
public class JobService extends BasicService implements InitializingBean {

    private static final Logger logger = LoggerFactory.getLogger(JobService.class);

    @Autowired
    private AclEvaluate aclEvaluate;

    /*
    * (non-Javadoc)
    *
    * @see
    * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
    */
    @SuppressWarnings("unchecked")
    @Override
    public void afterPropertiesSet() throws Exception {

        String timeZone = getConfig().getTimeZone();
        TimeZone tzone = TimeZone.getTimeZone(timeZone);
        TimeZone.setDefault(tzone);

        final KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
        final Scheduler scheduler = (Scheduler) SchedulerFactory
                .scheduler(kylinConfig.getSchedulerType());

        scheduler.init(new JobEngineConfig(kylinConfig), new ZookeeperJobLock());

        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    scheduler.shutdown();
                } catch (SchedulerException e) {
                    logger.error("error occurred to shutdown scheduler", e);
                }
            }
        }));
    }

    private Set convertStatusEnumToStates(List statusList) {
        Set states;
        if (statusList == null || statusList.isEmpty()) {
            states = EnumSet.allOf(ExecutableState.class);
        } else {
            states = Sets.newHashSet();
            for (JobStatusEnum status : statusList) {
                states.add(parseToExecutableState(status));
            }
        }
        return states;
    }

    private ExecutableState parseToExecutableState(JobStatusEnum status) {
        Message msg = MsgPicker.getMsg();

        switch (status) {
        case DISCARDED:
            return ExecutableState.DISCARDED;
        case ERROR:
            return ExecutableState.ERROR;
        case FINISHED:
            return ExecutableState.SUCCEED;
        case NEW:
            return ExecutableState.READY;
        case PENDING:
            return ExecutableState.READY;
        case RUNNING:
            return ExecutableState.RUNNING;
        case STOPPED:
            return ExecutableState.STOPPED;
        default:
            throw new BadRequestException(String.format(Locale.ROOT, msg.getILLEGAL_EXECUTABLE_STATE(), status));
        }
    }

    private long getTimeStartInMillis(Calendar calendar, JobTimeFilterEnum timeFilter) {
        Message msg = MsgPicker.getMsg();

        switch (timeFilter) {
        case LAST_ONE_DAY:
            calendar.add(Calendar.DAY_OF_MONTH, -1);
            return calendar.getTimeInMillis();
        case LAST_ONE_WEEK:
            calendar.add(Calendar.WEEK_OF_MONTH, -1);
            return calendar.getTimeInMillis();
        case LAST_ONE_MONTH:
            calendar.add(Calendar.MONTH, -1);
            return calendar.getTimeInMillis();
        case LAST_ONE_YEAR:
            calendar.add(Calendar.YEAR, -1);
            return calendar.getTimeInMillis();
        case ALL:
            return 0;
        default:
            throw new BadRequestException(String.format(Locale.ROOT, msg.getILLEGAL_TIME_FILTER(), timeFilter));
        }
    }

    public JobInstance submitJob(CubeInstance cube, TSRange tsRange, SegmentRange segRange, //
            Map sourcePartitionOffsetStart, Map sourcePartitionOffsetEnd,
            CubeBuildTypeEnum buildType, boolean force, String submitter) throws IOException {
        aclEvaluate.checkProjectOperationPermission(cube);
        JobInstance jobInstance = submitJobInternal(cube, tsRange, segRange, sourcePartitionOffsetStart,
                sourcePartitionOffsetEnd, buildType, force, submitter);

        return jobInstance;
    }

    public JobInstance submitJobInternal(CubeInstance cube, TSRange tsRange, SegmentRange segRange, //
            Map sourcePartitionOffsetStart, Map sourcePartitionOffsetEnd, //
            CubeBuildTypeEnum buildType, boolean force, String submitter) throws IOException {
        Message msg = MsgPicker.getMsg();

        if (cube.getStatus() == RealizationStatusEnum.DESCBROKEN) {
            throw new BadRequestException(String.format(Locale.ROOT, msg.getBUILD_BROKEN_CUBE(), cube.getName()));
        }

        checkCubeDescSignature(cube);
        checkAllowBuilding(cube);

        if (buildType == CubeBuildTypeEnum.BUILD || buildType == CubeBuildTypeEnum.REFRESH) {
            checkAllowParallelBuilding(cube);
        }

        DefaultChainedExecutable job;

        CubeSegment newSeg = null;
        try {
            if (buildType == CubeBuildTypeEnum.BUILD) {
                ISource source = SourceManager.getSource(cube);
                SourcePartition src = new SourcePartition(tsRange, segRange, sourcePartitionOffsetStart,
                        sourcePartitionOffsetEnd);
                src = source.enrichSourcePartitionBeforeBuild(cube, src);
                newSeg = getCubeManager().appendSegment(cube, src);
                job = EngineFactory.createBatchCubingJob(newSeg, submitter);
            } else if (buildType == CubeBuildTypeEnum.MERGE) {
                newSeg = getCubeManager().mergeSegments(cube, tsRange, segRange, force);
                job = EngineFactory.createBatchMergeJob(newSeg, submitter);
            } else if (buildType == CubeBuildTypeEnum.REFRESH) {
                newSeg = getCubeManager().refreshSegment(cube, tsRange, segRange);
                job = EngineFactory.createBatchCubingJob(newSeg, submitter);
            } else {
                throw new BadRequestException(String.format(Locale.ROOT, msg.getINVALID_BUILD_TYPE(), buildType));
            }

            getExecutableManager().addJob(job);

        } catch (Exception e) {
            if (newSeg != null) {
                logger.error("Job submission might failed for NEW segment {}, will clean the NEW segment from cube",
                        newSeg.getName());
                try {
                    // Remove this segment
                    getCubeManager().updateCubeDropSegments(cube, newSeg);
                } catch (Exception ee) {
                    // swallow the exception
                    logger.error("Clean New segment failed, ignoring it", e);
                }
            }
            throw e;
        }

        JobInstance jobInstance = getSingleJobInstance(job);

        return jobInstance;
    }

    public Pair> submitOptimizeJob(CubeInstance cube, Set cuboidsRecommend,
            String submitter) throws IOException, JobException {

        Pair> result = submitOptimizeJobInternal(cube, cuboidsRecommend, submitter);
        return result;
    }

    private Pair> submitOptimizeJobInternal(CubeInstance cube,
            Set cuboidsRecommend, String submitter) throws IOException {
        Message msg = MsgPicker.getMsg();

        if (cube.getStatus() == RealizationStatusEnum.DESCBROKEN) {
            throw new BadRequestException(String.format(Locale.ROOT, msg.getBUILD_BROKEN_CUBE(), cube.getName()));
        }

        checkCubeDescSignature(cube);
        checkAllowOptimization(cube, cuboidsRecommend);

        CubeSegment[] optimizeSegments = null;
        try {
            /** Add optimize segments */
            optimizeSegments = getCubeManager().optimizeSegments(cube, cuboidsRecommend);
            List optimizeJobInstances = Lists.newLinkedList();

            /** Add optimize jobs */
            List optimizeJobList = Lists.newArrayListWithExpectedSize(optimizeSegments.length);
            for (CubeSegment optimizeSegment : optimizeSegments) {
                DefaultChainedExecutable optimizeJob = EngineFactory.createBatchOptimizeJob(optimizeSegment, submitter);
                getExecutableManager().addJob(optimizeJob);

                optimizeJobList.add(optimizeJob);
                optimizeJobInstances.add(getSingleJobInstance(optimizeJob));
            }

            /** Add checkpoint job for batch jobs */
            CheckpointExecutable checkpointJob = new BatchOptimizeJobCheckpointBuilder(cube, submitter).build();
            checkpointJob.addTaskListForCheck(optimizeJobList);

            getExecutableManager().addJob(checkpointJob);

            return new Pair(getCheckpointJobInstance(checkpointJob), optimizeJobInstances);
        } catch (Exception e) {
            if (optimizeSegments != null) {
                logger.error("Job submission might failed for NEW segments {}, will clean the NEW segments from cube",
                        optimizeSegments);
                try {
                    // Remove this segments
                    getCubeManager().updateCubeDropSegments(cube, optimizeSegments);
                } catch (Exception ee) {
                    // swallow the exception
                    logger.error("Clean New segments failed, ignoring it", e);
                }
            }
            throw e;
        }
    }

    public JobInstance submitRecoverSegmentOptimizeJob(CubeSegment segment, String submitter)
            throws IOException, JobException {
        CubeInstance cubeInstance = segment.getCubeInstance();

        checkCubeDescSignature(cubeInstance);

        String cubeName = cubeInstance.getName();
        List jobInstanceList = searchJobsByCubeName(cubeName, null,
                Lists.newArrayList(JobStatusEnum.NEW, JobStatusEnum.PENDING, JobStatusEnum.ERROR),
                JobTimeFilterEnum.ALL, JobSearchMode.CHECKPOINT_ONLY);
        if (jobInstanceList.size() > 1) {
            throw new IllegalStateException("Exist more than one CheckpointExecutable for cube " + cubeName);
        } else if (jobInstanceList.size() == 0) {
            throw new IllegalStateException("There's no CheckpointExecutable for cube " + cubeName);
        }
        CheckpointExecutable checkpointExecutable = (CheckpointExecutable) getExecutableManager()
                .getJob(jobInstanceList.get(0).getId());

        AbstractExecutable toBeReplaced = null;
        for (AbstractExecutable taskForCheck : checkpointExecutable.getSubTasksForCheck()) {
            if (taskForCheck instanceof CubingJob) {
                CubingJob subCubingJob = (CubingJob) taskForCheck;
                String segmentName = CubingExecutableUtil.getSegmentName(subCubingJob.getParams());
                if (segmentName != null && segmentName.equals(segment.getName())) {
                    String segmentID = CubingExecutableUtil.getSegmentId(subCubingJob.getParams());
                    CubeSegment beingOptimizedSegment = cubeInstance.getSegmentById(segmentID);
                    if (beingOptimizedSegment != null) { // beingOptimizedSegment exists & should not be recovered
                        throw new IllegalStateException("Segment " + beingOptimizedSegment.getName() + "-"
                                + beingOptimizedSegment.getUuid()
                                + " still exists. Please delete it or discard the related optimize job first!!!");
                    }
                    toBeReplaced = taskForCheck;
                    break;
                }
            }
        }
        if (toBeReplaced == null) {
            throw new IllegalStateException("There's no CubingJob for segment " + segment.getName()
                    + " in CheckpointExecutable " + checkpointExecutable.getName());
        }

        /** Add CubingJob for the related segment **/
        CubeSegment optimizeSegment = getCubeManager().appendSegment(cubeInstance, segment.getTSRange());

        DefaultChainedExecutable optimizeJob = EngineFactory.createBatchOptimizeJob(optimizeSegment, submitter);

        getExecutableManager().addJob(optimizeJob);

        JobInstance optimizeJobInstance = getSingleJobInstance(optimizeJob);

        /** Update the checkpoint job */
        checkpointExecutable.getSubTasksForCheck().set(checkpointExecutable.getSubTasksForCheck().indexOf(toBeReplaced),
                optimizeJob);

        getExecutableManager().updateCheckpointJob(checkpointExecutable.getId(),
                checkpointExecutable.getSubTasksForCheck());

        return optimizeJobInstance;
    }

    public JobInstance submitLookupSnapshotJob(CubeInstance cube, String lookupTable, List segmentIDs,
            String submitter) throws IOException {
        Message msg = MsgPicker.getMsg();
        TableDesc tableDesc = getTableManager().getTableDesc(lookupTable, cube.getProject());
        if (tableDesc.isView()) {
            throw new BadRequestException(
                    String.format(Locale.ROOT, msg.getREBUILD_SNAPSHOT_OF_VIEW(), tableDesc.getName()));
        }
        LookupSnapshotBuildJob job = new LookupSnapshotJobBuilder(cube, lookupTable, segmentIDs, submitter).build();
        getExecutableManager().addJob(job);

        JobInstance jobInstance = getLookupSnapshotBuildJobInstance(job);
        return jobInstance;
    }

    private void checkCubeDescSignature(CubeInstance cube) {
        Message msg = MsgPicker.getMsg();

        if (!cube.getDescriptor().checkSignature())
            throw new BadRequestException(
                    String.format(Locale.ROOT, msg.getINCONSISTENT_CUBE_DESC_SIGNATURE(), cube.getDescriptor()));
    }

    private void checkAllowBuilding(CubeInstance cube) {
        if (cube.getConfig().isCubePlannerEnabled()) {
            Segments readyPendingSegments = cube.getSegments(SegmentStatusEnum.READY_PENDING);
            if (readyPendingSegments.size() > 0) {
                throw new BadRequestException("The cube " + cube.getName() + " has READY_PENDING segments "
                        + readyPendingSegments + ". It's not allowed for building");
            }
        }
    }

    private void checkAllowParallelBuilding(CubeInstance cube) {
        if (cube.getConfig().isCubePlannerEnabled()) {
            if (cube.getCuboids() == null) {
                Segments cubeSegments = cube.getSegments();
                if (cubeSegments.size() > 0 && cubeSegments.getSegments(SegmentStatusEnum.READY).size() <= 0) {
                    throw new BadRequestException("The cube " + cube.getName() + " has segments " + cubeSegments
                            + ", but none of them is READY. It's not allowed for parallel building");
                }
            }
        }
    }

    private void checkAllowOptimization(CubeInstance cube, Set cuboidsRecommend) {
        Segments buildingSegments = cube.getBuildingSegments();
        if (buildingSegments.size() > 0) {
            throw new BadRequestException("The cube " + cube.getName() + " has building segments " + buildingSegments
                    + ". It's not allowed for optimization");
        }
        long baseCuboid = cube.getCuboidScheduler().getBaseCuboidId();
        if (!cuboidsRecommend.contains(baseCuboid)) {
            throw new BadRequestException("The recommend cuboids should contain the base cuboid " + baseCuboid);
        }
        Set currentCuboidSet = cube.getCuboidScheduler().getAllCuboidIds();
        if (currentCuboidSet.equals(cuboidsRecommend)) {
            throw new BadRequestException(
                    "The recommend cuboids are the same as the current cuboids. It's no need to do optimization.");
        }
    }

    public JobInstance getJobInstance(String uuid) {
        AbstractExecutable job = getExecutableManager().getJob(uuid);
        if (job instanceof CheckpointExecutable) {
            return getCheckpointJobInstance(job);
        } else {
            return getSingleJobInstance(job);
        }
    }

    public Output getOutput(String id) {
        return getExecutableManager().getOutput(id);
    }

    protected JobInstance getSingleJobInstance(AbstractExecutable job) {
        Message msg = MsgPicker.getMsg();

        if (job == null) {
            return null;
        }
        if (!(job instanceof CubingJob)) {
            throw new BadRequestException(String.format(Locale.ROOT, msg.getILLEGAL_JOB_TYPE(), job.getId()));
        }

        CubingJob cubeJob = (CubingJob) job;
        CubeInstance cube = CubeManager.getInstance(KylinConfig.getInstanceFromEnv())
                .getCube(CubingExecutableUtil.getCubeName(cubeJob.getParams()));
        Output output = cubeJob.getOutput();
        final JobInstance result = new JobInstance();
        result.setName(job.getName());
        if (cube != null) {
            result.setRelatedCube(cube.getName());
            result.setDisplayCubeName(cube.getDisplayName());
        } else {
            String cubeName = CubingExecutableUtil.getCubeName(cubeJob.getParams());
            result.setRelatedCube(cubeName);
            result.setDisplayCubeName(cubeName);
        }
        result.setRelatedSegment(CubingExecutableUtil.getSegmentId(cubeJob.getParams()));
        result.setLastModified(cubeJob.getLastModified());
        result.setSubmitter(cubeJob.getSubmitter());
        result.setUuid(cubeJob.getId());
        result.setType(CubeBuildTypeEnum.BUILD);
        result.setStatus(JobInfoConverter.parseToJobStatus(job.getStatus()));
        result.setMrWaiting(cubeJob.getMapReduceWaitTime() / 1000);
        result.setBuildInstance(AbstractExecutable.getBuildInstance(output));
        result.setDuration(cubeJob.getDuration() / 1000);
        for (int i = 0; i < cubeJob.getTasks().size(); ++i) {
            AbstractExecutable task = cubeJob.getTasks().get(i);
            result.addStep(JobInfoConverter.parseToJobStep(task, i, getExecutableManager().getOutput(task.getId())));
        }
        return result;
    }

    protected JobInstance getLookupSnapshotBuildJobInstance(LookupSnapshotBuildJob job) {
        if (job == null) {
            return null;
        }
        Output output = job.getOutput();
        final JobInstance result = new JobInstance();
        result.setName(job.getName());
        result.setRelatedCube(CubingExecutableUtil.getCubeName(job.getParams()));
        result.setRelatedSegment(CubingExecutableUtil.getSegmentId(job.getParams()));
        result.setLastModified(job.getLastModified());
        result.setSubmitter(job.getSubmitter());
        result.setUuid(job.getId());
        result.setType(CubeBuildTypeEnum.BUILD);
        result.setStatus(JobInfoConverter.parseToJobStatus(job.getStatus()));
        result.setBuildInstance(AbstractExecutable.getBuildInstance(output));
        result.setDuration(job.getDuration() / 1000);
        for (int i = 0; i < job.getTasks().size(); ++i) {
            AbstractExecutable task = job.getTasks().get(i);
            result.addStep(JobInfoConverter.parseToJobStep(task, i, getExecutableManager().getOutput(task.getId())));
        }
        return result;
    }

    protected JobInstance getCheckpointJobInstance(AbstractExecutable job) {
        Message msg = MsgPicker.getMsg();

        if (job == null) {
            return null;
        }
        if (!(job instanceof CheckpointExecutable)) {
            throw new BadRequestException(String.format(Locale.ROOT, msg.getILLEGAL_JOB_TYPE(), job.getId()));
        }

        CheckpointExecutable checkpointExecutable = (CheckpointExecutable) job;
        Output output = checkpointExecutable.getOutput();
        final JobInstance result = new JobInstance();
        result.setName(job.getName());
        result.setRelatedCube(CubingExecutableUtil.getCubeName(job.getParams()));
        result.setDisplayCubeName(CubingExecutableUtil.getCubeName(job.getParams()));
        result.setLastModified(job.getLastModified());
        result.setSubmitter(job.getSubmitter());
        result.setUuid(job.getId());
        result.setType(CubeBuildTypeEnum.CHECKPOINT);
        result.setStatus(JobInfoConverter.parseToJobStatus(job.getStatus()));
        result.setBuildInstance(AbstractExecutable.getBuildInstance(output));
        result.setDuration(job.getDuration() / 1000);
        for (int i = 0; i < checkpointExecutable.getTasks().size(); ++i) {
            AbstractExecutable task = checkpointExecutable.getTasks().get(i);
            result.addStep(JobInfoConverter.parseToJobStep(task, i, getExecutableManager().getOutput(task.getId())));
        }
        return result;
    }

    public void resumeJob(JobInstance job) {
        aclEvaluate.checkProjectOperationPermission(job);
        getExecutableManager().resumeJob(job.getId());
    }

    public void rollbackJob(JobInstance job, String stepId) {
        aclEvaluate.checkProjectOperationPermission(job);
        getExecutableManager().rollbackJob(job.getId(), stepId);
    }

    public void cancelJob(JobInstance job) throws IOException {
        aclEvaluate.checkProjectOperationPermission(job);
        if (null == job.getRelatedCube() || null == getCubeManager().getCube(job.getRelatedCube())
                || null == job.getRelatedSegment()) {
            getExecutableManager().discardJob(job.getId());
        }

        logger.info("Cancel job [" + job.getId() + "] trigger by "
                + SecurityContextHolder.getContext().getAuthentication().getName());
        if (job.getStatus() == JobStatusEnum.FINISHED) {
            throw new IllegalStateException(
                    "The job " + job.getId() + " has already been finished and cannot be discarded.");
        }

        if (job.getStatus() != JobStatusEnum.DISCARDED) {
            AbstractExecutable executable = getExecutableManager().getJob(job.getId());
            if (executable instanceof CubingJob) {
                cancelCubingJobInner((CubingJob) executable);
            } else if (executable instanceof CheckpointExecutable) {
                cancelCheckpointJobInner((CheckpointExecutable) executable);
            } else {
                getExecutableManager().discardJob(executable.getId());
            }
        }
    }

    private void cancelCubingJobInner(CubingJob cubingJob) throws IOException {
        CubeInstance cubeInstance = getCubeManager().getCube(CubingExecutableUtil.getCubeName(cubingJob.getParams()));
        // might not a cube job
        final String segmentIds = CubingExecutableUtil.getSegmentId(cubingJob.getParams());
        if (!StringUtils.isEmpty(segmentIds)) {
            for (String segmentId : StringUtils.split(segmentIds)) {
                final CubeSegment segment = cubeInstance.getSegmentById(segmentId);
                if (segment != null
                        && (segment.getStatus() == SegmentStatusEnum.NEW || segment.getTSRange().end.v == 0)) {
                    // Remove this segment
                    getCubeManager().updateCubeDropSegments(cubeInstance, segment);
                }
            }
        }
        getExecutableManager().discardJob(cubingJob.getId());
    }

    private void cancelCheckpointJobInner(CheckpointExecutable checkpointExecutable) throws IOException {
        List segmentIdList = Lists.newLinkedList();
        List jobIdList = Lists.newLinkedList();
        jobIdList.add(checkpointExecutable.getId());
        setRelatedIdList(checkpointExecutable, segmentIdList, jobIdList);

        CubeInstance cubeInstance = getCubeManager()
                .getCube(CubingExecutableUtil.getCubeName(checkpointExecutable.getParams()));
        if (!segmentIdList.isEmpty()) {
            List toRemoveSegments = Lists.newLinkedList();
            for (String segmentId : segmentIdList) {
                final CubeSegment segment = cubeInstance.getSegmentById(segmentId);
                if (segment != null && segment.getStatus() != SegmentStatusEnum.READY) {
                    toRemoveSegments.add(segment);
                }
            }

            CubeUpdate cubeBuilder = new CubeUpdate(cubeInstance);
            cubeBuilder.setToRemoveSegs(toRemoveSegments.toArray(new CubeSegment[toRemoveSegments.size()]));
            cubeBuilder.setCuboidsRecommend(Sets. newHashSet()); //Set recommend cuboids to be null
            getCubeManager().updateCube(cubeBuilder);
        }

        for (String jobId : jobIdList) {
            getExecutableManager().discardJob(jobId);
        }
    }

    private void setRelatedIdList(CheckpointExecutable checkpointExecutable, List segmentIdList,
            List jobIdList) {
        for (AbstractExecutable taskForCheck : checkpointExecutable.getSubTasksForCheck()) {
            jobIdList.add(taskForCheck.getId());
            if (taskForCheck instanceof CubingJob) {
                segmentIdList.addAll(Lists
                        .newArrayList(StringUtils.split(CubingExecutableUtil.getSegmentId(taskForCheck.getParams()))));
            } else if (taskForCheck instanceof CheckpointExecutable) {
                setRelatedIdList((CheckpointExecutable) taskForCheck, segmentIdList, jobIdList);
            }
        }
    }

    public JobInstance pauseJob(JobInstance job) {
        aclEvaluate.checkProjectOperationPermission(job);
        getExecutableManager().pauseJob(job.getId());
        job.setStatus(JobStatusEnum.STOPPED);
        return job;
    }

    public void dropJob(JobInstance job) {
        aclEvaluate.checkProjectOperationPermission(job);
        if (job.getRelatedCube() != null && getCubeManager().getCube(job.getRelatedCube()) != null) {
            if (job.getStatus() != JobStatusEnum.FINISHED && job.getStatus() != JobStatusEnum.DISCARDED) {
                throw new BadRequestException(
                        "Only FINISHED and DISCARDED job can be deleted. Please wait for the job finishing or discard the job!!!");
            }
        }
        getExecutableManager().deleteJob(job.getId());
        logger.info("Delete job [" + job.getId() + "] trigger by + "
                + SecurityContextHolder.getContext().getAuthentication().getName());
    }

    //******************************** Job search apis for Job controller V1 *******************************************
    /**
    * currently only support substring match
    *
    * @return
    */
    public List searchJobs(final String cubeNameSubstring, final String projectName,
            final List statusList, final Integer limitValue, final Integer offsetValue,
            final JobTimeFilterEnum timeFilter, JobSearchMode jobSearchMode) {
        Integer limit = (null == limitValue) ? 30 : limitValue;
        Integer offset = (null == offsetValue) ? 0 : offsetValue;
        List jobs = searchJobsByCubeName(cubeNameSubstring, projectName, statusList, timeFilter,
                jobSearchMode);

        Collections.sort(jobs);

        if (jobs.size() <= offset) {
            return Collections.emptyList();
        }

        if ((jobs.size() - offset) < limit) {
            return jobs.subList(offset, jobs.size());
        }

        return jobs.subList(offset, offset + limit);
    }

    /**
     * it loads all metadata of "execute" and "execute_output", and parses all job instances within the scope of the given filters
     *
     * @return List of job instances searched by the method
     *
     */
    public List searchJobsByCubeName(final String cubeNameSubstring, final String projectName,
            final List statusList, final JobTimeFilterEnum timeFilter,
            final JobSearchMode jobSearchMode) {
        return innerSearchJobs(cubeNameSubstring, null, projectName, statusList, timeFilter, jobSearchMode);
    }

    public List innerSearchJobs(final String cubeName, final String jobName, final String projectName,
            final List statusList, final JobTimeFilterEnum timeFilter, JobSearchMode jobSearchMode) {
        List result = Lists.newArrayList();
        switch (jobSearchMode) {
        case ALL:
            result.addAll(innerSearchCubingJobs(cubeName, jobName, projectName, statusList, timeFilter));
            result.addAll(innerSearchCheckpointJobs(cubeName, jobName, projectName, statusList, timeFilter));
            break;
        case CHECKPOINT_ONLY:
            result.addAll(innerSearchCheckpointJobs(cubeName, jobName, projectName, statusList, timeFilter));
            break;
        case CUBING_ONLY:
        default:
            result.addAll(innerSearchCubingJobs(cubeName, jobName, projectName, statusList, timeFilter));
        }
        return result;
    }

    public List innerSearchCubingJobs(final String cubeName, final String jobName,
            final String projectName, final List statusList, final JobTimeFilterEnum timeFilter) {
        if (null == projectName) {
            aclEvaluate.checkIsGlobalAdmin();
        } else {
            aclEvaluate.checkProjectOperationPermission(projectName);
        }
        // prepare time range
        Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
        calendar.setTime(new Date());
        long timeStartInMillis = getTimeStartInMillis(calendar, timeFilter);
        long timeEndInMillis = Long.MAX_VALUE;
        Set states = convertStatusEnumToStates(statusList);
        final Map allOutputs = getExecutableManager().getAllOutputs(timeStartInMillis, timeEndInMillis);

        return Lists
                .newArrayList(
                        FluentIterable
                                .from(innerSearchCubingJobs(cubeName, jobName, states, timeStartInMillis,
                                        timeEndInMillis, allOutputs, false, projectName))
                                .transform(new Function() {
                                    @Override
                                    public JobInstance apply(CubingJob cubingJob) {
                                        return JobInfoConverter.parseToJobInstanceQuietly(cubingJob, allOutputs);
                                    }
                                }).filter(new Predicate() {
                                    @Override
                                    public boolean apply(@Nullable JobInstance input) {
                                        return input != null;
                                    }
                                }));
    }

    /**
     * loads all metadata of "execute" and returns list of cubing job within the scope of the given filters
     *
     * @param allOutputs map of executable output data with type DefaultOutput parsed from ExecutableOutputPO
     *
     */
    public List innerSearchCubingJobs(final String cubeName, final String jobName,
            final Set statusList, long timeStartInMillis, long timeEndInMillis,
            final Map allOutputs, final boolean nameExactMatch, final String projectName) {
        List results = Lists.newArrayList(
                FluentIterable.from(getExecutableManager().getAllExecutables(timeStartInMillis, timeEndInMillis))
                        .filter(new Predicate() {
                            @Override
                            public boolean apply(AbstractExecutable executable) {
                                if (executable instanceof CubingJob) {
                                    if (StringUtils.isEmpty(cubeName)) {
                                        return true;
                                    }
                                    String executableCubeName = CubingExecutableUtil
                                            .getCubeName(executable.getParams());
                                    if (executableCubeName == null)
                                        return true;
                                    if (nameExactMatch)
                                        return executableCubeName.equalsIgnoreCase(cubeName);
                                    else
                                        return executableCubeName.toLowerCase(Locale.ROOT)
                                                .contains(cubeName.toLowerCase(Locale.ROOT));
                                } else {
                                    return false;
                                }
                            }
                        }).transform(new Function() {
                            @Override
                            public CubingJob apply(AbstractExecutable executable) {
                                return (CubingJob) executable;
                            }
                        }).filter(Predicates.and(new Predicate() {
                            @Override
                            public boolean apply(CubingJob executable) {
                                if (null == projectName || null == getProjectManager().getProject(projectName)) {
                                    return true;
                                } else {
                                    return projectName.equalsIgnoreCase(executable.getProjectName());
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(CubingJob executable) {
                                try {
                                    Output output = allOutputs.get(executable.getId());
                                    if (output == null) {
                                        return false;
                                    }

                                    ExecutableState state = output.getState();
                                    boolean ret = statusList.contains(state);
                                    return ret;
                                } catch (Exception e) {
                                    throw e;
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(@Nullable CubingJob cubeJob) {
                                if (cubeJob == null) {
                                    return false;
                                }

                                if (Strings.isEmpty(jobName)) {
                                    return true;
                                }

                                if (nameExactMatch) {
                                    return cubeJob.getName().equalsIgnoreCase(jobName);
                                } else {
                                    return cubeJob.getName().toLowerCase(Locale.ROOT)
                                            .contains(jobName.toLowerCase(Locale.ROOT));
                                }
                            }
                        })));
        return results;
    }

    public List innerSearchCheckpointJobs(final String cubeName, final String jobName,
            final String projectName, final List statusList, final JobTimeFilterEnum timeFilter) {
        // TODO: use cache of jobs for this method
        // prepare time range
        Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
        calendar.setTime(new Date());
        long timeStartInMillis = getTimeStartInMillis(calendar, timeFilter);
        long timeEndInMillis = Long.MAX_VALUE;
        Set states = convertStatusEnumToStates(statusList);
        final Map allOutputs = getExecutableManager().getAllOutputs(timeStartInMillis, timeEndInMillis);

        return Lists
                .newArrayList(FluentIterable
                        .from(innerSearchCheckpointJobs(cubeName, jobName, states, timeStartInMillis, timeEndInMillis,
                                allOutputs, false, projectName))
                        .transform(new Function() {
                            @Override
                            public JobInstance apply(CheckpointExecutable checkpointExecutable) {
                                return JobInfoConverter.parseToJobInstanceQuietly(checkpointExecutable, allOutputs);
                            }
                        }));
    }

    public List innerSearchCheckpointJobs(final String cubeName, final String jobName,
            final Set statusList, long timeStartInMillis, long timeEndInMillis,
            final Map allOutputs, final boolean nameExactMatch, final String projectName) {
        List results = Lists.newArrayList(
                FluentIterable.from(getExecutableManager().getAllExecutables(timeStartInMillis, timeEndInMillis))
                        .filter(new Predicate() {
                            @Override
                            public boolean apply(AbstractExecutable executable) {
                                if (executable instanceof CheckpointExecutable) {
                                    if (StringUtils.isEmpty(cubeName)) {
                                        return true;
                                    }
                                    String executableCubeName = CubingExecutableUtil
                                            .getCubeName(executable.getParams());
                                    if (executableCubeName == null)
                                        return true;
                                    if (nameExactMatch)
                                        return executableCubeName.equalsIgnoreCase(cubeName);
                                    else
                                        return executableCubeName.toLowerCase(Locale.ROOT)
                                                .contains(cubeName.toLowerCase(Locale.ROOT));
                                } else {
                                    return false;
                                }
                            }
                        }).transform(new Function() {
                            @Override
                            public CheckpointExecutable apply(AbstractExecutable executable) {
                                return (CheckpointExecutable) executable;
                            }
                        }).filter(Predicates.and(new Predicate() {
                            @Override
                            public boolean apply(CheckpointExecutable executable) {
                                if (null == projectName || null == getProjectManager().getProject(projectName)) {
                                    return true;
                                } else {
                                    return projectName.equalsIgnoreCase(executable.getProjectName());
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(CheckpointExecutable executable) {
                                try {
                                    Output output = allOutputs.get(executable.getId());
                                    if (output == null) {
                                        return false;
                                    }

                                    ExecutableState state = output.getState();
                                    boolean ret = statusList.contains(state);
                                    return ret;
                                } catch (Exception e) {
                                    throw e;
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(@Nullable CheckpointExecutable checkpointExecutable) {
                                if (checkpointExecutable == null) {
                                    return false;
                                }

                                if (Strings.isEmpty(jobName)) {
                                    return true;
                                }

                                if (nameExactMatch) {
                                    return checkpointExecutable.getName().equalsIgnoreCase(jobName);
                                } else {
                                    return checkpointExecutable.getName().toLowerCase(Locale.ROOT)
                                            .contains(jobName.toLowerCase(Locale.ROOT));
                                }
                            }
                        })));
        return results;
    }
    //****************************** Job search apis for Job controller V1 end *****************************************

    //******************************** Job search apis for Job controller V2 *******************************************
    public List searchJobsV2(final String cubeNameSubstring, final String projectName,
            final List statusList, final Integer limitValue, final Integer offsetValue,
            final JobTimeFilterEnum timeFilter, JobSearchMode jobSearchMode) {
        Integer limit = (null == limitValue) ? 30 : limitValue;
        Integer offset = (null == offsetValue) ? 0 : offsetValue;
        List jobSearchResultList = searchJobsByCubeNameV2(cubeNameSubstring, projectName, statusList,
                timeFilter, jobSearchMode);

        Collections.sort(jobSearchResultList);

        if (jobSearchResultList.size() <= offset) {
            return Collections.emptyList();
        }

        // Fetch instance data of jobs for the searched job results
        List subJobSearchResultList;
        if ((jobSearchResultList.size() - offset) < limit) {
            subJobSearchResultList = jobSearchResultList.subList(offset, jobSearchResultList.size());
        } else {
            subJobSearchResultList = jobSearchResultList.subList(offset, offset + limit);
        }

        List jobInstanceList = new ArrayList<>();
        for (JobSearchResult result : subJobSearchResultList) {
            JobInstance jobInstance = getJobInstance(result.getId());
            jobInstanceList.add(jobInstance);
        }

        return jobInstanceList;
    }

    /**
     * it loads all cache for digest metadata of "execute" and "execute_output", and returns the search results within the scope of the given filters
     *
     * @return List of search results searched by the method
     *
     */
    public List searchJobsByCubeNameV2(final String cubeNameSubstring, final String projectName,
            final List statusList, final JobTimeFilterEnum timeFilter,
            final JobSearchMode jobSearchMode) {
        List result = Lists.newArrayList();
        switch (jobSearchMode) {
        case ALL:
            result.addAll(innerSearchCubingJobsV2(cubeNameSubstring, null, projectName, statusList, timeFilter));
            result.addAll(innerSearchCheckpointJobsV2(cubeNameSubstring, null, projectName, statusList, timeFilter));
            break;
        case CHECKPOINT_ONLY:
            result.addAll(innerSearchCheckpointJobsV2(cubeNameSubstring, null, projectName, statusList, timeFilter));
            break;
        case CUBING_ONLY:
        default:
            result.addAll(innerSearchCubingJobsV2(cubeNameSubstring, null, projectName, statusList, timeFilter));
        }
        return result;
    }

    public List innerSearchCubingJobsV2(final String cubeName, final String jobName,
            final String projectName, final List statusList, final JobTimeFilterEnum timeFilter) {
        if (null == projectName) {
            aclEvaluate.checkIsGlobalAdmin();
        } else {
            aclEvaluate.checkProjectOperationPermission(projectName);
        }
        // prepare time range
        Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
        calendar.setTime(new Date());
        long timeStartInMillis = getTimeStartInMillis(calendar, timeFilter);
        long timeEndInMillis = Long.MAX_VALUE;
        Set states = convertStatusEnumToStates(statusList);
        final Map allOutputDigests = getExecutableManager()
                .getAllOutputDigests(timeStartInMillis, timeEndInMillis);
        return Lists
                .newArrayList(FluentIterable
                        .from(innerSearchCubingJobsV2(cubeName, jobName, states, timeStartInMillis, timeEndInMillis,
                                allOutputDigests, false, projectName))
                        .transform(new Function() {
                            @Override
                            public JobSearchResult apply(CubingJob cubingJob) {
                                return JobInfoConverter.parseToJobSearchResult(cubingJob, allOutputDigests);
                            }
                        }).filter(new Predicate() {
                            @Override
                            public boolean apply(@Nullable JobSearchResult input) {
                                return input != null;
                            }
                        }));
    }

    public List innerSearchCheckpointJobsV2(final String cubeName, final String jobName,
            final String projectName, final List statusList, final JobTimeFilterEnum timeFilter) {
        if (null == projectName) {
            aclEvaluate.checkIsGlobalAdmin();
        } else {
            aclEvaluate.checkProjectOperationPermission(projectName);
        }
        // prepare time range
        Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
        calendar.setTime(new Date());
        long timeStartInMillis = getTimeStartInMillis(calendar, timeFilter);
        long timeEndInMillis = Long.MAX_VALUE;
        Set states = convertStatusEnumToStates(statusList);
        final Map allOutputDigests = getExecutableManager()
                .getAllOutputDigests(timeStartInMillis, timeEndInMillis);
        return Lists.newArrayList(FluentIterable
                .from(innerSearchCheckpointJobsV2(cubeName, jobName, states, timeStartInMillis, timeEndInMillis,
                        allOutputDigests, false, projectName))
                .transform(new Function() {
                    @Override
                    public JobSearchResult apply(CheckpointExecutable checkpointExecutable) {
                        return JobInfoConverter.parseToJobSearchResult(checkpointExecutable, allOutputDigests);
                    }
                }).filter(new Predicate() {
                    @Override
                    public boolean apply(@Nullable JobSearchResult input) {
                        return input != null;
                    }
                }));
    }

    /**
     * Called by searchJobsByCubeNameV2, it loads all cache of digest metadata of "execute" and returns list of cubing job within the scope of the given filters
     *
     * @param allExecutableOutputPO map of executable output data with type ExecutableOutputPO
     *
     */
    public List innerSearchCubingJobsV2(final String cubeName, final String jobName,
            final Set statusList, long timeStartInMillis, long timeEndInMillis,
            final Map allExecutableOutputPO, final boolean nameExactMatch,
            final String projectName) {
        List results = Lists.newArrayList(
                FluentIterable.from(getExecutableManager().getAllExecutableDigests(timeStartInMillis, timeEndInMillis))
                        .filter(new Predicate() {
                            @Override
                            public boolean apply(AbstractExecutable executable) {
                                if (executable instanceof CubingJob) {
                                    if (StringUtils.isEmpty(cubeName)) {
                                        return true;
                                    }
                                    String executableCubeName = CubingExecutableUtil
                                            .getCubeName(executable.getParams());
                                    if (executableCubeName == null)
                                        return true;
                                    if (nameExactMatch)
                                        return executableCubeName.equalsIgnoreCase(cubeName);
                                    else
                                        return executableCubeName.toLowerCase(Locale.ROOT)
                                                .contains(cubeName.toLowerCase(Locale.ROOT));
                                } else {
                                    return false;
                                }
                            }
                        }).transform(new Function() {
                            @Override
                            public CubingJob apply(AbstractExecutable executable) {
                                return (CubingJob) executable;
                            }
                        }).filter(Predicates.and(new Predicate() {
                            @Override
                            public boolean apply(CubingJob executable) {
                                if (null == projectName || null == getProjectManager().getProject(projectName)) {
                                    return true;
                                } else {
                                    return projectName.equalsIgnoreCase(executable.getProjectName());
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(CubingJob executable) {
                                try {
                                    ExecutableOutputPO executableOutputPO = allExecutableOutputPO
                                            .get(executable.getId());
                                    ExecutableState state = ExecutableState.valueOf(executableOutputPO.getStatus());
                                    return statusList.contains(state);

                                } catch (Exception e) {
                                    throw e;
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(@Nullable CubingJob cubeJob) {
                                if (cubeJob == null) {
                                    return false;
                                }

                                if (Strings.isEmpty(jobName)) {
                                    return true;
                                }

                                if (nameExactMatch) {
                                    return cubeJob.getName().equalsIgnoreCase(jobName);
                                } else {
                                    return cubeJob.getName().toLowerCase(Locale.ROOT)
                                            .contains(jobName.toLowerCase(Locale.ROOT));
                                }
                            }
                        })));
        return results;
    }

    public List innerSearchCheckpointJobsV2(final String cubeName, final String jobName,
            final Set statusList, long timeStartInMillis, long timeEndInMillis,
            final Map allExecutableOutputPO, final boolean nameExactMatch,
            final String projectName) {
        List results = Lists.newArrayList(
                FluentIterable.from(getExecutableManager().getAllExecutableDigests(timeStartInMillis, timeEndInMillis))
                        .filter(new Predicate() {
                            @Override
                            public boolean apply(AbstractExecutable executable) {
                                if (executable instanceof CheckpointExecutable) {
                                    if (StringUtils.isEmpty(cubeName)) {
                                        return true;
                                    }
                                    String executableCubeName = CubingExecutableUtil
                                            .getCubeName(executable.getParams());
                                    if (executableCubeName == null)
                                        return true;
                                    if (nameExactMatch)
                                        return executableCubeName.equalsIgnoreCase(cubeName);
                                    else
                                        return executableCubeName.toLowerCase(Locale.ROOT)
                                                .contains(cubeName.toLowerCase(Locale.ROOT));
                                } else {
                                    return false;
                                }
                            }
                        }).transform(new Function() {
                            @Override
                            public CheckpointExecutable apply(AbstractExecutable executable) {
                                return (CheckpointExecutable) executable;
                            }
                        }).filter(Predicates.and(new Predicate() {
                            @Override
                            public boolean apply(CheckpointExecutable executable) {
                                if (null == projectName || null == getProjectManager().getProject(projectName)) {
                                    return true;
                                } else {
                                    return projectName.equalsIgnoreCase(executable.getProjectName());
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(CheckpointExecutable executable) {
                                try {
                                    ExecutableOutputPO executableOutputPO = allExecutableOutputPO
                                            .get(executable.getId());
                                    ExecutableState state = ExecutableState.valueOf(executableOutputPO.getStatus());
                                    return statusList.contains(state);

                                } catch (Exception e) {
                                    throw e;
                                }
                            }
                        }, new Predicate() {
                            @Override
                            public boolean apply(@Nullable CheckpointExecutable checkpointExecutable) {
                                if (checkpointExecutable == null) {
                                    return false;
                                }

                                if (Strings.isEmpty(jobName)) {
                                    return true;
                                }

                                if (nameExactMatch) {
                                    return checkpointExecutable.getName().equalsIgnoreCase(jobName);
                                } else {
                                    return checkpointExecutable.getName().toLowerCase(Locale.ROOT)
                                            .contains(jobName.toLowerCase(Locale.ROOT));
                                }
                            }
                        })));
        return results;
    }

    //****************************** Job search apis for Job controller V2 end *****************************************

    public List listJobsByRealizationName(final String realizationName, final String projectName,
            final Set statusList) {
        return innerSearchCubingJobs(realizationName, null, statusList, 0L, Long.MAX_VALUE,
                getExecutableManager().getAllOutputs(), true, projectName);
    }

    public List listJobsByRealizationName(final String realizationName, final String projectName) {
        return listJobsByRealizationName(realizationName, projectName, EnumSet.allOf(ExecutableState.class));
    }

    public enum JobSearchMode {
        CUBING_ONLY, CHECKPOINT_ONLY, ALL
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy