/*
* dCache - http://www.dcache.org/
*
* Copyright (C) 2016 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.pool.nearline;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.InProgressCacheException;
import org.dcache.pool.nearline.spi.FlushRequest;
import org.dcache.pool.nearline.spi.NearlineRequest;
import org.dcache.pool.nearline.spi.NearlineStorage;
import org.dcache.pool.nearline.spi.RemoveRequest;
import org.dcache.pool.nearline.spi.StageRequest;
import org.dcache.util.Checksum;
import org.dcache.util.NDC;
import org.dcache.vehicles.FileAttributes;
import static com.google.common.base.Preconditions.checkState;
/**
* Abstract base class for NearlineStorage implementations that follow the
* one-thread-per-task paradigm.
*
* <p>Implements request activation, request termination callbacks, request cancellation,
* and nearline storage shutdown logic. Request cancellation is implemented by interrupting
* the thread executing the cancelled task.
*
* <p>Subclasses must implement the abstract methods for flush, stage and remove, as well as
* the three methods for providing an executor for each of the there operations.
*/
public abstract class AbstractBlockingNearlineStorage implements NearlineStorage
{
protected final String type;
protected final String name;
private final Map<UUID, Task<?, ?>> requests = new ConcurrentHashMap<>();
public AbstractBlockingNearlineStorage(String type, String name)
{
this.type = type;
this.name = name;
}
@Override
public void cancel(UUID uuid)
{
Task<?, ?> task = requests.get(uuid);
if (task != null) {
task.cancel();
}
}
@Override
public void flush(Iterable<FlushRequest> requests)
{
for (FlushRequest request : requests) {
Task<FlushRequest, Set<URI>> task =
new Task<FlushRequest, Set<URI>>(request)
{
@Override
public Set<URI> call() throws Exception
{
FileAttributes fileAttributes = request.getFileAttributes();
NDC.push(fileAttributes.getPnfsId().toString());
try {
return flush(request);
} finally {
NDC.pop();
}
}
@Override
protected void execute()
{
getFlushExecutor().execute(this);
}
};
task.execute();
}
}
@Override
public void stage(Iterable<StageRequest> requests)
{
for (StageRequest request : requests) {
Task<StageRequest, Set<Checksum>> task =
new Task<StageRequest, Set<Checksum>>(request)
{
@Override
public Set<Checksum> call() throws Exception
{
FileAttributes attributes = request.getFileAttributes();
NDC.push(attributes.getPnfsId().toString());
try {
request.allocate().get();
return stage(request);
} finally {
NDC.pop();
}
}
@Override
protected void execute()
{
getStageExecutor().execute(this);
}
};
task.execute();
}
}
@Override
public void remove(Iterable<RemoveRequest> requests)
{
for (RemoveRequest request : requests) {
Task<RemoveRequest, Void> task =
new Task<RemoveRequest, Void>(request)
{
@Override
public Void call() throws Exception
{
NDC.push(request.getUri().toString());
try {
remove(request);
} finally {
NDC.pop();
}
return null;
}
@Override
protected void execute()
{
getRemoveExecutor().execute(this);
}
};
task.execute();
}
}
@Override
public void shutdown()
{
requests.values().forEach(Task::cancel);
}
/**
* Returns the nearline storage locations of a file for this nearline storage.
*
* @param fileAttributes Attributes of a file
* @return The storage locations of the file on this nearline storage.
*/
protected List<URI> getLocations(FileAttributes fileAttributes)
{
return filteredLocations(fileAttributes).collect(Collectors.toList());
}
/**
* Returns the nearline storage locations of a file for this nearline storage.
*
* @param fileAttributes Attributes of a file
* @return The storage locations of the file on this nearline storage.
*/
private Stream<URI> filteredLocations(FileAttributes fileAttributes)
{
return fileAttributes.getStorageInfo().locations().stream()
.filter(uri -> uri.getScheme().equals(type))
.filter(uri -> uri.getAuthority().equals(name));
}
/**
* Returns the executor to use for flush requests.
*
* <p>Must be implemented by subclasses. Provides control over scheduling policy
* for flush requests.
*
* @see AbstractBlockingNearlineStorage#flush
*/
protected abstract Executor getFlushExecutor();
/**
* Returns the executor to use for stage requests.
*
* <p>Must be implemented by subclasses. Provides control over scheduling policy
* for stage requests.
*
* @see AbstractBlockingNearlineStorage#stage
*/
protected abstract Executor getStageExecutor();
/**
* Returns the executor to use for remove requests.
*
* <p>Must be implemented by subclasses. Provides control over scheduling policy
* for remove requests.
*
* @see AbstractBlockingNearlineStorage#remove
*/
protected abstract Executor getRemoveExecutor();
/**
* Process a flush request.
*
* <p>Implemented by subclasses to provide flush functionality. Called from the
* flush executor. The calling thread is interrupted if the request is aborted.
*
* <p>In case of {@link InProgressCacheException} the request is placed back in the
* executor and will be silently retried. Implementations should prefer to use
* {@link CacheException} for signalling errors as it provides control over the
* error code (similar to the exit code of HSM scripts).
*
* @throws InProgressCacheException in case of transient errors
* @throws InterruptedException when the calling thread is interrupted
* @throws Exception any other error
* @see AbstractBlockingNearlineStorage#retry(Runnable)
*/
protected abstract Set<URI> flush(FlushRequest request)
throws Exception;
/**
* Process a stage request.
*
* <p>Implemented by subclasses to provide stage functionality. Called from the
* stage executor. The calling thread is interrupted if the request is aborted.
*
* <p>In case of {@link InProgressCacheException} the request is placed back in the
* executor and will be silently retried. Implementations should prefer to use
* {@link CacheException} for signalling errors as it provides control over the
* error code (similar to the exit code of HSM scripts).
*
* @throws InProgressCacheException in case of transient errors
* @throws InterruptedException when the calling thread is interrupted
* @throws Exception any other error
* @see AbstractBlockingNearlineStorage#retry(Runnable)
*/
protected abstract Set<Checksum> stage(StageRequest request)
throws Exception;
/**
* Process a remove request.
*
* <p>Implemented by subclasses to provide remove functionality. Called from the
* remove executor. The calling thread is interrupted if the request is aborted.
*
* <p>In case of {@link InProgressCacheException}, the request is placed back in the
* executor and will be silently retried. Implementations should prefer to use
* {@link CacheException} for signalling errors as it provides control over the
* error code (similar to the exit code of HSM scripts).
*
* @throws InProgressCacheException in case of transient errors
* @throws InterruptedException when the calling thread is interrupted
* @throws Exception any other error
* @see AbstractBlockingNearlineStorage#retry(Runnable)
*/
protected abstract void remove(RemoveRequest request)
throws Exception;
/**
* Hook called when a request is retried because {@link InProgressCacheException} was thrown.
*
* <p>When {@code runnable} is executed, the failed request is executed on the appropriate
* executor. Subclasses may choose to delay invocation of the runnable, e.g., by scheduling
* execution of the {@code runnable} on a {@link java.util.concurrent.ScheduledExecutorService}.
*
* @since 3.0
*/
protected void retry(Runnable runnable)
{
runnable.run();
}
/**
* Base class for tasks processing nearline requests.
*
* @param <R> Request type
* @param <T> Result type provided to the callback upon completion
*/
private abstract class Task<R extends NearlineRequest<T>, T> implements Runnable
{
protected final R request;
private Thread thread;
private boolean isDone;
private boolean isActivated;
protected Task(R request)
{
this.request = request;
requests.put(request.getId(), this);
}
public synchronized void cancel()
{
if (!isDone) {
isDone = true;
if (thread != null) {
thread.interrupt();
} else {
request.failed(new CancellationException());
}
requests.remove(request.getId());
}
}
/**
* Binds task to a particular thread. When the request is cancelled, the thread
* is interrupted.
*/
private synchronized boolean bind(Thread thread)
{
checkState(this.thread == null);
if (isDone) {
return false;
}
this.thread = thread;
return true;
}
/**
* Releases task from its thread. If the task was cancelled, InterruptedException
* is thrown.
*/
private synchronized void release(boolean isRetrying) throws InterruptedException
{
thread = null;
if (isDone) {
/* If done it must because the task was cancelled.
*/
throw new InterruptedException();
}
if (!isRetrying) {
isDone = true;
requests.remove(request.getId());
}
}
/** Returns true the first time it is called, otherwise false. */
private synchronized boolean activate()
{
if (!isActivated) {
isActivated = true;
return true;
}
return false;
}
public void run()
{
try {
Thread thread = Thread.currentThread();
if (bind(thread)) {
boolean isSuccess = false;
T result = null;
try {
try {
result = processRequest();
release(false);
isSuccess = true;
} catch (InProgressCacheException e) {
release(true);
throw e;
} catch (Exception e) {
release(false);
throw e;
}
} catch (InProgressCacheException e) {
retry(this::execute);
} catch (InterruptedException e) {
request.failed(new CancellationException());
} catch (Exception cause) {
request.failed(cause);
}
if (isSuccess) {
request.completed(result);
}
}
} catch (Throwable e) {
thread.getUncaughtExceptionHandler().uncaughtException(thread, e);
}
}
private T processRequest() throws Throwable
{
if (activate()) {
try {
request.activate().get();
} catch (ExecutionException e) {
throw e.getCause();
}
}
NDC.push(request.getId().toString());
try {
return call();
} finally {
NDC.pop();
}
}
protected abstract void execute();
protected abstract T call() throws Exception;
}
}