/* dCache - http://www.dcache.org/
*
* Copyright (C) 2015 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.dcache.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.Ordering;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import static java.util.Arrays.asList;
public class Callables
{
private Callables()
{
}
/**
* Returns a Caller that caches the instance returned by the delegate and
* removes the cached value after the specified time has passed. Subsequent
* calls to {@code call()} return the cached value if the expiration time has
* not passed. After the expiration time, a new value is retrieved, cached,
* and returned.
*
* <p>The returned Caller is thread-safe.
*/
public static <T> Callable<T> memoizeWithExpiration(
Callable<T> delegate, long duration, TimeUnit unit)
{
return new ExpiringMemoizingCallable<>(delegate, duration, unit);
}
/**
* Returns a Caller that caches the instance returned by the delegate and
* removes the cached value when any of the supplied files have been modified.
* Subsequent calls to {@code call()} return the cached value if files have
* not been modified since the cached value was retrieved. If the files have
* been modified, a new value is retrieved.
*
* <p>The returned Caller is thread-safe.
*/
public static <T> Callable<T> memoizeFromFiles(Callable<T> delegate, Path... files)
{
return new MemoizingCallableFromFiles<>(delegate, files);
}
/**
* Adapted from com.google.common.base.Suppliers.
*
* Copyright (C) 2007 The Guava Authors
*/
private static class ExpiringMemoizingCallable<T> implements Callable<T>
{
final Callable<T> delegate;
final long durationNanos;
transient volatile T value;
// The special value 0 means "not yet initialized".
transient volatile long expirationNanos;
ExpiringMemoizingCallable(
Callable<T> delegate, long duration, TimeUnit unit)
{
Preconditions.checkArgument(duration > 0);
this.delegate = Preconditions.checkNotNull(delegate);
this.durationNanos = unit.toNanos(duration);
}
@Override public T call() throws Exception
{
// Another variant of Double Checked Locking.
//
// We use two volatile reads. We could reduce this to one by
// putting our fields into a holder class, but (at least on x86)
// the extra memory consumption and indirection are more
// expensive than the extra volatile reads.
long nanos = expirationNanos;
long now = System.nanoTime();
if (nanos == 0 || now - nanos >= 0) {
synchronized (this) {
if (nanos == expirationNanos) { // recheck for lost race
T t = delegate.call();
value = t;
nanos = now + durationNanos;
// In the very unlikely event that nanos is 0, set it to 1;
// no one will notice 1 ns of tardiness.
expirationNanos = (nanos == 0) ? 1 : nanos;
return t;
}
}
}
return value;
}
@Override public String toString() {
// This is a little strange if the unit the user provided was not NANOS,
// but we don't want to store the unit just for toString
return "Suppliers.memoizeWithExpiration(" + delegate + ", " +
durationNanos + ", NANOS)";
}
}
private static class MemoizingCallableFromFiles<T> implements Callable<T>
{
final Path[] files;
final Callable<T> delegate;
FileTime lastLastModifiedTime;
T value;
public MemoizingCallableFromFiles(Callable<T> delegate, Path[] files)
{
this.delegate = delegate;
this.files = files;
}
@Override
public T call() throws Exception
{
FileTime lastModified = getLastModifiedTime();
synchronized (this) {
if (lastModified == null || lastLastModifiedTime == null ||
lastModified.compareTo(lastLastModifiedTime) > 0) {
value = delegate.call();
lastLastModifiedTime = lastModified;
}
return value;
}
}
private FileTime getLastModifiedTime()
{
try {
FileTime[] times = new FileTime[files.length];
for (int i = 0; i < files.length; i++) {
times[i] = java.nio.file.Files.getLastModifiedTime(files[i]);
}
return Ordering.natural().max(asList(times));
} catch (IOException e) {
return null;
}
}
}
}