From f30cb74ace39a12b0e074e8d7e8159e706b5d9f5 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 18 Jul 2008 14:30:17 +0000 Subject: [PATCH] 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 --- .../stats/data/IntSetStatAdder.java | 42 +++++++ .../stats/data/IntStatIncrementer.java | 42 +++++++ src/java/com/threerings/stats/data/Stat.java | 3 +- .../threerings/stats/data/StatModifier.java | 56 ++++++++++ .../stats/server/persist/StatRepository.java | 105 ++++++++++-------- 5 files changed, 202 insertions(+), 46 deletions(-) create mode 100644 src/java/com/threerings/stats/data/IntSetStatAdder.java create mode 100644 src/java/com/threerings/stats/data/IntStatIncrementer.java create mode 100644 src/java/com/threerings/stats/data/StatModifier.java 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; }