| // 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(); |
| } |
| } |