package edu.harvard.econcs.turkserver.server;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ConfigurableServerChannel.Initializer;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.andrewmao.misc.Utils;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapMaker;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import edu.harvard.econcs.turkserver.Codec;
import edu.harvard.econcs.turkserver.api.Configurator;
import edu.harvard.econcs.turkserver.api.HITWorker;
import edu.harvard.econcs.turkserver.api.HITWorkerGroup;
import edu.harvard.econcs.turkserver.api.IntervalEvent;
import edu.harvard.econcs.turkserver.cometd.MockLocalSession;
import edu.harvard.econcs.turkserver.config.TSConfig;
import edu.harvard.econcs.turkserver.server.mysql.ExperimentDataTracker;
@Singleton
public class Experiments {
// Injector for creating bean classes
@Inject Injector injector;
BayeuxServer bayeux;
protected final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
static final Initializer persistent = new Initializer() {
@Override
public void configureChannel(ConfigurableServerChannel channel) {
channel.setPersistent(true);
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
}
};
static final int TRIGGER_HACK_MILLIS = 1000;
final EventAnnotationManager manager;
final ExperimentDataTracker tracker;
final Configurator configurator;
final Class<?> expClass;
final ConcurrentMap<HITWorker, String> currentExps;
final Queue<ExperimentListener> listeners;
final ScheduledExecutorService eventScheduler;
final ConcurrentMap<String, List<ScheduledFuture<?>>> scheduledIntervals;
@Inject
Experiments(
@Named(TSConfig.EXP_CLASS) Class<?> expClass,
@Named(TSConfig.EXP_CONFIGURATOR) Configurator configurator,
ExperimentDataTracker tracker,
EventAnnotationManager manager
) {
MapMaker concMapMaker = new MapMaker();
this.expClass = expClass;
this.configurator = configurator;
this.tracker = tracker;
this.manager = manager;
this.currentExps = concMapMaker.makeMap();
this.listeners = new ConcurrentLinkedQueue<ExperimentListener>();
this.eventScheduler = Executors.newScheduledThreadPool(1);
this.scheduledIntervals = concMapMaker.makeMap();
}
// TODO remove this hack once things are properly wired up
public void setReferences(BayeuxServer bayeux) {
this.bayeux = bayeux;
}
void registerListener(ExperimentListener listener) {
listeners.add(listener);
}
/**
* Injects classes for a single-worker experiment
* @param hitw
* @return
*/
ExperimentControllerImpl startSingle(final HITWorkerImpl hitw) {
ExperimentControllerImpl cont = null;
Object experimentBean = null;
ThreadLocalScope scope = injector.getInstance(ThreadLocalScope.class);
scope.enter();
try {
// Create an experiment instance with specific binding to this HITWorker
scope.seed(HITWorker.class, hitw);
scope.seed(HITWorkerGroup.class, hitw);
cont = injector.getInstance(ExperimentControllerImpl.class);
hitw.setExperiment(cont);
experimentBean = injector.getInstance(expClass);
}
finally {
scope.exit();
}
startExperiment(hitw, cont, experimentBean);
return cont;
}
/**
* Injects classes for a group experiment.
* @param group
* @return
*/
ExperimentControllerImpl startGroup(final HITWorkerGroupImpl group) {
ExperimentControllerImpl cont = null;
Object experimentBean = null;
ThreadLocalScope scope = injector.getInstance(ThreadLocalScope.class);
scope.enter();
try {
// Create an experiment instance with specific binding to this HITWorkerGroup
scope.seed(HITWorkerGroup.class, group);
cont = injector.getInstance(ExperimentControllerImpl.class);
group.setExperiment(cont);
experimentBean = injector.getInstance(expClass);
} finally {
scope.exit();
}
startExperiment(group, cont, experimentBean);
return cont;
}
void startExperiment(final HITWorkerGroup group,
final ExperimentControllerImpl cont, Object experimentBean) {
// Create a unique ID for an experiment, based on current timestamp
long startTime = System.currentTimeMillis();
final String expId = Utils.getTimeString(startTime);
String expChannel = expId.replace(" ", "_");
// Initialize the experiment data
String inputData = configurator.configure(experimentBean, expId, group);
// Register callbacks on the experiment class
manager.processExperiment(expId, experimentBean);
/*
* Create necessary channels for this experiment
* Note that hostServlet automatically routes these already
*/
LocalSession ls = null;
if( bayeux == null ) {
logger.warn("Skipping bayeux channel creation...we'd better be in test mode!");
ls = new MockLocalSession();
}
else {
bayeux.createIfAbsent(Codec.expChanPrefix + expChannel, persistent);
bayeux.createIfAbsent(Codec.expSvcPrefix + expChannel, persistent);
ls = bayeux.newLocalSession(expId);
ls.handshake();
}
/*
* Send experiment channel to clients to notify connection
*
* TODO this may be unnecessary, fix protocol
* possible fix: server subscribes clients; clients just listen
*/
Map<String, Object> data = ImmutableMap.of(
"status", Codec.status_connectexp,
"channel", (Object) expChannel);
for( HITWorker hitw : group.getHITWorkers() ) {
try { ((HITWorkerImpl) hitw).deliverUserService(data);
} catch (MessageException e) { e.printStackTrace(); }
}
// Initialize controller, which also initializes the log
cont.initialize(startTime, expId, inputData, expChannel, ls);
/* Update tracking information for experiment
* TODO does this start directing requests to the server before it's started?
*/
mapWorkers(group, expId);
tracker.newExperimentStarted(cont);
for( ExperimentListener el : listeners ) {
el.experimentStarted(cont);
}
/*
* TODO this may not be enough time for every client to register channel...
* reconcile this with the timing stuff above
* fix how clients find channels
*/
eventScheduler.schedule(new Runnable() {
public void run() {
manager.triggerStart(expId);
}
}, TRIGGER_HACK_MILLIS, TimeUnit.MILLISECONDS);
// Schedule interval tasks
List<ScheduledFuture<?>> scheduled = new LinkedList<>();
List<Method> intervals = manager.getIntervalEvents(expId);
for( final Method method : intervals ) {
IntervalEvent ie = method.getAnnotation(IntervalEvent.class);
ScheduledFuture<?> f = eventScheduler.scheduleAtFixedRate(new Runnable() {
public void run() {
manager.triggerInterval(expId, method);
}
},
ie.unit().convert(TRIGGER_HACK_MILLIS, TimeUnit.MILLISECONDS) + ie.interval(),
ie.interval(), ie.unit());
scheduled.add(f);
}
scheduledIntervals.put(expId, scheduled);
}
private void mapWorkers(HITWorkerGroup hitw, String expId) {
if( hitw instanceof HITWorkerImpl ) {
currentExps.put((HITWorkerImpl) hitw, expId);
return;
}
if( hitw instanceof HITWorkerGroupImpl ) {
for( HITWorker worker : ((HITWorkerGroupImpl) hitw).getHITWorkers() ) {
currentExps.put(worker, expId);
}
return;
}
}
/**
* Remove mappings from workers to experiments.
* @param group
* @return
*/
private String unmapWorkers(HITWorkerGroup group) {
if( group instanceof HITWorkerImpl ) {
return currentExps.remove((HITWorkerImpl) group);
}
String expId = null;
if( group instanceof HITWorkerGroupImpl ) {
// TODO unit test to check that all these are the same
for( HITWorker worker : ((HITWorkerGroupImpl) group).getHITWorkers() ) {
expId = currentExps.remove(worker);
}
return expId;
}
return null;
}
public boolean workerIsInProgress(HITWorkerImpl hitw) {
return currentExps.get(hitw) != null;
}
public void scheduleRound(final ExperimentControllerImpl expCont, final int round) {
eventScheduler.schedule(new Runnable() {
public void run() {
startRound( expCont, round );
}
},
0, TimeUnit.MILLISECONDS);
}
private void startRound(ExperimentControllerImpl expCont, int round) {
// TODO use proper start time here
tracker.experimentRoundStarted(expCont, System.currentTimeMillis());
Object data = ImmutableMap.of(
"status", Codec.roundStartMsg,
"round", round);
for( HITWorker id : expCont.group.getHITWorkers() )
SessionUtils.sendServiceMsg(((HITWorkerImpl) id).cometdSession.get(), data);
for( ExperimentListener el : listeners)
el.roundStarted(expCont);
manager.triggerRound(expCont.getExpId(), round);
}
public void saveRoundInput(ExperimentControllerImpl expCont, String inputData) {
tracker.saveRoundInput(expCont, inputData);
}
public void saveLogRound(ExperimentControllerImpl expCont, String roundLog) {
/* save log results for this round
* TODO save proper end time
*/
tracker.experimentRoundComplete(expCont, System.currentTimeMillis(), roundLog);
}
public void setBonusAmount(HITWorkerImpl hitWorker, double amount) {
tracker.saveBonusAmount(hitWorker, amount);
}
public void rcvServiceMsg(HITWorkerImpl worker, Map<String, Object> message) {
String expId = currentExps.get(worker);
if( expId == null ) {
logger.info("{} not in experiment, ignoring service message", worker);
return;
}
manager.deliverServiceMsg(expId, worker, message);
}
public boolean rcvBroadcastMsg(HITWorkerImpl worker, Map<String, Object> message) {
String expId = currentExps.get(worker);
if( expId == null ) {
logger.info("{} not in experiment, ignoring service message", worker);
return false;
}
return manager.deliverBroadcastMsg(expId, worker, message);
}
void workerReconnected(final HITWorkerImpl worker) {
worker.reconnected();
final String expId = currentExps.get(worker);
if( expId == null ) {
logger.info("{} not in experiment, ignoring reconnect callback", worker);
return;
}
// Re-send channel information
Map<String, String> data = ImmutableMap.of(
"status", Codec.status_connectexp,
"channel", worker.expCont.expChannel
);
SessionUtils.sendServiceMsg(worker.cometdSession.get(), data);
// Give worker a chance to subscribe to channels
eventScheduler.schedule(new Runnable() {
public void run() {
manager.triggerWorkerConnect(expId, worker);
}
}, TRIGGER_HACK_MILLIS, TimeUnit.MILLISECONDS);
}
void workerDisconnected(final HITWorkerImpl worker) {
worker.disconnected();
String expId = currentExps.get(worker);
if( expId == null ) {
logger.info("{} not in experiment, ignoring disconnect callback", worker);
return;
}
manager.triggerWorkerDisconnect(expId, worker);
}
void scheduleFinishExperiment(final ExperimentControllerImpl cont) {
eventScheduler.schedule(new Runnable() {
public void run() {
finishExperiment(cont);
}
},
0, TimeUnit.MILLISECONDS);
}
private void finishExperiment(ExperimentControllerImpl cont) {
// Cancel all scheduled interval tasks
List<ScheduledFuture<?>> scheduled = scheduledIntervals.remove(cont.getExpId());
for( ScheduledFuture<?> future : scheduled )
future.cancel(true);
// Tell clients they are done!
for( HITWorker id : cont.group.getHITWorkers() )
SessionUtils.sendStatus(((HITWorkerImpl) id).cometdSession.get(), Codec.doneExpMsg);
manager.deprocessExperiment(cont.getExpId());
// unsubscribe from and/or remove channels
if( bayeux == null )
logger.warn("Skipping bayeux channel destruction...we'd better be in test mode!");
else {
ServerChannel toRemove = null;
if( (toRemove = bayeux.getChannel(Codec.expChanPrefix + cont.expChannel)) != null ) {
toRemove.setPersistent(false);
}
if( (toRemove = bayeux.getChannel(Codec.expSvcPrefix + cont.expChannel)) != null ) {
toRemove.setPersistent(false);
}
}
// save the log to db
String logOutput = cont.log.getOutput();
// This also counts the inactive time of anyone who disconnected before finish
tracker.experimentFinished(cont, logOutput);
// String filename = String.format("%s/%s %d.log", path, expFile, clients.groupSize());
// logger.info("Trying to open file " + filename);
for( ExperimentListener el : listeners ) {
el.experimentFinished(cont);
}
unmapWorkers(cont.group);
}
public ScheduledExecutorService stop() {
eventScheduler.shutdown();
return eventScheduler;
}
}