/* * Copyright 2011-2017 Amazon.com, Inc. or its affiliates. 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. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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.amazonaws.auth; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.annotation.NotThreadSafe; import com.amazonaws.annotation.SdkInternalApi; import com.amazonaws.annotation.ThreadSafe; import com.amazonaws.internal.SdkPredicate; import com.amazonaws.util.ValidationUtils; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Handles refreshing a value with a simple synchronization policy. Does a blocking, synchronous * refresh if needed, otherwise queues an asynchronous refresh and returns the current value. */ @ThreadSafe @SdkInternalApi class RefreshableTask<T> { /** * Maximum time to wait for a blocking refresh lock before calling refresh again. This is to * rate limit how many times we call refresh. In the ideal case, refresh always occurs in a * timely fashion and only one thread actually does the refresh. */ private static final long BLOCKING_REFRESH_MAX_WAIT_IN_SECONDS = 5; /** * Used to synchronize a blocking refresh. Used when a caller can't return without getting the * refreshed value. */ private final Lock blockingRefreshLock = new ReentrantLock(); /** * Atomic holder for refreshable value. */ private final AtomicReference<T> refreshableValueHolder = new AtomicReference<T>(); /** * Single threaded executor to asynchronous refresh the value. */ private final ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { Thread thread = Executors.defaultThreadFactory().newThread(runnable); thread.setDaemon(true); return thread; } }); /** * Used to ensure only one thread at any given time refreshes the value. */ private final AtomicBoolean asyncRefreshing = new AtomicBoolean(false); /** * Callback to get a new refreshed value. */ private final Callable<T> refreshCallable; /** * Predicate to determine whether a blocking refresh should be performed */ private final SdkPredicate<T> shouldDoBlockingRefresh; /** * Predicate to determine whether a async refresh can be done rather than a blocking refresh. */ private final SdkPredicate<T> shouldDoAsyncRefresh; private RefreshableTask(Callable<T> refreshCallable, SdkPredicate<T> shouldDoBlockingRefresh, SdkPredicate<T> shouldDoAsyncRefresh) { this.refreshCallable = ValidationUtils.assertNotNull(refreshCallable, "refreshCallable"); this.shouldDoBlockingRefresh = ValidationUtils .assertNotNull(shouldDoBlockingRefresh, "shouldDoBlockingRefresh"); this.shouldDoAsyncRefresh = ValidationUtils .assertNotNull(shouldDoAsyncRefresh, "shouldDoAsyncRefresh"); } @NotThreadSafe public static class Builder<T> { private Callable<T> refreshCallable; private SdkPredicate<T> shouldDoBlockingRefresh; private SdkPredicate<T> shouldDoAsyncRefresh; /** * Set the callable that will provide the value when a refresh occurs. * * @return This object for method chaining. */ public Builder withRefreshCallable(Callable<T> refreshCallable) { this.refreshCallable = refreshCallable; return this; } /** * Set the predicate that will determine when the task will do a blocking refresh. * * @return This object for method chaining. */ public Builder withBlockingRefreshPredicate(SdkPredicate<T> shouldDoBlockingRefresh) { this.shouldDoBlockingRefresh = shouldDoBlockingRefresh; return this; } /** * Set the predicate that will determine when the task will queue a non-blocking, async * refresh. * * @return This object for method chaining. */ public Builder withAsyncRefreshPredicate(SdkPredicate<T> shouldDoAsyncRefresh) { this.shouldDoAsyncRefresh = shouldDoAsyncRefresh; return this; } /** * @return The configured RefreshableTask */ public RefreshableTask<T> build() { return new RefreshableTask<T>(refreshCallable, shouldDoBlockingRefresh, shouldDoAsyncRefresh); } } /** * Return a valid value, refreshing if necessary. May return the current value, do an async * refresh if possible, or do a blocking refresh if needed. * * @throws AmazonClientException If error occurs during refresh. * @throws IllegalStateException If value if invalid after refreshing. */ public T getValue() throws AmazonClientException, IllegalStateException { if (shouldDoBlockingRefresh()) { blockingRefresh(); } else if (shouldDoAsyncRefresh()) { asyncRefresh(); } return getRefreshedValue(); } /** * Forces a refresh of the value. This method will not attempt to lock on calls to refresh the * value. * * @throws AmazonClientException If error occurs during refresh. * @throws IllegalStateException If value if invalid after refreshing. */ public T forceGetValue() { refreshValue(); return getRefreshedValue(); } /** * @return The refreshed value. * @throws IllegalStateException If the refreshed value is still invalid. */ private T getRefreshedValue() throws IllegalStateException { T refreshableValue = refreshableValueHolder.get(); if (refreshableValue != null) { return refreshableValue; } else { throw new IllegalStateException("Refreshed value should never be null."); } } private boolean shouldDoBlockingRefresh() { return shouldDoBlockingRefresh.test(refreshableValueHolder.get()); } /** * @return True if we should kick of an asynchronous refresh of the value. False otherwise. */ private boolean shouldDoAsyncRefresh() { return shouldDoAsyncRefresh.test(refreshableValueHolder.get()); } /** * Used when there is no valid value to return. Callers are blocked until a new value is created * or an exception is thrown. */ private void blockingRefresh() { try { if (blockingRefreshLock .tryLock(BLOCKING_REFRESH_MAX_WAIT_IN_SECONDS, TimeUnit.SECONDS)) { try { // Return if successful refresh occurred while waiting for the lock if (!shouldDoBlockingRefresh()) { return; } else { // Otherwise do a synchronous refresh if the last lock holder was unable to // refresh the value refreshValue(); return; } } finally { blockingRefreshLock.unlock(); } } } catch (InterruptedException ex) { handleInterruptedException("Interrupted waiting to refresh the value.", ex); } // Couldn't acquire the lock. Just try a synchronous refresh refreshValue(); } /** * Used to asynchronously refresh the value. Caller is never blocked. */ private void asyncRefresh() { // Immediately return if refresh already in progress if (asyncRefreshing.compareAndSet(false, true)) { try { executor.submit(new Runnable() { @Override public void run() { try { refreshValue(); } finally { asyncRefreshing.set(false); } } }); } catch (RuntimeException ex) { asyncRefreshing.set(false); throw ex; } } } /** * Invokes the callback to get a new value. */ private void refreshValue() { try { refreshableValueHolder .compareAndSet(refreshableValueHolder.get(), refreshCallable.call()); } catch (AmazonServiceException ase) { // Preserve the original ASE throw ase; } catch (AmazonClientException ace) { // Preserve the original ACE throw ace; } catch (Exception e) { throw new AmazonClientException(e); } } /** * If we are interrupted while waiting for a lock we just restore the interrupt status and throw * an AmazonClientException back to the caller. */ private void handleInterruptedException(String message, InterruptedException cause) { Thread.currentThread().interrupt(); throw new AmazonClientException(message, cause); } }