package edu.harvard.econcs.turkserver.server; import java.lang.ref.WeakReference; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.cometd.bayeux.server.ServerSession; import edu.harvard.econcs.turkserver.api.HITWorker; import edu.harvard.econcs.turkserver.api.HITWorkerGroup; import edu.harvard.econcs.turkserver.schema.Session; public class HITWorkerImpl implements HITWorker, HITWorkerGroup { volatile WeakReference<ServerSession> cometdSession; volatile Session record; volatile ExperimentControllerImpl expCont; final AtomicInteger numDisconnects = new AtomicInteger(); final AtomicReference<Long> lastDisconnectTime = new AtomicReference<Long>(); final AtomicReference<Long> lastInactiveStart = new AtomicReference<Long>(); final AtomicLong totalDisconnectedMillis = new AtomicLong(); final AtomicLong pastInactiveMillis = new AtomicLong(); final AtomicLong currentInactiveMillis = new AtomicLong(); public HITWorkerImpl(ServerSession cometdSession, Session dbSession) { this.cometdSession = new WeakReference<ServerSession>(cometdSession); this.record = dbSession; } void setServerSession(ServerSession newSession) { cometdSession = new WeakReference<ServerSession>(newSession); } void setExperiment(ExperimentControllerImpl cont) { this.expCont = cont; } @Override public String getHitId() { return record.getHitId(); } @Override public String getAssignmentId() { return record.getAssignmentId(); } @Override public String getWorkerId() { return record.getWorkerId(); } @Override public String getUsername() { String username = record.getUsername(); if( username != null ) return username; return "User for " + record.getHitId(); } @Override public InetAddress getIPAddress() { try { String ip = record.getIpAddr(); if (ip == null) return null; return InetAddress.getByName(ip); } catch (UnknownHostException e) { return null; } } @Override public boolean isConnected() { ServerSession session = cometdSession.get(); if( session == null ) return false; return session.isConnected(); } @Override public void deliverExperimentService(Map<String, Object> msg) throws MessageException { // Find the experiment this user is on and send a message on that channel expCont.sendExperimentService(this, msg); } public void deliverUserService(Map<String, Object> msg) throws MessageException { ServerSession session = cometdSession.get(); if( session == null || !session.isConnected() ) throw new MessageException(); session.deliver(session, "/service/user", msg, null); } /** * call when this worker has disconnected previous and reconnects * records the time since last disconnection, if any */ void reconnected() { Long lastDisc = lastDisconnectTime.getAndSet(null); if( lastDisc == null ) { // This currently happens on a reload. System.out.println("reconnected but don't have record of last disconnect"); return; } /* * Add disconnected time to total inactive time * * TODO this doesn't take care of the case where inactive is double-counted * after a non-reloading reconnect */ long disconnectedMillis = System.currentTimeMillis() - lastDisc; pastInactiveMillis.addAndGet(disconnectedMillis); totalDisconnectedMillis.addAndGet(disconnectedMillis); } /** * call when this worker disconnects * records the time */ void disconnected() { lastDisconnectTime.set(System.currentTimeMillis()); numDisconnects.incrementAndGet(); } /** * Add milliseconds of inactive time * @param millis * @param inactiveTime */ void addInactiveTime(long inactiveStart, long inactiveTime) { Long lastStart = lastInactiveStart.get(); if( lastStart == null ) { currentInactiveMillis.set(inactiveTime); lastInactiveStart.set(inactiveStart); } else if( lastStart == inactiveStart ) { currentInactiveMillis.set(inactiveTime); } else { // New inactive segment, add previous segment to total long pastMillis = currentInactiveMillis.getAndSet(inactiveTime); lastInactiveStart.set(inactiveStart); pastInactiveMillis.addAndGet(pastMillis); } } /** * Call at the end of an experiment to finish any uncomputed values */ public void finalizeActivity() { if( !isConnected() ) { Long lastDisc = lastDisconnectTime.getAndSet(null); if( lastDisc == null ) { System.out.println("not connected but don't have record of last disconnect"); return; } else if( lastDisc > expCont.expFinishTime ) { System.out.println("ignoring last disconnect after experiment end"); return; } long disconnectedMillis = expCont.expFinishTime - lastDisc; totalDisconnectedMillis.addAndGet(disconnectedMillis); // Add inactive time since disconnection as well addInactiveTime(lastDisc, disconnectedMillis); } // Clear out current inactive time long pastMillis = currentInactiveMillis.getAndSet(0); lastInactiveStart.set(null); pastInactiveMillis.addAndGet(pastMillis); } @Override public long getLastDisconnectedTime() { Long lastDisc = lastDisconnectTime.get(); if( lastDisc == null ) return -1; else return System.currentTimeMillis() - lastDisc; } @Override public long getTotalDisconnectedTime() { Long lastDisc = lastDisconnectTime.get(); if( lastDisc == null ) { return totalDisconnectedMillis.get(); } else { return totalDisconnectedMillis.get() + (System.currentTimeMillis() - lastDisc); } } @Override public int getNumDisconnects() { return numDisconnects.get(); } @Override public long getLastInactiveTime() { return currentInactiveMillis.get(); } @Override public long getTotalInactiveTime() { return pastInactiveMillis.get() + currentInactiveMillis.get(); } @Override public double getInactivePercent() { if( expCont == null ) return 0d; long totalTime = System.currentTimeMillis() - expCont.expStartTime; return 1.0d * getTotalInactiveTime() / totalTime; } @Override public String getInactiveInfo() { return String.format("Inactive a total of %d secs, last inactive %d secs", getTotalInactiveTime() / 1000, getLastInactiveTime() / 1000); } @Override public int groupSize() { return 1; } @Override public boolean contains(HITWorker hitWorker) { return this.equals(hitWorker); } @Override public List<HITWorker> getHITWorkers() { return Collections.singletonList((HITWorker) this); } @Override public List<String> getHITIds() { return Collections.singletonList( this.getHitId() ); } @Override public List<String> getWorkerIds() { return Collections.singletonList( this.getWorkerId() ); } @Override public HITWorker findByHITId(String hitId) { if( hitId == null || !hitId.equals(this.getHitId())) return null; return this; } public Session getSessionRecord() { return record; } @Override public boolean equals(Object other) { // TODO: not sure if we want to implement things this way if( ! (other instanceof HITWorker) ) return false; HITWorker otherhw = (HITWorker) other; String thisHIT = record.getHitId(); String otherHIT = otherhw.getHitId(); String thisWorker = record.getWorkerId(); String otherWorker = otherhw.getWorkerId(); if( thisHIT == null || otherHIT == null ) return false; if( thisWorker == null || otherWorker == null ) return false; return thisHIT.equals(otherHIT) && thisWorker.equals(otherWorker); } public String toString() { return String.format("%s @ HIT %s", getWorkerId(), getHitId()); } }