package org.dcache.util; import com.google.common.base.Throwables; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import javax.security.auth.Subject; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import diskCacheV111.util.CacheException; import diskCacheV111.util.FsPath; import diskCacheV111.util.PnfsHandler; import org.dcache.auth.attributes.Restriction; import static com.google.common.base.Preconditions.checkState; /** * A transfer where the mover can send a redirect message to the door * asynchronously. * * The transfer startup phase is identical to a regular Transfer, however notification of * redirect and transfer completion is done asynchronously through callbacks. Subclasses * are to implement onQueued, onRedirect, onFinish and onFailure. The class deals with out * of order notifications and guarantees that: * * - onQueued is always called before onRedirect * - onRedirect is always called before onFinish * - that onRedirect and onFinish are not called once onFailure was called * * The class implements automatic killing of the mover in case the transfer is * aborted. */ public abstract class AsynchronousRedirectedTransfer<T> extends Transfer { private final Executor executor; private final Monitor monitor = new Monitor(); public AsynchronousRedirectedTransfer(Executor executor, PnfsHandler pnfs, Subject namespaceSubject, Restriction restriction, Subject subject, FsPath path) { super(pnfs, namespaceSubject, restriction, subject, path); this.executor = executor; } public AsynchronousRedirectedTransfer(Executor executor, PnfsHandler pnfs, Subject subject, Restriction restriction, FsPath path) { super(pnfs, subject, restriction, path); this.executor = executor; } @Override public ListenableFuture<Void> selectPoolAndStartMoverAsync(TransferRetryPolicy policy) { return monitor.setQueueFuture(super.selectPoolAndStartMoverAsync(policy)); } /** * Signals that the transfer is redirected. */ public void redirect(T object) { executor.execute(() -> monitor.redirect(object)); } /** * Aborts the transfer unless already completed. * * This cancels pool selection and kills the mover. The onFailure callback is called. This method * blocks until the callback completes. */ public void abort(Throwable t) { try { FutureTask<Object> task = new FutureTask<>(() -> monitor.doAbort(t), null); executor.execute(task); task.get(); } catch (ExecutionException e) { Throwables.throwIfUnchecked(e.getCause()); throw new RuntimeException(e.getCause()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } @Override public void finished(CacheException error) { super.finished(error); executor.execute(() -> monitor.finished(error)); } protected abstract void onQueued() throws Exception; protected abstract void onRedirect(T object) throws Exception; protected abstract void onFinish() throws Exception; protected abstract void onFailure(Throwable t); protected String explain(Throwable t) { if (t instanceof RuntimeException) { return "bug: " + t.toString(); } else { return String.valueOf(t); } } /** * To avoid locking the monitor of the Transfer object during callbacks, * we have an explicit monitor guarding our state machine and ensuring * that callbacks are executed sequentially. */ private class Monitor { private T redirectObject; private boolean isQueued; private boolean isRedirected; private boolean isFinished; private boolean isDone; private ListenableFuture<Void> queueFuture; private synchronized ListenableFuture<Void> setQueueFuture(ListenableFuture<Void> future) { checkState(queueFuture == null); queueFuture = future; Futures.addCallback(future, new FutureCallback<Void>() { @Override public void onSuccess(Void result) { executor.execute(monitor::doQueued); } @Override public void onFailure(Throwable t) { executor.execute(() -> monitor.doAbort(t)); } }); return future; } private synchronized void doQueued() { try { isQueued = true; if (isDone) { doKill("transfer aborted"); } else { onQueued(); doRedirect(); } } catch (Exception e) { doAbort(e); } } private synchronized void doRedirect() { try { if (!isDone && isQueued && isRedirected) { onRedirect(redirectObject); doFinish(); } } catch (Exception e) { doAbort(e); } } private synchronized void doFinish() { try { if (!isDone && isQueued && isRedirected && isFinished) { onFinish(); isDone = true; } } catch (Exception e) { doAbort(e); } } private synchronized void doAbort(Throwable t) { if (!isDone) { doKill(explain(t)); onFailure(t); isDone = true; } } private synchronized void doKill(String explanation) { if (queueFuture != null) { queueFuture.cancel(true); } killMover(0, "killed by door: " + explanation); } /** * Signals that the transfer is redirected. */ public synchronized void redirect(T object) { checkState(!isRedirected); redirectObject = object; isRedirected = true; doRedirect(); } public synchronized void finished(CacheException error) { if (error != null) { doAbort(error); } else { isFinished = true; doFinish(); } } } }