/** * Copyright 2010 The University of Nottingham * * This file is part of lobbyservice. * * lobbyservice is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * lobbyservice is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with lobbyservice. If not, see <http://www.gnu.org/licenses/>. * */ package uk.ac.horizon.ug.lobby.server; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.Query; import com.google.appengine.api.datastore.Key; import uk.ac.horizon.ug.lobby.ConfigurationUtils; import uk.ac.horizon.ug.lobby.Constants; import uk.ac.horizon.ug.lobby.model.EMF; import uk.ac.horizon.ug.lobby.model.GameInstance; import uk.ac.horizon.ug.lobby.model.GameInstanceFactory; import uk.ac.horizon.ug.lobby.model.GameInstanceNominalStatus; import uk.ac.horizon.ug.lobby.model.GameInstanceStatus; import uk.ac.horizon.ug.lobby.model.GameServer; import uk.ac.horizon.ug.lobby.model.GameServerStatus; import uk.ac.horizon.ug.lobby.model.ServerConfiguration; /** GameInstance (background) tasks. * * @author cmg * */ public class GameInstanceTasks implements Constants { static Logger logger = Logger.getLogger(FactoryUtils.class.getName()); /** check all GameInstances - periodic task */ public static void checkAllGameInstances() { EntityManager em = EMF.get().createEntityManager(); try { // TODO narrow down set of GameInstances needing checking? Query q = em.createQuery("SELECT x FROM GameInstance x ORDER BY x."+START_TIME); List<GameInstance> gis = (List<GameInstance>)q.getResultList(); for (GameInstance gi : gis) { try { checkGameInstance(gi); } catch (Exception e) { logger.log(Level.WARNING,"doing checkGameInstance("+gi+")", e); } } } finally { em.close(); } } /** check one GameInstance - periodic. * (watch out for races if called from elsewhere/concurrently) */ private static void checkGameInstance(GameInstance gi) { // don't fiddle with UNMANAGED instances if (gi.getStatus()==GameInstanceStatus.UNMANAGED || gi.getStatus()==null) return; // GameInstanceFactory (currently) creates new instances with nominalStatus 'PLANNED'. // Query returns GameInstances with nominalStatus IN ( 'PLANNED', 'POSSIBLE', 'AVAILABLE', 'TEMPORARILY_UNAVAILABLE' ) // i.e. not CANCELLED or ENDED // Join returns TRY_LATER for instances with nominalStatus 'PLANNED', 'POSSIBLE', 'TEMPORARILY_UNAVAILABLE' // returns ERROR_CANCELLED/_ENDED for instances with nominalStatus 'CANCELLED' or 'ENDED' // returns ERROR_ENDED for instances with nominalStatus AVAILABLE after endTime // Join, for instances which are AVAILABLE, before endTime... // returns TRY_LAYER for servers with targetStatus not 'UP' (i.e. UNKNOWN, DOES_NOT_EXIST, STOPPED, DOWN, ERROR) // else calls serverProtocol.handlePlayRequest GameInstanceFactory factory = null; GameServer server = null; EntityManager em = EMF.get().createEntityManager(); try { if (gi.getGameInstanceFactoryKey()!=null) { factory = em.find(GameInstanceFactory.class, gi.getGameInstanceFactoryKey()); if (factory==null) { // TODO audit? logger.warning("Could not find Factory "+gi.getGameInstanceFactoryKey()+" for instance "+gi.getKey()); return; } } // can't (won't) do anything with non-factory instances (not enough information) if (factory==null) return; Key serverKey = gi.getGameServerId(); if (serverKey==null && factory!=null) serverKey = factory.getGameServerId(); if (serverKey==null) { logger.warning("No Server specified for instance "+gi.getKey()+"(or factory "+gi.getGameInstanceFactoryKey()+")"); // TODO audit? return; } server = em.find(GameServer.class, serverKey); if (server==null) { logger.warning("Could not find Server "+serverKey+" for instance "+gi.getKey()+"(or factory "+gi.getGameInstanceFactoryKey()+")"); // TODO audit? return; } } finally { em.close(); } // Factory tells us server(Create,Start,Ending,End)TimeOffsetMs. // also serverConfigJson // Server tells us baseUrl if not already in Instance (Factory doesn't put it there) // GameInstance status is our view of the 'real' (external) game instance status. // Do we need to change the instance's nominalStatus? long now = System.currentTimeMillis(); GameInstanceNominalStatus targetNominalStatus = gi.getNominalStatus(); switch(targetNominalStatus) { case AVAILABLE: if (now>=gi.getEndTime()) // should have ended now targetNominalStatus = GameInstanceNominalStatus.ENDED; break; case CANCELLED: // no op?! break; case ENDED: // no op break; case PLANNED: case TEMPORARILY_UNAVAILABLE: if (now>=gi.getEndTime()) // should have ended now targetNominalStatus = GameInstanceNominalStatus.ENDED; else if (now>=gi.getStartTime() || now>=gi.getStartTime()+factory.getServerCreateTimeOffsetMs()) // should be available now targetNominalStatus = GameInstanceNominalStatus.AVAILABLE; break; } // given that target nominal status, what 'real' status would we expect at the moment? GameInstanceStatus targetStatus = null; switch (targetNominalStatus) { case PLANNED: case AVAILABLE: case ENDED: case TEMPORARILY_UNAVAILABLE: // never actually a target status! if (now<gi.getStartTime()+factory.getServerCreateTimeOffsetMs()) // not yet time targetStatus = GameInstanceStatus.PLANNED; else if (now<gi.getStartTime()+factory.getServerStartTimeOffsetMs()) // not yet start targetStatus = GameInstanceStatus.READY; else if (now<gi.getEndTime()+factory.getServerEndingTimeOffsetMs()) // not yet ending targetStatus = GameInstanceStatus.ACTIVE; else if (now<gi.getEndTime()+factory.getServerEndTimeOffsetMs()) // not yet end targetStatus = GameInstanceStatus.ENDING; else targetStatus = GameInstanceStatus.ENDED; break; case CANCELLED: if (gi.getStatus()==GameInstanceStatus.PLANNED || gi.getStatus()==GameInstanceStatus.CANCELLED) targetStatus = GameInstanceStatus.CANCELLED; else // it has already started in some way, so the best we can do is end it targetStatus = GameInstanceStatus.ENDED; break; } // some basic consistency checking... if (targetNominalStatus==GameInstanceNominalStatus.AVAILABLE && (targetStatus!=GameInstanceStatus.READY && targetStatus!=GameInstanceStatus.ACTIVE && targetStatus!=GameInstanceStatus.ENDING)) { logger.warning("GameInstance(Factory) configuration problem: targetNominalStatus is "+targetNominalStatus+" but targetStatus="+targetStatus+" (not joinable): "+factory); } else if (targetNominalStatus==GameInstanceNominalStatus.ENDED && (targetStatus!=GameInstanceStatus.ENDING && targetStatus!=GameInstanceStatus.ENDED)) { logger.warning("GameInstance(Factory) configuration problem: targetNominalStatus is "+targetNominalStatus+" but targetStatus="+targetStatus+" (not ending): "+factory); } try { logger.info("NominalStatus="+gi.getNominalStatus()+", target="+targetNominalStatus+", status="+gi.getStatus()+", target="+targetStatus); // lets try to achieve these target states... switch (gi.getStatus()) { case ACTIVE: switch (targetStatus) { case ACTIVE: // no-op break; case ENDED: case CANCELLED: gi = doEndFromPreparing(gi, factory, server); break; case ENDING: gi = doEndingFromActive(gi, factory, server); break; default: // PLANNED, READY // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; case CANCELLED: switch (targetStatus) { case ENDED: case CANCELLED: // no op break; default: // PLANNED, READY, ACTIVE // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; case ENDED: switch (targetStatus) { case ENDED: case CANCELLED: // no op break; default: // PLANNED, READY, ACTIVE // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; case ENDING: switch (targetStatus) { case ENDING: // no-op break; case ENDED: case CANCELLED: gi = doEndFromPreparing(gi, factory, server); break; default: // ACTIVE, PLANNED, READY // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; //case ERROR: // case FAILED: // case PAUSED: case PLANNED: switch (targetStatus) { case PLANNED: // no-op break; case ENDED: case CANCELLED: gi = doCancelFromPlanned(gi, factory, server); break; case READY: gi = doPreparingFromPlanned(gi, factory, server); gi = doReadyFromPreparing(gi, factory, server); break; case ACTIVE: gi = doPreparingFromPlanned(gi, factory, server); gi = doReadyFromPreparing(gi, factory, server); gi = doActiveFromReady(gi, factory, server); break; case ENDING: gi = doPreparingFromPlanned(gi, factory, server); gi = doReadyFromPreparing(gi, factory, server); gi = doActiveFromReady(gi, factory, server); gi = doEndingFromActive(gi, factory, server); break; default: // ? // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; // case POSSIBLE: case PREPARING: // assume game exists but not yet ready switch (targetStatus) { case ENDED: case CANCELLED: gi = doEndFromPreparing(gi, factory, server); break; case READY: gi = doReadyFromPreparing(gi, factory, server); break; case ACTIVE: gi = doReadyFromPreparing(gi, factory, server); gi = doActiveFromReady(gi, factory, server); break; case ENDING: gi = doReadyFromPreparing(gi, factory, server); gi = doActiveFromReady(gi, factory, server); gi = doEndingFromActive(gi, factory, server); break; default: // ? // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; case READY: switch (targetStatus) { case ENDED: case CANCELLED: gi = doEndFromPreparing(gi, factory, server); break; case READY: // no op break; case ACTIVE: gi = doActiveFromReady(gi, factory, server); break; case ENDING: gi = doActiveFromReady(gi, factory, server); gi = doEndingFromActive(gi, factory, server); break; default: // ? // just leave it logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); } break; // case STOPPED: case UNMANAGED: // shouldn't be here anyway! logger.warning("desired instance status change "+gi.getStatus()+" -> "+targetStatus+" not possible: "+gi); break; } } catch (Exception e) { logger.log(Level.WARNING, "Error managing game instance ", e); } em = EMF.get().createEntityManager(); EntityTransaction et= em.getTransaction(); // transaction! et.begin(); try { // check / update nominal status GameInstance ngi = em.find(GameInstance.class, gi.getKey()); if (ngi.getStatus()==targetStatus || // at the instance level ENDED and CANCELLED are essentially the same outcome (no more game) (ngi.getStatus()==GameInstanceStatus.CANCELLED && targetStatus==GameInstanceStatus.ENDED) || (ngi.getStatus()==GameInstanceStatus.ENDED && targetStatus==GameInstanceStatus.CANCELLED)) { if (ngi.getNominalStatus()!=targetNominalStatus) { // met target so presumably met nominal target ngi.setNominalStatus(targetNominalStatus); em.merge(ngi); et.commit(); logger.info("GameInstance reached targetStatus="+targetStatus+"; updating nominalStatus to "+targetNominalStatus); } else // no op et.rollback(); } else { // didn't meet target, so... // AVAILABLE is the only one with teeth! if (targetNominalStatus==GameInstanceNominalStatus.AVAILABLE && ngi.getNominalStatus()==GameInstanceNominalStatus.AVAILABLE && (ngi.getStatus()!=GameInstanceStatus.READY && ngi.getStatus()!=GameInstanceStatus.ACTIVE && ngi.getStatus()!=GameInstanceStatus.ENDING)) { logger.warning("Failed to reach AVAILABLE status: "+ngi.getStatus()+"; marking "+gi.getKey()+" as TEMPORARILY_UNAVAILABLE"); ngi.setNominalStatus(GameInstanceNominalStatus.TEMPORARILY_UNAVAILABLE); em.merge(ngi); et.commit(); } else logger.warning("Failed to reach target status "+targetStatus+": "+ngi.getStatus()+"; leaving nominal status ("+ngi.getNominalStatus()+")"); } } finally { if (et.isActive()) et.rollback(); em.close(); } } private static GameInstance updateStatus(GameInstance gi, GameInstanceStatus oldStatus, GameInstanceStatus newStatus) { EntityManager em = EMF.get().createEntityManager(); EntityTransaction et = em.getTransaction(); try { et.begin(); GameInstance ngi = em.find(GameInstance.class, gi.getKey()); if (oldStatus!=null && ngi.getStatus()!=oldStatus) throw new RuntimeException("updateStatus found status "+ngi.getStatus()+" vs "+oldStatus+" - refused"); ngi.setStatus(newStatus); em.merge(ngi); et.commit(); return ngi; } finally { if (et.isActive()) et.rollback(); em.close(); } // don't fiddle with cache } private static ServerProtocol getServerProtocol(GameServer server) { if (server.getTargetStatus()!=GameServerStatus.UP) throw new RuntimeException("GameServer "+server.getTitle()+" is not intended to be up ("+server.getTargetStatus()+")"); ServerProtocol serverProtocol = server.getType().serverProtocol(); return serverProtocol; } private static GameInstance doCancelFromPlanned(GameInstance gi, GameInstanceFactory factory, GameServer server) { return updateStatus(gi, GameInstanceStatus.PLANNED, GameInstanceStatus.CANCELLED); } private static GameInstance doPreparingFromPlanned(GameInstance gi, GameInstanceFactory factory, GameServer server) throws ConfigurationException, IOException { getServerProtocol(server).handleGameInstancePreparingFromPlanned(gi, factory, server); return updateStatus(gi, GameInstanceStatus.PLANNED, GameInstanceStatus.PREPARING); } private static GameInstance doReadyFromPreparing(GameInstance gi, GameInstanceFactory factory, GameServer server) throws ConfigurationException, IOException { getServerProtocol(server).handleGameInstanceReadyFromPreparing(gi, factory, server); return updateStatus(gi, GameInstanceStatus.PREPARING, GameInstanceStatus.READY); } private static GameInstance doActiveFromReady(GameInstance gi, GameInstanceFactory factory, GameServer server) throws ConfigurationException, IOException { getServerProtocol(server).handleGameInstanceActiveFromReady(gi, factory, server); return updateStatus(gi, GameInstanceStatus.READY, GameInstanceStatus.ACTIVE); } private static GameInstance doEndingFromActive(GameInstance gi, GameInstanceFactory factory, GameServer server) throws ConfigurationException, IOException { getServerProtocol(server).handleGameInstanceEndingFromActive(gi, factory, server); return updateStatus(gi, GameInstanceStatus.ACTIVE, GameInstanceStatus.ENDING); } private static GameInstance doEndFromPreparing(GameInstance gi, GameInstanceFactory factory, GameServer server) throws ConfigurationException, IOException { // Note: this can also be called from READY, ACTIVE and ENDING getServerProtocol(server).handleGameInstanceEnd(gi, factory, server); return updateStatus(gi, null, GameInstanceStatus.ENDED); } }