/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.bus; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.locks.ReentrantLock; import divconq.bus.net.Session; import divconq.bus.net.SocketInfo; import divconq.bus.net.StreamMessage; import divconq.bus.net.StreamSession; import divconq.hub.DomainInfo; import divconq.hub.Hub; import divconq.hub.HubEvents; import divconq.lang.op.FuncCallback; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationObserver; import divconq.lang.op.OperationResult; import divconq.lang.op.UserContext; import divconq.log.Logger; import divconq.schema.SchemaManager.OpInfo; import divconq.session.DataStreamChannel; import divconq.struct.FieldStruct; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.struct.Struct; import divconq.util.StringUtil; import divconq.work.Task; public class HubRouter { protected String hubid = null; protected String squad = null; protected CopyOnWriteArraySet<String> services = new CopyOnWriteArraySet<>(); // used with local router protected boolean local = false; protected boolean gateway = false; protected ConcurrentHashMap<String, IService> registered = new ConcurrentHashMap<String, IService>(); protected ReplyService localReplies = null; protected boolean usekeepalive = true; // used with direct connections protected List<Session> sessions = new CopyOnWriteArrayList<>(); // direct connection protected List<HubRouter> tunnels = new CopyOnWriteArrayList<>(); // tunnel (proxy) connection protected ReentrantLock sessionlock = new ReentrantLock(); protected int next = 0; protected List<StreamSession> streamsessions = new CopyOnWriteArrayList<>(); // direct connection protected int streamnext = 0; // used with tunnel connections protected HashMap<String, HubRouter> proxied = new HashMap<>(); // use with direct and tunnel protected HashMap<String, StreamPath> streampaths = new HashMap<>(); public String getHubId() { return this.hubid; } public String getSquadId() { return this.squad; } public Collection<String> getServices() { return this.services; } public Collection<HubRouter> getProxiedHubs() { return this.proxied.values(); } public boolean isLocal() { return this.local; } public boolean isDirect() { return (this.sessions.size() > 0) && (this.streamsessions.size() > 0); } public boolean isTunneled() { return (this.tunnels.size() > 0); } public boolean isActive() { return this.local || (this.sessions.size() > 0) || (this.tunnels.size() > 0); } public void setUseKeepAlive(boolean v) { this.usekeepalive = v; } // use for remote router public HubRouter(String id, boolean gateway) { this.hubid = id; this.gateway = gateway; } // use for local router public HubRouter() { this.local = true; this.hubid = OperationContext.getHubId(); this.squad = Hub.instance.getResources().getSquadId(); this.localReplies = new ReplyService(); this.registerService(localReplies); } // for use with local only public void registerService(IService callback) { this.registered.put(callback.serviceName(), callback); this.services.add(callback.serviceName()); Hub.instance.getBus().indexServices(this); } // for use with local only public void removeService(String name) { this.registered.remove(name); this.services.remove(name); Hub.instance.getBus().indexServices(this); } // for use with local only public Message buildHello(String to) { Message cmd = new Message(); cmd.setField("Kind", "HELLO"); cmd.setField("Id", OperationContext.getHubId()); cmd.setField("Squad", this.squad); // if we are idle then don't list our non-core services with other servers anymore // TODO we should still support the Reply service and maybe a few others...just not app level services? if (!Hub.instance.isIdled()) { cmd.setField("Services", new ListStruct(this.services)); if (Hub.instance.getBus().isProxyMode()) { // make sure we list each hub only once in ProxiedServices HashMap<String, HubRouter> proxied = new HashMap<>(); for (HubRouter hub : Hub.instance.getBus().getHubs()) { if (!hub.isLocal() && hub.isActive() && !hub.getHubId().equals(to)) { proxied.put(hub.getHubId(), hub); for (HubRouter phub : hub.getProxiedHubs()) if (phub.isActive() && !phub.getHubId().equals(to)) proxied.put(phub.getHubId(), phub); } } ListStruct plist = new ListStruct(); for (HubRouter hub : proxied.values()) { plist.addItem(new RecordStruct( new FieldStruct("Id", hub.getHubId()), new FieldStruct("Squad", hub.getSquadId()), new FieldStruct("Services", new ListStruct(hub.getServices())) )); } cmd.setField("ProxiedServices", plist); } } return cmd; } // for use with local only public StreamMessage buildStreamHello(String to) { StreamMessage cmd = new StreamMessage("HELLO"); cmd.setField("Id", OperationContext.getHubId()); cmd.setField("Squad", this.squad); return cmd; } // for use with local only public String registerForReply(Message msg, ServiceResult callback) { return this.localReplies.registerForReply(msg, callback); } // for use with local only public ReplyService getReplyService() { return this.localReplies; } public OperationResult deliverMessage(Message msg) { OperationResult or = new OperationResult(); if (msg == null) { or.error(1, "Message missing."); // TODO code return or; } // route to local if (this.local) { msg.removeField("Kind"); // not used locally //System.out.println("dcBus received message: " + msg); String srv = msg.getFieldAsString("Service"); if (srv == null) { or.error(1, "Message missing service."); // TODO code return or; } // TODO now that TaskContext is immutable we could optimize local Bus calls by not freezing and thawing except remotely - future optimization OperationContext tc = OperationContext.allocate(msg); if ((tc == null) || (tc.getUserContext() == null)) { or.errorTr(442); return or; } DomainInfo di = tc.getDomain(); IService cb = (di != null) ? di.getService(srv) : null; if (cb == null) cb = this.registered.get(srv); if (cb == null) { or.error(1, "Service not on this hub."); // TODO code return or; } IService serv = cb; Task tb = new Task() .withTitle("Hub Router: " + srv) .withContext(tc) .withParams(msg) .withBucket("Bus") // typically this will fall back into Default .withWork(task -> { FuncCallback<UserContext> fcb = new FuncCallback<UserContext>() { @Override public void callback() { UserContext uc = this.getResult(); // update the operation context if (uc != null) OperationContext.switchUser(tc, uc); // this should update the task context too if (task.hasErrors()) { task.complete(); return; } // validate the structure of the message OperationResult vres = tc.getSchema().validateRequest(msg); // when making a valid call to any service, you are elevated to system access for the duration of the request // RPC users get a new context with each call though, and reply will not violate any security // so it is up to each service to call only other services as appropriate - that is each service // is ultimately responsible for security if (!tc.isElevated()) OperationContext.elevate(tc); // this should update the task context too // if invalid structure then do not continue, but we do need the elevate above to // call reply service if (vres.hasErrors()) { task.complete(); return; } serv.handle(task); //System.out.println("d3: " + msg); //System.out.println("d4: " + rmsg); // reply may be async so this could be null - when async the service handler calls sendReply directly // which is also a reason not to add a lot of logic here, as it won't get called in async cases //if (rmsg != null) // Hub.instance.getBus().sendReply(rmsg, msg); //task.complete(); } }; // don't verify a Verify request or it'll be stuck forever making new verify checks if (msg.isVerifyRequest()) { fcb.setResult(tc.getUserContext()); fcb.complete(); } else tc.verify(fcb); }); Hub.instance.getWorkPool().submit(tb, new OperationObserver() { @Override public void completed(OperationContext or) { // set to this so that we can use the proper - elevated - context during // the reply routing OperationContext.set(or); // send a response, be it just error messages or a full body Hub.instance.getBus().sendReply(or.getTaskRun().toLogMessage(), msg); } }); //System.out.println("Call to " + srv + " -- " + tb.getId()); return or; } if (this.gateway) { // TODO consider stripping AuthToken and SessionId and Credentials from message // when sending to gateway... review how this would work } // route to remote Session sess = this.nextDirectRoute(); if (sess == null) { HubRouter tunnel = this.nextTunnelRoute(); if (tunnel != null) return tunnel.deliverMessage(msg); if (!"HELLO".equals(msg.getFieldAsString("Kind"))) or.error(1, "Unable to route message to proxied hub: " + msg); // TODO log, better code return or; } if (!sess.write(msg)) or.error(1, "Unable to route message to remote hub: " + msg); // TODO log, better code return or; } public void receiveMessage(Session session, Message msg) { // update our service list if SERVICES message if ("HELLO".equals(msg.getFieldAsString("Kind"))) { this.squad = msg.getFieldAsString("Squad"); // copy the existing list Collection<HubRouter> oldplist = new ArrayList<>(this.proxied.values()); this.proxied.clear(); this.services.clear(); ListStruct plist = msg.getFieldAsList("ProxiedServices"); if (plist != null) { for (Struct pitem : plist.getItems()) { RecordStruct prec = (RecordStruct) pitem; HubRouter phub = Hub.instance.getBus().allocateOrGetHub(prec.getFieldAsString("Id"), session.getSocketInfo().isGateway()); /* if (phub == null) { System.out.println("Could not allocate hub: " + prec.getFieldAsString("Id") + " >> me -- " + TaskContext.getHubId()); continue; } */ phub.addTunnel(prec, this); // we don't need to clear our tunnel from this proxied hub oldplist.remove(phub); this.proxied.put(phub.getHubId(), phub); } } this.clearMyTunnels(oldplist); ListStruct slist = msg.getFieldAsList("Services"); if (slist != null) { this.services.addAll(slist.toStringList()); int sessionsize = this.sessions.size(); Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_Sessions", sessionsize); } Hub.instance.getBus().indexServices(this); return; } String service = msg.getFieldAsString("Service"); String feature = msg.getFieldAsString("Feature"); boolean looksLikeReply = ("Replies".equals(service) || ("Session".equals(service) && "Reply".equals(feature))); // ===================================================================== // when coming from a gateway be very picky about what we allow through // however, all calls to Replies service are allowed for now, we can get more specific later // and of course verify requests are allowed since we are the verifier :) // ===================================================================== if (this.gateway && !msg.isVerifyRequest() && !looksLikeReply) { boolean isguest = true; RecordStruct context = msg.getFieldAsRecord("Context"); // session must be present if not Guest if (context == null) { System.out.println("dcBus " + this.getHubId() + " tried to call without context, got: " + msg); return; } // let everyone know this is from a gateway - via op context context.setField("Gateway", true); String uid = context.getFieldAsString("UserId"); // session must be present if not Guest if (StringUtil.isEmpty(uid)) { System.out.println("dcBus " + this.getHubId() + " tried to call without userid, got: " + msg); return; } if (!"00000_000000000000002".equals(uid)) isguest = false; else if (!context.isFieldEmpty("AuthToken") || !context.isFieldEmpty("Credentials")) isguest = false; else { ListStruct tags = context.getFieldAsList("AuthTags"); if ((tags == null) || (tags.getSize() != 1)) isguest = false; else if (!"Guest".equals(tags.getItemAsString(0))) isguest = false; } // if not guest then we are even more picky if (!isguest) { OpInfo op = OperationContext.get().getSchema().getServiceOp(service, feature, msg.getFieldAsString("Op")); // operations tagged as Gateway can be called by gateway no matter what...even when gateway is hacked // normal user tag check applies, Gateway only means it gets past here, not pass message validation // though if gateway is hacked then a Gateway tag pretty much = callable as hacker can send Root context if (!op.isTagged("Gateway")) { String sid = context.getFieldAsString("SessionId"); // session must be present if not Guest if (StringUtil.isEmpty(sid)) { System.out.println("dcBus " + this.getHubId() + " tried to call as user without session, got: " + msg); return; } String atoken = context.getFieldAsString("AuthToken"); // session must be present if not Guest if (StringUtil.isEmpty(atoken)) { System.out.println("dcBus " + this.getHubId() + " tried to call as user without authtoken, got: " + msg); return; } String expectedhubid = session.getSocketInfo().getHubId(); // session must be present if not Guest - session must come from gateway if (StringUtil.isNotEmpty(expectedhubid) && !sid.startsWith(expectedhubid)) { System.out.println("dcBus " + this.getHubId() + " tried to call with session " + sid + ", got: " + msg); return; } divconq.session.Session us = Hub.instance.getSessions().lookup(sid); if (us == null) { System.out.println("dcBus " + this.getHubId() + " tried to call with missing session " + sid + ", got: " + msg); return; } if (!atoken.equals(us.getUser().getAuthToken())) { System.out.println("dcBus " + this.getHubId() + " tried to call with bad token " + atoken + ", got: " + msg); return; } if (!uid.equals(us.getUser().getUserId())) { System.out.println("dcBus " + this.getHubId() + " tried to call with user id for session " + uid + ", got: " + msg); return; } //System.out.println("Gateway request passed checks, before context: " + context); // OK, we got this far, go forward but only with the context we had at login // copy the user context into the message us.getUser().freeze(context); //System.out.println("Gateway request passed checks, after context: " + context); } } //System.out.println("Gateway request passed checks z: " + msg); } else if (this.gateway) { RecordStruct context = msg.getFieldAsRecord("Context"); if (context != null) context.setField("Gateway", true); } /* // TODO temp - show me messages coming into server from gateway else if (!looksLikeReply) { System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&"); System.out.println("gateway request passed checks z: " + msg); System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&"); } */ // ===================================================================== // if message is not HELLO then it needs to be routed to the correct hub String srv = msg.getFieldAsString("Service"); ServiceRouter router = Hub.instance.getBus().getServiceRouter(srv); if (router == null) { // TODO log warning return; } // this will end up in the proper work pool after routing OperationResult routeres = router.sendMessage(msg); if (routeres.hasErrors()) { // TODO log error return; } } // TODO from this point on release StreamMessages public void receiveMessage(StreamSession session, StreamMessage msg) { // update our service list if SERVICES message if ("HELLO".equals(msg.getFieldAsString("Op"))) { msg.release(); this.squad = msg.getFieldAsString("Squad"); return; } String hub = msg.getFieldAsString("ToHub"); HubRouter router = Hub.instance.getBus().allocateOrGetHub(hub, session.getSocketInfo().isGateway()); OperationResult routeres = router.deliverMessage(msg); if (routeres.hasErrors()) { StreamMessage rmsg = MessageUtil.streamMessages(routeres); Hub.instance.getBus().sendReply(rmsg, msg); } } public void addSession(Session session) { this.sessionlock.lock(); int sessionsize = this.sessions.size(); try { this.sessions.add(session); sessionsize = this.sessions.size(); Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_Sessions", sessionsize); if ((sessionsize == 1) && this.isDirect()) // let hub know we are connected, in another thread Hub.instance.getWorkPool().submit(trun -> { Hub.instance.fireEvent(HubEvents.BusConnected, null); trun.complete(); }); } finally { this.sessionlock.unlock(); } Logger.info("Connect on dcBus, " + this.getHubId() + " sessions available: " + sessionsize); } public void remove(SocketInfo info) { for (Session sess : this.sessions) { if (sess.getSocketInfo() == info) { this.removeSession(sess); break; } } for (StreamSession sess : this.streamsessions) { if (sess.getSocketInfo() == info) { this.removeSession(sess); break; } } } public void removeSession(Session session) { this.sessionlock.lock(); boolean direct = this.isDirect(); int sessionsize = this.sessions.size(); int priorsize = sessionsize; try { this.sessions.remove(session); sessionsize = this.sessions.size(); Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_Sessions", sessionsize); session.close(); } finally { this.sessionlock.unlock(); } if (sessionsize != priorsize) Logger.info("Disconnect on dcBus, " + this.getHubId() + " sessions available: " + sessionsize); // if no longer connected then get the agent out of the manager's hair if (sessionsize == 0) { // let hub know we are disconnected, in another thread if (direct) Hub.instance.getWorkPool().submit(trun -> { Hub.instance.fireEvent(HubEvents.BusDisconnected, null); trun.complete(); }); this.clearMyTunnels(this.proxied.values()); Hub.instance.getBus().indexServices(this); } } public void addSession(StreamSession session) { this.sessionlock.lock(); int sessionsize = this.streamsessions.size(); try { this.streamsessions.add(session); sessionsize = this.streamsessions.size(); // TODO make debug Logger.info("Connect on dcBus, " + this.getHubId() + " stream added: " + sessionsize); Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_SteramSessions", sessionsize); if ((sessionsize == 1) && this.isDirect()) // let hub know we are connected, TODO in another thread Hub.instance.getWorkPool().submit(trun -> { Hub.instance.fireEvent(HubEvents.BusConnected, null); trun.complete(); }); } finally { this.sessionlock.unlock(); } Logger.info("Connect on dcBus, " + this.getHubId() + " stream sessions available: " + sessionsize); } public void removeSession(StreamSession session) { this.sessionlock.lock(); boolean direct = this.isDirect(); int sessionsize = this.streamsessions.size(); int priorsize = sessionsize; try { this.streamsessions.remove(session); sessionsize = this.streamsessions.size(); Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_StreamSessions", sessionsize); session.close(); } finally { this.sessionlock.unlock(); } if (sessionsize != priorsize) Logger.info("Disconnect on dcBus, " + this.getHubId() + " stream sessions available: " + sessionsize); // if no longer connected then get the agent out of the manager's hair if (sessionsize == 0) { if (direct) // let hub know we are disconnected, in another thread Hub.instance.getWorkPool().submit(trun -> { Hub.instance.fireEvent(HubEvents.BusDisconnected, null); trun.complete(); }); } } public void addTunnel(RecordStruct prec, HubRouter tunnel) { this.squad = prec.getFieldAsString("Squad"); ListStruct slist = prec.getFieldAsList("Services"); this.services.clear(); this.services.addAll(slist.toStringList()); this.sessionlock.lock(); int proxysize = this.tunnels.size(); int priorsize = proxysize; try { if (!this.tunnels.contains(tunnel)) { this.tunnels.add(tunnel); proxysize = this.tunnels.size(); } Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_Proxies", proxysize); } finally { this.sessionlock.unlock(); } if (proxysize != priorsize) Logger.info("Connect on dcBus, " + this.getHubId() + " proxies available: " + proxysize); // if no longer connected then get the agent out of the manager's hair if (proxysize == 1) Hub.instance.getBus().indexServices(this); } public void removeTunnel(HubRouter tunnel) { this.sessionlock.lock(); int proxysize = this.tunnels.size(); try { this.tunnels.remove(tunnel); proxysize = this.tunnels.size(); Hub.instance.getCountManager().allocateSetNumberCounter("dcBus_" + this.getHubId() + "_Proxies", proxysize); } finally { this.sessionlock.unlock(); } Logger.info("Disconnect on dcBus, " + this.getHubId() + " proxies available: " + proxysize); // if no longer connected then get the agent out of the manager's hair if (proxysize == 0) Hub.instance.getBus().indexServices(this); } public void close() { // calling close will trigger a call to remove (above) via the Manager for (Session s : this.sessions) s.close(); } public void clearMyTunnels(Collection<HubRouter> list) { for (HubRouter hub : list) hub.removeTunnel(this); } // round robin approach to finding routes public Session nextDirectRoute() { this.sessionlock.lock(); try { int subcount = this.sessions.size(); if (subcount == 0) return null; if (this.next >= subcount) this.next = 0; Session np = this.sessions.get(this.next); this.next++; return np; } finally { this.sessionlock.unlock(); } } // round robin approach to finding routes public HubRouter nextTunnelRoute() { this.sessionlock.lock(); try { int subcount = this.tunnels.size(); if (subcount == 0) return null; if (this.next >= subcount) this.next = 0; HubRouter np = this.tunnels.get(this.next); this.next++; return np; } finally { this.sessionlock.unlock(); } } public Collection<Session> getSessions() { return this.sessions; } public int getCountSessions(SocketInfo info) { int cnt = 0; for (Session sess : this.sessions) if (sess.getSocketInfo() == info) cnt++; return cnt; } public int getCountStreamSessions(SocketInfo info) { int cnt = 0; for (StreamSession sess : this.streamsessions) if (sess.getSocketInfo() == info) cnt++; return cnt; } // from this point on release StreamMessage public OperationResult deliverMessage(final StreamMessage msg) { OperationResult or = new OperationResult(); if (msg == null) { or.error(1, "Message missing."); // TODO code return or; } String sessid = msg.getFieldAsString("ToSession"); String chanid = msg.getFieldAsString("ToChannel"); // route to local if (this.local) { //System.out.println("dcBus received stream message: " + msg); final divconq.session.Session sess = Hub.instance.getSessions().lookup(sessid); if (sess == null) { or.error(1, "Unable to find session: " + sessid); // TODO make sure if this came off network that a response is sent msg.release(); return or; } final DataStreamChannel chan = sess.getChannel(chanid); if (chan == null) { or.error(1, "Unable to find channel: " + chanid); msg.release(); return or; } // stream in foreground to keep data in order // (over wire this is accomplished by always using the same network path) chan.deliverMessage(msg); // TODO return errors if any return or; } String pathid = (StringUtil.isEmpty(sessid) || StringUtil.isEmpty(chanid)) ? null : sessid + "_" + chanid; if (StringUtil.isNotEmpty(pathid)) { StreamPath path = null; // get the path while locked because other wise the cleanup might steal the path after we get it but before we // touch it - thus removing the path that is just about to get used - thus allowing for possible out of order // data packets this.sessionlock.lock(); try { path = this.streampaths.get(pathid); if (path != null) path.touched = System.currentTimeMillis(); } finally { this.sessionlock.unlock(); } if (path != null) { if (path.direct != null) { if (!path.direct.write(msg)) or.error(1, "Unable to route message to remote hub: " + msg); // TODO log, better code return or; } if (path.tunneled != null) { // TODO check that it is still connected/active path.tunneled.deliverMessage(msg); return or; } } } // route to remote StreamSession sess = this.nextDirectStreamRoute(); if (sess != null) { if (sess.write(msg)) { if (StringUtil.isNotEmpty(pathid)) { StreamPath sp = new StreamPath(); sp.direct = sess; sp.id = pathid; // lock so we are not interfering with cleanup (see below) // if we get to here though our path is not a candidate for cleanup yet so // our operation is not a concern for cleanup, just don't change hash while cleanup is using it this.sessionlock.lock(); try { this.streampaths.put(pathid, sp); } finally { this.sessionlock.unlock(); } } } else or.error(1, "Unable to route message to remote hub: " + msg); // TODO log, better code return or; } HubRouter tunnel = this.nextTunnelRoute(); if (tunnel != null) { if (StringUtil.isNotEmpty(pathid)) { StreamPath sp = new StreamPath(); sp.tunneled = tunnel; sp.id = pathid; // lock so we are not interfering with cleanup (see below) // if we get to here though our path is not a candidate for cleanup yet so // our operation is not a concern for cleanup, just don't change hash while cleanup is using it this.sessionlock.lock(); try { this.streampaths.put(pathid, sp); } finally { this.sessionlock.unlock(); } } return tunnel.deliverMessage(msg); } if (!"HELLO".equals(msg.getFieldAsString("Kind"))) or.error(1, "Unable to route message to proxied hub: " + msg); // TODO log, better code return or; } // round robin approach to finding routes public StreamSession nextDirectStreamRoute() { this.sessionlock.lock(); try { int subcount = this.streamsessions.size(); if (subcount == 0) { System.out.println("Missing stream routes to " + this.hubid); return null; } if (this.streamnext >= subcount) this.streamnext = 0; StreamSession np = this.streamsessions.get(this.streamnext); this.next++; return np; } finally { this.sessionlock.unlock(); } } public class StreamPath { protected String id = null; protected StreamSession direct = null; protected HubRouter tunneled = null; protected long touched = System.currentTimeMillis(); } public void keepAlive() { if (!this.usekeepalive) return; // loop direct connections for (Session sess : this.sessions) sess.keepAlive(); for (StreamSession sess : this.streamsessions) sess.keepAlive(); } public void cleanup() { // clearing the stream paths has no real damaging impact - it's purpose is to corral data sent by one session/channel // into using the same StreamSession for a burst of activity - such that data will not get out of sequence by // going across different StreamSessions and packet 2 arriving before packet 1 // however, if data has been silent for 1 minute then packets are not out of order and as such not an issue if // clear this and assign an alternative StreamSession later - should the data sender still be active but very slow long timeout = System.currentTimeMillis() - 60000; // 1 min of no activity List<StreamPath> cleanlist = new ArrayList<>(); this.sessionlock.lock(); try { for (StreamPath path : this.streampaths.values()) { // has path been quiet for too long? if (path.touched < timeout) cleanlist.add(path); } for (StreamPath path : cleanlist) this.streampaths.remove(path.id); } finally { this.sessionlock.unlock(); } } }