CommandScheduler.java :  » android-core » platform-tools-tradefederation » com » android » tradefed » command » Android Open Source

Android Open Source » android core » platform tools tradefederation 
platform tools tradefederation » com » android » tradefed » command » CommandScheduler.java
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tradefed.command;

import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationFactory;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceManager;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceSelectionMatcher;
import com.android.tradefed.device.DeviceSelectionOptions;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
import com.android.tradefed.invoker.ITestInvocation;
import com.android.tradefed.invoker.TestInvocation;
import com.android.tradefed.util.ConditionPriorityBlockingQueue;
import com.android.tradefed.util.ConditionPriorityBlockingQueue.IMatcher;

import java.lang.UnsupportedOperationException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

/**
 * A scheduler for running TradeFederation configs across all available devices.
 * <p/>
 * Will attempt to prioritize configurations to run based on a total running count of their
 * execution time. e.g. infrequent or fast running configs will get prioritized over long running
 * configs.
 * <p/>
 * Runs forever in background until shutdown.
 */
public class CommandScheduler extends Thread implements ICommandScheduler {

    private static final String LOG_TAG = "ConfigScheduler";
    private ConditionPriorityBlockingQueue<ConfigCommand> mConfigQueue;
    /**  list of active invocation threads */
    private Set<InvocationThread> mInvocationThreads;

    /** timer for scheduling the configurations so invocations honor the mMinLoopTime constraint */
    private Timer mConfigTimer;
    private boolean mShutdown = false;

    /**
     * Container for common options for each config.
     */
    static class CommandOptions {
        @Option(name="help", description="display the help text")
        private boolean mHelpMode = false;

        @Option(name="min-loop-time", description=
            "the minimum invocation time in ms when in loop mode. Default is 1 minute.")
        private long mMinLoopTime = 60 * 1000;

        @Option(name="loop", description="keep running continuously")
        private boolean mLoopMode = true;

        /**
         * Set the help mode for the config.
         * <p/>
         * Exposed for testing.
         */
        void setHelpMode(boolean helpMode) {
            mHelpMode = helpMode;
        }

        /**
         * Gets the help mode.
         */
        public boolean isHelpMode() {
            return mHelpMode;
        }

        /**
         * Set the loop mode for the config.
         * <p/>
         * Exposed for testing.
         */
        void setLoopMode(boolean loopMode) {
            mLoopMode = loopMode;
        }

        /**
         * Return the loop mode for the config.
         */
        boolean isLoopMode() {
            return mLoopMode;
        }

        /**
         * Set the min loop time for the config.
         * <p/>
         * Exposed for testing.
         */
        void setMinLoopTime(long loopTime) {
            mMinLoopTime = loopTime;
        }

        /**
         * Get the min loop time for the config.
         */
        public long getMinLoopTime() {
            return mMinLoopTime;
        }
    }

    /**
     * Represents one config to be executed
     */
    private static class ConfigCommand {
        private final String[] mArgs;

        private final CommandOptions mCmdOptions;
        private final DeviceSelectionOptions mDeviceOptions;

        /** the total amount of time this config was executing. Used to prioritize */
        private long mTotalExecTime = 0;

        ConfigCommand(String[] args, CommandOptions cmdOptions,
                DeviceSelectionOptions deviceOptions) {
            mArgs = args;
            mCmdOptions = cmdOptions;
            mDeviceOptions = deviceOptions;
        }

        synchronized void incrementExecTime(long execTime) {
            mTotalExecTime += execTime;
        }

        /**
         * Get the {@link CommandOptions} associated with this command.
         */
        CommandOptions getCommandOptions() {
            return mCmdOptions;
        }

        /**
         * Get the {@link DeviceSelectionOptions} associated with this command.
         */
        DeviceSelectionOptions getDeviceOptions() {
            return mDeviceOptions;
        }

        /**
         * Get the full list of config arguments associated with this command.
         */
        String[] getArgs() {
            return mArgs;
        }
    }

    /**
     * Comparator for ConfigCommmand.
     * <p/>
     * Compares by mTotalExecTime, prioritizing configs with lower execution time
     */
    private static class ConfigComparator implements Comparator<ConfigCommand> {

        /**
         * {@inheritDoc}
         */
        public int compare(ConfigCommand c1, ConfigCommand c2) {
            if (c1.mTotalExecTime == c2.mTotalExecTime) {
                return 0;
            } else if (c1.mTotalExecTime < c2.mTotalExecTime) {
                return -1;
            } else {
                return 1;
            }
        }
    }

    /**
     * Class that matches a device against a {@link ConfigCommand}
     */
    private static class DeviceCmdMatcher implements IMatcher<ConfigCommand> {
        private final ITestDevice mDevice;

        DeviceCmdMatcher(ITestDevice device) {
            mDevice = device;
        }

        /**
         * {@inheritDoc}
         */
        public boolean matches(ConfigCommand cmd) {
            DeviceSelectionOptions deviceOptions = cmd.getDeviceOptions();
            return DeviceSelectionMatcher.matches(mDevice.getIDevice(), deviceOptions);
        }
    }

    private class InvocationThread extends Thread {
        private IDeviceManager mManager;
        private ITestDevice mDevice;
        private ITestInvocation mInvocation = null;
        private boolean mIsStarted = false;

        public InvocationThread(String name, IDeviceManager manager, ITestDevice device) {
            // create a thread group so LoggerRegistry can identify this as an invocationThread
            super (new ThreadGroup(name), name);
            mManager = manager;
            mDevice = device;
        }

        private synchronized ITestInvocation createInvocation() {
            mInvocation  = createRunInstance();
            return mInvocation;
        }

        @Override
        public void run() {
            mIsStarted = true;
            FreeDeviceState deviceState = FreeDeviceState.AVAILABLE;
            ConfigCommand cmd = dequeueConfigCommand(mDevice);
            if (cmd == null) {
                Log.d(LOG_TAG, String.format("No configs to test for device %s.",
                        mDevice.getSerialNumber()));
                mManager.freeDevice(mDevice, deviceState);
                removeInvocationThread(this);
                return;
            }
            long startTime = System.currentTimeMillis();
            ITestInvocation instance = createInvocation();
            try {
                IConfiguration config = getConfigFactory().createConfigurationFromArgs(
                        cmd.getArgs(), new CommandOptions(), new DeviceSelectionOptions());
                instance.invoke(mDevice, config);
            } catch (DeviceUnresponsiveException e) {
                Log.w(LOG_TAG, String.format("Device %s is unresponsive",
                        mDevice.getSerialNumber()));
                deviceState = FreeDeviceState.UNRESPONSIVE;
            } catch (DeviceNotAvailableException e) {
                Log.w(LOG_TAG, String.format("Device %s is not available",
                        mDevice.getSerialNumber()));
                deviceState = FreeDeviceState.UNAVAILABLE;
            } catch (ConfigurationException e) {
                Log.e(LOG_TAG, e);
            } catch (FatalHostError e) {
                Log.logAndDisplay(LogLevel.ERROR, LOG_TAG, String.format(
                        "Fatal error occurred: %s, shutting down", e.getMessage()));
                if (e.getCause() != null) {
                    Log.e(LOG_TAG, e.getCause());
                }
                shutdown();
            } catch (Throwable e) {
                Log.e(LOG_TAG, e);
            } finally {
                long elapsedTime = System.currentTimeMillis() - startTime;
                Log.i(LOG_TAG, String.format("Updating config '%s' with elapsed time %d ms",
                        getArgString(cmd.getArgs()), elapsedTime));
                cmd.incrementExecTime(elapsedTime);
                mManager.freeDevice(mDevice, deviceState);
                removeInvocationThread(this);
            }
        }

        private synchronized ITestInvocation getInvocation() {
            return mInvocation;
        }

        /**
         * Attempt to gracefully shut down this invocation.
         * <p/>
         * There's three possible cases to handle:
         * <ol>
         * <li>Thread has not started yet: We want to wait for it to start, so it can free its
         * allocated device
         * <li>Thread has started but invocation has not been started (ie thread is blocked waiting
         * for a config): Interrupt the thread in this case
         * <li>Thread is running the invocation: Do nothing - wait for it to complete normally
         * </ol>
         */
        public void shutdownInvocation() {
            if (getInvocation() == null && mIsStarted) {
                interrupt();
            }
        }
    }

    /**
     * Creates a {@link CommandScheduler}.
     */
    CommandScheduler() {
        mConfigQueue = new ConditionPriorityBlockingQueue<ConfigCommand>(new ConfigComparator());
        mInvocationThreads = new HashSet<InvocationThread>();
    }

    /**
     * Factory method for creating a {@link TestInvocation}.
     *
     * @return the {@link ITestInvocation} to use
     */
    ITestInvocation createRunInstance() {
        return new TestInvocation();
    }

    /**
     * Factory method for getting a reference to the {@link IDeviceManager}
     *
     * @return the {@link IDeviceManager} to use
     */
    IDeviceManager getDeviceManager() {
        return DeviceManager.getInstance();
    }

    /**
     * Factory method for getting a reference to the {@link IConfigurationFactory}
     *
     * @return the {@link IConfigurationFactory} to use
     */
    IConfigurationFactory getConfigFactory() {
        return ConfigurationFactory.getInstance();
    }

    /**
     * The main execution block of this thread.
     */
    @Override
    public void run() {
        mConfigTimer = new Timer("config timer");
        IDeviceManager manager = getDeviceManager();
        while (!isShutdown()) {
            Log.d(LOG_TAG, "Waiting for device to test");
            // Spawn off a thread for each allocated device.
            // The retrieval of a config to run on the device is done on this separate thread, to
            // prevent configs which only run on a specific device from blocking the rest
            final ITestDevice device = manager.allocateDevice();
            if (device != null) {
                InvocationThread invThread = startInvocation(manager, device);
                addInvocationThread(invThread);
            }
        }
        Log.i(LOG_TAG, "Waiting for invocation threads to complete");
        List<InvocationThread> threadListCopy;
        synchronized (this) {
            threadListCopy = new ArrayList<InvocationThread>(
                    mInvocationThreads.size());
            threadListCopy.addAll(mInvocationThreads);
        }
        for (Thread thread : threadListCopy) {
            waitForThread(thread);
        }
        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "All done");
        exit(manager);
    }

    private void waitForThread(Thread thread) {
        try {
            thread.join();
        } catch (InterruptedException e) {
            // ignore
            waitForThread(thread);
        }
    }

    private void exit(IDeviceManager manager) {
        if (manager != null) {
            manager.terminate();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addConfig(String[] args) {
        CommandOptions cmdOptions = createCommandOptions();
        DeviceSelectionOptions deviceOptions = createDeviceOptions();
        try {
            // load a config to parse options and validate arguments up front
            getConfigFactory().createConfigurationFromArgs(args, cmdOptions, deviceOptions);
            if (cmdOptions.isHelpMode()) {
                getConfigFactory().printHelp(args, System.out, CommandOptions.class,
                        DeviceSelectionOptions.class);
            } else {
                ConfigCommand cmd = new ConfigCommand(args, cmdOptions, deviceOptions);
                mConfigQueue.add(cmd);
            }
        } catch (ConfigurationException e) {
            System.out.println(String.format("Unrecognized arguments: %s", e.getMessage()));
            getConfigFactory().printHelp(args, System.out, CommandOptions.class,
                    DeviceSelectionOptions.class);
        }
    }

    /**
     * Factory method for creating {@link CommandOptions}.
     * <p/>
     * Exposed for testing.
     */
    CommandOptions createCommandOptions() {
        return new CommandOptions();
    }

    /**
     * Factory method for creating {@link DeviceSelectionOptions}.
     * <p/>
     * Exposed for testing.
     */
    DeviceSelectionOptions createDeviceOptions() {
        return new DeviceSelectionOptions();
    }

    /**
     * Dequeue the highest priority config from the queue that can run against the provided device.
     *
     * @param device the {@link ITestDevice} to run against
     * @return the {@link ConfigCommand} or <code>null</code>
     */
    private ConfigCommand dequeueConfigCommand(ITestDevice device) {
        if (isShutdown()) {
            return null;
        }
        ConfigCommand cmd = null;
        try {
            cmd = mConfigQueue.take(new DeviceCmdMatcher(device));
            if (cmd.getCommandOptions().isLoopMode()) {
                returnConfigToQueue(cmd);
            }
        } catch (InterruptedException e) {
            Log.i(LOG_TAG, "Waiting for config command interrupted");
        }
        return cmd;
    }

    /**
     * Return config to queue, with delay if necessary
     *
     * @param cmd the {@link ConfigCommand} to return to queue
     */
    private void returnConfigToQueue(final ConfigCommand cmd) {
        final long minLoopTime = cmd.getCommandOptions().getMinLoopTime();
        if (minLoopTime > 0) {
            // delay before adding config back to queue
            TimerTask delayConfig = new TimerTask() {
                @Override
                public void run() {
                    Log.d(LOG_TAG, String.format("Adding config '%s' back to queue",
                            getArgString(cmd.getArgs())));
                    mConfigQueue.add(cmd);
                }
            };
            Log.d(LOG_TAG, String.format("Delay adding config '%s' back to queue for %d ms",
                    getArgString(cmd.getArgs()), minLoopTime));
            mConfigTimer.schedule(delayConfig, minLoopTime);
        } else {
            // return to queue immediately
            mConfigQueue.add(cmd);
        }
    }

    /**
     * Helper method to return an array of {@link String} elements as a readable {@link String}
     *
     * @param args the {@link String}[] to use
     * @return a display friendly {@link String} of args contents
     */
    private String getArgString(String[] args) {
        StringBuilder builder = new StringBuilder();
        for (String arg : args) {
            builder.append(arg);
            builder.append(" ");
        }
        return builder.toString();
    }

    /**
     * Spawns off thread to run invocation for given configuration
     *
     * @param manager the {@link IDeviceManager} to return device to when complete
     * @param device the {@link ITestDevice}
     * @param config the {@link IConfiguration} to run
     * @return the invocation's thread
     */
    private InvocationThread startInvocation(IDeviceManager manager, ITestDevice device) {
        final String invocationName = String.format("Invocation-%s", device.getSerialNumber());
        InvocationThread invocationThread = new InvocationThread(invocationName, manager, device);
        invocationThread.start();
        return invocationThread;
    }

    /**
     * Removes a {@link InvocationThread} from the active list.
     */
    private synchronized void removeInvocationThread(InvocationThread invThread) {
        mInvocationThreads.remove(invThread);
    }

    /**
     * Adds a {@link InvocationThread} to the active list.
     */
    private synchronized void addInvocationThread(InvocationThread invThread) {
        mInvocationThreads.add(invThread);
    }

    private synchronized boolean isShutdown() {
        return mShutdown;
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void shutdown() {
        if (!mShutdown) {
            mShutdown = true;
            mConfigQueue.clear();
            if (mConfigTimer != null) {
                mConfigTimer.cancel();
            }
            // interrupt current thread in case its blocked on allocateDevice call
            interrupt();

            for (InvocationThread invThread : mInvocationThreads) {
                invThread.shutdownInvocation();
            }
        }
    }

    // Implementations of the optional managment interfaces
    /**
     * {@inheritDoc}
     */
    public Collection<ITestInvocation> listInvocations() throws UnsupportedOperationException {
        Collection<ITestInvocation> invs = new ArrayList<ITestInvocation>(mInvocationThreads.size());

        if (mInvocationThreads == null) {
            return null;
        }

        for (InvocationThread invThread : mInvocationThreads) {
            invs.add(invThread.getInvocation());
        }

        return invs;
    }

    /**
     * {@inheritDoc}
     */
    public boolean stopInvocation(ITestInvocation invocation) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public Collection<String> listConfigs() throws UnsupportedOperationException {
        Iterator<ConfigCommand> configIter = mConfigQueue.iterator();
        Collection<String> stringConfigs = new ArrayList<String>();
        ConfigCommand config;

        while (configIter.hasNext()) {
            config = configIter.next();
            stringConfigs.add(getArgString(config.getArgs()));
        }

        return stringConfigs;
    }

}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.