com.google.gerrit.server.git.MultiProgressMonitor.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.git.MultiProgressMonitor.java

Source

// Copyright (C) 2012 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.google.gerrit.server.git;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

import com.google.common.base.Strings;

import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Progress reporting interface that multiplexes multiple sub-tasks.
 * <p>
 * Output is of the format:
 * <pre>
 *   Task: subA: 1, subB: 75% (3/4) (-)\r
 *   Task: subA: 2, subB: 75% (3/4), subC: 1 (\)\r
 *   Task: subA: 2, subB: 100% (4/4), subC: 1 (|)\r
 *   Task: subA: 4, subB: 100% (4/4), subC: 4, done    \n
 * </pre>
 * <p>
 * Callers should try to keep task and sub-task descriptions short, since the
 * output should fit on one terminal line. (Note that git clients do not accept
 * terminal control characters, so true multi-line progress messages would be
 * impossible.)
 */
public class MultiProgressMonitor {
    private static final Logger log = LoggerFactory.getLogger(MultiProgressMonitor.class);

    /** Constant indicating the total work units cannot be predicted. */
    public static final int UNKNOWN = 0;

    private static final char[] SPINNER_STATES = new char[] { '-', '\\', '|', '/' };
    private static final char NO_SPINNER = ' ';

    /** Handle for a sub-task. */
    public class Task implements ProgressMonitor {
        private final String name;
        private final int total;
        private int count;
        private int lastPercent;

        Task(final String subTaskName, final int totalWork) {
            this.name = subTaskName;
            this.total = totalWork;
        }

        /**
         * Indicate that work has been completed on this sub-task.
         * <p>
         * Must be called from a worker thread.
         *
         * @param completed number of work units completed.
         */
        @Override
        public void update(final int completed) {
            boolean w = false;
            synchronized (this) {
                count += completed;
                if (total != UNKNOWN) {
                    int percent = count * 100 / total;
                    if (percent > lastPercent) {
                        lastPercent = percent;
                        w = true;
                    }
                }
            }
            if (w) {
                wakeUp();
            }
        }

        /**
         * Indicate that this sub-task is finished.
         * <p>
         * Must be called from a worker thread.
         */
        public void end() {
            if (total == UNKNOWN && getCount() > 0) {
                wakeUp();
            }
        }

        @Override
        public void start(int totalTasks) {
        }

        @Override
        public void beginTask(String title, int totalWork) {
        }

        @Override
        public void endTask() {
        }

        @Override
        public boolean isCancelled() {
            return false;
        }

        public synchronized int getCount() {
            return count;
        }
    }

    private final OutputStream out;
    private final String taskName;
    private final List<Task> tasks = new CopyOnWriteArrayList<>();
    private int spinnerIndex;
    private char spinnerState = NO_SPINNER;
    private boolean done;
    private boolean write = true;

    private final long maxIntervalNanos;

    /**
     * Create a new progress monitor for multiple sub-tasks.
     *
     * @param out stream for writing progress messages.
     * @param taskName name of the overall task.
     */
    public MultiProgressMonitor(final OutputStream out, final String taskName) {
        this(out, taskName, 500, TimeUnit.MILLISECONDS);
    }

    /**
     * Create a new progress monitor for multiple sub-tasks.
     *
     * @param out stream for writing progress messages.
     * @param taskName name of the overall task.
     * @param maxIntervalTime maximum interval between progress messages.
     * @param maxIntervalUnit time unit for progress interval.
     */
    public MultiProgressMonitor(final OutputStream out, final String taskName, long maxIntervalTime,
            TimeUnit maxIntervalUnit) {
        this.out = out;
        this.taskName = taskName;
        maxIntervalNanos = NANOSECONDS.convert(maxIntervalTime, maxIntervalUnit);
    }

    /**
     * Wait for a task managed by a {@link Future}, with no timeout.
     *
     * @see #waitFor(Future, long, TimeUnit)
     */
    public void waitFor(final Future<?> workerFuture) throws ExecutionException {
        waitFor(workerFuture, 0, null);
    }

    /**
     * Wait for a task managed by a {@link Future}.
     * <p>
     * Must be called from the main thread, <em>not</em> a worker thread. Once a
     * worker thread calls {@link #end()}, the future has an additional
     * {@code maxInterval} to finish before it is forcefully cancelled and
     * {@link ExecutionException} is thrown.
     *
     * @param workerFuture a future that returns when worker threads are finished.
     * @param timeoutTime overall timeout for the task; the future is forcefully
     *     cancelled if the task exceeds the timeout. Non-positive values indicate
     *     no timeout.
     * @param timeoutUnit unit for overall task timeout.
     * @throws ExecutionException if this thread or a worker thread was
     *     interrupted, the worker was cancelled, or timed out waiting for a
     *     worker to call {@link #end()}.
     */
    public void waitFor(final Future<?> workerFuture, final long timeoutTime, final TimeUnit timeoutUnit)
            throws ExecutionException {
        long overallStart = System.nanoTime();
        long deadline;
        String detailMessage = "";
        if (timeoutTime > 0) {
            deadline = overallStart + NANOSECONDS.convert(timeoutTime, timeoutUnit);
        } else {
            deadline = 0;
        }

        synchronized (this) {
            long left = maxIntervalNanos;
            while (!done) {
                long start = System.nanoTime();
                try {
                    NANOSECONDS.timedWait(this, left);
                } catch (InterruptedException e) {
                    throw new ExecutionException(e);
                }

                // Send an update on every wakeup (manual or spurious), but only move
                // the spinner every maxInterval.
                long now = System.nanoTime();

                if (deadline > 0 && now > deadline) {
                    workerFuture.cancel(true);
                    if (workerFuture.isCancelled()) {
                        detailMessage = String.format("(timeout %sms, cancelled)",
                                TimeUnit.MILLISECONDS.convert(now - deadline, NANOSECONDS));
                        log.warn(String.format("MultiProgressMonitor worker killed after %sms" + detailMessage, //
                                TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS)));
                    }
                    break;
                }

                left -= now - start;
                if (left <= 0) {
                    moveSpinner();
                    left = maxIntervalNanos;
                }
                sendUpdate();
                if (!done && workerFuture.isDone()) {
                    // The worker may not have called end() explicitly, which is likely a
                    // programming error.
                    log.warn("MultiProgressMonitor worker did not call end()" + " before returning");
                    end();
                }
            }
            sendDone();
        }

        // The loop exits as soon as the worker calls end(), but we give it another
        // maxInterval to finish up and return.
        try {
            workerFuture.get(maxIntervalNanos, NANOSECONDS);
        } catch (InterruptedException e) {
            throw new ExecutionException(e);
        } catch (CancellationException e) {
            throw new ExecutionException(detailMessage, e);
        } catch (TimeoutException e) {
            workerFuture.cancel(true);
            throw new ExecutionException(e);
        }
    }

    private synchronized void wakeUp() {
        notifyAll();
    }

    /**
     * Begin a sub-task.
     *
     * @param subTask sub-task name.
     * @param subTaskWork total work units in sub-task, or {@link #UNKNOWN}.
     * @return sub-task handle.
     */
    public Task beginSubTask(final String subTask, final int subTaskWork) {
        Task task = new Task(subTask, subTaskWork);
        tasks.add(task);
        return task;
    }

    /**
     * End the overall task.
     * <p>
     * Must be called from a worker thread.
     */
    public synchronized void end() {
        done = true;
        wakeUp();
    }

    private void sendDone() {
        spinnerState = NO_SPINNER;
        StringBuilder s = format();
        boolean any = false;
        for (Task t : tasks) {
            if (t.count != 0) {
                any = true;
                break;
            }
        }
        if (any) {
            s.append(",");
        }
        s.append(" done    \n");
        send(s);
    }

    private void moveSpinner() {
        spinnerIndex = (spinnerIndex + 1) % SPINNER_STATES.length;
        spinnerState = SPINNER_STATES[spinnerIndex];
    }

    private void sendUpdate() {
        send(format());
    }

    private StringBuilder format() {
        StringBuilder s = new StringBuilder().append("\r").append(taskName).append(':');

        if (!tasks.isEmpty()) {
            boolean first = true;
            for (Task t : tasks) {
                int count = t.count;
                if (count == 0) {
                    continue;
                }

                if (!first) {
                    s.append(',');
                } else {
                    first = false;
                }

                s.append(' ');
                if (!Strings.isNullOrEmpty(t.name)) {
                    s.append(t.name).append(": ");
                }
                if (t.total == UNKNOWN) {
                    s.append(count);
                } else {
                    s.append(String.format("%d%% (%d/%d)", count * 100 / t.total, count, t.total));
                }
            }
        }

        if (spinnerState != NO_SPINNER) {
            // Don't output a spinner until the alarm fires for the first time.
            s.append(" (").append(spinnerState).append(')');
        }
        return s;
    }

    private void send(StringBuilder s) {
        if (write) {
            try {
                out.write(Constants.encode(s.toString()));
                out.flush();
            } catch (IOException e) {
                write = false;
            }
        }
    }
}