package edu.harvard.econcs.turkserver.server;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.configuration.Configuration;
import org.cometd.bayeux.server.ServerSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.andrewmao.misc.ConcurrentBooleanCounter;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import edu.harvard.econcs.turkserver.api.Configurator;
import edu.harvard.econcs.turkserver.api.HITWorker;
import edu.harvard.econcs.turkserver.config.TSConfig;
/**
* Default lobby implementation where users that are ready are grouped into
* @author mao
*
*/
public class ReadyStateLobby implements Lobby {
protected final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
final boolean defaultStatus;
final Configurator configurator;
private final ConcurrentBooleanCounter<HITWorkerImpl> lobbyStatus;
final AtomicReference<String> serverMessage;
private LobbyListener lobbyListener;
@Inject
public ReadyStateLobby(
@Named(TSConfig.EXP_CONFIGURATOR) Configurator configurator,
Configuration conf
) {
this.configurator = configurator;
this.defaultStatus = conf.getBoolean(TSConfig.SERVER_LOBBY_DEFAULT);
lobbyStatus = new ConcurrentBooleanCounter<HITWorkerImpl>();
serverMessage = new AtomicReference<String>("");
}
@Override
public void setListener(LobbyListener listener) {
lobbyListener = listener;
}
@Override
public void setMessage(String text) {
serverMessage.set(text);
/* publish the message to lobby
* It's fine to broadcast here as it is a rare occurrence
*/
broadcastLobbyStatus();
}
@Override
public void userJoined(HITWorkerImpl hitw) {
/*
* NOTE: Seemed possible to remove synchronization from this method
* Giving it a shot with lobby unit tests
*/
// Put default lobby status in here
lobbyStatus.put(hitw, defaultStatus);
if( defaultStatus ) {
// Create debug experiments if we have enough players, default to ready
logger.info("lobby has {} people, {} ready", lobbyStatus.size(), lobbyStatus.getTrueCount());
tryExperimentStart();
}
// TODO remove this and let users send their own ready message
broadcastLobbyStatus();
}
private synchronized boolean tryExperimentStart() {
// Don't try anything if not enough people in lobby
int expSize = configurator.groupSize();
if( lobbyStatus.getTrueCount() < expSize ) return false;
// Generate the list of experiment clients
HITWorkerGroupImpl expClients = new HITWorkerGroupImpl();
// Count up exactly expSize people for the new experiment
int counter = 0;
for( Map.Entry<HITWorkerImpl, Boolean> e : lobbyStatus.entrySet() ) {
if( e.getValue() == true ) {
expClients.add(e.getKey());
counter++;
}
// Don't put more than the required number of people, even if more are ready
if(counter == expSize) break;
}
// System.out.println("Current lobby before sending: " + lobbyStatus);
// System.out.println("Lobby sending out a group: " + expClients);
lobbyListener.createNewExperiment(expClients);
/*
* NOTE: They must be in experiment first before we can safely remove from lobby
*
* Avoids race condition where workers removed from lobby but not yet in experiment
* may get placed in the lobby a second time, causing big problems
*/
for( HITWorker id : expClients.getHITWorkers() )
lobbyStatus.remove((HITWorkerImpl) id);
// System.out.println("Current lobby after sending: " + lobbyStatus);
return true;
}
@Override
public boolean updateStatus(HITWorkerImpl hitw, Map<String, Object> data) {
boolean newStatus = defaultStatus || Boolean.parseBoolean(data.get("ready").toString());
/*
* Ignore lobby updates for people not in lobby
* MONUMENT FOR MASSIVE DOUBLE-JOIN MYSTERY BUG
*/
Boolean oldStatus = lobbyStatus.replace(hitw, newStatus);
/*
* Do nothing if user was already removed from lobby
* or there was no change to the status
*/
if( oldStatus == null || oldStatus == newStatus ) {
broadcastLobbyStatus(); // TODO remove this once lobby updates are fixed
return false;
}
tryExperimentStart();
checkLobbyState();
// Notify everyone who is remaining in the lobby
broadcastLobbyStatus();
return true;
}
void checkLobbyState() {
if( !defaultStatus && lobbyStatus.size() < configurator.groupSize() ) {
// Make sure everyone's ready is disabled
for( HITWorkerImpl id : lobbyStatus.keySet() ) {
lobbyStatus.replace(id, false);
}
}
}
@Override
public boolean userQuit(HITWorkerImpl worker) {
// Was this dude in the lobby? If so remove him from the lobby and notify lobby ppl
if( worker == null ) return false;
synchronized(this) {
if( lobbyStatus.remove(worker) == null ) return false;
}
logger.info(String.format("%s (%s) removed from lobby",
worker.getHitId(), worker.getUsername()));
checkLobbyState();
// TODO forward this quit message to lobby instead of a whole list
// ServerSession session = worker.cometdSession.get();
// Map<String, Object> data = new TreeMap<String, Object>();
// data.put("status", "quit");
// if( session != null ) data.put("id", session.getId());
// data.put("username", worker.getUsername());
// lobbyListener.broadcastLobbyMessage(data);
broadcastLobbyStatus();
return true;
}
@Override
public Set<HITWorkerImpl> getLobbyUsers() {
return lobbyStatus.keySet();
}
@Override
public Object getStatus(HITWorkerImpl hitw) {
return lobbyStatus.get(hitw);
}
void broadcastLobbyStatus() {
Map<String, Object> data = new TreeMap<String, Object>();
int usersInLobby = lobbyStatus.size();
int usersNeeded = configurator.groupSize();
data.put("status", "update");
data.put("numusers", usersInLobby);
data.put("numneeded", usersNeeded);
data.put("joinenabled", usersInLobby >= usersNeeded);
data.put("servermsg", serverMessage.get());
data.put("currentexps", lobbyListener.getNumExperimentsRunning());
data.put("totalusers", lobbyListener.getNumUsersConnected());
/* TODO could be some race conditions here if lobby size changes?
* i.e. ArrayIndexOutOfBounds when array size changes
*/
List<Object> users = new LinkedList<Object>();
for( Map.Entry<HITWorkerImpl, Boolean> e : lobbyStatus.entrySet() ) {
HITWorkerImpl user = e.getKey();
ServerSession session = user.cometdSession.get();
if( session == null ) continue;
// clientId, username, and status
users.add(ImmutableMap.<String, Object>of(
"id", session.getId(),
"username", user.getUsername(),
"ready", e.getValue() ));
}
data.put("users", users);
lobbyListener.broadcastLobbyMessage(data);
}
}