/* * Mojito Distributed Hash Table (Mojito DHT) * Copyright (C) 2006-2007 LimeWire LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.limewire.mojito.handler.response; import java.io.IOException; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.mojito.Context; import org.limewire.mojito.KUID; import org.limewire.mojito.db.DHTValueEntity; import org.limewire.mojito.db.Database; import org.limewire.mojito.exceptions.DHTException; import org.limewire.mojito.messages.RequestMessage; import org.limewire.mojito.messages.ResponseMessage; import org.limewire.mojito.messages.StoreRequest; import org.limewire.mojito.messages.StoreResponse; import org.limewire.mojito.messages.StoreResponse.StoreStatusCode; import org.limewire.mojito.result.StoreResult; import org.limewire.mojito.routing.Contact; import org.limewire.mojito.settings.KademliaSettings; import org.limewire.mojito.settings.StoreSettings; import org.limewire.mojito.util.CollectionUtils; import org.limewire.security.SecurityToken; /** * The StoreResponseHandler class handles/manages storing of * DHTValues on remote Nodes. */ public class StoreResponseHandler extends AbstractResponseHandler<StoreResult> { private static final Log LOG = LogFactory.getLog(StoreResponseHandler.class); private final Collection<? extends DHTValueEntity> entities; /** * A list of all StoreProcesses. */ private final List<StoreProcess> processes = new ArrayList<StoreProcess>(); /** * An Iterator of StoreProcesses (see processList). */ private Iterator<StoreProcess> toProcess = null; /** * Map of currently active StoreProcesses (see parallelism). */ private Map<KUID, StoreProcess> activeProcesses = new HashMap<KUID, StoreProcess>(); /** * The number of parallel stores. */ private final int parallelism = StoreSettings.PARALLEL_STORES.getValue(); public StoreResponseHandler(Context context, Collection<? extends Entry<? extends Contact, ? extends SecurityToken>> path, Collection<? extends DHTValueEntity> entities) { super(context); this.entities = entities; if (path.size() > KademliaSettings.REPLICATION_PARAMETER.getValue()) { if (LOG.isWarnEnabled()) { LOG.warn("Path is longer than K: " + path.size() + " > " + KademliaSettings.REPLICATION_PARAMETER.getValue()); } } for (Entry<? extends Contact, ? extends SecurityToken> entry : path) { Contact node = entry.getKey(); SecurityToken securityToken = entry.getValue(); if (context.isLocalNode(node)) { processes.add(new LocalStoreProcess(node, securityToken, entities)); } else { processes.add(new RemoteStoreProcess(node, securityToken, entities)); } } } @Override public void start() throws DHTException { toProcess = processes.iterator(); sendNextAndExitIfDone(); } @Override protected void response(ResponseMessage message, long time) throws IOException { Contact node = message.getContact(); KUID nodeId = node.getNodeID(); StoreProcess process = activeProcesses.get(nodeId); if (process != null) { if (process.response(message)) { activeProcesses.remove(nodeId); } } sendNextAndExitIfDone(); } @Override protected void timeout(KUID nodeId, SocketAddress dst, RequestMessage message, long time) throws IOException { StoreProcess process = activeProcesses.get(nodeId); if (process != null) { if (process.timeout(message, time)) { activeProcesses.remove(nodeId); } } sendNextAndExitIfDone(); } @Override protected void error(KUID nodeId, SocketAddress dst, RequestMessage message, IOException e) { StoreProcess process = activeProcesses.get(nodeId); if (process != null) { if (process.error(message, e)) { activeProcesses.remove(nodeId); } } sendNextAndExitIfDone(); } /** * Tries to maintain parallel store requests and fires * an event if storing is done. */ private void sendNextAndExitIfDone() { while(activeProcesses.size() < parallelism && toProcess.hasNext()) { StoreProcess process = toProcess.next(); try { boolean done = process.store(); if (!done) { Contact node = process.getContact(); activeProcesses.put(node.getNodeID(), process); } } catch (IOException err) { process.setIOException(err); process.finish(); LOG.error("IOException", err); } } // No active processes left? We're done! if (activeProcesses.isEmpty()) { done(); } } /** * Called if all values were stored. */ private void done() { Map<Contact, Collection<StoreStatusCode>> map = new LinkedHashMap<Contact, Collection<StoreStatusCode>>(); Map<KUID, Collection<Contact>> locations = new HashMap<KUID, Collection<Contact>>(); for (StoreProcess process : processes) { Contact node = process.getContact(); Collection<StoreStatusCode> statusCodes = process.getStoreStatusCodes(); map.put(node, statusCodes); for (StoreStatusCode statusCode : statusCodes) { if (!statusCode.getStatusCode().equals(StoreResponse.OK)) { continue; } KUID secondaryKey = statusCode.getSecondaryKey(); Collection<Contact> nodes = locations.get(secondaryKey); if (nodes == null) { nodes = new ArrayList<Contact>(); locations.put(secondaryKey, nodes); } nodes.add(node); } } if (processes.size() == 1) { StoreProcess process = processes.get(0); IOException exception = process.getIOException(); long timeout = process.getTimeout(); if (exception != null) { setException(new DHTException(exception)); return; } else if (timeout != -1L) { Contact node = process.getContact(); KUID nodeId = node.getNodeID(); SocketAddress dst = node.getContactAddress(); fireTimeoutException(nodeId, dst, null, timeout); return; } } StoreResult result = new StoreResult(map, entities); setReturnValue(result); } /** * A StoreProcess process manages storing of n values at * a single Node. */ private abstract class StoreProcess { private final Contact node; private final SecurityToken securityToken; private final Collection<? extends DHTValueEntity> entities; private final Iterator<? extends DHTValueEntity> iterator; private final Collection<StoreStatusCode> codes = new ArrayList<StoreStatusCode>(); private IOException exception; private long timeout = -1L; private StoreProcess(Contact node, SecurityToken securityToken, Collection<? extends DHTValueEntity> entities) { this.node = node; this.securityToken = securityToken; this.entities = entities; this.iterator = entities.iterator(); } /** * The Contact where we're storing the values. */ public Contact getContact() { return node; } /** * The SecurityToken we got from the Contact. */ public SecurityToken getSecurityToken() { return securityToken; } /** * List of values we're storing at the Node. */ public Collection<? extends DHTValueEntity> getEntities() { return entities; } /** * Returns true if there are more elements to sore. */ public boolean hasNext() { return iterator.hasNext(); } /** * Returns the next element to store. */ public DHTValueEntity next() { return iterator.next(); } /** * Adds the StoreStatusCode to an internal list of StoreStatusCodes. */ public void addStoreStatusCode(StoreStatusCode code) { codes.add(code); } /** * Returns all StoreStatusCodes. */ public Collection<StoreStatusCode> getStoreStatusCodes() { return codes; } /** * Sets an IOException that may occurred. */ public void setIOException(IOException exception) { this.exception = exception; } /** * Returns an IOException that may occurred. */ public IOException getIOException() { return exception; } /** * Sets a timeout that may occurred. */ public void setTimeout(long timeout) { this.timeout = timeout; } /** * Returns a timeout that may occurred. */ public long getTimeout() { return timeout; } /** * Finishes the lookup process. */ public void finish() { while(hasNext()) { DHTValueEntity entity = next(); addStoreStatusCode(new StoreStatusCode(entity, StoreResponse.ERROR)); } } /** * Starts the StoreProcess. Returns true if storing is done. */ public abstract boolean store() throws IOException; /** * Handles a store response. Returns true if storing is done. */ public abstract boolean response(ResponseMessage msg) throws IOException; /** * Handles a store error. Returns true if storing is done. */ public abstract boolean error(RequestMessage msg, IOException err); /** * Handles a store timeout. Returns true if storing is done. */ public abstract boolean timeout(RequestMessage msg, long timeout) throws IOException; } /** * Stores values at the local Node. */ private class LocalStoreProcess extends StoreProcess { private LocalStoreProcess(Contact node, SecurityToken securityToken, Collection<? extends DHTValueEntity> entities) { super(node, securityToken, entities); } @Override public boolean store() throws IOException { Database database = context.getDatabase(); while(hasNext()) { DHTValueEntity entity = next(); boolean stored = database.store(entity); if (stored) { addStoreStatusCode(new StoreStatusCode(entity, StoreResponse.OK)); } else { addStoreStatusCode(new StoreStatusCode(entity, StoreResponse.ERROR)); } } return true; } @Override public boolean response(ResponseMessage msg) throws IOException { throw new UnsupportedOperationException(); } @Override public boolean error(RequestMessage msg, IOException err) { throw new UnsupportedOperationException(); } @Override public boolean timeout(RequestMessage msg, long time) throws IOException { throw new UnsupportedOperationException(); } } /** * Stores values at a remote Node. */ private class RemoteStoreProcess extends StoreProcess { private DHTValueEntity currentEntity = null; private RemoteStoreProcess(Contact node, SecurityToken securityToken, Collection<? extends DHTValueEntity> entities) { super(node, securityToken, entities); } @Override public boolean store() throws IOException { currentEntity = null; // Nothing left? We're done! if (!hasNext()) { return true; } // Get the next value and try to store it currentEntity = next(); StoreRequest request = context.getMessageHelper() .createStoreRequest(getContact().getContactAddress(), getSecurityToken(), Collections.singleton(currentEntity)); context.getMessageDispatcher().send(getContact(), request, StoreResponseHandler.this); return false; } @Override public boolean response(ResponseMessage msg) throws IOException { StoreResponse response = (StoreResponse)msg; Collection<StoreStatusCode> codes = response.getStoreStatusCodes(); // We store one value per request! If the remote Node // sends us a different number of StoreStatusCodes back // then there is something wrong! if (codes.size() != 1) { if (LOG.isErrorEnabled()) { LOG.error(getContact() + " sent a wrong number of StoreStatusCodes: " + codes); } // Exit finish(); return true; } // The returned StoreStatusCode must have the same primary and // secondaryKeys as the value we requested to store. StoreStatusCode code = codes.iterator().next(); if (!code.isFor(currentEntity)) { if (LOG.isErrorEnabled()) { LOG.error(getContact() + " sent a wrong [" + code + "] for " + currentEntity + "\n" + CollectionUtils.toString(getEntities())); } // Exit finish(); return true; } // Store next value return store(); } @Override public boolean error(RequestMessage msg, IOException err) { if (LOG.isErrorEnabled()) { LOG.error("Couldn't store " + currentEntity + " at " + getContact(), err); } setIOException(err); addStoreStatusCode(new StoreStatusCode(currentEntity, StoreResponse.ERROR)); try { return store(); } catch (IOException iox) { if (LOG.isErrorEnabled()) { LOG.error("IOException", iox); } // Exit finish(); return true; } } @Override public boolean timeout(RequestMessage msg, long timeout) throws IOException { if (LOG.isInfoEnabled()) { LOG.info("Couldn't store " + currentEntity + " at " + getContact()); } setTimeout(timeout); addStoreStatusCode(new StoreStatusCode(currentEntity, StoreResponse.ERROR)); return store(); } @Override public void finish() { if (currentEntity != null) { addStoreStatusCode(new StoreStatusCode(currentEntity, StoreResponse.ERROR)); currentEntity = null; } super.finish(); } } }