/*
* Copyright 2013 Ben Manes. 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.
* 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.github.benmanes.multiway;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.multiway.TransferPool.LoadingTransferPool;
import com.google.common.base.Objects;
import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheStats;
import com.google.common.cache.Weigher;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* A MultiwayPoolBuilder of {@link MultiwayPool} instances with support for least-recently-used
* eviction and time-based expiration of resources. A notification is made when a resource is
* created, borrowed, released, or removed. By default instances will not perform any type of
* eviction.
* <p>
* Usage example:
* <pre> {@code
* MultiwayPool<File, RandomAccessFile> files = MultiwayPoolBuilder.newBuilder()
* .expireAfterWrite(10, TimeUnit.MINUTES)
* .maximumSize(100)
* .build(
* new ResourceLoader<File, RandomAccessFile>() {
* public RandomAccessFile load(File file) throws AnyException {
* return new RandomAccessFile(file);
* }
* });
* }</pre>
*
* @author Ben Manes (ben.manes@gmail.com)
*/
public final class MultiwayPoolBuilder<K, R> {
static final ResourceLifecycle<Object, Object> DISCARDING_LIFECYCLE =
new ResourceLifecycle<Object, Object>() {};
static final int DEFAULT_CONCURRENCY_LEVEL = 4;
static final int UNSET_INT = -1;
boolean recordStats;
long maximumSize = UNSET_INT;
long maximumWeight = UNSET_INT;
Weigher<? super K, ? super R> weigher;
Ticker ticker;
long expireAfterWriteNanos = UNSET_INT;
long expireAfterAccessNanos = UNSET_INT;
int concurrencyLevel = UNSET_INT;
ResourceLifecycle<? super K, ? super R> lifecycle;
MultiwayPoolBuilder() {}
Ticker getTicker() {
return Objects.firstNonNull(ticker, Ticker.systemTicker());
}
@SuppressWarnings("unchecked")
ResourceLifecycle<? super K, ? super R> getResourceLifecycle() {
return (ResourceLifecycle<? super K, ? super R>) Objects.firstNonNull(
lifecycle, DISCARDING_LIFECYCLE);
}
int getConcurrencyLevel() {
return (concurrencyLevel == UNSET_INT) ? DEFAULT_CONCURRENCY_LEVEL : concurrencyLevel;
}
/** Constructs a new builder with no automatic eviction of any kind. */
public static MultiwayPoolBuilder<Object, Object> newBuilder() {
return new MultiwayPoolBuilder<Object, Object>();
}
/**
* Guides the allowed concurrency among update operations. Used as a hint for internal sizing.
* <p>
* Defaults to 4.
*
* @throws IllegalArgumentException if {@code concurrencyLevel} is not positive
* @throws IllegalStateException if a concurrency level was already set
*/
public MultiwayPoolBuilder<K, R> concurrencyLevel(int concurrencyLevel) {
checkState(this.concurrencyLevel == UNSET_INT, "concurrency level was already set to %s",
this.concurrencyLevel);
checkArgument(concurrencyLevel > 0);
this.concurrencyLevel = concurrencyLevel;
return this;
}
/**
* Specifies the maximum number of resources the pool may contain, regardless of the category
* it is associated with. Note that the pool <b>may evict a resource before this limit is
* exceeded</b>. As the pool size grows close to the maximum, the pool evicts entries that are
* less likely to be used again.
*
* @param size the maximum size of the cache
* @throws IllegalArgumentException if {@code size} is negative
*/
public MultiwayPoolBuilder<K, R> maximumSize(long size) {
checkState(maximumSize == UNSET_INT, "maximum size was already set to %s", maximumSize);
checkArgument(size >= 0, "maximum size must not be negative");
maximumSize = size;
return this;
}
/**
* Specifies the maximum weight of resources the pool may contain, regardless of the category
* it is associated with. Weight is determined using the {@link Weigher} specified with
* {@link #weigher}, and use of this method requires a corresponding call to {@link #weigher}
* prior to calling {@link #build}.
* <p>
* Note that the cache <b>may evict a resource before this limit is exceeded</b>. As the pool
* size grows close to the maximum, the pool evicts entries that are less likely to be used
* again.
* <p>
* When {@code weight} is zero, resources will be evicted immediately after being loaded into
* pool. This can be useful in testing, or to disable the pool temporarily without a code
* change.
* <p>
* Note that weight is only used to determine whether the pool is over capacity; it has no
* effect on selecting which resource should be evicted next.
* <p>
* This feature cannot be used in conjunction with {@link #maximumSize}.
*
* @param weight the maximum total weight of entries the cache may contain
* @throws IllegalArgumentException if {@code weight} is negative
* @throws IllegalStateException if a maximum weight or size was already set
*/
public MultiwayPoolBuilder<K, R> maximumWeight(long weight) {
checkState(maximumWeight == UNSET_INT, "maximum weight was already set to %s", maximumWeight);
checkState(maximumSize == UNSET_INT, "maximum size was already set to %s", maximumSize);
checkArgument(weight >= 0, "maximum weight must not be negative");
this.maximumWeight = weight;
return this;
}
/**
* Specifies the weigher to use in determining the weight of resources. The weight is taken
* into consideration by {@link #maximumWeight(long)} when determining which resources to evict,
* and use of this method requires a corresponding call to {@link #maximumWeight(long)} prior to
* calling {@link #build}. Weights are measured and recorded when resources are inserted into
* the pool, and are thus effectively static during the lifetime of the resource.
*
* <p>When the weight of a resource is zero it will not be considered for size-based eviction
* (though it still may be evicted by other means).
*
* @param weigher the weigher to use in calculating the weight of cache entries
* @throws IllegalArgumentException if {@code size} is negative
* @throws IllegalStateException if a maximum size was already set
*/
public <K1 extends K, R1 extends R> MultiwayPoolBuilder<K1, R1> weigher(
Weigher<? super K1, ? super R1> weigher) {
checkState(this.weigher == null);
checkState(this.maximumSize == UNSET_INT, "weigher can not be combined with maximum size");
// safely limiting the kinds of caches this can produce
@SuppressWarnings("unchecked")
MultiwayPoolBuilder<K1, R1> self = (MultiwayPoolBuilder<K1, R1>) this;
self.weigher = checkNotNull(weigher);
return self;
}
/**
* Specifies that each resource should be automatically removed from the pool once a fixed
* duration has elapsed after the resource's creation.
*
* @param duration the length of time after a resource is created when it should
* be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or time to idle was already set
*/
public MultiwayPoolBuilder<K, R> expireAfterWrite(long duration, TimeUnit unit) {
checkState(expireAfterWriteNanos == UNSET_INT,
"expireAfterWrite was already set to %s ns", expireAfterWriteNanos);
checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
expireAfterWriteNanos = unit.toNanos(duration);
return this;
}
/**
* Specifies that each entry should be automatically removed from the pool once a fixed duration
* has elapsed after the resource's creation or its last access. Access time is reset when the
* resource is borrowed or released. A resource is considered eligible for eviction when it is
* idle in the pool, e.g. it is not being used.
*
* @param duration the length of time after a resource is last accessed that it should
* be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @throws IllegalArgumentException if {@code duration} is negative
*/
public MultiwayPoolBuilder<K, R> expireAfterAccess(long duration, TimeUnit unit) {
checkState(expireAfterAccessNanos == UNSET_INT,
"expireAfterAccess was already set to %s ns", expireAfterAccessNanos);
checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
expireAfterAccessNanos = unit.toNanos(duration);
return this;
}
/**
* Specifies a nanosecond-precision time source for use in determining when entries should be
* expired. By default, {@link System#nanoTime} is used.
* <p>
* The primary intent of this method is to facilitate testing of caches which have been
* configured with {@link #expireAfterWrite} or {@link #expireAfterAccess}.
*
* @throws IllegalStateException if a ticker was already set
*/
public MultiwayPoolBuilder<K, R> ticker(Ticker ticker) {
checkState(this.ticker == null);
this.ticker = checkNotNull(ticker);
return this;
}
/**
* Enable the accumulation of {@link CacheStats} during the operation of the pool. Without this
* {@link Cache#stats} will return zero for all statistics. Note that recording stats requires
* bookkeeping to be performed with each operation, and thus imposes a performance penalty on
* cache operation.
*/
public MultiwayPoolBuilder<K, R> recordStats() {
recordStats = true;
return this;
}
/**
* Specifies a life cycle listener instance that pools should notify each time a resource is
* created, borrowed, released, or removed.
*
* @param lifecycle the listener used for resource life cycle events
*/
public <K1 extends K, R1 extends R> MultiwayPoolBuilder<K1, R1> lifecycle(
ResourceLifecycle<? super K1, ? super R1> lifecycle) {
checkState(this.lifecycle == null);
// safely limiting the kinds of caches this can produce
@SuppressWarnings("unchecked")
MultiwayPoolBuilder<K1, R1> self = (MultiwayPoolBuilder<K1, R1>) this;
self.lifecycle = checkNotNull(lifecycle);
return self;
}
/**
* Builds a multiway pool, which either returns an available resource for a given key or
* atomically computes or retrieves it using the method supplied {@code Callable}.
*
* @return a multiway pool having the requested features
*/
public <K1 extends K, R1 extends R> MultiwayPool<K1, R1> build() {
return new TransferPool<K1, R1>(this);
}
/**
* Builds a multiway pool, which either returns an available resource for a given key or
* atomically computes or retrieves it using the supplied {@link ResourceLoader} or
* method supplied {@code Callable}.
*
* @param loader the resource loader to create new instances
* @return a multiway pool having the requested features
*/
public <K1 extends K, R1 extends R> LoadingMultiwayPool<K1, R1> build(
ResourceLoader<K1, R1> loader) {
checkNotNull(loader);
return new LoadingTransferPool<K1, R1>(this, loader);
}
@Override
public String toString() {
Objects.ToStringHelper s = Objects.toStringHelper(this);
if (concurrencyLevel != UNSET_INT) {
s.add("concurrencyLevel", concurrencyLevel);
}
if (maximumSize != UNSET_INT) {
s.add("maximumSize", maximumSize);
}
if (maximumWeight != UNSET_INT) {
s.add("maximumWeight", maximumWeight);
}
if (expireAfterWriteNanos != UNSET_INT) {
s.add("expireAfterWrite", expireAfterWriteNanos + "ns");
}
if (expireAfterAccessNanos != UNSET_INT) {
s.add("expireAfterAccess", expireAfterAccessNanos + "ns");
}
if (lifecycle != null) {
s.addValue("resourceLifecycle");
}
return s.toString();
}
}