From 2d5a5ca033377544d370c429ed8fe35ab5cbb2a8 Mon Sep 17 00:00:00 2001 From: Par Winzell Date: Thu, 26 Jul 2007 20:28:50 +0000 Subject: [PATCH] Depot implementation of the stat system persistence - hopefully schema compatible with the old version. This one is a little more complicated and Whirled doesn't have any stats yet so I was a little at a loss at how to test all of it. Perhaps somebody at Bang could make sure nothing crashes and burns horribly when e.g. a new string code is assigned? git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@383 c613c5cb-e716-0410-b11b-feb51c14d237 --- .../stats/server/persist/StatRecord.java | 105 +++++++ .../stats/server/persist/StatRepository.java | 270 +++++++----------- .../server/persist/StringCodeRecord.java | 109 +++++++ 3 files changed, 321 insertions(+), 163 deletions(-) create mode 100644 src/java/com/threerings/stats/server/persist/StatRecord.java create mode 100644 src/java/com/threerings/stats/server/persist/StringCodeRecord.java diff --git a/src/java/com/threerings/stats/server/persist/StatRecord.java b/src/java/com/threerings/stats/server/persist/StatRecord.java new file mode 100644 index 00000000..1ca35ced --- /dev/null +++ b/src/java/com/threerings/stats/server/persist/StatRecord.java @@ -0,0 +1,105 @@ +// +// $Id: PuzzleManagerDelegate.java 209 2007-02-24 00:37:33Z mdb $ +// +// 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.server.persist; + +import com.samskivert.jdbc.depot.Key; +import com.samskivert.jdbc.depot.PersistentRecord; +import com.samskivert.jdbc.depot.annotation.Column; +import com.samskivert.jdbc.depot.annotation.Entity; +import com.samskivert.jdbc.depot.annotation.Id; +import com.samskivert.jdbc.depot.expression.ColumnExp; + +@Entity(name="STAT_RECORD") +public class StatRecord extends PersistentRecord +{ + // AUTO-GENERATED: FIELDS START + /** The column identifier for the {@link #playerId} field. */ + public static final String PLAYER_ID = "playerId"; + + /** The qualified column identifier for the {@link #playerId} field. */ + public static final ColumnExp PLAYER_ID_C = + new ColumnExp(StatRecord.class, PLAYER_ID); + + /** The column identifier for the {@link #statCode} field. */ + public static final String STAT_CODE = "statCode"; + + /** The qualified column identifier for the {@link #statCode} field. */ + public static final ColumnExp STAT_CODE_C = + new ColumnExp(StatRecord.class, STAT_CODE); + + /** The column identifier for the {@link #statData} field. */ + public static final String STAT_DATA = "statData"; + + /** The qualified column identifier for the {@link #statData} field. */ + public static final ColumnExp STAT_DATA_C = + new ColumnExp(StatRecord.class, STAT_DATA); + // AUTO-GENERATED: FIELDS END + + public static final int SCHEMA_VERSION = 2; + + /** The identifier of the player this is a stat for. */ + @Id + @Column(name="PLAYER_ID") + public int playerId; + + /** The code of the stat. */ + @Id + @Column(name="STAT_CODE") + public int statCode; + + /** The data associated with the stat. */ + @Column(name="STAT_DATA") + public byte[] statData; + + /** + * An empty constructor for unmarshalling. + */ + public StatRecord () + { + super(); + } + + /** + * A constructor that populates all our fields. + */ + public StatRecord (int playerId, int statCode, byte[] data) + { + super(); + this.playerId = playerId; + this.statCode = statCode; + this.statData = data; + } + + // AUTO-GENERATED: METHODS START + /** + * Create and return a primary {@link Key} to identify a {@link #StatRecord} + * with the supplied key values. + */ + public static Key getKey (int playerId, int statCode) + { + return new Key( + StatRecord.class, + new String[] { PLAYER_ID, STAT_CODE }, + new Comparable[] { playerId, statCode }); + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/stats/server/persist/StatRepository.java b/src/java/com/threerings/stats/server/persist/StatRepository.java index 06e9d156..b0444ee8 100644 --- a/src/java/com/threerings/stats/server/persist/StatRepository.java +++ b/src/java/com/threerings/stats/server/persist/StatRepository.java @@ -5,28 +5,34 @@ package com.threerings.stats.server.persist; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.Serializable; import java.sql.Connection; -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; +import java.util.Set; import java.util.logging.Level; import com.samskivert.io.ByteArrayOutInputStream; import com.samskivert.io.PersistenceException; import com.samskivert.jdbc.ConnectionProvider; -import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.jdbc.JDBCUtil; -import com.samskivert.jdbc.SimpleRepository; +import com.samskivert.jdbc.depot.CacheInvalidator; +import com.samskivert.jdbc.depot.DepotRepository; +import com.samskivert.jdbc.depot.PersistenceContext; +import com.samskivert.jdbc.depot.PersistentRecord; +import com.samskivert.jdbc.depot.PersistenceContext.CacheEvictionFilter; +import com.samskivert.jdbc.depot.annotation.Computed; +import com.samskivert.jdbc.depot.annotation.Entity; +import com.samskivert.jdbc.depot.clause.FieldOverride; +import com.samskivert.jdbc.depot.clause.FromOverride; +import com.samskivert.jdbc.depot.clause.QueryClause; +import com.samskivert.jdbc.depot.clause.Where; +import com.samskivert.jdbc.depot.expression.FunctionExp; import com.samskivert.util.HashIntMap; -import com.samskivert.util.StringUtil; import com.threerings.io.ObjectInputStream; import com.threerings.io.ObjectOutputStream; @@ -38,7 +44,7 @@ import static com.threerings.stats.Log.log; /** * Responsible for the persistent storage of per-player statistics. */ -public class StatRepository extends SimpleRepository +public class StatRepository extends DepotRepository implements Stat.AuxDataSource { /** @@ -56,7 +62,16 @@ public class StatRepository extends SimpleRepository public StatRepository (ConnectionProvider conprov) throws PersistenceException { - super(conprov, STAT_DB_IDENT); + this(new PersistenceContext(STAT_DB_IDENT, conprov)); + } + + /** + * Constructs a new statistics repository with the specified persistence context. + */ + public StatRepository (PersistenceContext context) + throws PersistenceException + { + super(context); // load up our string set mappings loadStringCodes(null); @@ -65,43 +80,38 @@ public class StatRepository extends SimpleRepository /** * Loads the stats associated with the specified player. */ - public ArrayList loadStats (final int playerId) + public ArrayList loadStats (int playerId) throws PersistenceException { - final ArrayList stats = new ArrayList(); - final String query = "select STAT_CODE, STAT_DATA from STATS where PLAYER_ID = " + playerId; - execute(new Operation() { - public Object invoke (Connection conn, DatabaseLiaison liaison) - throws SQLException, PersistenceException - { - Statement stmt = conn.createStatement(); - try { - ResultSet rs = stmt.executeQuery(query); - while (rs.next()) { - Stat stat = decodeStat( - rs.getInt(1), (byte[])rs.getObject(2)); - if (stat != null) { - stats.add(stat); - } - } - } finally { - JDBCUtil.close(stmt); - } - return null; + 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); + if (stat != null) { + stats.add(stat); } - }); + } return stats; } /** * Deletes all stats associated with the specified player. */ - public void deleteStats (int playerId) + public void deleteStats (final int playerId) throws PersistenceException { - update("delete from STATS where PLAYER_ID = " + playerId); + CacheInvalidator invalidator = new CacheInvalidator() { + public void invalidate (PersistenceContext ctx) { + ctx.cacheTraverse(StatRecord.class.getName(), new CacheEvictionFilter() { + public boolean testForEviction (Serializable key, StatRecord record) { + return record != null && record.playerId == playerId; + } + }); + } + }; + deleteAll(StatRecord.class, new Where(StatRecord.PLAYER_ID_C, playerId), invalidator); } - + /** * 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. @@ -222,11 +232,7 @@ public class StatRepository extends SimpleRepository protected void updateStat (int playerId, final Stat stat) throws PersistenceException { - final String uquery = "update STATS set STAT_DATA = ?" + - " where PLAYER_ID = " + playerId + " and STAT_CODE = " + stat.getCode(); - final String iquery = "insert into STATS (PLAYER_ID, STAT_CODE, " + - "STAT_DATA) values (" + playerId + ", " + stat.getCode() + ", ?)"; - final ByteArrayOutInputStream out = new ByteArrayOutInputStream(); + ByteArrayOutInputStream out = new ByteArrayOutInputStream(); try { stat.persistTo(new ObjectOutputStream(out), this); } catch (IOException ioe) { @@ -234,111 +240,71 @@ public class StatRepository extends SimpleRepository throw new PersistenceException(errmsg, ioe); } - // now update (or insert) the flattened data into the database - executeUpdate(new Operation() { - public Object invoke (Connection conn, DatabaseLiaison liaison) - throws SQLException, PersistenceException - { - PreparedStatement stmt = conn.prepareStatement(uquery); - try { - stmt.setBinaryStream(1, out.getInputStream(), out.size()); - if (stmt.executeUpdate() == 0) { - JDBCUtil.close(stmt); - stmt = conn.prepareStatement(iquery); - stmt.setBinaryStream(1, out.getInputStream(), out.size()); - JDBCUtil.checkedUpdate(stmt, 1); - } - return null; - } finally { - JDBCUtil.close(stmt); - } - } - }); + store(new StatRecord(playerId, stat.getCode(), out.toByteArray())); } + @Computed @Entity + protected static class MaxStatCodeRecord extends PersistentRecord { + int maxCode; + }; + /** Helper function for {@link #getStringCode}. */ protected Integer assignStringCode (final Stat.Type type, final String value) throws PersistenceException { - return executeUpdate(new Operation() { - public Integer invoke (Connection conn, DatabaseLiaison liaison) - throws SQLException, PersistenceException - { - for (int ii = 0; ii < 10; ii++) { - try { - int code = getNextCode(conn, type); - // DEBUG: uncomment this to test code collision - // if (ii == 0 && code > 0) { - // code = code-1; - // } - insertStringCode(conn, type, value, code); - return code; + for (int ii = 0; ii < 10; ii++) { + MaxStatCodeRecord maxRecord = load( + MaxStatCodeRecord.class, + new FromOverride(StringCodeRecord.class), + new FieldOverride("maxCode", new FunctionExp("MAX", StringCodeRecord.CODE_C)), + new Where(StringCodeRecord.STAT_CODE_C, type.code())); - } catch (SQLException sqe) { - // if this is not a duplicate row exception, something is booched and we - // just fail - if (!liaison.isDuplicateRowException(sqe)) { - throw sqe; - } + int code = maxRecord != null ? maxRecord.maxCode + 1 : 1; - // if it is a duplicate row exception, possibly someone inserted our value - // before we could, in which case we can just look up the new mapping - int code = getCurrentCode(conn, type, value); - if (code != -1) { - log.info("Value collision assigning string code [type=" + type + - ", value=" + value + "]."); - return code; - } + // DEBUG: uncomment this to test code collision + // if (ii == 0 && code > 0) { + // code = code-1; + // } + + try { + insert(new StringCodeRecord(type.code(), value, code)); + return code; - // otherwise someone used the code we were trying to use and we just need - // to loop around and get the next highest code - log.info("Code collision assigning string code [type=" + type + - ", value=" + value + "]."); - } + } catch (PersistenceException pe) { + // if this is not a duplicate row exception, something is booched and we + // just fail + +// if (!liaison.isDuplicateRowException(sqe)) { +// throw sqe; +// } + + // if it is a duplicate row exception, possibly someone inserted our value + // before we could, in which case we can just look up the new mapping + StringCodeRecord record = load( + StringCodeRecord.class, + new Where(StringCodeRecord.STAT_CODE_C, type.code(), + StringCodeRecord.VALUE_C, value)); + if (record != null) { + log.info("Value collision assigning string code [type=" + type + + ", value=" + value + "]."); + return code; } - throw new SQLException("Unable to assign code after 10 attempts " + - "[type=" + type + ", value=" + value + "]"); + + // otherwise someone used the code we were trying to use and we just need + // to loop around and get the next highest code + log.info("Code collision assigning string code [type=" + type + + ", value=" + value + "]."); } - }); - } - - /** Helper function for {@link #assignStringCode}. */ - protected int getNextCode (Connection conn, Stat.Type type) - throws SQLException - { - return getCode(conn, "select MAX(CODE)+1 from STRING_CODES where STAT_CODE = " + - type.code()); - } - - /** Helper function for {@link #assignStringCode}. */ - protected int getCurrentCode (Connection conn, Stat.Type type, String value) - throws SQLException - { - return getCode(conn, "select CODE from STRING_CODES where STAT_CODE = " + type.code() + - " and VALUE = " + JDBCUtil.escape(value)); - } - - /** Helper function for {@link #getNextCode} and {@link #getCurrentCode}. */ - protected int getCode (Connection conn, String query) - throws SQLException - { - int code = -1; - Statement stmt = conn.createStatement(); - try { - ResultSet rs = stmt.executeQuery(query); - while (rs.next()) { - code = rs.getInt(1); - } - } finally { - JDBCUtil.close(stmt); } - return code; + throw new PersistenceException( + "Unable to assign code after 10 attempts [type=" + type + ", value=" + value + "]"); } /** Helper function for {@link #assignStringCode}. */ protected void insertStringCode (Connection conn, Stat.Type type, String value, int code) throws SQLException { + String query = "insert into STRING_CODES (STAT_CODE, VALUE, CODE) values(" + type.code() + ", " + JDBCUtil.escape(value) + ", " + code + ")"; Statement stmt = conn.createStatement(); @@ -358,24 +324,16 @@ public class StatRepository extends SimpleRepository protected void loadStringCodes (Stat.Type type) throws PersistenceException { - final String query = "select STAT_CODE, VALUE, CODE from STRING_CODES " + - ((type == null) ? "" : (" where STAT_CODE = " + type.code())); - execute(new Operation() { - public Object invoke (Connection conn, DatabaseLiaison liaison) - throws SQLException, PersistenceException - { - Statement stmt = conn.createStatement(); - try { - ResultSet rs = stmt.executeQuery(query); - while (rs.next()) { - mapStringCode(Stat.getType(rs.getInt(1)), rs.getString(2), rs.getInt(3)); - } - } finally { - JDBCUtil.close(stmt); - } - return null; - } - }); + QueryClause[] clauses; + if (type != null) { + clauses = new QueryClause[] { new Where(StringCodeRecord.STAT_CODE_C, type.code()) }; + } else { + clauses = new QueryClause[0]; + } + + for (StringCodeRecord record : findAll(StringCodeRecord.class, clauses)) { + mapStringCode(Stat.getType(record.statCode), record.value, record.code); + } } /** Helper function used at repository startup. */ @@ -393,25 +351,11 @@ public class StatRepository extends SimpleRepository rmap.put(code, value); } - @Override // documentation inherited - protected void migrateSchema (Connection conn, DatabaseLiaison liaison) - throws SQLException, PersistenceException + @Override // from DepotRepository + protected void getManagedRecords (Set> classes) { - JDBCUtil.createTableIfMissing(conn, "STATS", new String[] { - "PLAYER_ID INTEGER NOT NULL", - "STAT_CODE INTEGER NOT NULL", - "STAT_DATA BLOB NOT NULL", - "KEY (PLAYER_ID)", - "KEY (STAT_CODE)", - }, ""); - - JDBCUtil.createTableIfMissing(conn, "STRING_CODES", new String[] { - "STAT_CODE INTEGER NOT NULL", - "VALUE VARCHAR(255) NOT NULL", - "CODE INTEGER NOT NULL", - "UNIQUE (STAT_CODE, VALUE)", - "UNIQUE (STAT_CODE, CODE)", - }, ""); + classes.add(StatRecord.class); + classes.add(StringCodeRecord.class); } protected HashMap> _stringToCode = diff --git a/src/java/com/threerings/stats/server/persist/StringCodeRecord.java b/src/java/com/threerings/stats/server/persist/StringCodeRecord.java new file mode 100644 index 00000000..e7b60c65 --- /dev/null +++ b/src/java/com/threerings/stats/server/persist/StringCodeRecord.java @@ -0,0 +1,109 @@ +// +// $Id: PuzzleManagerDelegate.java 209 2007-02-24 00:37:33Z mdb $ +// +// 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.server.persist; + +import com.samskivert.jdbc.depot.Key; +import com.samskivert.jdbc.depot.PersistentRecord; +import com.samskivert.jdbc.depot.annotation.Column; +import com.samskivert.jdbc.depot.annotation.Entity; +import com.samskivert.jdbc.depot.annotation.Id; +import com.samskivert.jdbc.depot.annotation.Table; +import com.samskivert.jdbc.depot.annotation.UniqueConstraint; +import com.samskivert.jdbc.depot.expression.ColumnExp; + +@Entity(name="STRING_CODES") +@Table(uniqueConstraints={ + @UniqueConstraint(columnNames={StringCodeRecord.STAT_CODE, StringCodeRecord.VALUE}), + @UniqueConstraint(columnNames={StringCodeRecord.STAT_CODE, StringCodeRecord.CODE})}) +public class StringCodeRecord extends PersistentRecord +{ + // AUTO-GENERATED: FIELDS START + /** The column identifier for the {@link #statCode} field. */ + public static final String STAT_CODE = "statCode"; + + /** The qualified column identifier for the {@link #statCode} field. */ + public static final ColumnExp STAT_CODE_C = + new ColumnExp(StringCodeRecord.class, STAT_CODE); + + /** The column identifier for the {@link #value} field. */ + public static final String VALUE = "value"; + + /** The qualified column identifier for the {@link #value} field. */ + public static final ColumnExp VALUE_C = + new ColumnExp(StringCodeRecord.class, VALUE); + + /** The column identifier for the {@link #code} field. */ + public static final String CODE = "code"; + + /** The qualified column identifier for the {@link #code} field. */ + public static final ColumnExp CODE_C = + new ColumnExp(StringCodeRecord.class, CODE); + // AUTO-GENERATED: FIELDS END + + public static final int SCHEMA_VERSION = 2; + + /** The code of the stat. */ + @Id + @Column(name="STAT_CODE") + public int statCode; + + @Id + @Column(name="VALUE") + public String value; + + @Id + @Column(name="CODE") + public int code; + + /** + * An empty constructor for unmarshalling. + */ + public StringCodeRecord () + { + super(); + } + + /** + * A constructor that populates all our fields. + */ + public StringCodeRecord (int statCode, String value, int code) + { + super(); + this.statCode = statCode; + this.value = value; + this.code = code; + } + + // AUTO-GENERATED: METHODS START + /** + * Create and return a primary {@link Key} to identify a {@link #StringCodeRecord} + * with the supplied key values. + */ + public static Key getKey (int statCode, String value, int code) + { + return new Key( + StringCodeRecord.class, + new String[] { STAT_CODE, VALUE, CODE }, + new Comparable[] { statCode, value, code }); + } + // AUTO-GENERATED: METHODS END +}