diff --git a/src/java/com/threerings/stats/data/Stat.java b/src/java/com/threerings/stats/data/Stat.java index 6d40fc1d..8307da7b 100644 --- a/src/java/com/threerings/stats/data/Stat.java +++ b/src/java/com/threerings/stats/data/Stat.java @@ -122,6 +122,23 @@ public abstract class Stat _modified = modified; } + /** + * Returns the modification count of this Stat when it was loaded from the repository. + */ + public byte getModCount () + { + return _modCount; + } + + /** + * Sets the modification count for this Stat. StatRepository calls this when converting + * a StatRecord into a Stat; it shouldn't be called otherwise. + */ + public void setModCount (byte modCount) + { + _modCount = modCount; + } + /** Writes our custom streamable fields. */ public void writeObject (ObjectOutputStream out) throws IOException @@ -192,6 +209,9 @@ public abstract class Stat * was loaded from the database. */ protected transient boolean _modified; + /** The last known modification count for this stat, if it was read from the database. */ + protected transient byte _modCount; + /** Used for computing our stat codes. */ protected static CRC32 _crc = new CRC32(); diff --git a/src/java/com/threerings/stats/server/persist/StatRecord.java b/src/java/com/threerings/stats/server/persist/StatRecord.java index 102d9043..118eabd8 100644 --- a/src/java/com/threerings/stats/server/persist/StatRecord.java +++ b/src/java/com/threerings/stats/server/persist/StatRecord.java @@ -52,9 +52,16 @@ public class StatRecord extends PersistentRecord /** The qualified column identifier for the {@link #statData} field. */ public static final ColumnExp STAT_DATA_C = new ColumnExp(StatRecord.class, STAT_DATA); + + /** The column identifier for the {@link #modCount} field. */ + public static final String MOD_COUNT = "modCount"; + + /** The qualified column identifier for the {@link #modCount} field. */ + public static final ColumnExp MOD_COUNT_C = + new ColumnExp(StatRecord.class, MOD_COUNT); // AUTO-GENERATED: FIELDS END - public static final int SCHEMA_VERSION = 2; + public static final int SCHEMA_VERSION = 4; /** The identifier of the player this is a stat for. */ @Id @@ -70,6 +77,13 @@ public class StatRecord extends PersistentRecord @Column(name="STAT_DATA", length=65535) public byte[] statData; + /** + * The number of times this stat has been updated, so that simultaneous attempts to update + * the same stat in the repository can be caught and handled. Stored as a byte because + * we don't expect to ever have more than a couple simultaneous attempts to update a stat. + */ + public byte modCount; + /** * An empty constructor for unmarshalling. */ @@ -82,11 +96,17 @@ public class StatRecord extends PersistentRecord * A constructor that populates all our fields. */ public StatRecord (int playerId, int statCode, byte[] data) + { + this(playerId, statCode, data, (byte)0); + } + + public StatRecord (int playerId, int statCode, byte[] data, byte modCount) { super(); this.playerId = playerId; this.statCode = statCode; this.statData = data; + this.modCount = modCount; } // AUTO-GENERATED: METHODS START diff --git a/src/java/com/threerings/stats/server/persist/StatRepository.java b/src/java/com/threerings/stats/server/persist/StatRepository.java index a5624051..abb7b3fb 100644 --- a/src/java/com/threerings/stats/server/persist/StatRepository.java +++ b/src/java/com/threerings/stats/server/persist/StatRepository.java @@ -10,8 +10,6 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; -import java.util.logging.Level; - import com.google.inject.Singleton; import com.google.inject.Inject; @@ -57,6 +55,32 @@ public class StatRepository extends DepotRepository loadStringCodes(null); } + /** + * 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 ? decodeStat(record.statCode, record.statData, record.modCount) + : null); + } + + /** + * 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). + * + * @return true if the stat was written to the repository, false if its modCount prevented + * it from being written. + */ + public boolean updateStatIfCurrent (int playerId, Stat stat) + throws PersistenceException + { + return this.updateStat(playerId, stat, true); + } + /** * Loads the stats associated with the specified player. * @@ -67,7 +91,7 @@ public class StatRepository extends DepotRepository ArrayList stats = new ArrayList(); Where where = new Where(StatRecord.PLAYER_ID_C, playerId); for (StatRecord record : findAll(StatRecord.class, where)) { - Stat stat = decodeStat(record.statCode, record.statData); + Stat stat = decodeStat(record.statCode, record.statData, record.modCount); if (stat != null) { stats.add(stat); } @@ -97,6 +121,8 @@ public class StatRepository extends DepotRepository /** * 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) { @@ -104,7 +130,7 @@ public class StatRepository extends DepotRepository try { if (stats[ii].getType().isPersistent() && stats[ii].isModified()) { - updateStat(playerId, stats[ii]); + updateStat(playerId, stats[ii], false); } } catch (Exception e) { log.warning("Error flushing modified stat [stat=" + stats[ii] + "].", e); @@ -171,20 +197,20 @@ public class StatRepository extends DepotRepository /** * Instantiates the appropriate stat class and decodes the stat from the data. */ - protected Stat decodeStat (int statCode, byte[] 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); + 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) + protected Stat decodeStat (Stat stat, byte[] data, byte modCount) { String errmsg = null; Exception error = null; @@ -193,6 +219,7 @@ public class StatRepository extends DepotRepository // 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) { @@ -210,8 +237,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. */ - protected void updateStat (int playerId, final Stat stat) + protected boolean updateStat (int playerId, final Stat stat, boolean failIfUncurrent) throws PersistenceException { ByteArrayOutInputStream out = new ByteArrayOutInputStream(); @@ -222,7 +251,42 @@ public class StatRepository extends DepotRepository throw new PersistenceException(errmsg, ioe); } - store(new StatRecord(playerId, stat.getCode(), out.toByteArray())); + byte nextModCount = (byte)((stat.getModCount() + 1) % Byte.MAX_VALUE); + + // update the row in the database only if it has the expected modCount + int numRows = updatePartial( + StatRecord.class, + new Where(StatRecord.PLAYER_ID_C, playerId, + StatRecord.STAT_CODE_C, stat.getCode(), + StatRecord.MOD_COUNT_C, stat.getModCount()), + StatRecord.getKey(playerId, stat.getCode()), + StatRecord.STAT_DATA_C, out.toByteArray(), StatRecord.MOD_COUNT_C, nextModCount); + + 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) { + try { + insert(new StatRecord(playerId, stat.getCode(), out.toByteArray(), + nextModCount)); + numRows = 1; + } catch (DuplicateKeyException e) { + // somebody else inserted the StatRecord before we were able to. + numRows = 0; + } + } + + if (numRows == 0 && !failIfUncurrent) { + // give up and store the stat anyway + log.warning("Possible collision while storing StatRecord (playerId: " + + playerId + " stat: " + stat.getType().name() + ")"); + store(new StatRecord(playerId, stat.getCode(), out.toByteArray(), nextModCount)); + numRows = 1; + } + } + + return (numRows > 0); } /** Helper function for {@link #getStringCode}. */