com.threerings.stats.server.persist.StatRepository Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vilya Show documentation
Show all versions of vilya Show documentation
Facilities for making networked multiplayer games.
//
// $Id: StatRepository.java 1061 2011-03-16 18:18:50Z mdb $
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.stats.server.persist;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.io.ByteArrayOutInputStream;
import com.samskivert.util.IntMap;
import com.samskivert.util.IntMaps;
import com.samskivert.depot.DatabaseException;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.DuplicateKeyException;
import com.samskivert.depot.Funcs;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.FieldDefinition;
import com.samskivert.depot.clause.FromOverride;
import com.samskivert.depot.clause.QueryClause;
import com.samskivert.depot.clause.Where;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.stats.data.Stat;
import com.threerings.stats.data.StatModifier;
import static com.threerings.stats.Log.log;
/**
* Responsible for the persistent storage of per-player statistics.
*/
@Singleton
public class StatRepository extends DepotRepository
implements Stat.AuxDataSource
{
/**
* Constructs a new statistics repository with the specified persistence context.
*/
@Inject public StatRepository (PersistenceContext context)
{
super(context);
}
/**
* Applies a modification to a single stat. If the stat in question does not exist, a blank
* instance will be created via {@link com.threerings.stats.data.Stat.Type#newStat}.
*
* @return the modified Stat, if any modification took place; or null if the modification had
* no effect on the stat's data.
*/
public T updateStat (int playerId, StatModifier modifier)
{
Where where = new Where(StatRecord.PLAYER_ID, playerId,
StatRecord.STAT_CODE, modifier.getType().code());
for (int ii = 0; ii < MAX_UPDATE_TRIES; ii++) {
StatRecord record = load(StatRecord.class, where); // TODO: force cache skip on ii > 0
Stat stat = (record == null) ? modifier.getType().newStat() :
decodeStat(record.statCode, record.statData, record.modCount);
@SuppressWarnings("unchecked") T tstat = (T)stat;
modifier.modify(tstat);
if (!tstat.isModified()) {
return null;
}
if (updateStat(playerId, tstat, false)) {
return tstat;
}
}
throw new DatabaseException(
"Unable to update stat after " + MAX_UPDATE_TRIES + " attempts " +
"[stat=" + modifier.getType() + ", pid=" + playerId + "]");
}
/**
* Loads the stats associated with the specified player.
*
*/
public ArrayList loadStats (int playerId)
{
ArrayList stats = Lists.newArrayList();
Where where = new Where(StatRecord.PLAYER_ID, playerId);
for (StatRecord record : findAll(StatRecord.class, where)) {
Stat stat = decodeStat(record.statCode, record.statData, record.modCount);
if (stat != null) {
stats.add(stat);
}
}
return stats;
}
/**
* Deletes all stats associated with the specified player.
*/
public void deleteStats (final int playerId)
{
deleteAll(StatRecord.class, new Where(StatRecord.PLAYER_ID, playerId));
}
/**
* Writes out any of the stats in the supplied array that have been modified since they were
* first loaded. Exceptions that occur while writing the stats will be caught and logged.
*/
public void writeModified (int playerId, Stat[] stats)
{
writeModified(playerId, Arrays.asList(stats));
}
/**
* Writes out any of the stats in the supplied iterable that have been modified since they were
* first loaded. Exceptions that occur while writing the stats will be caught and logged.
*/
public void writeModified (int playerId, Iterable stats)
{
for (Stat stat : stats) {
try {
if (stat.getType().isPersistent() && stat.isModified()) {
updateStat(playerId, stat, true);
}
} catch (Exception e) {
log.warning("Error flushing modified stat", "stat", stat, e);
}
}
}
// documentation inherited from interface Stat.AuxDataSource
public int getStringCode (Stat.Type type, String value)
{
Map map = _stringToCode.get(type);
if (map == null) {
_stringToCode.put(type, map = Maps.newHashMap());
}
Integer code = map.get(value);
if (code == null) {
try {
code = assignStringCode(type, value);
} catch (DatabaseException pe) {
log.warning("Failed to assign code", "type", type, "value", value, pe);
// at this point the database is probably totally hosed, so we can just punt here,
// and assume that this value will never be persisted
code = -1;
}
mapStringCode(type, value, code);
}
return code;
}
// documentation inherited from interface Stat.AuxDataSource
public String getCodeString (Stat.Type type, int code)
{
IntMap map = _codeToString.get(type);
String value = (map == null) ? null : map.get(code);
if (value == null) {
// our value may have been mapped on a different server, so refresh this mapping table
// from the database; then try again
try {
loadStringCodes(type);
} catch (DatabaseException pe) {
log.warning("Failed to reload string codes", "type", type, "code", code, pe);
}
map = _codeToString.get(type);
value = (map == null) ? null : map.get(code);
if (value == null) {
log.warning("Missing reverse maping", "type", type, "code", code);
value = "__UNKNOWN:" + code + "__"; // we don't want to return null
}
}
return value;
}
/**
* This is only used for testing. Do not call this method.
*/
public void clearMapping (Stat.Type type, String value)
{
int ocode = _stringToCode.get(type).remove(value);
_codeToString.get(type).remove(ocode);
}
/**
* Deletes all data associated with the supplied players.
*/
public void purgePlayers (Collection playerIds)
{
deleteAll(StatRecord.class, new Where(StatRecord.PLAYER_ID.in(playerIds)));
}
/**
* Instantiates the appropriate stat class and decodes the stat from the data.
*/
protected Stat decodeStat (int statCode, byte[] data, byte modCount)
{
Stat.Type type = Stat.getType(statCode);
if (type == null) {
log.warning("Unable to decode stat, unknown type", "code", statCode);
return null;
}
return decodeStat(type.newStat(), data, modCount);
}
/**
* Instantiates the appropriate stat class and decodes the stat from the data.
*/
protected Stat decodeStat (Stat stat, byte[] data, byte modCount)
{
String errmsg = null;
Exception error = null;
try {
// decode its contents from the serialized data
ByteArrayInputStream bin = new ByteArrayInputStream(data);
stat.unpersistFrom(new ObjectInputStream(bin), this);
stat.setModCount(modCount);
return stat;
} catch (ClassNotFoundException cnfe) {
error = cnfe;
errmsg = "Unable to instantiate stat";
} catch (IOException ioe) {
error = ioe;
errmsg = "Unable to decode stat";
}
log.warning(errmsg, "type", stat.getType(), error);
return null;
}
/**
* Updates the specified stat in the database, inserting it if necessary.
*
* @return true if the update was successful, false if it failed due to the stat being
* simultaneously modified by another database client.
*/
protected boolean updateStat (int playerId, final Stat stat, boolean forceWrite)
{
ByteArrayOutInputStream out = new ByteArrayOutInputStream();
try {
stat.persistTo(new ObjectOutputStream(out), this);
} catch (IOException ioe) {
throw new DatabaseException("Error serializing stat " + stat, ioe);
}
byte[] data = out.toByteArray();
byte nextModCount = (byte)((stat.getModCount() + 1) % Byte.MAX_VALUE);
Key key = StatRecord.getKey(playerId, stat.getCode());
// update the row in the database only if it has the expected modCount
int numRows = updatePartial(
StatRecord.class,
new Where(StatRecord.PLAYER_ID, playerId,
StatRecord.STAT_CODE, stat.getCode(),
StatRecord.MOD_COUNT, stat.getModCount()),
key,
StatRecord.STAT_DATA, data, StatRecord.MOD_COUNT, nextModCount);
// if we failed to update any rows, it could be because we saw an unexpected modCount, or
// because the stat did not already exist in the repo
if (numRows == 0) {
// if it didn't exist, let's try to create it
if (load(StatRecord.class, key) == null) {
try {
insert(new StatRecord(playerId, stat.getCode(), data, nextModCount));
numRows = 1;
} catch (DuplicateKeyException e) {
// someone else inserted the StatRecord before we were able to
numRows = 0;
}
}
// if it did exist but we collided with another writer, we may want to write anyway
if (numRows == 0 && forceWrite) {
log.warning("Possible collision while storing StatRecord",
"playerId", playerId, "stat", stat.getType().name(),
"modCount", nextModCount, "overwriting", load(StatRecord.class, key));
store(new StatRecord(playerId, stat.getCode(), data, nextModCount));
numRows = 1;
}
}
return (numRows > 0);
}
/** Helper function for {@link #getStringCode}. */
protected Integer assignStringCode (final Stat.Type type, final String value)
{
for (int ii = 0; ii < 10; ii++) {
MaxStatCodeRecord maxRecord = load(
MaxStatCodeRecord.class,
new FromOverride(StringCodeRecord.class),
new FieldDefinition(MaxStatCodeRecord.MAX_CODE, Funcs.max(StringCodeRecord.CODE)),
new Where(StringCodeRecord.STAT_CODE, type.code()));
int code = maxRecord != null ? maxRecord.maxCode + 1 : 1;
// DEBUG: uncomment this to test code collision
// if (ii == 0 && code > 0) {
// code = code-1;
// }
try {
insert(new StringCodeRecord(type.code(), value, code));
return code;
} catch (DatabaseException pe) {
// if this is not a duplicate row exception, something is booched and we just fail
if (!(pe instanceof DuplicateKeyException)) {
throw pe;
}
// if it is a duplicate row exception, possibly someone inserted our value before
// we could, in which case we can just look up the new mapping
StringCodeRecord record = load(
StringCodeRecord.class, StringCodeRecord.getKey(type.code(), value));
if (record != null) {
log.info("Value collision assigning string code", "type", type, "value", value);
return code;
}
// otherwise someone used the code we were trying to use and we just need to loop
// around and get the next highest code
log.info("Code collision assigning string code", "type", type, "value", value);
}
}
throw new DatabaseException(
"Unable to assign code after 10 attempts [type=" + type + ", value=" + value + "]");
}
/** Helper function used at repository startup. */
protected void loadStringCodes (Stat.Type type)
{
QueryClause[] clauses;
if (type != null) {
clauses = new QueryClause[] { new Where(StringCodeRecord.STAT_CODE, type.code()) };
} else {
clauses = new QueryClause[0];
}
for (StringCodeRecord record : findAll(StringCodeRecord.class, clauses)) {
mapStringCode(Stat.getType(record.statCode), record.value, record.code);
}
}
/** Helper function used at repository startup. */
protected void mapStringCode (Stat.Type type, String value, int code)
{
Map fmap = _stringToCode.get(type);
if (fmap == null) {
_stringToCode.put(type, fmap = Maps.newHashMap());
}
fmap.put(value, code);
IntMap rmap = _codeToString.get(type);
if (rmap == null) {
_codeToString.put(type, rmap = IntMaps.newHashIntMap());
}
rmap.put(code, value);
}
@Override // from DepotRepository
protected void init ()
{
super.init();
// load up our string set mappings
loadStringCodes(null);
}
@Override // from DepotRepository
protected void getManagedRecords (Set> classes)
{
classes.add(StatRecord.class);
classes.add(StringCodeRecord.class);
}
protected Map> _stringToCode = Maps.newHashMap();
protected Map> _codeToString = Maps.newHashMap();
protected static final int MAX_UPDATE_TRIES = 5;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy