blob: 390c76f22282ce04e4e335d437109886cb685eb7 [file] [log] [blame]
// 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();
}
}