/* * Part of the CCNx Java Library. * * Copyright (C) 2012 Palo Alto Research Center, Inc. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. * This library 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 * Lesser General Public License for more details. You should have received * a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301 USA. */ package org.ccnx.ccn.impl.sync; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.logging.Level; import org.ccnx.ccn.CCNContentHandler; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.CCNInterestHandler; import org.ccnx.ccn.CCNSyncHandler; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.content.ConfigSlice; import org.ccnx.ccn.profiles.sync.Sync; import org.ccnx.ccn.protocol.Component; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.ContentObject; import org.ccnx.ccn.protocol.Interest; /** * Snoops on the sync protocol to report new files seen by sync on a slice to a registered handler * To do this we use "comparators" which compare a sync tree of "what we already have" to a sync tree * from sync (@see {@link SliceComparator}). We can have multiple monitors on the same slice and initially * these will each need their own comparator since the starting point of what we will monitor depends on * user parameters. However once we have completed an initial round of comparison on a slice, we will have * created a tree of "what we have" which will be the same for every comparator, and since all input from * sync will also be identical, we could end up with multiple comparators all doing exactly the same thing. * To avoid this, we introduce the "ComparatorGroup" which is per slice and contains a "lead comparator" and * other "active comparators". The first comparator created on a slice becomes the "lead comparator" and * subsequent ones are added to the active comparators. After a comparator which is not a lead comparator has * completed its first round, its callback is added to the lead comparators callbacks and that comparator * becomes deactivated. Except in the case noted below, this takes place within the comparator since it * alone knows when it has completed a round. * * Note also that all comparators can share the same node data but must keep their own version of where they * are in the treewalk through the data. Node data is shared in _snc below. */ public class ProtocolBasedSyncMonitor extends SyncMonitor implements CCNContentHandler, CCNInterestHandler { private class SliceData { protected SyncNodeCache _snc = new SyncNodeCache(); SliceComparator _leadComparator; ArrayList<SliceComparator> _activeComparators; public SliceData() { _activeComparators = new ArrayList<SliceComparator>(); } public void setLeadComparator(SliceComparator sc) { _leadComparator = sc; } } protected CCNHandle _handle; protected HashMap<SyncHashEntry, SliceData> _sliceData = new HashMap<SyncHashEntry, SliceData>(); public ProtocolBasedSyncMonitor(CCNHandle handle) { _handle = handle; } /** * Register callback and output a root advise interest to start the process of looking at sync * hashes to find new files. We also start looking at interests for root advises on this slice * which are used to notice new hashes coming through which may contain unseen files. */ public void registerCallback(CCNSyncHandler syncHandler, ConfigSlice slice, byte[] startHash, ContentName startName) throws IOException { boolean sendRootAdviseRequest = true; synchronized (this) { SyncHashEntry she = new SyncHashEntry(slice.getHash()); SliceData sd = _sliceData.get(she); if (null != sd && null != startHash && startHash.length == 0) { // For 0 length hash (== start with current hash) we can just add the handler to the leadComparator if there is one since it should // already know the latest hash sd._leadComparator.addCallback(syncHandler, startHash); sendRootAdviseRequest = false; } else { boolean newData = false; if (null == sd) { newData = true; sd = new SliceData(); } SliceComparator sc = new SliceComparator(newData ? null : sd._leadComparator, sd._snc, syncHandler, slice, startHash, startName, _handle); if (newData) sd.setLeadComparator(sc); sd._activeComparators.add(sc); _sliceData.put(she, sd); } } if (sendRootAdviseRequest) { ContentName rootAdvise = new ContentName(slice.topo, Sync.SYNC_ROOT_ADVISE_MARKER, slice.getHash()); Interest interest = new Interest(rootAdvise); interest.scope(1); _handle.registerFilter(rootAdvise, this); _handle.expressInterest(interest, this); } } /** * We don't want to shutdown just because there are no more callbacks because if someone asks * for a new sync starting at the current hash, having something running is the best way to * find the current hash. If someone wants to shutdown, they should explicitly ask for it. */ public void removeCallback(CCNSyncHandler syncHandler, ConfigSlice slice) { ArrayList<SliceComparator> removes = new ArrayList<SliceComparator>(); SyncHashEntry she = new SyncHashEntry(slice.getHash()); synchronized (this) { SliceData sd = _sliceData.get(she); if (null != sd) { for (SliceComparator sc : sd._activeComparators) { sc.removeCallback(syncHandler); if (sc != sd._leadComparator) { if (sc.shutdownIfUseless()) removes.add(sc); } } sd._activeComparators.removeAll(removes); } } } public void shutdown(ConfigSlice slice) { if (Log.isLoggable(Log.FAC_SYNC, Level.INFO)) Log.info(Log.FAC_SYNC, "Shutting down sync on slice: {0}", slice.prefix); SyncHashEntry she = new SyncHashEntry(slice.getHash()); synchronized (this) { SliceData sd = _sliceData.get(she); if (null != sd) { // remove all callbacks - therefore shutting down all current comparators except // the lead for (SliceComparator sc : sd._activeComparators) { ArrayList<CCNSyncHandler> callbacks = sc.getCallbacks(); ArrayList<CCNSyncHandler> shutdownCallbacks = new ArrayList<CCNSyncHandler>(); shutdownCallbacks.addAll(callbacks); for (CCNSyncHandler syncHandler : shutdownCallbacks) sc.removeCallback(syncHandler); } // Now shutdown the lead sd._leadComparator.shutdownIfUseless(); _sliceData.remove(she); } } } /** * Output interest to request a node * @param hash * @return * @throws IOException */ public static boolean requestNode(ConfigSlice slice, byte[] hash, CCNHandle handle, CCNContentHandler handler) throws SyncException { boolean ret = false; Interest interest = new Interest(new ContentName(slice.topo, Sync.SYNC_NODE_FETCH_MARKER, slice.getHash(), hash)); interest.scope(1); if (Log.isLoggable(Log.FAC_SYNC, Level.FINE)) Log.fine(Log.FAC_TEST, "Requesting node for hash: {0}", interest.name()); try { handle.expressInterest(interest, handler); ret = true; } catch (IOException e) { Log.warning(Log.FAC_SYNC, "Node request failed: {0}", e.getMessage()); throw new SyncException(e.getMessage()); } return ret; } /** * Start sync hash compare process after receiving content back from the * original root advise interest. */ public Interest handleContent(ContentObject data, Interest interest) { ContentName name = data.name(); int hashComponent = name.containsWhere(Sync.SYNC_ROOT_ADVISE_MARKER); if (hashComponent < 0 || name.count() < hashComponent + 3) { if (Log.isLoggable(Log.FAC_SYNC, Level.INFO)) Log.info(Log.FAC_SYNC, "Received incorrect content in sync: {0}", name); return null; } if (Log.isLoggable(Log.FAC_SYNC, Level.INFO)) Log.info(Log.FAC_SYNC, "Saw new content from sync: {0}", name); SliceData sd = null; synchronized (this) { sd = _sliceData.get(new SyncHashEntry(name.component(hashComponent + 1))); } if (null != sd) { for (SliceComparator sc : sd._activeComparators) { synchronized (sc) { sc.addPendingContent(data.content()); sc.checkNextRound(); sc.kickCompare(); } } } return null; } /** * We have seen a new hash. Feed it into the compare process to discover possible new * unseen files. */ public boolean handleInterest(Interest interest) { Log.info("Saw an interest for {0}", interest.name()); ContentName name = interest.name(); int hashComponent = name.containsWhere(Sync.SYNC_ROOT_ADVISE_MARKER); if (hashComponent < 0 || name.count() < hashComponent + 3) { return false; } byte[] hash = name.component(hashComponent + 2); if (hash.length == 0) return false; SliceData cg = null; synchronized (this) { cg = _sliceData.get(new SyncHashEntry(name.component(hashComponent + 1))); if (Log.isLoggable(Log.FAC_SYNC, Level.INFO)) Log.info(Log.FAC_SYNC, "Saw data from interest: hash: {0}", Component.printURI(hash)); if (null != cg) { for (SliceComparator sc : cg._activeComparators) { SyncTreeEntry ste = sc.getHashCache().addHash(hash, sc.getNodeCache()); if (sc == cg._leadComparator || !sc.shutdownIfUseless()) { if (sc.addPending(ste)) { sc.checkNextRound(); sc.kickCompare(); } } } } } return false; // We're just snooping so don't say we've handled this } public SyncNodeCache getNodeCache(ConfigSlice slice) { SyncHashEntry she = new SyncHashEntry(slice.getHash()); synchronized (this) { SliceData sd = _sliceData.get(she); if (null == sd) return null; return sd._snc; } } }