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

software.amazon.awssdk.utils.cache.CachedSupplier Maven / Gradle / Ivy

Go to download

A single bundled dependency that includes all service and dependent JARs with third-party libraries relocated to different namespaces.

There is a newer version: 2.5.20
Show newest version
/*
 * Copyright 2010-2018 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 software.amazon.awssdk.utils.cache;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.ReviewBeforeRelease;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.utils.SdkAutoCloseable;
import software.amazon.awssdk.utils.Validate;

/**
 * A wrapper for a {@link Supplier} that applies certain caching rules to the retrieval of its value, including customizable
 * pre-fetching behaviors for updating values as they get close to expiring so that not all threads have to block to update the
 * value.
 *
 * For example, the {@link OneCallerBlocks} strategy will have a single caller block to update the value, and the
 * {@link NonBlocking} strategy maintains a thread pool for updating the value asynchronously in the background.
 *
 * This should be created using {@link #builder(Supplier)}.
 */
@SdkProtectedApi
public final class CachedSupplier implements Supplier, SdkAutoCloseable {
    /**
     * 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 Duration BLOCKING_REFRESH_MAX_WAIT = Duration.ofSeconds(5);

    /**
     * Used as a primitive form of rate limiting for the speed of our refreshes. This will make sure that the backing supplier has
     * a period of time to update the value when the {@link RefreshResult#staleTime} arrives without getting called by every
     * thread that initiates a {@link #get()}.
     */
    private final Lock refreshLock = new ReentrantLock();

    /**
     * The strategy we should use for pre-fetching the cached data when the {@link RefreshResult#prefetchTime} arrives. This is
     * configured when the cache is created via {@link Builder#prefetchStrategy(PrefetchStrategy)}.
     */
    private final PrefetchStrategy prefetchStrategy;

    /**
     * The value currently stored in this cache.
     */
    private volatile RefreshResult cachedValue = RefreshResult.builder((T) null)
                                                                 .staleTime(Instant.MIN)
                                                                 .prefetchTime(Instant.MIN)
                                                                 .build();

    /**
     * The "expensive" to call supplier that is used to refresh the {@link #cachedValue}.
     */
    private final Supplier> valueSupplier;

    private CachedSupplier(Builder builder) {
        this.valueSupplier = Validate.notNull(builder.supplier, "builder.supplier");
        this.prefetchStrategy = Validate.notNull(builder.prefetchStrategy, "builder.prefetchStrategy");
    }

    /**
     * Retrieve a builder that can be used for creating a {@link CachedSupplier}.
     *
     * @param valueSupplier The value supplier that should have its value cached.
     */
    public static  CachedSupplier.Builder builder(Supplier> valueSupplier) {
        return new CachedSupplier.Builder<>(valueSupplier);
    }

    @Override
    public T get() {
        if (cacheIsStale()) {
            refreshCache();
        } else if (shouldInitiateCachePrefetch()) {
            prefetchCache();
        }

        return this.cachedValue.value();
    }

    /**
     * Determines whether the value in this cache is stale, and all threads should block and wait for an updated value.
     */
    private boolean cacheIsStale() {
        return Instant.now().isAfter(cachedValue.staleTime());
    }

    /**
     * Determines whether the cached value's prefetch time has passed and we should initiate a pre-fetch on the value using the
     * configured {@link #prefetchStrategy}.
     */
    private boolean shouldInitiateCachePrefetch() {
        return Instant.now().isAfter(cachedValue.prefetchTime());
    }

    /**
     * Initiate a pre-fetch of the data using the configured {@link #prefetchStrategy}.
     */
    private void prefetchCache() {
        prefetchStrategy.prefetch(this::refreshCache);
    }

    /**
     * Perform a blocking refresh of the cached value. This will rate limit synchronous refresh calls based on the
     * {@link #BLOCKING_REFRESH_MAX_WAIT} time. This ensures that when the data needs to be updated, we won't immediately hammer
     * the underlying value refresher if it can get back to us in a reasonable time.
     */
    private void refreshCache() {
        try {
            boolean lockAcquired = refreshLock.tryLock(BLOCKING_REFRESH_MAX_WAIT.getSeconds(), TimeUnit.SECONDS);

            try {
                // Make sure the value was not refreshed while we waited for the lock.
                if (cacheIsStale() || shouldInitiateCachePrefetch()) {
                    // It wasn't, call the supplier to update it.
                    cachedValue = valueSupplier.get();
                }
            } finally {
                if (lockAcquired) {
                    refreshLock.unlock();
                }
            }
        } catch (InterruptedException e) {
            handleInterruptedException("Interrupted waiting to refresh the value.", e);
        }
    }

    @ReviewBeforeRelease("Should this throw a different exception, like AbortedException, from the core?")
    private void handleInterruptedException(String message, InterruptedException cause) {
        Thread.currentThread().interrupt();
        throw new IllegalStateException(message, cause);
    }

    /**
     * Free any resources consumed by the prefetch strategy this supplier is using.
     */
    @Override
    public void close() {
        prefetchStrategy.close();
    }

    /**
     * A Builder for {@link CachedSupplier}, created by {@link #builder(Supplier)}.
     */
    public static final class Builder {
        private final Supplier> supplier;
        private PrefetchStrategy prefetchStrategy = new OneCallerBlocks();

        private Builder(Supplier> supplier) {
            this.supplier = supplier;
        }

        /**
         * Configure the way in which data in the cache should be pre-fetched when the data's {@link RefreshResult#prefetchTime()}
         * arrives.
         *
         * By default, this uses the {@link OneCallerBlocks} strategy, which will block a single {@link #get()} caller to update
         * the value.
         */
        public Builder prefetchStrategy(PrefetchStrategy prefetchStrategy) {
            this.prefetchStrategy = prefetchStrategy;
            return this;
        }

        /**
         * Create a {@link CachedSupplier} using the current configuration of this builder.
         */
        public CachedSupplier build() {
            return new CachedSupplier<>(this);
        }
    }

    /**
     * The way in which the cache should be pre-fetched when the data's {@link RefreshResult#prefetchTime()} arrives.
     *
     * @see OneCallerBlocks
     * @see NonBlocking
     */
    @FunctionalInterface
    public interface PrefetchStrategy extends SdkAutoCloseable {
        /**
         * Execute the provided value updater to update the cache. The specific implementation defines how this is invoked.
         */
        void prefetch(Runnable valueUpdater);

        /**
         * Free any resources associated with the strategy. This is invoked when the {@link CachedSupplier#close()} method is
         * invoked.
         */
        default void close() {}
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy