/**
* 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.util.List;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;
import uk.ac.horizon.ug.lobby.ConfigurationUtils;
import uk.ac.horizon.ug.lobby.Constants;
import uk.ac.horizon.ug.lobby.model.Account;
import uk.ac.horizon.ug.lobby.model.AuditRecordLevel;
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.GameInstanceFactoryType;
import uk.ac.horizon.ug.lobby.model.GameInstanceNominalStatus;
import uk.ac.horizon.ug.lobby.model.GameInstanceStatus;
import uk.ac.horizon.ug.lobby.model.GameTemplate;
import uk.ac.horizon.ug.lobby.model.GameTemplateAuditRecordType;
import uk.ac.horizon.ug.lobby.model.GameTemplateVisibility;
import uk.ac.horizon.ug.lobby.model.ServerConfiguration;
/** GameInstanceFactory tasks, e.g. create instances.
*
* @author cmg
*
*/
public class FactoryTasks implements Constants {
static Logger logger = Logger.getLogger(FactoryUtils.class.getName());
/** check all GameInstanceFactorys - periodic task */
public static void checkAllGameInstanceFactories() {
ServerConfiguration sc = ConfigurationUtils.getServerConfiguration();
EntityManager em = EMF.get().createEntityManager();
try {
Query q = em.createQuery("SELECT x FROM GameInstanceFactory x ORDER BY x."+LAST_INSTANCE_CHECK_TIME+" ASC");
List<GameInstanceFactory> gifs = (List<GameInstanceFactory>)q.getResultList();
for (GameInstanceFactory gif : gifs) {
checkGameInstanceFactory(sc, gif);
}
}
finally {
em.close();
}
}
/** check all GameInstanceFactorys for a GameTemplate */
public static void checkGameInstanceFactories(GameTemplate gt) {
ServerConfiguration sc = ConfigurationUtils.getServerConfiguration();
EntityManager em = EMF.get().createEntityManager();
try {
Query q = em.createQuery("SELECT x FROM GameInstanceFactory x WHERE x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID+" ORDER BY x."+LAST_INSTANCE_CHECK_TIME+" ASC");
q.setParameter(GAME_TEMPLATE_ID, gt.getId());
List<GameInstanceFactory> gifs = (List<GameInstanceFactory>)q.getResultList();
for (GameInstanceFactory gif : gifs) {
checkGameInstanceFactory(sc, gif);
}
}
finally {
em.close();
}
}
/** one hour */
public static final long ONE_HOUR = 3600000;
/** max nominal check interval - limits rate at which tokens can be added (5 minutes) */
public static final long MAX_CHECK_INTERVAL_MS = ONE_HOUR;
/**
* @param em
* @param gif
*/
public static void checkGameInstanceFactory(ServerConfiguration sc, GameInstanceFactory gif) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();
long now = System.currentTimeMillis();
try {
GameInstanceFactory ngif = em.find(GameInstanceFactory.class, gif.getKey());
// update tokens
long lastCheckTime = ngif.getLastInstanceCheckTime();
if (lastCheckTime==0)
lastCheckTime = now;
long elapsed = now-lastCheckTime;
if (elapsed > MAX_CHECK_INTERVAL_MS)
{
logger.warning("Limiting checkGameInstanceFactory elapsed to "+MAX_CHECK_INTERVAL_MS+" (was "+elapsed+"ms)");
elapsed = MAX_CHECK_INTERVAL_MS;
}
// lastCheckTime offset into hour
long lastCheckTimeHourOffsetMs = lastCheckTime % ONE_HOUR;
int perHour = ngif.getNewInstanceTokensPerHour();
if (perHour > sc.getMaxNewInstanceTokensPerHour())
perHour = sc.getMaxNewInstanceTokensPerHour();
// careful with those rounding errors, Eugene...
int newTokens = (int)((lastCheckTimeHourOffsetMs+elapsed)*perHour/ONE_HOUR-(lastCheckTimeHourOffsetMs)*perHour/ONE_HOUR);
// paranoid
if (newTokens>perHour) {
logger.warning("newTokens came out > perHour ("+newTokens+" vs "+perHour+") for "+gif);
newTokens = perHour;
}
if (newTokens<0) {
logger.warning("newTokens came out < 0 ("+newTokens+") for "+gif);
newTokens = 0;
}
int tokens = ngif.getNewInstanceTokens()+newTokens;
if (tokens > ngif.getNewInstanceTokensMax())
tokens = ngif.getNewInstanceTokensMax();
if (tokens > sc.getMaxNewInstanceTokensMax())
tokens = sc.getMaxNewInstanceTokensMax();
if (tokens!=ngif.getNewInstanceTokens()) {
logger.info("Changed newInstanceTokens to "+tokens+" from "+ngif.getNewInstanceTokens()+" for "+gif.getTitle());
}
ngif.setNewInstanceTokens(tokens);
ngif.setLastInstanceCheckTime(now);
em.merge(ngif);
et.commit();
gif = ngif;
}
finally {
if (et.isActive())
et.rollback();
em.close();
}
if (gif.getType()!=GameInstanceFactoryType.SCHEDULED)
// not scheduled
return;
if (gif.getStartTimeOptionsJson()==null) {
// no cron...
AuditUtils.logGameTemplateAuditRecordIfNovel(gif.getGameTemplateId(), gif.getKey(), /*gameInstanceKey*/null, /*accountKey*/null, /*clientIp*/null, System.currentTimeMillis(), GameTemplateAuditRecordType.SYSTEM_CREATE_GAME_INSTANCE_FAILED, AuditRecordLevel.WARNING, /*detailsJson*/null, "Unable to check GameInstanceFactory - no startTimeOptionsJson");
return;
}
int tokensCache = gif.getNewInstanceTokens();
long checkTime = gif.getLastInstanceStartTime();
if (checkTime < now) {
logger.warning("Skipping checks from lastInstanceStartTime "+checkTime+" to now ("+now+") for "+gif.getTitle());
// TODO audit record
checkTime = now;
}
if (checkTime < gif.getMinTime()-1)
// will be advanced by one before first check!
checkTime = gif.getMinTime()-1;
long maxTime = now+gif.getInstanceCreateTimeWindowMs();
if (gif.getMaxTime() < maxTime)
maxTime = gif.getMaxTime();
try {
TreeSet values[] = FactoryUtils.parseTimeOptionsJson(gif.getStartTimeOptionsJson());
// starting from last check time repeatedly find the next start time,
while (checkTime<=maxTime) {
// advance first
checkTime = checkTime+1;
// TODO make more efficient by cacheing parsed state
long nextStartTime = FactoryUtils.getNextCronTime(gif.getStartTimeCron(), values, checkTime, maxTime);
if (nextStartTime<=0 || nextStartTime>maxTime)
// done
break;
// check if the instance already exists,
// if not create it
tokensCache = checkGameInstanceFactoryInstance(gif, nextStartTime, tokensCache);
if (tokensCache<0) {
// audit
logger.warning("GameInstanceFactory could not create instance at "+nextStartTime+" due to token limit: "+gif);
AuditUtils.logGameTemplateAuditRecordIfNovel(gif.getGameTemplateId(), gif.getKey(), /*gameInstanceKey*/null, /*accountKey*/null, /*clientIp*/null, System.currentTimeMillis(), GameTemplateAuditRecordType.SYSTEM_CREATE_GAME_INSTANCE_FAILED, AuditRecordLevel.WARNING, /*detailsJson*/null, "Unable to create GameInstance - no tokens");
break;
}
checkTime = nextStartTime;
}
} catch (CronExpressionException e) {
logger.warning("Unable to checkGameInstanceFactory "+gif.getKey()+": "+e);
AuditUtils.logGameTemplateAuditRecordIfNovel(gif.getGameTemplateId(), gif.getKey(), /*gameInstanceKey*/null, /*accountKey*/null, /*clientIp*/null, System.currentTimeMillis(), GameTemplateAuditRecordType.SYSTEM_CREATE_GAME_INSTANCE_FAILED, AuditRecordLevel.WARNING, /*detailsJson*/null, "Unable to check GameInstanceFactory ("+e.getMessage()+")");
} catch (Exception e) {
logger.log(Level.WARNING, "error checking GameInstanceFactory "+gif, e);
AuditUtils.logGameTemplateAuditRecordIfNovel(gif.getGameTemplateId(), gif.getKey(), /*gameInstanceKey*/null, /*accountKey*/null, /*clientIp*/null, System.currentTimeMillis(), GameTemplateAuditRecordType.SYSTEM_CREATE_GAME_INSTANCE_FAILED, AuditRecordLevel.WARNING, /*detailsJson*/null, "Unable to check GameInstanceFactory ("+e.getMessage()+")");
}
}
/**
* @param gif
* @param nextStartTime
* @return new value of newInstanceTokens; returns -1 to signal could not create
*/
private static int checkGameInstanceFactoryInstance(
GameInstanceFactory gif, long startTime, int tokensCache) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();
try {
// does this instance already exist?
Query q = em.createQuery("SELECT x FROM GameInstance x WHERE x."+GAME_INSTANCE_FACTORY_KEY+" = :"+GAME_INSTANCE_FACTORY_KEY+" AND x."+GAME_TEMPLATE_ID+" = :"+GAME_TEMPLATE_ID+" AND x."+START_TIME+" = :"+START_TIME);
q.setParameter(GAME_INSTANCE_FACTORY_KEY, gif.getKey());
q.setParameter(GAME_TEMPLATE_ID, gif.getGameTemplateId());
q.setParameter(START_TIME, startTime);
List<GameInstance> fgis = q.getResultList();
if (fgis.size()>0) {
GameInstance fgi = fgis.get(0);
if (fgi.getVisibility()!=GameTemplateVisibility.PUBLIC) {
logger.warning("GameInstance "+gif.getKey()+" / "+startTime+" exists but is "+fgi.getVisibility());
}
else if (fgi.getNominalStatus()==GameInstanceNominalStatus.CANCELLED || fgi.getNominalStatus()==GameInstanceNominalStatus.ENDED) {
logger.warning("GameInstance "+gif.getKey()+" / "+startTime+" exists but is "+fgi.getNominalStatus());
}
return tokensCache;
}
if (tokensCache<=0)
return -1;
et.rollback();
// if you reach here you need to create it...
int rtokens[] = new int[1];
createGameInstanceFactoryInstance(gif, null, startTime, null, null, rtokens);
return rtokens[0];
}
finally {
if (et.isActive())
et.rollback();
em.close();
}
}
/**
* @param gif
* @param nextStartTime
* @return new value of newInstanceTokens; returns -1 to signal could not create
*/
public static GameInstance createGameInstanceFactoryInstance(
GameInstanceFactory gif, GameTemplateVisibility visibility, long startTime, Account account, String clientIp, int rtokens[]) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();
try {
// create GameInstance on demand?!
GameInstance ngi = new GameInstance();
ngi.setAllowAnonymousClients(gif.isAllowAnonymousClients());
//ngi.setBaseUrl();
ngi.setCreatedTime(System.currentTimeMillis());
ngi.setEndTime(startTime+gif.getDurationMs());
ngi.setGameInstanceFactoryKey(gif.getKey());
ngi.setGameServerId(gif.getGameServerId());
ngi.setGameTemplateId(gif.getGameTemplateId());
switch(gif.getLocationType()) {
case GLOBAL:
ngi.setRadiusMetres(0);
break;
case SPECIFIED_LOCATION:
ngi.setLatitudeE6(gif.getLatitudeE6());
ngi.setLongitudeE6(gif.getLongitudeE6());
break;
}
ngi.setLocationName(gif.getLocationName());
ngi.setMaxNumSlots(gif.getMaxNumSlots());
// SCHEDULED => PLANNED
ngi.setNominalStatus(GameInstanceNominalStatus.PLANNED);
ngi.setStatus(GameInstanceStatus.PLANNED);
ngi.setNumSlotsAllocated(0);
ngi.setStartTime(startTime);
//ngi.setStatus()
// TODO symbol subst? in title
ngi.setTitle(gif.getInstanceTitle());
ngi.setVisibility(visibility!=null ? visibility : gif.getInstanceVisibility());
// cache
ngi.setFull(ngi.getNumSlotsAllocated()>=ngi.getMaxNumSlots());
em.persist(ngi);
et.commit();
// delete one from tokens
et.begin();
GameInstanceFactory ngif = em.find(GameInstanceFactory.class, gif.getKey());
ngif.setNewInstanceTokens(ngif.getNewInstanceTokens()-1);
if (startTime>ngif.getLastInstanceStartTime())
ngif.setLastInstanceStartTime(startTime);
else
logger.warning("GameInstanceFactory startTime "+startTime+" before lastInstanceStartTime "+ngif.getLastInstanceStartTime());
em.merge(ngif);
et.commit();
gif = ngif;
AuditUtils.logGameTemplateAuditRecord(gif.getGameTemplateId(), gif.getKey(), ngi.getKey(), account!=null ? account.getKey() : null, clientIp, System.currentTimeMillis(), GameTemplateAuditRecordType.SYSTEM_CREATE_GAME_INSTANCE, AuditRecordLevel.NORMAL, /*detailsJson*/"{}", "Created GameInstance");
//logger.info("Added GameInstance "+ngi+" (tokens="+ngif.getNewInstanceTokens()+")");
//if (ngif.getNewInstanceTokens()>0)
//return ngif.getNewInstanceTokens();
if (rtokens!=null)
rtokens[0] = ngif.getNewInstanceTokens();
if (ngif.getNewInstanceTokens()<0)
logger.warning("GameInstanceFactory in token-debt ("+ngif.getNewInstanceTokens()+"): "+ngif);
return ngi;
}
finally {
if (et.isActive())
et.rollback();
em.close();
}
}
}