diff --git a/src/java/com/threerings/stats/data/IntSetStatAdder.java b/src/java/com/threerings/stats/data/IntSetStatAdder.java new file mode 100644 index 00000000..dac25d8f --- /dev/null +++ b/src/java/com/threerings/stats/data/IntSetStatAdder.java @@ -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 +{ + 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; +} diff --git a/src/java/com/threerings/stats/data/IntStatIncrementer.java b/src/java/com/threerings/stats/data/IntStatIncrementer.java new file mode 100644 index 00000000..bc296fd0 --- /dev/null +++ b/src/java/com/threerings/stats/data/IntStatIncrementer.java @@ -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 +{ + 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; +} diff --git a/src/java/com/threerings/stats/data/Stat.java b/src/java/com/threerings/stats/data/Stat.java index 8307da7b..eedc5c90 100644 --- a/src/java/com/threerings/stats/data/Stat.java +++ b/src/java/com/threerings/stats/data/Stat.java @@ -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 (); diff --git a/src/java/com/threerings/stats/data/StatModifier.java b/src/java/com/threerings/stats/data/StatModifier.java new file mode 100644 index 00000000..4622336a --- /dev/null +++ b/src/java/com/threerings/stats/data/StatModifier.java @@ -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 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; +} diff --git a/src/java/com/threerings/stats/server/persist/StatRepository.java b/src/java/com/threerings/stats/server/persist/StatRepository.java index 1caa332e..8fa5eb5a 100644 --- a/src/java/com/threerings/stats/server/persist/StatRepository.java +++ b/src/java/com/threerings/stats/server/persist/StatRepository.java @@ -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 boolean updateStat (int playerId, StatModifier 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 map = _stringToCode.get(type); + Map map = _stringToCode.get(type); if (map == null) { - _stringToCode.put(type, map = new HashMap()); + _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 map = _codeToString.get(type); + 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 @@ -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 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 fmap = _stringToCode.get(type); + Map fmap = _stringToCode.get(type); if (fmap == null) { - _stringToCode.put(type, fmap = new HashMap()); + _stringToCode.put(type, fmap = Maps.newHashMap()); } fmap.put(value, code); - HashIntMap rmap = _codeToString.get(type); + IntMap rmap = _codeToString.get(type); if (rmap == null) { - _codeToString.put(type, rmap = new HashIntMap()); + _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> _stringToCode = - new HashMap>(); - protected HashMap> _codeToString = - new HashMap>(); + protected Map> _stringToCode = Maps.newHashMap(); + protected Map> _codeToString = Maps.newHashMap(); + + protected static final int MAX_UPDATE_TRIES = 5; }