blob: 93ff6238f302065d7ad95ff63ede6fb97fb52a19 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.server;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeoutException;
/**
* Runs cleanup-related tasks during an idle period in the server.
*
* <p>A fresh instance must be constructed to manage each individual idle period. The idle period
* begins when {@link #idle} is called and ends when {@link #busy} is called.
*/
public final class IdleTaskManager {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private enum State {
INITIALIZED,
IDLE,
BUSY
}
private static final class IdleTaskWrapper implements Callable<IdleTask.Result> {
private final IdleTask task;
IdleTaskWrapper(IdleTask task) {
this.task = task;
}
@Override
public IdleTask.Result call() {
String name = task.displayName();
Stopwatch stopwatch = Stopwatch.createStarted();
try {
logger.atInfo().log("%s idle task started", name);
task.run();
logger.atInfo().log("%s idle task finished", name);
return new IdleTask.Result(name, IdleTask.Status.SUCCESS, stopwatch.elapsed());
} catch (IdleTaskException e) {
logger.atWarning().withCause(e.getCause()).log("%s idle task failed", name);
return new IdleTask.Result(name, IdleTask.Status.FAILURE, stopwatch.elapsed());
} catch (InterruptedException e) {
// There's no point in restoring the interrupt bit since this thread belongs to an executor
// service that is shutting down.
logger.atWarning().withCause(e).log("%s idle task interrupted", name);
return new IdleTask.Result(name, IdleTask.Status.INTERRUPTED, stopwatch.elapsed());
}
}
}
@GuardedBy("this")
private State state = State.INITIALIZED;
// Use a single-threaded ScheduledThreadPoolExecutor to ensure that tasks execute serially.
private final ScheduledThreadPoolExecutor executor =
new ScheduledThreadPoolExecutor(
1, new ThreadFactoryBuilder().setNameFormat("idle-server-tasks-%d").build());
private final ImmutableList<IdleTask> idleTasks;
private final ArrayList<Future<IdleTask.Result>> taskFutures;
/**
* Creates a new {@link IdleTaskManager}.
*
* @param idleTasks tasks to run while idle
*/
public IdleTaskManager(ImmutableList<IdleTask> idleTasks) {
this.idleTasks = idleTasks;
this.taskFutures = new ArrayList<>(idleTasks.size());
}
/**
* Called by the main thread when the server becomes idle.
*
* <p>Does not block, but may schedule tasks in the background.
*/
public synchronized void idle() {
checkState(state == State.INITIALIZED);
state = State.IDLE;
for (IdleTask task : idleTasks) {
taskFutures.add(
executor.schedule(new IdleTaskWrapper(task), task.delay().toSeconds(), SECONDS));
}
}
/**
* Called by the main thread when the server gets to work.
*
* <p>Interrupts any pending idle tasks and blocks for their completion before returning.
*
* @return stats for each idle task, in the same order they were registered
*/
public synchronized ImmutableList<IdleTask.Result> busy() {
checkState(state == State.IDLE);
state = State.BUSY;
// Interrupt pending tasks.
var unused = executor.shutdownNow();
// Wait for all tasks to complete, so they cannot interfere with a subsequent command.
Uninterruptibles.awaitTerminationUninterruptibly(executor);
ImmutableList.Builder<IdleTask.Result> results =
ImmutableList.builderWithExpectedSize(idleTasks.size());
for (int i = 0; i < idleTasks.size(); i++) {
IdleTask task = idleTasks.get(i);
String name = task.displayName();
Future<IdleTask.Result> future = taskFutures.get(i);
IdleTask.Result result;
try {
// Don't wait: task might not have had a chance to start.
result = Uninterruptibles.getUninterruptibly(future, Duration.ZERO);
} catch (ExecutionException e) {
// Must be an unchecked exception since all checked exceptions thrown by an IdleTask are
// handled by its IdleTaskWrapper.
throw new IllegalStateException("Unexpected exception thrown by idle task", e.getCause());
} catch (TimeoutException e) {
// Task was never started.
result = new IdleTask.Result(name, IdleTask.Status.NOT_STARTED, Duration.ZERO);
} catch (CancellationException e) {
// Task was interrupted.
result = new IdleTask.Result(name, IdleTask.Status.INTERRUPTED, Duration.ZERO);
}
results.add(result);
}
return results.build();
}
}