/* ************************************************************************ # # 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.session; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import divconq.bus.IService; import divconq.bus.Message; import divconq.count.CountManager; import divconq.hub.Hub; import divconq.hub.ISystemWork; import divconq.hub.SysReporter; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationContextBuilder; import divconq.lang.op.OperationObserver; import divconq.lang.op.OperationResult; import divconq.log.Logger; import divconq.struct.RecordStruct; import divconq.util.StringUtil; import divconq.work.TaskRun; import divconq.work.Task; import divconq.xml.XElement; public class Sessions implements IService { protected ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<String, Session>(); public String serviceName() { return "Session"; } public Collection<Session> list() { return this.sessions.values(); } public void init(OperationResult or, XElement config) { ISystemWork sessioncleanup = new ISystemWork() { @Override public void run(SysReporter reporter) { reporter.setStatus("Reviewing session plans"); if (!Hub.instance.isStopping()) { // guest sessions only last 1 minute, users 5 minutes long clearGuest = System.currentTimeMillis() - (75 * 1000); // TODO configure - 1 minute, 15 secs long clearUser = System.currentTimeMillis() - (195 * 1000); // TODO config - 3 minutes, 15 secs for (Session sess : Sessions.this.sessions.values()) { if (!sess.reviewPlan(clearGuest, clearUser)) { Logger.info("Killing inactive session: " + sess.getId()); Sessions.this.terminate(sess.getId()); } } } reporter.setStatus("After reviewing session plans"); } @Override public int period() { return 60; // TODO configure? } }; Hub.instance.getClock().addSlowSystemWorker(sessioncleanup); } @Override public void handle(TaskRun request) { Message msg = (Message) request.getTask().getParams(); String feature = msg.getFieldAsString("Feature"); String op = msg.getFieldAsString("Op"); if ("Manager".equals(feature)) { RecordStruct req = msg.getFieldAsRecord("Body"); if ("Start".equals(op)) { Session s = new Session(OperationContext.get()); this.sessions.put(s.getId(), s); //return s.call(req.getFieldAsList("Batch")); request.complete(); return; } else if ("End".equals(op)) { Session s = this.sessions.get(req.getFieldAsString("Id")); if ((s != null) && s.getKey().equals(req.getFieldAsString("AccessCode"))) this.terminate(s.getId()); else request.error(1, "Unable to end session, missing Id or Code or session already terminated."); request.complete(); return; } else if ("Touch".equals(op)) { Session s = this.sessions.get(req.getFieldAsString("Id")); if (s != null) s.touch(); else request.error(1, "Unable to touch session, missing Id or Code or session terminated."); request.complete(); return; } } else if ("Session".equals(feature)) { RecordStruct req = msg.getFieldAsRecord("Body"); if (!req.isFieldEmpty("Id") && !req.isFieldEmpty("AccessCode")) { Session s = this.sessions.get(req.getFieldAsString("Id")); if ((s != null) && s.getKey().equals(req.getFieldAsString("AccessCode"))) //return s.call(req.getFieldAsList("Batch")); request.complete(); return; } request.error(1, "Unable to use session, missing Id or Code or session already terminated."); // TODO better codes request.complete(); return; } else if ("Reply".equals(feature)) { String tag = msg.getFieldAsString("Tag"); // pull session id out of the tag int pos = tag.indexOf('_', 30); String sessionid = tag.substring(0, pos); tag = tag.substring(pos + 1); // strip out session id, restore original tag msg.setField("Tag", tag); msg.setField("Service", "Replies"); Session s = this.sessions.get(sessionid); if (s == null) { request.error("Missing session"); request.complete(); return; } // if we get this far consider it delivered - as a far as we know anyway request.complete(); // session activity, don't time out s.touch(); s.deliver(msg); return; } /* TODO probably out of date else if ("InBox".equals(feature)) { if ("Deliver".equals(op)) { String tag = msg.getFieldAsString("Tag"); // pull session id out of the tag int pos = tag.indexOf('_', 30); if (pos == -1) return MessageUtil.errorTr(451, tag); String sessionid = tag.substring(0, pos); Session s = this.sessions.get(sessionid); if (s == null) return MessageUtil.errorTr(450, sessionid); tag = tag.substring(pos + 1); // pull destination out of the tag pos = tag.indexOf('_'); if (pos == -1) { msg.setField("Service", tag); msg.removeField("Feature"); msg.removeField("Op"); msg.removeField("Tag"); } else { msg.setField("Service", tag.substring(0, pos)); tag = tag.substring(pos + 1); pos = tag.indexOf('_'); if (pos == -1) { msg.setField("Feature", tag); msg.removeField("Op"); msg.removeField("Tag"); } else { msg.setField("Feature", tag.substring(0, pos)); tag = tag.substring(pos + 1); pos = tag.indexOf('_'); if (pos == -1) { msg.setField("Op", tag); msg.removeField("Tag"); } else { msg.setField("Op", tag.substring(0, pos)); tag = tag.substring(pos + 1); // restore original tag msg.setField("Tag", tag); } } } s.deliver(msg); // TODO get some results from delivery attempt Hub.instance.getBus().sendReply(MessageUtil.success(), msg, "Sessions", "InBox", "Deliver"); return null; } } */ // Enlist request.error(1, "Sessions does not support this feature or operation."); // TODO better codes request.complete(); } public Session lookup(String sessionid) { if (StringUtil.isEmpty(sessionid)) return null; return this.sessions.get(sessionid); } public Session lookupAuth(String sessionid, String accesscode) { Session s = this.sessions.get(sessionid); if ((s != null) && s.getKey().equals(accesscode)) return s; return null; } public Session create(String origin, String domain) { Session s = new Session(origin, Hub.instance.getDomains().resolveDomainId(domain)); this.sessions.put(s.getId(), s); return s; } // a root and elevated session that doesn't timeout public Session createForService() { Session s = new Session(new OperationContextBuilder().withRootUserTemplate()); s.setKeep(true); this.sessions.put(s.getId(), s); return s; } // based on current context/user public Session createForContext(OperationContext ctx) { Session s = new Session(ctx); this.sessions.put(s.getId(), s); return s; } // based on current context/user public Session findOrCreateTether(OperationContext ctx) { String sid = ctx.getSessionId(); Session s = this.sessions.get(sid); if (s == null) { s = new Session(ctx); s.id = ctx.getSessionId(); // we are not copying and do not have secret key...may need to review this in future this.sessions.put(s.getId(), s); } else { s.user = ctx.getUserContext(); } return s; } // runs a single task and then terminates session public SessionTaskInfo createForSingleTaskAndDie(Task info) { Session session = this.createForContext(info.getContext()); if (session == null) return null; // this maybe overkill but setting up a single use reference to the session // for use with terminating session later - this is to help with GC // which is a causing issues so we want it to be as obvious as possible // that a session and related tasks and task contexts are free when done final AtomicReference<Session> sessref = new AtomicReference<>(); sessref.set(session); OperationObserver listener = new OperationObserver() { @Override public void completed(OperationContext or) { Session session = sessref.get(); if (session != null) { Hub.instance.getSessions().terminate(session.id); sessref.set(null); } } }; TaskRun run = session.submitTask(info, listener); // completion listener will be run even if we get here with errors // so no need to worry about terminate return new SessionTaskInfo(session, run); } // runs a single task and then idles for "30" minutes /* TODO review, we no longer give 30 minutes public SessionTaskInfo createForSingleTaskAndWait(Task info) { final Session session = this.createForContext(info.getContext()); if (session == null) return null; OperationObserver listener = new OperationObserver() { @Override public void completed(OperationContext or) { session.touch(); // keep alive relative to end time } }; TaskRun run = session.submitTask(info, listener); return new SessionTaskInfo(session, run); } */ public List<TaskRun> collectTasks(String... tags) { List<TaskRun> matches = new ArrayList<TaskRun>(); for (Session sess : Sessions.this.sessions.values()) sess.collectTasks(matches, tags); return matches; } public int countTasks(String... tags) { int num = 0; for (Session sess : Sessions.this.sessions.values()) num += sess.countTasks(tags); return num; } public int countIncompleteTasks(String... tags) { int num = 0; for (Session sess : Sessions.this.sessions.values()) num += sess.countIncompleteTasks(tags); return num; } public void terminate(String id) { if (StringUtil.isEmpty(id)) return; // remove before end so we don't end up with infinite recursive terminate with dcAuth sign out Session s = this.sessions.remove(id); if (s != null) s.end(); } public void recordCounters() { CountManager cm = Hub.instance.getCountManager(); long totalKeepers = 0; long totalTasks = 0; long totalIncompleteTasks = 0; Collection<Session> lsessions = this.sessions.values(); HashMap<String,Long> tagcount = new HashMap<>(); for (Session sess : lsessions) { if (sess.getKeep()) totalKeepers++; totalTasks += sess.countTasks(); totalIncompleteTasks += sess.countIncompleteTasks(); sess.countTags(tagcount); } cm.allocateSetNumberCounter("dcSessionCount", lsessions.size()); cm.allocateSetNumberCounter("dcSessionKeepersCount", totalKeepers); cm.allocateSetNumberCounter("dcSessionTaskCount", totalTasks); cm.allocateSetNumberCounter("dcSessionTaskIncompleteCount", totalIncompleteTasks); for (Entry<String, Long> tagentity : tagcount.entrySet()) cm.allocateSetNumberCounter("dcSessionTag_" + tagentity.getKey() + "_Count", tagentity.getValue()); } }