Whirled's Passport system is using player stats to award "badges," and a few changes to stats are necessary to allow mutliple clients to safely modify stats simultaneously:
- add a modCount field to Stat to assist detecting if a stat has been updated in the repository since being loaded into memory - StatRepository.updateStat() is rewritten to support optionally failing if modCount has been updated (existing Stat code should not be affected by this change) - added StatRepository.loadStat() and StatRepository.updateStatIfCurrent() to support loadingand updating of single stats. git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@661 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
@@ -122,6 +122,23 @@ public abstract class Stat
|
|||||||
_modified = modified;
|
_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. */
|
/** Writes our custom streamable fields. */
|
||||||
public void writeObject (ObjectOutputStream out)
|
public void writeObject (ObjectOutputStream out)
|
||||||
throws IOException
|
throws IOException
|
||||||
@@ -192,6 +209,9 @@ public abstract class Stat
|
|||||||
* was loaded from the database. */
|
* was loaded from the database. */
|
||||||
protected transient boolean _modified;
|
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. */
|
/** Used for computing our stat codes. */
|
||||||
protected static CRC32 _crc = new CRC32();
|
protected static CRC32 _crc = new CRC32();
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,16 @@ public class StatRecord extends PersistentRecord
|
|||||||
/** The qualified column identifier for the {@link #statData} field. */
|
/** The qualified column identifier for the {@link #statData} field. */
|
||||||
public static final ColumnExp STAT_DATA_C =
|
public static final ColumnExp STAT_DATA_C =
|
||||||
new ColumnExp(StatRecord.class, STAT_DATA);
|
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
|
// 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. */
|
/** The identifier of the player this is a stat for. */
|
||||||
@Id
|
@Id
|
||||||
@@ -70,6 +77,13 @@ public class StatRecord extends PersistentRecord
|
|||||||
@Column(name="STAT_DATA", length=65535)
|
@Column(name="STAT_DATA", length=65535)
|
||||||
public byte[] statData;
|
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.
|
* An empty constructor for unmarshalling.
|
||||||
*/
|
*/
|
||||||
@@ -82,11 +96,17 @@ public class StatRecord extends PersistentRecord
|
|||||||
* A constructor that populates all our fields.
|
* A constructor that populates all our fields.
|
||||||
*/
|
*/
|
||||||
public StatRecord (int playerId, int statCode, byte[] data)
|
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();
|
super();
|
||||||
this.playerId = playerId;
|
this.playerId = playerId;
|
||||||
this.statCode = statCode;
|
this.statCode = statCode;
|
||||||
this.statData = data;
|
this.statData = data;
|
||||||
|
this.modCount = modCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUTO-GENERATED: METHODS START
|
// AUTO-GENERATED: METHODS START
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import java.io.Serializable;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
@@ -57,6 +55,32 @@ public class StatRepository extends DepotRepository
|
|||||||
loadStringCodes(null);
|
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.
|
* Loads the stats associated with the specified player.
|
||||||
*
|
*
|
||||||
@@ -67,7 +91,7 @@ public class StatRepository extends DepotRepository
|
|||||||
ArrayList<Stat> stats = new ArrayList<Stat>();
|
ArrayList<Stat> stats = new ArrayList<Stat>();
|
||||||
Where where = new Where(StatRecord.PLAYER_ID_C, playerId);
|
Where where = new Where(StatRecord.PLAYER_ID_C, playerId);
|
||||||
for (StatRecord record : findAll(StatRecord.class, where)) {
|
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) {
|
if (stat != null) {
|
||||||
stats.add(stat);
|
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
|
* 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.
|
* first loaded. Exceptions that occur while writing the stats will be caught and logged.
|
||||||
|
*
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public void writeModified (int playerId, Stat[] stats)
|
public void writeModified (int playerId, Stat[] stats)
|
||||||
{
|
{
|
||||||
@@ -104,7 +130,7 @@ public class StatRepository extends DepotRepository
|
|||||||
try {
|
try {
|
||||||
if (stats[ii].getType().isPersistent() &&
|
if (stats[ii].getType().isPersistent() &&
|
||||||
stats[ii].isModified()) {
|
stats[ii].isModified()) {
|
||||||
updateStat(playerId, stats[ii]);
|
updateStat(playerId, stats[ii], false);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warning("Error flushing modified stat [stat=" + stats[ii] + "].", 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.
|
* 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);
|
Stat.Type type = Stat.getType(statCode);
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
log.warning("Unable to decode stat, unknown type [code=" + statCode + "].");
|
log.warning("Unable to decode stat, unknown type [code=" + statCode + "].");
|
||||||
return null;
|
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.
|
* 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;
|
String errmsg = null;
|
||||||
Exception error = null;
|
Exception error = null;
|
||||||
@@ -193,6 +219,7 @@ public class StatRepository extends DepotRepository
|
|||||||
// decode its contents from the serialized data
|
// decode its contents from the serialized data
|
||||||
ByteArrayInputStream bin = new ByteArrayInputStream(data);
|
ByteArrayInputStream bin = new ByteArrayInputStream(data);
|
||||||
stat.unpersistFrom(new ObjectInputStream(bin), this);
|
stat.unpersistFrom(new ObjectInputStream(bin), this);
|
||||||
|
stat.setModCount(modCount);
|
||||||
return stat;
|
return stat;
|
||||||
|
|
||||||
} catch (ClassNotFoundException cnfe) {
|
} catch (ClassNotFoundException cnfe) {
|
||||||
@@ -210,8 +237,10 @@ public class StatRepository extends DepotRepository
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the specified stat in the database, inserting it if necessary.
|
* 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
|
throws PersistenceException
|
||||||
{
|
{
|
||||||
ByteArrayOutInputStream out = new ByteArrayOutInputStream();
|
ByteArrayOutInputStream out = new ByteArrayOutInputStream();
|
||||||
@@ -222,7 +251,42 @@ public class StatRepository extends DepotRepository
|
|||||||
throw new PersistenceException(errmsg, ioe);
|
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}. */
|
/** Helper function for {@link #getStringCode}. */
|
||||||
|
|||||||
Reference in New Issue
Block a user