All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.grpc.Context Maven / Gradle / Ivy

There is a newer version: 1.65.1
Show newest version
/*
 * Copyright 2015, Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 *    * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.grpc;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import io.grpc.internal.ExperimentalApi;
import io.grpc.internal.SharedResourceHolder;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

/**
 * A context propagation mechanism which carries deadlines, cancellation signals,
 * and other scoped values across API boundaries and between threads. Examples of functionality
 * propagated via context include:
 * 
    *
  • Deadlines for a local operation or remote call.
  • *
  • Security principals and credentials.
  • *
  • Local and distributed tracing context.
  • *
* *

Context objects make their state available by being attached to the executing thread using * a {@link ThreadLocal}. The context object bound to a thread is considered {@link #current()}. * Context objects are immutable and inherit state from their parent. To add or overwrite the * current state a new context object must be created and then attached to the thread replacing the * previously bound context. For example: *

 *   Context withCredential = Context.current().withValue(CRED_KEY, cred);
 *   executorService.execute(withCredential.wrap(new Runnable() {
 *     public void run() {
 *        readUserRecords(userId, CRED_KEY.get());
 *     }
 *   }));

 * 
* *

Context objects will cascade cancellation from their parent and propagate it to their * children. You can add a {@link CancellationListener} to a context to be notified when it or * one of its ancestors has been cancelled. Cancellation does not release the state stored by * a context and it's perfectly valid to {@link #attach()} an already cancelled context to a * thread to make it current. To cancel a context (and its descendants) you first create a * {@link CancellableContext} and when you need to signal cancellation call * {@link CancellableContext#cancel} or {@link CancellableContext#detachAndCancel}. For example: *

 *   CancellableContext withCancellation = Context.current().withCancellation();
 *   try {
   *   executorService.execute(withCancellation.wrap(new Runnable() {
 *       public void run() {
 *         while (waitingForData() && !Context.current().isCancelled()) {}
 *       }
 *     });
 *     doSomeWork();
 *   } catch (Throwable t) {
 *      withCancellation.cancel(t);
 *   }
 * 
* * *

Notes and cautions on use: *

    *
  • While Context objects are immutable they do not place such a restriction on the state * they store.
  • *
  • Context is not intended for passing optional parameters to an API and developers should * take care to avoid excessive dependence on context when designing an API.
  • *
  • If Context is being used in an environment that needs to support class unloading it is the * responsibility of the application to ensure that all contexts are properly cancelled.
  • *
*/ @ExperimentalApi public class Context { private static final Logger LOG = Logger.getLogger(Context.class.getName()); /** * Use a shared resource to retain the {@link ScheduledExecutorService} used to * implement deadline based context cancellation. This allows the executor to be * shutdown if its not in use thereby allowing Context to be unloaded. */ static final SharedResourceHolder.Resource SCHEDULER = new SharedResourceHolder.Resource() { private static final String name = "context-scheduler"; @Override public ScheduledExecutorService create() { return Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder() .setNameFormat(name + "-%d") .setDaemon(true) .build()); } @Override public void close(ScheduledExecutorService instance) { instance.shutdown(); } @Override public String toString() { return name; } }; /** * Stack of context objects which is used to record attach & detach history on a thread. */ private static final ThreadLocal> contextStack = new ThreadLocal>() { @Override protected ArrayDeque initialValue() { return new ArrayDeque(); } }; private static final Object[][] EMPTY_ENTRIES = new Object[0][2]; /** * The logical root context which is {@link #current()} if no other context is bound. This context * is not cancellable and so will not cascade cancellation or retain listeners. */ public static final Context ROOT = new Context(null); /** * Create a {@link Key} with the given name. */ public static Key key(String name) { return new Key(name); } /** * Create a {@link Key} with the given name and default value. */ public static Key keyWithDefault(String name, T defaultValue) { return new Key(name, defaultValue); } /** * Return the context associated with the current thread, will never return {@code null} as * the {@link #ROOT} context is implicitly associated with all threads. * *

Will never return {@link CancellableContext} even if one is attached, instead a * {@link Context} is returned with the same properties and lifetime. This is to avoid * code stealing the ability to cancel arbitrarily. */ public static Context current() { ArrayDeque stack = contextStack.get(); if (stack.isEmpty()) { return ROOT; } return stack.peekLast(); } private final Context parent; private final Object[][] keyValueEntries; private final boolean cascadesCancellation; private ArrayList listeners; private CancellationListener parentListener = new CancellationListener() { @Override public void cancelled(Context context) { if (Context.this instanceof CancellableContext) { // Record cancellation with its cause. ((CancellableContext) Context.this).cancel(context.cause()); } else { notifyAndClearListeners(); } } }; /** * Construct a context that cannot be cancelled and will not cascade cancellation from its parent. */ private Context(Context parent) { this.parent = parent; keyValueEntries = EMPTY_ENTRIES; cascadesCancellation = false; } /** * Construct a context that cannot be cancelled but will cascade cancellation from its parent if * it is cancellable. */ private Context(Context parent, Object[][] keyValueEntries) { this.parent = parent; this.keyValueEntries = keyValueEntries; cascadesCancellation = true; } /** * Create a new context which is independently cancellable and also cascades cancellation from * its parent. Callers should ensure that either {@link CancellableContext#cancel(Throwable)} * or {@link CancellableContext#detachAndCancel(Throwable)} are called to notify listeners and * release the resources associated with them. * *

Sample usage: *

   *   Context.CancellableContext withCancellation = Context.current().withCancellation();
   *   try {
   *     executorService.execute(withCancellation.wrap(new Runnable() {
   *       public void run() {
   *         Context current = Context.current();
   *         while (!current.isCancelled()) {
   *           keepWorking();
   *         }
   *       }
   *     });
   *     doSomethingRelatedWork();
   *   } catch (Throwable t) {
   *     withCancellation.cancel(t);
   *   }
   * 
*/ public CancellableContext withCancellation() { return new CancellableContext(this); } /** * Create a new context which will cancel itself after an absolute deadline expressed as * nanoseconds in the {@link System#nanoTime()} clock. The returned context will cascade * cancellation of its parent. Callers may explicitly cancel the returned context prior to * the deadline just as for {@link #withCancellation()}, * *

It is recommended that callers only use this method when propagating a derivative of * a received existing deadline. When establishing a new deadline, {@link #withDeadlineAfter} * is the better mechanism. */ public CancellableContext withDeadlineNanoTime(long deadlineNanoTime) { return withDeadlineAfter(deadlineNanoTime - System.nanoTime(), TimeUnit.NANOSECONDS); } /** * Create a new context which will cancel itself after the given {@code duration} from now. * The returned context will cascade cancellation of its parent. Callers may explicitly cancel * the returned context prior to the deadline just as for {@link #withCancellation()}, * *

Sample usage: *

   *   Context.CancellableContext withDeadline = Context.current().withDeadlineAfter(5,
   *       TimeUnit.SECONDS);
   *   executorService.execute(withDeadline.wrap(new Runnable() {
   *     public void run() {
   *       Context current = Context.current();
   *       while (!current.isCancelled()) {
   *         keepWorking();
   *       }
   *     }
   *   });
   * 
*/ public CancellableContext withDeadlineAfter(long duration, TimeUnit unit) { Preconditions.checkArgument(duration >= 0, "duration must be greater than or equal to 0"); Preconditions.checkNotNull(unit, "unit"); return new CancellableContext(this, unit.toNanos(duration)); } /** * Create a new context with the given key value set. The new context will cascade cancellation * from its parent. *
   *   Context withCredential = Context.current().withValue(CRED_KEY, cred);
   *   executorService.execute(withCredential.wrap(new Runnable() {
   *     public void run() {
   *        readUserRecords(userId, CRED_KEY.get());
   *     }
   *   }));
   * 
* */ public Context withValue(Key k1, V v1) { return new Context(this, new Object[][]{{k1, v1}}); } /** * Create a new context with the given key value set. The new context will cascade cancellation * from its parent. */ public Context withValues(Key k1, V1 v1, Key k2, V2 v2) { return new Context(this, new Object[][]{{k1, v1}, {k2, v2}}); } /** * Create a new context with the given key value set. The new context will cascade cancellation * from its parent. */ public Context withValues(Key k1, V1 v1, Key k2, V2 v2, Key k3, V3 v3) { return new Context(this, new Object[][]{{k1, v1}, {k2, v2}, {k3, v3}}); } /** * Create a new context which copies the values of this context but does not propagate its * cancellation and is its own independent root for cancellation. */ public CancellableContext fork() { return new Context(this).withCancellation(); } boolean canBeCancelled() { // A context is cancellable if it cascades from its parent and its parent is // cancellable. return (cascadesCancellation && this.parent != null && this.parent.canBeCancelled()); } /** * Attach this context to the thread and make it {@link #current}, the previously current context * will be restored when detach is called. It is allowed to attach contexts where * {@link #isCancelled()} is {@code true}. */ public void attach() { contextStack.get().addLast(this); } // Visible for testing boolean isCurrent() { return current() == this; } /** * Detach the current context from the thread and restore the context that was previously * attached to the thread as the 'current' context. * * @throws java.lang.IllegalStateException if this context is not {@link #current()}. */ public void detach() { ArrayDeque stack = contextStack.get(); if (stack.isEmpty()) { if (this == ROOT) { throw new IllegalStateException("Cannot detach root"); } else { throw new IllegalStateException("Cannot detach non-root context when root is current"); } } if (stack.peekLast() != this) { throw new IllegalStateException("Cannot detach a context that is not current"); } stack.removeLast(); } /** * Is this context cancelled. */ public boolean isCancelled() { if (parent == null || !cascadesCancellation) { return false; } else { return parent.isCancelled(); } } /** * If a context {@link #isCancelled()} then return the cause of the cancellation or * {@code null} if context was cancelled without a cause. If the context is not yet cancelled * will always return {@code null}. * *

The cause is provided for informational purposes only and implementations should generally * assume that it has already been handled and logged properly. */ @Nullable public Throwable cause() { if (parent == null || !cascadesCancellation) { return null; } else { return parent.cause(); } } /** * Add a listener that will be notified when the context becomes cancelled. */ public void addListener(final CancellationListener cancellationListener, final Executor executor) { Preconditions.checkNotNull(cancellationListener); Preconditions.checkNotNull(executor); if (canBeCancelled()) { ExecutableListener executableListener = new ExecutableListener(executor, cancellationListener); synchronized (this) { if (isCancelled()) { executableListener.deliver(); } else { if (listeners == null) { // Now that we have a listener we need to listen to our parent so // we can cascade listener notification. listeners = new ArrayList(); listeners.add(executableListener); parent.addListener(parentListener, MoreExecutors.directExecutor()); } else { listeners.add(executableListener); } } } } else { // Discussion point: Should we throw or suppress. } } /** * Remove a {@link CancellationListener}. */ public void removeListener(CancellationListener cancellationListener) { synchronized (this) { if (listeners != null) { for (int i = listeners.size() - 1; i >= 0; i--) { if (listeners.get(i).listener == cancellationListener) { listeners.remove(i); // Just remove the first matching listener, given that we allow duplicate adds we should // allow for duplicates after remove. break; } } // We have no listeners so no need to listen to our parent if (listeners.isEmpty()) { parent.removeListener(parentListener); listeners = null; } } } } /** * Notify all listeners that this context has been cancelled and immediately release * any reference to them so that they may be garbage collected. */ void notifyAndClearListeners() { ArrayList tmpListeners; synchronized (this) { if (listeners == null) { return; } tmpListeners = listeners; listeners = null; } for (int i = 0; i < tmpListeners.size(); i++) { try { tmpListeners.get(i).deliver(); } catch (Throwable t) { LOG.log(Level.INFO, "Exception notifying context listener", t); } } parent.removeListener(parentListener); } // Used in tests to ensure that listeners are defined and released based on // cancellation propagation. It's very important to ensure that we do not // accidentally retain listeners. int listenerCount() { synchronized (this) { return listeners == null ? 0 : listeners.size(); } } /** * Wrap a {@link Runnable} so that it executes with this context as the {@link #current} context. */ public Runnable wrap(final Runnable r) { return new Runnable() { @Override public void run() { attach(); try { r.run(); } finally { detach(); } } }; } /** * Wrap a {@link Callable} so that it executes with this context as the {@link #current} context. */ public Callable wrap(final Callable c) { return new Callable() { @Override public C call() throws Exception { attach(); try { return c.call(); } finally { detach(); } } }; } /** * Lookup the value for a key in the context inheritance chain. */ private Object lookup(Key key) { for (int i = 0; i < keyValueEntries.length; i++) { if (key.equals(keyValueEntries[i][0])) { return keyValueEntries[i][1]; } } if (parent == null) { return null; } return parent.lookup(key); } /** * A context which inherits cancellation from its parent but which can also be independently * cancelled and which will propagate cancellation to its descendants. */ public static final class CancellableContext extends Context { private boolean cancelled; private Throwable cause; private final Context dummy; private ScheduledFuture scheduledFuture; /** * Create a cancellable context that does not have a deadline. */ private CancellableContext(Context parent) { super(parent, EMPTY_ENTRIES); // Create a dummy that inherits from this to attach and detach so that you cannot retrieve a // cancellable context from Context.current() dummy = new Context(this, EMPTY_ENTRIES); } /** * Create a cancellable context that has a deadline. */ private CancellableContext(Context parent, long delayNanos) { this(parent); final ScheduledExecutorService scheduler = SharedResourceHolder.get(SCHEDULER); scheduler.schedule(new Runnable() { @Override public void run() { try { cancel(new TimeoutException("context timed out")); } finally { SharedResourceHolder.release(SCHEDULER, scheduler); } } }, delayNanos, TimeUnit.NANOSECONDS); } @Override public void attach() { dummy.attach(); } @Override public void detach() { dummy.detach(); } @Override public boolean isCurrent() { return dummy.isCurrent(); } /** * Attach this context to the thread and return a {@link AutoCloseable} that can be * used with try-with-resource statements to properly {@link #detach} and {@link #cancel} * the context on completion. * * @return a {@link java.io.Closeable} which can be used with try-with-resource blocks. */ public Closeable attachAsCloseable() { attach(); return new Closeable() { @Override public void close() throws IOException { detachAndCancel(null); } }; } /** * Cancel this context and optionally provide a cause for the cancellation. This * will trigger notification of listeners. * * @return {@code true} if this context cancelled the context and notified listeners, * {@code false} if the context was already cancelled. */ public boolean cancel(@Nullable Throwable cause) { boolean triggeredCancel = false; synchronized (this) { if (!cancelled) { cancelled = true; if (scheduledFuture != null) { // If we have a scheduled cancellation pending attempt to cancel it. scheduledFuture.cancel(false); scheduledFuture = null; } this.cause = cause; triggeredCancel = true; } } if (triggeredCancel) { notifyAndClearListeners(); } return triggeredCancel; } /** * Cancel this context and detach it from the current context from the thread and restore the * context that was previously attached to the thread as the 'current' context. * * @throws java.lang.IllegalStateException if this context is not {@link #current()}. */ public void detachAndCancel(@Nullable Throwable cause) { try { detach(); } finally { cancel(cause); } } @Override protected boolean canBeCancelled() { return true; } @Override public boolean isCancelled() { synchronized (this) { if (cancelled) { return true; } } // Detect cancellation of parent in the case where we have no listeners and // record it. if (super.isCancelled()) { cancel(super.cause()); return true; } return false; } @Nullable @Override public Throwable cause() { if (isCancelled()) { return cause; } return null; } } /** * A listener notified on context cancellation. */ public interface CancellationListener { /** * @param context the newly cancelled context. */ public void cancelled(Context context); } /** * Key for indexing values stored in a context. */ public static class Key { private final String name; private final T defaultValue; Key(String name) { this(name, null); } Key(String name, T defaultValue) { this.name = Preconditions.checkNotNull(name); this.defaultValue = defaultValue; } /** * Get the value from the {@link #current()} context for this key. */ @SuppressWarnings("unchecked") public T get() { return get(Context.current()); } /** * Get the value from the specified context for this key. */ @SuppressWarnings("unchecked") public T get(Context context) { T value = (T) context.lookup(this); return value == null ? defaultValue : value; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Key key = (Key) o; return key.name.equals(this.name); } @Override public int hashCode() { return name.hashCode(); } } /** * Stores listener & executor pair. */ private class ExecutableListener implements Runnable { private final Executor executor; private final CancellationListener listener; private ExecutableListener(Executor executor, CancellationListener listener) { this.executor = executor; this.listener = listener; } private void deliver() { executor.execute(this); } @Override public void run() { listener.cancelled(Context.this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy