package com.robonobo.mina.instance;
import java.io.IOException;
import java.util.*;
import org.apache.commons.logging.Log;
import com.robonobo.common.concurrent.CatchingRunnable;
import com.robonobo.common.exceptions.SeekInnerCalmException;
import com.robonobo.core.api.proto.CoreApi.EndPoint;
import com.robonobo.core.api.proto.CoreApi.Node;
import com.robonobo.mina.agoric.AuctionState;
import com.robonobo.mina.external.FoundSourceListener;
import com.robonobo.mina.external.buffer.*;
import com.robonobo.mina.message.proto.MinaProtocol.SourceStatus;
import com.robonobo.mina.message.proto.MinaProtocol.StreamStatus;
import com.robonobo.mina.message.proto.MinaProtocol.UnAdvSource;
import com.robonobo.mina.network.*;
/**
* Main class for dealing with stream broadcast and reception.
*
* @author macavity
* @syncpriority 200
*/
public class StreamMgr {
protected Log log;
protected MinaInstance mina;
protected Set<String> broadcastingSids = new HashSet<String>();
protected Set<String> receivingSids = new HashSet<String>();
protected Set<String> rebroadcastingSids = new HashSet<String>();
protected Map<String, Set<FoundSourceListener>> listeners = new HashMap<String, Set<FoundSourceListener>>();
protected Map<String, Integer> streamPriorities = new HashMap<String, Integer>();
/**
* We keep page buffers for 'live' streams - those that either we are receiving, or that we are actively
* broadcasting to one or more listeners
*/
protected Map<String, PageBuffer> liveStreams = new HashMap<String, PageBuffer>();
public StreamMgr(MinaInstance mina) {
this.mina = mina;
log = mina.getLogger(getClass());
}
/**
* Checks to see if we still want sources, and if we don't, notifies the network
*
* @syncpriority 200
*/
private boolean checkWantingSources(String sid) {
// Don't do anything if we are shutting down, when we disconnect the
// supernode will figure it out
if (mina.getCCM().isShuttingDown())
return false;
boolean wantingSources = false;
synchronized (this) {
if (receivingSids.contains(sid))
wantingSources = true;
else if (listeners.containsKey(sid) && listeners.get(sid).size() > 0)
wantingSources = true;
}
if (!wantingSources)
mina.getSourceMgr().dontWantSources(sid);
return wantingSources;
}
/**
* Makes this stream live - must be called only inside sync block
*/
private void wakePageBuf(String sid) {
if (!liveStreams.containsKey(sid))
liveStreams.put(sid, mina.getPageBufProvider().getPageBuf(sid));
}
/**
* Must be called only inside sync block
*/
private void sleepPageBuf(String sid) {
PageBuffer pageBuf = liveStreams.remove(sid);
if (pageBuf != null) {
try {
pageBuf.sleep();
} catch (IOException e) {
log.error("Error sleeping pagebuf for stream " + sid, e);
}
}
}
/**
* @return 'Live' streams are those that either we are receiving, or that we are actively broadcasting to one or
* more listeners
* @syncpriority 200
*/
public synchronized String[] getLiveStreamIds() {
String[] result = new String[liveStreams.size()];
liveStreams.keySet().toArray(result);
return result;
}
/**
* @return The stream ids that are receiving. Safe to iterate over.
* @syncpriority 200
*/
public synchronized String[] getReceivingStreamIds() {
String[] result = new String[receivingSids.size()];
receivingSids.toArray(result);
return result;
}
/**
* @return The stream ids that are [re]broadcasting. Safe to iterate over.
* @syncpriority 200
*/
public synchronized List<String> getAdvertisingStreamIds() {
List<String> result = new ArrayList<String>();
result.addAll(broadcastingSids);
result.addAll(rebroadcastingSids);
return result;
}
/**
* @syncpriority 200
*/
public synchronized void clearStreamPriorities() {
streamPriorities.clear();
}
/**
* @syncpriority 200
*/
public synchronized void addFoundSourceListener(String sid, FoundSourceListener listener) {
boolean sendWantSources = false;
if (!listeners.containsKey(sid)) {
sendWantSources = true;
listeners.put(sid, new HashSet<FoundSourceListener>());
}
listeners.get(sid).add(listener);
if (sendWantSources)
mina.getSourceMgr().wantSources(sid);
}
/**
* @syncpriority 200
*/
public synchronized void removeFoundSourceListener(String sid, FoundSourceListener listener) {
Set<FoundSourceListener> set = listeners.get(sid);
if (set != null) {
set.remove(listener);
if (set.size() == 0)
listeners.remove(sid);
}
checkWantingSources(sid);
}
/**
* @syncpriority 180
*/
public Set<Node> getKnownSources(String sid) {
Set<Node> result = mina.getSourceMgr().getReadyNodes(sid);
for (ConnectionPair lcp : mina.getSCM().getListenConns(sid)) {
result.add(lcp.getCC().getNode());
}
return result;
}
public Set<String> getSourceNodeIds(String sid) {
Set<String> result = mina.getSourceMgr().getReadyNodeIds(sid);
for (ConnectionPair lcp : mina.getSCM().getListenConns(sid)) {
result.add(lcp.getCC().getNode().getId());
}
return result;
}
/**
* @syncpriority 160
*/
public int numSources(String sid) {
int result = mina.getSourceMgr().numReadySources(sid);
result += mina.getSCM().getNumListenConns(sid);
return result;
}
public StreamStatus buildStreamStatus(String sid, String toNodeId) {
StreamStatus.Builder bldr = StreamStatus.newBuilder();
if (toNodeId != null) {
bldr.setToNodeId(toNodeId);
bldr.setFromNodeId(mina.getMyNodeId());
}
bldr.setStreamId(sid);
PageBuffer pageBuf;
synchronized (this) {
if (liveStreams.containsKey(sid))
pageBuf = liveStreams.get(sid);
else
pageBuf = mina.getPageBufProvider().getPageBuf(sid);
}
if(pageBuf == null) {
log.error("Asked to provide StreamStatus for stream "+sid+", but no pagebuffer is available for that stream");
return null;
}
if (pageBuf.getTotalPages() > 0)
bldr.setTotalPages(pageBuf.getTotalPages());
StreamPosition sp = pageBuf.getStreamPosition();
bldr.setLastContiguousPage(sp.getLastContiguousPage());
if (sp.getPageMap() > 0)
bldr.setPageMap(sp.getPageMap());
return bldr.build();
}
/**
* @syncpriority 200
*/
public void foundSource(SourceStatus sourceStat, StreamStatus streamStat) {
String fromNodeId = sourceStat.getFromNode().getId();
String sid = streamStat.getStreamId();
// If we're already listening to this guy, ignore it
ControlConnection cc = mina.getCCM().getCCWithId(fromNodeId);
if (cc != null && cc.getLCPair(sid) != null)
return;
synchronized (this) {
// If we're not receiving, then we're just caching sources to pass to an external listener - so cache it
if (!receivingSids.contains(sid)) {
mina.getSourceMgr().cacheSourceUntilReady(sourceStat, streamStat);
notifyListenersOfSource(sid, fromNodeId);
return;
}
// If we haven't learned the total number of pages for this stream, do it now
if (streamStat.getTotalPages() > 0) {
PageBuffer pageBuf = liveStreams.get(sid);
if (pageBuf == null) {
log.error("Found source for stream " + sid + ", but that stream is not live");
return;
}
if (pageBuf.getTotalPages() <= 0)
pageBuf.setTotalPages(streamStat.getTotalPages());
}
}
// Check agorics are acceptable
if (mina.getConfig().isAgoric()) {
AuctionState as = new AuctionState(sourceStat.getAuctionState());
if (!mina.getBidStrategy().worthConnectingTo(sid, as)) {
log.debug("Not connecting to node " + fromNodeId + " for " + sid + " - bid strategy says no");
mina.getSourceMgr().cacheSourceUntilAgoricsAcceptable(sourceStat.getFromNode(), sid);
return;
}
if (!mina.getBuyMgr().canListenTo(as, mina.getBidStrategy().getStreamVelocity(sid))) {
log.debug("Not connecting to node " + fromNodeId + " for " + sid
+ " - no available listener slots at my price level");
mina.getSourceMgr().cacheSourceUntilAgoricsAcceptable(sourceStat.getFromNode(), sid);
return;
}
}
// Check that they have data useful to us
StreamPosition sp = new StreamPosition(streamStat.getLastContiguousPage(), streamStat.getPageMap());
if (!mina.getPRM().isUsefulSource(sid, sp)) {
// Useless at the moment - cache them and ask again later
mina.getSourceMgr().cacheSourceUntilDataAvailable(sourceStat.getFromNode(), sid);
return;
}
// This is a useful source - let our listeners know
notifyListenersOfSource(sid, fromNodeId);
// If we're already listening to our max sources, cache this one
if (mina.getSCM().getNumListenConns(sid) >= mina.getConfig().getMaxSources()) {
mina.getSourceMgr().cacheSourceUntilReady(sourceStat, streamStat);
return;
}
// Let's do this thing
mina.getSCM().makeListenConnectionTo(sid, sourceStat);
}
/**
* @syncpriority 200
*/
public synchronized void broadcastTo(String sid, ControlConnection cc, EndPoint listenEp, List<Long> pages) {
wakePageBuf(sid);
mina.getSCM().makeBroadcastConnectionTo(sid, cc, listenEp, pages);
}
private void notifyListenersOfSource(final String sid, final String sourceId) {
final FoundSourceListener[] lisArr;
synchronized (this) {
Set<FoundSourceListener> set = listeners.get(sid);
if (set == null)
return;
lisArr = new FoundSourceListener[set.size()];
set.toArray(lisArr);
}
mina.getExecutor().execute(new CatchingRunnable() {
public void doRun() throws Exception {
for (FoundSourceListener listener : lisArr) {
listener.foundBroadcaster(sid, sourceId);
}
}
});
}
/**
* @syncpriority 200
*/
public void receivePage(String sid, Page p) {
if (!isReceiving(sid))
return;
PageBuffer pageBuf;
synchronized (this) {
pageBuf = liveStreams.get(sid);
if (pageBuf == null) {
log.error("Received page " + p.getPageNumber() + " for stream " + sid
+ ", but that streams is not live");
return;
}
}
try {
pageBuf.putPage(p);
} catch (IOException e) {
log.error("Error putting page into buffer for stream " + sid, e);
stopReception(sid);
return;
}
mina.getPRM().notifyPageReceived(sid, p.getPageNumber());
updateStreamStatus(sid);
if (isReceiving(sid) && !isRebroadcasting(sid))
startRebroadcast(sid);
if (pageBuf.getLastContiguousPage() == (pageBuf.getTotalPages() - 1))
receptionCompleted(sid);
}
/**
* @syncpriority 160
*/
private void updateStreamStatus(String sid) {
StreamStatus ss = buildStreamStatus(sid, null);
for (BCPair bcPair : mina.getSCM().getBroadcastConns(sid)) {
bcPair.getCC().sendMessage("StreamStatus", ss);
}
}
/**
* @syncpriority 200
*/
public synchronized boolean isBroadcasting(String sid) {
return broadcastingSids.contains(sid);
}
/**
* @syncpriority 200
*/
public synchronized boolean isRebroadcasting(String sid) {
return rebroadcastingSids.contains(sid);
}
/**
* @syncpriority 200
*/
public boolean isReceiving(String sid) {
return receivingSids.contains(sid);
}
/**
* @syncpriority 200
*/
public void notifyDeadConnection(ConnectionPair pair) {
ControlConnection cc = pair.getCC();
String nid = cc.getNodeId();
String sid = pair.getStreamId();
synchronized (this) {
mina.getSCM().removeConnectionPair(pair);
if (pair instanceof LCPair) {
mina.getPRM().notifyDeadConnection(sid, nid);
if (mina.getSCM().getNumListenConns(sid) < mina.getConfig().getMaxSources())
requestCachedSources(sid);
if (cc.getLCPairs().length == 0)
mina.getBidStrategy().cleanupNode(nid);
// Make note of them in case they come back
LCPair lcp = (LCPair) pair;
mina.getSourceMgr().cachePossiblyDeadSource(lcp.getCC().getNode(), sid);
} else {
if (mina.getSCM().getNumBroadcastConns(sid) == 0 && !receivingSids.contains(sid))
sleepPageBuf(sid);
}
}
if (isReceiving(sid))
mina.getEventMgr().fireReceptionConnsChanged(sid);
}
/**
* @syncpriority 200
*/
public void requestCachedSources(String sid) {
if (!isReceiving(sid))
return;
Set<SourceStatus> sources = mina.getSourceMgr().getReadySources(sid);
for (SourceStatus sourceStat : sources) {
// This is a bit kludgy - get the streamstatus that applies to us
StreamStatus streamStat = null;
for (StreamStatus testSs : sourceStat.getSsList()) {
if (testSs.getStreamId().equals(sid)) {
streamStat = testSs;
break;
}
}
if (streamStat == null)
throw new SeekInnerCalmException();
foundSource(sourceStat, streamStat);
}
}
/**
* @syncpriority 200
*/
public synchronized void startBroadcast(String sid) {
if (broadcastingSids.contains(sid))
throw new SeekInnerCalmException();
log.info("Starting broadcast for stream " + sid);
broadcastingSids.add(sid);
// If we're already rebroadcasting, this means that we've completed
// reception and are now a broadcaster - so we don't need to announce
// ourselves again
if (rebroadcastingSids.contains(sid))
rebroadcastingSids.remove(sid);
else
mina.getStreamAdvertiser().advertiseStream(sid);
}
/**
* Bulk-add broadcasts - called at startup, we might have 10^4+ available broadcasting streams
*
* @syncpriority 200
*/
public synchronized void startBroadcasts(Collection<String> sids) {
log.info("Starting broadcast for " + sids.size() + " streams");
// If we're adding a lot of broadcasts, replace the hashset with a correctly-sized version, otherwise as we add
// it will be repeatedly resized, which is expensive
if (sids.size() > broadcastingSids.size()) {
HashSet<String> newSet = new HashSet<String>(sids.size() + broadcastingSids.size());
newSet.addAll(broadcastingSids);
newSet.addAll(sids);
broadcastingSids = newSet;
} else
broadcastingSids.addAll(sids);
mina.getStreamAdvertiser().advertiseStreams(sids);
}
/**
* @syncpriority 200
*/
public synchronized void startRebroadcast(String sid) {
if (rebroadcastingSids.contains(sid))
throw new SeekInnerCalmException();
rebroadcastingSids.add(sid);
log.info("Beginning rebroadcast for stream " + sid);
mina.getStreamAdvertiser().advertiseStream(sid);
}
/**
* @syncpriority 200
*/
public synchronized void startReception(String sid) {
if (receivingSids.contains(sid))
throw new SeekInnerCalmException();
wakePageBuf(sid);
// If we're already finished, just start rebroadcasting
PageBuffer pageBuf = liveStreams.get(sid);
if (pageBuf.isComplete())
startRebroadcast(sid);
else {
receivingSids.add(sid);
requestCachedSources(sid);
mina.getSourceMgr().wantSources(sid);
}
}
/**
* Assumes all streaming conns have been shut down already, and that page buffers will be safely persisted after
* mina is closed
*
* @syncpriority 200
*/
public synchronized void stop() {
log.debug("StreamMgr stopping");
liveStreams.clear();
receivingSids.clear();
rebroadcastingSids.clear();
broadcastingSids.clear();
streamPriorities.clear();
listeners.clear();
}
/**
* @syncpriority 200
*/
public synchronized void stopBroadcast(String sid) {
if (!broadcastingSids.contains(sid))
throw new SeekInnerCalmException();
broadcastingSids.remove(sid);
log.info("Stopping broadcast for stream " + sid);
deadvertiseStream(sid);
mina.getSCM().closeAllBroadcastConns(sid);
}
/**
* @syncpriority 200
*/
public synchronized void stopRebroadcast(String sid) {
if (!rebroadcastingSids.contains(sid))
throw new SeekInnerCalmException();
rebroadcastingSids.remove(sid);
// If we are broadcasting, it means we completed our reception and
// became a broadcaster - so keep our broadcast conns alive, and don't
// de-advertise ourselves
if (!broadcastingSids.contains(sid)) {
mina.getSCM().closeAllBroadcastConns(sid);
deadvertiseStream(sid);
}
}
private void deadvertiseStream(String sid) {
// Don't deadvertise if we're shutting down
if (!mina.getCCM().isShuttingDown()) {
List<String> streamIds = new ArrayList<String>();
streamIds.add(sid);
UnAdvSource uas = UnAdvSource.newBuilder().addAllStreamId(streamIds).build();
mina.getCCM().sendMessageToNetwork("UnAdvSource", uas);
}
}
/**
* @syncpriority 200
*/
public synchronized void stopReception(String sid) {
log.info("Stopping reception for stream " + sid);
if (!receivingSids.contains(sid)) // Leftover thread
return;
receivingSids.remove(sid);
// If we're still wanting to hear about sources, keep track of the ones we're receiving from
if (checkWantingSources(sid)) {
for (LCPair lcp : mina.getSCM().getListenConns(sid)) {
mina.getSourceMgr().cacheSourceUntilReady(lcp.getLastSourceStatus(), lcp.getLastStreamStatus());
}
}
// Cleanup
mina.getSCM().closeAllListenConns(sid);
if (mina.getSCM().getNumBroadcastConns(sid) == 0)
sleepPageBuf(sid);
mina.getBidStrategy().cleanupStream(sid);
mina.getPRM().cleanupStream(sid);
}
private void receptionCompleted(String sid) {
// Huzzah
log.info("Completed reception of " + sid);
// If we want to become a broadcaster when reception is completed
// (probable), then there needs to be a receptionlistener that calls
// startbroadcast in response to this event
mina.getEventMgr().fireReceptionCompleted(sid);
stopReception(sid);
}
public int getPriority(String sid) {
return streamPriorities.get(sid);
}
public void setPriority(String sid, int priority) {
streamPriorities.put(sid, priority);
}
}