| // Copyright (C) 2021 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.cancellation; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableSet; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * Context that allows to register {@link RequestStateProvider}s. |
| * |
| * <p>The registered {@link RequestStateProvider}s are stored in {@link ThreadLocal} so that they |
| * can be accessed during the request execution (via {@link #getRequestStateProviders()}. |
| * |
| * <p>On {@link #close()} the {@link RequestStateProvider}s that have been registered by this {@code |
| * RequestStateContext} instance are removed from {@link ThreadLocal}. |
| * |
| * <p>Nesting {@code RequestStateContext}s is possible. |
| * |
| * <p>Currently there is no logic to automatically copy the {@link RequestStateContext} to |
| * background threads, but implementing this may be considered in the future. This means that by |
| * default we only support cancellation of the main thread, but not of background threads. That's |
| * fine as all significant work is being done in the main thread. |
| * |
| * <p>{@link com.google.gerrit.server.util.RequestContext} is also a context that is available for |
| * the time of the request, but it is not suitable to manage registrations of {@link |
| * RequestStateProvider}s. Hence {@link RequestStateProvider} registrations are managed by a |
| * separate context, which is this class, {@link RequestStateContext}: |
| * |
| * <ul> |
| * <li>{@link com.google.gerrit.server.util.RequestContext} is an interface that has many |
| * implementations and hence cannot manage a {@link ThreadLocal} state. |
| * <li>{@link com.google.gerrit.server.util.RequestContext} is not an {@link AutoCloseable} and |
| * hence cannot cleanup any {@link ThreadLocal} state on close (turning it into an {@link |
| * AutoCloseable} would require a large refactoring). |
| * <li>Despite the name {@link com.google.gerrit.server.util.RequestContext} is not only used for |
| * requests scopes but also for other scopes that are not a request (e.g. plugin invocations, |
| * email sending, manual scopes). |
| * <li>{@link com.google.gerrit.server.util.RequestContext} is not copied to background and should |
| * not be, but for {@link RequestStateContext} we may consider doing this in the future. |
| * </ul> |
| */ |
| public class RequestStateContext implements AutoCloseable { |
| /** The {@link RequestStateProvider}s that have been registered for the thread. */ |
| private static final ThreadLocal<Set<RequestStateProvider>> threadLocalRequestStateProviders = |
| new ThreadLocal<>(); |
| |
| /** Whether currently a non-cancellable operation is being performed. */ |
| private static final ThreadLocal<Boolean> inNonCancellableOperation = new ThreadLocal<>(); |
| |
| /** |
| * Aborts the current request by throwing a {@link RequestCancelledException} if any of the |
| * registered {@link RequestStateProvider}s reports the request as cancelled. |
| * |
| * <p>If an atomic operation is currently being performed, request cancellations are ignored and |
| * the request doesn't get aborted. |
| * |
| * @throws RequestCancelledException thrown if the current request is cancelled and should be |
| * aborted |
| * @see #startNonCancellableOperation() |
| */ |
| public static void abortIfCancelled() throws RequestCancelledException { |
| if (inNonCancellableOperation.get() != null && inNonCancellableOperation.get()) { |
| // Do not cancel the request while an atomic operation is being performed. |
| return; |
| } |
| |
| getRequestStateProviders() |
| .forEach( |
| requestStateProvider -> |
| requestStateProvider.checkIfCancelled( |
| (reason, message) -> { |
| throw new RequestCancelledException(reason, message); |
| })); |
| } |
| |
| /** |
| * Starts a non-cancellable operation. |
| * |
| * <p>If the request was cancelled while the non-cancellable operation was running, it gets |
| * aborted on close of the returned {@link AutoCloseable}. |
| * |
| * @return {@link AutoCloseable} that finishes the non-cancellable operation on close. |
| */ |
| public static NonCancellableOperationContext startNonCancellableOperation() { |
| if (inNonCancellableOperation.get() != null && inNonCancellableOperation.get()) { |
| // atomic operation is already in progress |
| return () -> {}; |
| } |
| |
| inNonCancellableOperation.set(true); |
| return () -> { |
| inNonCancellableOperation.remove(); |
| abortIfCancelled(); |
| }; |
| } |
| |
| /** Returns the {@link RequestStateProvider}s that have been registered for the thread. */ |
| @VisibleForTesting |
| static ImmutableSet<RequestStateProvider> getRequestStateProviders() { |
| if (threadLocalRequestStateProviders.get() == null) { |
| return ImmutableSet.of(); |
| } |
| return ImmutableSet.copyOf(threadLocalRequestStateProviders.get()); |
| } |
| |
| /** Opens a {@code RequestStateContext}. */ |
| public static RequestStateContext open() { |
| return new RequestStateContext(); |
| } |
| |
| /** |
| * The {@link RequestStateProvider}s that have been registered by this {@code |
| * RequestStateContext}. |
| */ |
| private Set<RequestStateProvider> requestStateProviders = new HashSet<>(); |
| |
| private RequestStateContext() {} |
| |
| /** |
| * Registers a {@link RequestStateProvider}. |
| * |
| * @param requestStateProvider the {@link RequestStateProvider} that should be registered |
| * @return the {@code RequestStateContext} instance for chaining calls |
| */ |
| public RequestStateContext addRequestStateProvider(RequestStateProvider requestStateProvider) { |
| if (threadLocalRequestStateProviders.get() == null) { |
| threadLocalRequestStateProviders.set(new HashSet<>()); |
| } |
| if (threadLocalRequestStateProviders.get().add(requestStateProvider)) { |
| requestStateProviders.add(requestStateProvider); |
| } |
| return this; |
| } |
| |
| /** |
| * Closes this {@code RequestStateContext}. |
| * |
| * <p>Ensures that all {@link RequestStateProvider}s that have been registered by this {@code |
| * RequestStateContext} instance are removed from {@link #threadLocalRequestStateProviders}. |
| * |
| * <p>If no {@link RequestStateProvider}s remain in {@link #threadLocalRequestStateProviders}, |
| * {@link #threadLocalRequestStateProviders} is unset. |
| */ |
| @Override |
| public void close() { |
| if (threadLocalRequestStateProviders.get() != null) { |
| requestStateProviders.forEach( |
| requestStateProvider -> |
| threadLocalRequestStateProviders.get().remove(requestStateProvider)); |
| if (threadLocalRequestStateProviders.get().isEmpty()) { |
| threadLocalRequestStateProviders.remove(); |
| } |
| } |
| } |
| |
| /** |
| * Context for running a non-cancellable operation. |
| * |
| * <p>While open, the current request cannot be cancelled. |
| */ |
| public interface NonCancellableOperationContext extends AutoCloseable { |
| @Override |
| void close(); |
| } |
| } |