package com.robonobo.mina.instance; import static com.robonobo.common.util.TimeUtil.*; import java.util.*; import org.apache.commons.logging.Log; import com.robonobo.common.concurrent.*; import com.robonobo.common.exceptions.SeekInnerCalmException; import com.robonobo.core.api.proto.CoreApi.Node; import com.robonobo.mina.message.proto.MinaProtocol.DontWantSource; import com.robonobo.mina.message.proto.MinaProtocol.ReqSourceStatus; import com.robonobo.mina.message.proto.MinaProtocol.SourceStatus; import com.robonobo.mina.message.proto.MinaProtocol.StreamStatus; import com.robonobo.mina.message.proto.MinaProtocol.WantSource; import com.robonobo.mina.network.ControlConnection; /** Handles requesting and caching of source info * * @author macavity */ public class SourceMgr { static final int SOURCE_CHECK_FREQ = 30; // Secs private MinaInstance mina; Log log; /** Map<StreamId, Map<NodeId, SourceStatus>> - oh for typedefs */ private Map<String, Map<String, SourceStatus>> readySources = new HashMap<String, Map<String, SourceStatus>>(); /** Stream IDs that want sources */ private Set<String> wantSources = new HashSet<String>(); /** Information on possible sources, including when to query them next */ private Map<String, SourceDetails> possSourcesById = new HashMap<String, SourceDetails>(); /** Which sources to query next - this is not kept updated for performance, so there might be stale entries in here - * possSourcesById contains authoritative next query times */ private Queue<PossibleSource> possSourceQ = new PriorityQueue<PossibleSource>(); /** These are sources that have at least one endpoint, but that we can't connect to - we keep them around in case our * connection status changes */ private Map<String, Set<Node>> unreachableSources = new HashMap<String, Set<Node>>(); private Timeout queryTimeout; /** Batch up source requests, to avoid repeated requests to the same nodes */ private WantSourceBatcher wsBatch; private Map<String, ReqSourceStatusBatcher> rssBatchers = new HashMap<String, ReqSourceStatusBatcher>(); public SourceMgr(MinaInstance mina) { this.mina = mina; log = mina.getLogger(getClass()); wsBatch = new WantSourceBatcher(); queryTimeout = new Timeout(mina.getExecutor(), new CatchingRunnable() { public void doRun() throws Exception { querySources(); } }); } public void stop() { queryTimeout.cancel(); } /** Tells the network we want sources */ public void wantSources(String sid) { synchronized (this) { if (wantSources.contains(sid)) return; wantSources.add(sid); } if (mina.getBidStrategy().tolerateDelay(sid)) wsBatch.add(sid); else { WantSource ws = WantSource.newBuilder().addStreamId(sid).build(); mina.getCCM().sendMessageToNetwork("WantSource", ws); } } public synchronized List<String> sidsWantingSources() { List<String> result = new ArrayList<String>(); result.addAll(wantSources); return result; } public void dontWantSources(String streamId) { synchronized (this) { if (!wantSources.contains(streamId)) return; wantSources.remove(streamId); readySources.remove(streamId); unreachableSources.remove(streamId); } // We don't send DontWantSources on shutdown, so don't bother batching DontWantSource dws = DontWantSource.newBuilder().addStreamId(streamId).build(); mina.getCCM().sendMessageToNetwork("DontWantSource", dws); } /** @syncpriority 180 */ public void gotSource(String streamId, Node source) { synchronized (this) { if (!wantSources.contains(streamId)) return; } if (source.getId().equals(mina.getMyNodeId().toString())) return; ControlConnection cc = mina.getCCM().getCCWithId(source.getId()); if (cc != null && cc.getLCPair(streamId) != null) return; if (mina.getBadNodeList().checkBadNode(source.getId())) { log.debug("Ignoring Bad source " + source.getId()); return; } if (mina.getNetMgr().canConnectTo(source)) { cacheSourceInitially(source, streamId); if (log.isDebugEnabled()) log.debug("Querying source " + source.getId() + " for stream " + streamId); if (mina.getBidStrategy().tolerateDelay(streamId)) { ReqSourceStatusBatcher rssb; synchronized (this) { if (rssBatchers.containsKey(source.getId())) rssb = rssBatchers.get(source.getId()); else { rssb = new ReqSourceStatusBatcher(source); rssBatchers.put(source.getId(), rssb); } } rssb.add(streamId); } else { ReqSourceStatus.Builder rssb = ReqSourceStatus.newBuilder(); rssb.addStreamId(streamId); sendReqSourceStatus(source, rssb); } } else { // We can't connect to them - if they have an endpoint, keep them about, our connection status might change // - in particular, we might find we can do nat traversal if (source.getEndPointCount() > 0) { log.debug("Keep unreachable source " + source.getId() + " in case our network connection changes"); synchronized (this) { if (!unreachableSources.containsKey(streamId)) unreachableSources.put(streamId, new HashSet<Node>()); unreachableSources.get(streamId).add(source); } } } } /** Our endpoints have changed, we might be able to contact some sources we couldn't before */ public void networkDetailsChanged() { Map<String, Set<Node>> sourceCopy; synchronized (this) { sourceCopy = unreachableSources; unreachableSources = new HashMap<String, Set<Node>>(); } for (String streamId : sourceCopy.keySet()) { for (Node node : sourceCopy.get(streamId)) { gotSource(streamId, node); } } } /** @syncpriority 200 */ public void gotSourceStatus(SourceStatus sourceStat) { // Remove it from our list of waiting sources - sm.foundSource() might add it again synchronized (this) { String sourceId = sourceStat.getFromNode().getId(); SourceDetails sd = possSourcesById.get(sourceId); if (sd != null) { for (StreamStatus ss : sourceStat.getSsList()) { sd.streamIds.remove(ss.getStreamId()); } if (sd.streamIds.size() == 0) possSourcesById.remove(sourceId); } } for (StreamStatus streamStat : sourceStat.getSsList()) { synchronized (this) { if (!wantSources.contains(streamStat.getStreamId())) continue; } mina.getStreamMgr().foundSource(sourceStat, streamStat); } } /** Called when this source does not have a listener slot open, or else one is too expensive */ public void cacheSourceUntilAgoricsAcceptable(Node node, String streamId) { cacheSourceUntil(node, streamId, 1000 * mina.getConfig().getSourceAgoricsFailWaitTime(), "agorics unacceptable"); } /** Called when this source does not enough data to serve us */ public void cacheSourceUntilDataAvailable(Node node, String streamId) { cacheSourceUntil(node, streamId, 1000 * mina.getConfig().getSourceDataFailWaitTime(), "no useful data"); } /** When a connection to a node dies unexpectedly, it might be network randomness between us and them, so wait for a * while then retry them */ public void cachePossiblyDeadSource(Node node, String streamId) { cacheSourceUntil(node, streamId, 1000 * mina.getConfig().getDeadSourceQueryTime(), "network issue"); } private void cacheSourceInitially(Node node, String streamId) { cacheSourceUntil(node, streamId, 1000 * mina.getConfig().getInitialSourceQueryTime(), "initial query"); } /** Must only be called from inside sync block! */ private void setTimeout() { PossibleSource ps = possSourceQ.peek(); if (ps == null) { queryTimeout.clear(); return; } queryTimeout.set(msUntil(ps.nextQ)); } private synchronized void cacheSourceUntil(Node node, String streamId, int waitMs, String reason) { Date nextQ = timeInFuture(waitMs); SourceDetails sd; if (log.isDebugEnabled()) log.debug("Caching source " + node.getId() + " for stream " + streamId + " until " + getTimeFmt().format(nextQ)); boolean addSourceToQ = false; if (possSourcesById.containsKey(node.getId())) { sd = possSourcesById.get(node.getId()); if (nextQ.before(sd.nextQ)) { sd.nextQ = nextQ; addSourceToQ = true; } if (!sd.streamIds.contains(streamId)) sd.streamIds.add(streamId); } else { sd = new SourceDetails(node, nextQ, waitMs * 2); sd.streamIds.add(streamId); addSourceToQ = true; possSourcesById.put(node.getId(), sd); } // This might result in duplicate entries in possSourceQ, but we live with that by checking the nextQ time in // possSourcesById when we pop off possSourceQ if (addSourceToQ) { possSourceQ.add(new PossibleSource(node.getId(), nextQ)); setTimeout(); } } /** Query all sources whose time has come */ private void querySources() { while (true) { SourceDetails sd; synchronized (this) { if (possSourceQ.size() == 0) return; PossibleSource ps = possSourceQ.peek(); Date now = now(); if (ps.nextQ.after(now)) { setTimeout(); return; } possSourceQ.remove(); sd = possSourcesById.get(ps.nodeId); if (sd == null || sd.nextQ.after(now)) { // Duff entry in possSourceQ, just continue continue; } Node source = sd.node; possSourcesById.remove(source.getId()); // Check that we still want sources for all these streams boolean wantIt = false; for (String sid : sd.streamIds) { if (wantSources.contains(sid)) { wantIt = true; break; } } if (!wantIt) continue; // Re-add it again in case it doesn't answer - if it does, it'll get removed sd.retries = sd.retries + 1; sd.nextQ = timeInFuture(sd.retryAfterMs); sd.retryAfterMs = Math.min(sd.retryAfterMs * 2, mina.getConfig().getMaxSourceQueryTime() * 1000); possSourceQ.add(new PossibleSource(source.getId(), sd.nextQ)); possSourcesById.put(source.getId(), sd); if (log.isDebugEnabled()) log.debug("Setting retry time for possible source " + source.getId() + " to " + getTimeFmt().format(sd.nextQ)); } String sourceId = sd.node.getId(); if (log.isDebugEnabled()) log.debug("Querying source " + sourceId + " for streams " + sd.streamIds); ReqSourceStatusBatcher rssb; synchronized (this) { if (rssBatchers.containsKey(sourceId)) rssb = rssBatchers.get(sourceId); else { rssb = new ReqSourceStatusBatcher(sd.node); rssBatchers.put(sourceId, rssb); } } rssb.addAll(sd.streamIds); } } private void DEBUG_checkSourceStatContains(SourceStatus ss, String sid) { for (StreamStatus strStat : ss.getSsList()) { if(strStat.getStreamId().equals(sid)) return; } throw new SeekInnerCalmException(); } /** Called when this source is good to service us, but we are not ready or able to handle it */ public synchronized void cacheSourceUntilReady(SourceStatus sourceStat, StreamStatus streamStat) { if(streamStat == null) throw new SeekInnerCalmException(); DEBUG_checkSourceStatContains(sourceStat, streamStat.getStreamId()); if (!readySources.containsKey(streamStat.getStreamId())) readySources.put(streamStat.getStreamId(), new HashMap<String,SourceStatus>()); readySources.get(streamStat.getStreamId()).put(sourceStat.getFromNode().getId(), sourceStat); } /** Returns the set of ready sources, and removes trace of them - if you want to cache them, add them again */ public synchronized Set<SourceStatus> getReadySources(String streamId) { Set<SourceStatus> result = new HashSet<SourceStatus>(); if (readySources.containsKey(streamId)) result.addAll(readySources.remove(streamId).values()); for (SourceStatus ss : result) { DEBUG_checkSourceStatContains(ss, streamId); } return result; } /** Returns the set of ready nodes, but doesn't remove trace of them */ public synchronized Set<Node> getReadyNodes(String streamId) { Set<Node> result = new HashSet<Node>(); for (SourceStatus ss : readySources.get(streamId).values()) { result.add(ss.getFromNode()); } return result; } /** Returns the set of ready nodes, but doesn't remove trace of them */ public synchronized Set<String> getReadyNodeIds(String streamId) { Set<String> result = new HashSet<String>(); if (readySources.containsKey(streamId)) result.addAll(readySources.get(streamId).keySet()); return result; } public synchronized int numReadySources(String streamId) { if (!readySources.containsKey(streamId)) return 0; return readySources.get(streamId).size(); } private void sendReqSourceStatus(Node source, ReqSourceStatus.Builder sourceBldr) { ControlConnection cc = mina.getCCM().getCCWithId(source.getId()); // Use the right descriptor depending on whether they're a // local conn or not if (cc != null) { cc.sendMessage("ReqSourceStatus", sourceBldr.build()); } else { sourceBldr.setFromNode(mina.getNetMgr().getDescriptorForTalkingTo(source, false)); sourceBldr.setToNodeId(source.getId()); mina.getCCM().sendMessageToSupernodes("ReqSourceStatus", sourceBldr.build()); } } class PossibleSource implements Comparable<PossibleSource> { String nodeId; Date nextQ; public PossibleSource(String nodeId, Date nextQ) { this.nodeId = nodeId; this.nextQ = nextQ; } @Override public int compareTo(PossibleSource o) { return nextQ.compareTo(o.nextQ); } } /** A source that we do not need now, but which we might need in a bit */ class SourceDetails { Node node; Set<String> streamIds = new HashSet<String>(); Date nextQ; int retryAfterMs; int retries = 0; public SourceDetails(Node node, Date nextQ, int retryAfterMs) { this.node = node; this.nextQ = nextQ; this.retryAfterMs = retryAfterMs; } } class WantSourceBatcher extends Batcher<String> { WantSourceBatcher() { super(mina.getConfig().getSourceRequestBatchTime(), mina.getExecutor()); } @Override protected void runBatch(Collection<String> streamIds) { WantSource ws = WantSource.newBuilder().addAllStreamId(streamIds).build(); mina.getCCM().sendMessageToNetwork("WantSource", ws); } } /** Use UniqueBatcher here as if we get multiple GotSources for the same node in quick succession, we'll have * duplicate stream ids * * @author macavity */ class ReqSourceStatusBatcher extends UniqueBatcher<String> { Node source; ReqSourceStatusBatcher(Node source) { super(mina.getConfig().getSourceRequestBatchTime(), mina.getExecutor()); this.source = source; } @Override protected void runBatch(Collection<String> streamIdCol) { synchronized (SourceMgr.this) { rssBatchers.remove(source.getId()); } ReqSourceStatus.Builder rssb = ReqSourceStatus.newBuilder(); rssb.addAllStreamId(streamIdCol); sendReqSourceStatus(source, rssb); } } }