package freenet.client.async; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import freenet.client.InsertException; import freenet.client.InsertException.InsertExceptionMode; import freenet.client.Metadata; import freenet.keys.BaseClientKey; import freenet.support.ListUtils; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.api.Bucket; import freenet.support.io.ResumeFailedException; public class MultiPutCompletionCallback implements PutCompletionCallback, ClientPutState, Serializable { private static final long serialVersionUID = 1L; private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback() { @Override public void shouldUpdate() { logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } // ArrayLists rather than HashSet's for memory reasons. // This class will not be used with large sets, so O(n) is cheaper than O(1) - // at least it is on memory! private final ArrayList<ClientPutState> waitingFor; private final ArrayList<ClientPutState> waitingForBlockSet; private final ArrayList<ClientPutState> waitingForFetchable; private final PutCompletionCallback cb; private ClientPutState generator; private final BaseClientPutter parent; private InsertException e; private boolean cancelling; private boolean finished; private boolean started; private boolean calledFetchable; public final Object token; private final boolean persistent; private final boolean collisionIsOK; private final boolean finishOnFailure; private transient boolean resumed; private BaseClientKey encodedKey; public MultiPutCompletionCallback(PutCompletionCallback cb, BaseClientPutter parent, Object token, boolean persistent) { this(cb, parent, token, persistent, false); } public MultiPutCompletionCallback(PutCompletionCallback cb, BaseClientPutter parent, Object token, boolean persistent, boolean collisionIsOK) { this(cb, parent, token, persistent, collisionIsOK, false); } public MultiPutCompletionCallback(PutCompletionCallback cb, BaseClientPutter parent, Object token, boolean persistent, boolean collisionIsOK, boolean finishOnFailure) { this.cb = cb; this.collisionIsOK = collisionIsOK; this.finishOnFailure = finishOnFailure; waitingFor = new ArrayList<ClientPutState>(); waitingForBlockSet = new ArrayList<ClientPutState>(); waitingForFetchable = new ArrayList<ClientPutState>(); this.parent = parent; this.token = token; cancelling = false; finished = false; this.persistent = persistent; } @Override public void onSuccess(ClientPutState state, ClientContext context) { onBlockSetFinished(state, context); onFetchable(state); boolean complete = true; synchronized(this) { if(finished) { Logger.error(this, "Already finished but got onSuccess() for "+state+" on "+this); return; } ListUtils.removeBySwapLast(waitingFor,state); ListUtils.removeBySwapLast(waitingForBlockSet,state); ListUtils.removeBySwapLast(waitingForFetchable,state); if(!(waitingFor.isEmpty() && started)) { complete = false; } if(state == generator) { generator = null; } } if(complete) { Logger.minor(this, "Completing..."); complete(null, context); } } @Override public void onFailure(InsertException e, ClientPutState state, ClientContext context) { if(collisionIsOK && e.getMode() == InsertExceptionMode.COLLISION) { onSuccess(state, context); return; } boolean complete = true; boolean doCancel = false; synchronized(this) { if(finished) { Logger.error(this, "Already finished but got onFailure() for "+state+" on "+this); return; } ListUtils.removeBySwapLast(waitingFor,state); ListUtils.removeBySwapLast(waitingForBlockSet,state); ListUtils.removeBySwapLast(waitingForFetchable,state); if(!(waitingFor.isEmpty() && started)) { this.e = e; if(logMINOR) Logger.minor(this, "Still running: "+waitingFor.size()+" started = "+started); complete = false; } if(state == generator) { generator = null; } if(finishOnFailure) { if(started) doCancel = true; else { cancelling = true; } } } if(complete) complete(e, context); else if(doCancel) cancel(context); } private void complete(InsertException e, ClientContext context) { synchronized(this) { if(finished) return; finished = true; if(e != null && this.e != null && this.e != e) { if(e.getMode() == InsertExceptionMode.CANCELLED) { // Cancelled is okay, ignore it, we cancel after failure sometimes. // Ignore the new failure mode, use the old one e = this.e; if(persistent) { e = e.clone(); // Since we will remove it, we can't pass it on } } else { // Delete the old failure mode, use the new one this.e = e; } } if(e == null) { e = this.e; if(persistent && e != null) { e = e.clone(); // Since we will remove it, we can't pass it on } } } if(e != null) cb.onFailure(e, this, context); else cb.onSuccess(this, context); } public synchronized void addURIGenerator(ClientPutState ps) { add(ps); generator = ps; } public synchronized void add(ClientPutState ps) { if(finished) return; waitingFor.add(ps); waitingForBlockSet.add(ps); waitingForFetchable.add(ps); } public void arm(ClientContext context) { if(logMINOR) Logger.minor(this, "Arming "+this); boolean allDone; boolean allGotBlocks; boolean doCancel; synchronized(this) { started = true; allDone = waitingFor.isEmpty(); allGotBlocks = waitingForBlockSet.isEmpty(); doCancel = cancelling; } if(allGotBlocks) { cb.onBlockSetFinished(this, context); } if(allDone) { complete(e, context); } else if(doCancel) { cancel(context); } } @Override public BaseClientPutter getParent() { return parent; } @Override public void onEncode(BaseClientKey key, ClientPutState state, ClientContext context) { synchronized(this) { if(state != generator) return; if(encodedKey != null) { if(key.equals(encodedKey)) return; // Squash duplicated call to onEncode(). else Logger.error(this, "Encoded twice with different keys for "+this+" : "+encodedKey+" -> "+key); } encodedKey = key; } cb.onEncode(key, this, context); } @Override public void cancel(ClientContext context) { ClientPutState[] states = new ClientPutState[waitingFor.size()]; synchronized(this) { states = waitingFor.toArray(states); } boolean logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); for(int i=0;i<states.length;i++) { if(logDEBUG) Logger.minor(this, "Cancelling state "+i+" of "+states.length+" : "+states[i]); states[i].cancel(context); } } @Override public synchronized void onTransition(ClientPutState oldState, ClientPutState newState, ClientContext context) { if(generator == oldState) generator = newState; if(oldState == newState) return; for(int i=0;i<waitingFor.size();i++) { if(waitingFor.get(i) == oldState) { waitingFor.set(i, newState); } } for(int i=0;i<waitingForBlockSet.size();i++) { if(waitingForBlockSet.get(i) == oldState) { waitingForBlockSet.set(i, newState); } } for(int i=0;i<waitingForFetchable.size();i++) { if(waitingForFetchable.get(i) == oldState) { waitingForFetchable.set(i, newState); } } } @Override public synchronized void onMetadata(Metadata m, ClientPutState state, ClientContext context) { if(generator == state) { cb.onMetadata(m, this, context); } else { Logger.error(this, "Got metadata for "+state); } } @Override public synchronized void onMetadata(Bucket metadata, ClientPutState state, ClientContext context) { if(generator == state) { cb.onMetadata(metadata, this, context); } else { Logger.error(this, "Got metadata for "+state); } } @Override public void onBlockSetFinished(ClientPutState state, ClientContext context) { synchronized(this) { ListUtils.removeBySwapLast(this.waitingForBlockSet,state); if(!started) return; if(!waitingForBlockSet.isEmpty()) return; } cb.onBlockSetFinished(this, context); } @Override public void schedule(ClientContext context) throws InsertException { // Do nothing } @Override public Object getToken() { return token; } @Override public void onFetchable(ClientPutState state) { synchronized(this) { ListUtils.removeBySwapLast(this.waitingForFetchable,state); if(!started) return; if(!waitingForFetchable.isEmpty()) return; if(calledFetchable) { if(logMINOR) Logger.minor(this, "Trying to call onFetchable() twice"); return; } calledFetchable = true; } cb.onFetchable(this); } @Override public void onResume(ClientContext context) throws InsertException, ResumeFailedException { synchronized(this) { if(resumed) return; resumed = true; } for(ClientPutState s : getWaitingFor()) s.onResume(context); if(cb != parent) cb.onResume(context); } @Override public void onShutdown(ClientContext context) { for(ClientPutState state : getWaitingFor()) { state.onShutdown(context); } } private synchronized List<ClientPutState> getWaitingFor() { return new ArrayList<ClientPutState>(waitingFor); } }