diff options
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java')
-rw-r--r-- | gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java new file mode 100644 index 0000000000..dbb849c61d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java @@ -0,0 +1,325 @@ +// 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 org.eclipse.jgit.lib.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; + +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; +import java.util.List; + +/** + * 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 { + private final String name; + private final int total; + private volatile 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 the worker thread. + * + * @param completed number of work units completed. + */ + public void update(final int completed) { + count += completed; + if (total != UNKNOWN) { + int percent = count * 100 / total; + if (percent > lastPercent) { + lastPercent = percent; + wakeUp(); + } + } + } + + /** + * Indicate that this sub-task is finished. + * <p> + * Must be called from the worker thread. + */ + public void end() { + if (total == UNKNOWN && count > 0) { + wakeUp(); + } + } + } + + private final OutputStream out; + private final String taskName; + private final List<Task> tasks = new CopyOnWriteArrayList<Task>(); + 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> the worker thread. Once + * the worker thread calls {@link #end()}, the future has an additional + * <code>maxInterval</code> to finish before it is forcefully cancelled and + * {@link ExecutionException} is thrown. + * + * @param workerFuture a future that returns when the worker thread is + * 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 the worker thread was + * interrupted, the worker was cancelled, or the worker timed out. + */ + public void waitFor(final Future<?> workerFuture, final long timeoutTime, + final TimeUnit timeoutUnit) throws ExecutionException { + long overallStart = System.nanoTime(); + long deadline; + 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) { + log.warn(String.format( + "MultiProgressMonitor worker killed after %sms", + TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS))); + workerFuture.cancel(true); + 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(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 the 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(' ').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; + } + } + } +} |