Create a notion of a StatModifier which encapsulates a change to a stat. Use

this to automatically retry colliding stat modifications in the repository.

We'll also use the StatModifier to package up and send the modification to the
server that is hosting a user's in-memory stats so that the same adjustment can
be made to those. As a result of this requirement, Stat.Type became
Serializable which means that Stat will probably also need to be serializable
because the pattern we've adopted for our Stat.Type implementationsn is for
them to contain a prototype Stat object which is cloned to create new stats.

Maybe we'll stick with that and make Stat serializable, or maybe we'll do
something else. Not sure yet.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@669 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
Michael Bayne
2008-07-18 14:30:17 +00:00
parent faa237b85d
commit f30cb74ace
5 changed files with 202 additions and 46 deletions
@@ -0,0 +1,42 @@
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.data;
/**
* Adds a value to an {@link IntSetStat}.
*/
public class IntSetStatAdder extends StatModifier<IntSetStat>
{
public IntSetStatAdder (Stat.Type type, int value)
{
super(type);
_value = value;
}
@Override // from StatModifier
public void modify (IntSetStat stat)
{
stat.add(_value);
}
protected int _value;
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.data;
/**
* Increments a particular int stat by a specified amount.
*/
public class IntStatIncrementer extends StatModifier<IntStat>
{
public IntStatIncrementer (Stat.Type type, int delta)
{
super(type);
_delta = delta;
}
@Override // from StatModifier
public void modify (IntStat stat)
{
stat.increment(_delta);
}
protected int _delta;
}
+2 -1
View File
@@ -4,6 +4,7 @@
package com.threerings.stats.data;
import java.io.IOException;
import java.io.Serializable;
import java.util.zip.CRC32;
import com.samskivert.util.HashIntMap;
@@ -27,7 +28,7 @@ public abstract class Stat
/**
* Defines the various per-player tracked statistics.
*/
public interface Type
public interface Type extends Serializable
{
/** Returns a new blank stat instance of the specified type. */
public Stat newStat ();
@@ -0,0 +1,56 @@
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.data;
import java.io.Serializable;
/**
* Encapsulates a modification to a single stat that can be serialized and sent to a different
* server, if needed, to update stats loaded at runtime.
*/
public abstract class StatModifier<T extends Stat> implements Serializable
{
/**
* Creates a modifier that will operate on the supplied stat type. Note that this type may be
* serialized and shipped between servers.
*/
public StatModifier (Stat.Type type)
{
_type = type;
}
/**
* Returns the {@link Stat.Type} of the stat being modified.
*/
public Stat.Type getType ()
{
return _type;
}
/**
* Applies the modification to the stat in question.
*/
public abstract void modify (T stat);
/** The type of the stat on which we're operating. */
protected Stat.Type _type;
}
@@ -8,19 +8,23 @@ import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.google.inject.Singleton;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.io.ByteArrayOutInputStream;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.IntMap;
import com.samskivert.util.IntMaps;
import com.samskivert.jdbc.DuplicateKeyException;
import com.samskivert.jdbc.depot.CacheInvalidator;
import com.samskivert.jdbc.depot.DepotRepository;
import com.samskivert.jdbc.depot.PersistenceContext.CacheEvictionFilter;
import com.samskivert.jdbc.depot.Key;
import com.samskivert.jdbc.depot.PersistenceContext;
import com.samskivert.jdbc.depot.PersistentRecord;
import com.samskivert.jdbc.depot.clause.FieldDefinition;
@@ -33,6 +37,7 @@ 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;
@@ -56,29 +61,35 @@ public class StatRepository extends DepotRepository
}
/**
* Loads a single stat for the specified player.
*/
public Stat loadStat (int playerId, int statCode)
throws PersistenceException
{
Where where = new Where(StatRecord.PLAYER_ID_C, playerId, StatRecord.STAT_CODE_C, statCode);
StatRecord record = load(StatRecord.class, where);
return (null == record) ? null :
decodeStat(record.statCode, record.statData, record.modCount);
}
/**
* Saves a single stat for the specified player, if the stat hasn't been modified in the
* repository since being loaded (that is, if its modCount field in the repository hasn't
* changed).
* Applies a modification to a single stat. If the stat in question does not exist, a blank
* instance will be created via {@link Stat.Type#newStat}.
*
* @return true if the stat was written to the repository, false if its modCount prevented
* it from being written.
* @return true if the stat was modified and written to the database, false if the modifier had
* no effect on the stat's data.
*/
public boolean updateStatIfCurrent (int playerId, Stat stat)
public <T extends Stat> boolean updateStat (int playerId, StatModifier<T> modifier)
throws PersistenceException
{
return updateStat(playerId, stat, true);
Where where = new Where(StatRecord.PLAYER_ID_C, playerId,
StatRecord.STAT_CODE_C, 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 false;
}
if (updateStat(playerId, stat, false)) {
return true;
}
}
throw new PersistenceException(
"Unable to update stat after " + MAX_UPDATE_TRIES + " attempts " +
"[stat=" + modifier.getType() + ", pid=" + playerId + "]");
}
/**
@@ -128,7 +139,7 @@ public class StatRepository extends DepotRepository
try {
if (stats[ii].getType().isPersistent() &&
stats[ii].isModified()) {
updateStat(playerId, stats[ii], false);
updateStat(playerId, stats[ii], true);
}
} catch (Exception e) {
log.warning("Error flushing modified stat [stat=" + stats[ii] + "].", e);
@@ -139,9 +150,9 @@ public class StatRepository extends DepotRepository
// documentation inherited from interface Stat.AuxDataSource
public int getStringCode (Stat.Type type, String value)
{
HashMap<String,Integer> map = _stringToCode.get(type);
Map<String,Integer> map = _stringToCode.get(type);
if (map == null) {
_stringToCode.put(type, map = new HashMap<String,Integer>());
_stringToCode.put(type, map = Maps.newHashMap());
}
Integer code = map.get(value);
if (code == null) {
@@ -162,7 +173,7 @@ public class StatRepository extends DepotRepository
// documentation inherited from interface Stat.AuxDataSource
public String getCodeString (Stat.Type type, int code)
{
HashIntMap<String> map = _codeToString.get(type);
IntMap<String> 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
@@ -236,9 +247,10 @@ public class StatRepository extends DepotRepository
/**
* Updates the specified stat in the database, inserting it if necessary.
*
* @return true if the update was successful, false if it failed.
* @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 failIfUncurrent)
protected boolean updateStat (int playerId, final Stat stat, boolean forceWrite)
throws PersistenceException
{
ByteArrayOutInputStream out = new ByteArrayOutInputStream();
@@ -247,8 +259,10 @@ public class StatRepository extends DepotRepository
} catch (IOException ioe) {
throw new PersistenceException("Error serializing stat " + stat, ioe);
}
byte[] data = out.toByteArray();
byte nextModCount = (byte)((stat.getModCount() + 1) % Byte.MAX_VALUE);
Key<StatRecord> key = StatRecord.getKey(playerId, stat.getCode());
// update the row in the database only if it has the expected modCount
int numRows = updatePartial(
@@ -256,27 +270,28 @@ public class StatRepository extends DepotRepository
new Where(StatRecord.PLAYER_ID_C, playerId,
StatRecord.STAT_CODE_C, stat.getCode(),
StatRecord.MOD_COUNT_C, stat.getModCount()),
StatRecord.getKey(playerId, stat.getCode()),
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 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 it didn't exist, let's
// try to create it.
if (loadStat(playerId, stat.getCode()) == null) {
// 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) {
// somebody else inserted the StatRecord before we were able to.
// someone else inserted the StatRecord before we were able to
numRows = 0;
}
}
if (numRows == 0 && !failIfUncurrent) {
// give up and store the stat anyway
// 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());
"playerId", playerId, "stat", stat.getType().name(),
"overwriting", load(StatRecord.class, key));
store(new StatRecord(playerId, stat.getCode(), data, nextModCount));
numRows = 1;
}
@@ -355,14 +370,14 @@ public class StatRepository extends DepotRepository
/** Helper function used at repository startup. */
protected void mapStringCode (Stat.Type type, String value, int code)
{
HashMap<String,Integer> fmap = _stringToCode.get(type);
Map<String,Integer> fmap = _stringToCode.get(type);
if (fmap == null) {
_stringToCode.put(type, fmap = new HashMap<String,Integer>());
_stringToCode.put(type, fmap = Maps.newHashMap());
}
fmap.put(value, code);
HashIntMap<String> rmap = _codeToString.get(type);
IntMap<String> rmap = _codeToString.get(type);
if (rmap == null) {
_codeToString.put(type, rmap = new HashIntMap<String>());
_codeToString.put(type, rmap = IntMaps.newHashIntMap());
}
rmap.put(code, value);
}
@@ -374,8 +389,8 @@ public class StatRepository extends DepotRepository
classes.add(StringCodeRecord.class);
}
protected HashMap<Stat.Type,HashMap<String,Integer>> _stringToCode =
new HashMap<Stat.Type,HashMap<String,Integer>>();
protected HashMap<Stat.Type,HashIntMap<String>> _codeToString =
new HashMap<Stat.Type,HashIntMap<String>>();
protected Map<Stat.Type,Map<String,Integer>> _stringToCode = Maps.newHashMap();
protected Map<Stat.Type,IntMap<String>> _codeToString = Maps.newHashMap();
protected static final int MAX_UPDATE_TRIES = 5;
}