All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.stats.server.persist.StatRepository Maven / Gradle / Ivy

The newest version!
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import com.samskivert.io.ByteArrayOutInputStream;
import com.samskivert.util.IntMap;
import com.samskivert.util.IntMaps;

import com.samskivert.depot.DatabaseException;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.DuplicateKeyException;
import com.samskivert.depot.Funcs;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.FieldDefinition;
import com.samskivert.depot.clause.FromOverride;
import com.samskivert.depot.clause.QueryClause;
import com.samskivert.depot.clause.Where;
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;

/**
 * Responsible for the persistent storage of per-player statistics.
 */
@Singleton
public class StatRepository extends DepotRepository
    implements Stat.AuxDataSource
{
    /**
     * Constructs a new statistics repository with the specified persistence context.
     */
    @Inject public StatRepository (PersistenceContext context)
    {
        super(context);
    }

    /**
     * Applies a modification to a single stat. If the stat in question does not exist, a blank
     * instance will be created via {@link com.threerings.stats.data.Stat.Type#newStat}.
     *
     * @return the modified Stat, if any modification took place; or null if the modification had
     * no effect on the stat's data.
     */
    public  T updateStat (int playerId, StatModifier modifier)
    {
        Where where = new Where(StatRecord.PLAYER_ID, playerId,
                                StatRecord.STAT_CODE, 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 null;
            }
            if (updateStat(playerId, tstat, false)) {
                return tstat;
            }
        }

        throw new DatabaseException(
            "Unable to update stat after " + MAX_UPDATE_TRIES + " attempts " +
            "[stat=" + modifier.getType() + ", pid=" + playerId + "]");
    }

    /**
     * Loads the stats associated with the specified player.
     *
     */
    public ArrayList loadStats (int playerId)
    {
        ArrayList stats = Lists.newArrayList();
        Where where = new Where(StatRecord.PLAYER_ID, playerId);
        for (StatRecord record : findAll(StatRecord.class, where)) {
            Stat stat = decodeStat(record.statCode, record.statData, record.modCount);
            if (stat != null) {
                stats.add(stat);
            }
        }
        return stats;
    }

    /**
     * Deletes all stats associated with the specified player.
     */
    public void deleteStats (final int playerId)
    {
        deleteAll(StatRecord.class, new Where(StatRecord.PLAYER_ID, playerId));
    }

    /**
     * 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)
    {
        writeModified(playerId, Arrays.asList(stats));
    }

    /**
     * Writes out any of the stats in the supplied iterable 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, Iterable stats)
    {
        for (Stat stat : stats) {
            try {
                if (stat.getType().isPersistent() && stat.isModified()) {
                    updateStat(playerId, stat, true);
                }
            } catch (Exception e) {
                log.warning("Error flushing modified stat", "stat", stat, e);
            }
        }
    }

    // documentation inherited from interface Stat.AuxDataSource
    public int getStringCode (Stat.Type type, String value)
    {
        Map map = _stringToCode.get(type);
        if (map == null) {
            _stringToCode.put(type, map = Maps.newHashMap());
        }
        Integer code = map.get(value);
        if (code == null) {
            try {
                code = assignStringCode(type, value);
            } catch (DatabaseException pe) {
                log.warning("Failed to assign code", "type", type, "value", value, pe);
                // at this point the database is probably totally hosed, so we can just punt here,
                // and assume that this value will never be persisted
                code = -1;
            }
            mapStringCode(type, value, code);
        }
        return code;
    }

    // documentation inherited from interface Stat.AuxDataSource
    public String getCodeString (Stat.Type type, int code)
    {
        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 this mapping table
            // from the database; then try again
            try {
                loadStringCodes(type);
            } catch (DatabaseException pe) {
                log.warning("Failed to reload string codes", "type", type, "code", code, pe);
            }
            map = _codeToString.get(type);
            value = (map == null) ? null : map.get(code);
            if (value == null) {
                log.warning("Missing reverse maping", "type", type, "code", code);
                value = "__UNKNOWN:" + code + "__"; // we don't want to return null
            }
        }
        return value;
    }

    /**
     * This is only used for testing. Do not call this method.
     */
    public void clearMapping (Stat.Type type, String value)
    {
        int ocode = _stringToCode.get(type).remove(value);
        _codeToString.get(type).remove(ocode);
    }

    /**
     * Deletes all data associated with the supplied players.
     */
    public void purgePlayers (Collection playerIds)
    {
        deleteAll(StatRecord.class, new Where(StatRecord.PLAYER_ID.in(playerIds)));
    }

    /**
     * Instantiates the appropriate stat class and decodes the stat from the 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, modCount);
    }

    /**
     * Instantiates the appropriate stat class and decodes the stat from the data.
     */
    protected Stat decodeStat (Stat stat, byte[] data, byte modCount)
    {
        String errmsg = null;
        Exception error = null;

        try {
            // 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) {
            error = cnfe;
            errmsg = "Unable to instantiate stat";

        } catch (IOException ioe) {
            error = ioe;
            errmsg = "Unable to decode stat";
        }

        log.warning(errmsg, "type", stat.getType(), error);
        return null;
    }

    /**
     * Updates the specified stat in the database, inserting it if necessary.
     *
     * @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 forceWrite)
    {
        ByteArrayOutInputStream out = new ByteArrayOutInputStream();
        try {
            stat.persistTo(new ObjectOutputStream(out), this);
        } catch (IOException ioe) {
            throw new DatabaseException("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(
            StatRecord.class,
            new Where(StatRecord.PLAYER_ID, playerId,
                      StatRecord.STAT_CODE, stat.getCode(),
                      StatRecord.MOD_COUNT, stat.getModCount()),
            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 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) {
                    // someone else inserted the StatRecord before we were able to
                    numRows = 0;
                }
            }

            // 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(),
                            "modCount", nextModCount, "overwriting", load(StatRecord.class, key));
                store(new StatRecord(playerId, stat.getCode(), data, nextModCount));
                numRows = 1;
            }
        }

        return (numRows > 0);
    }

    /** Helper function for {@link #getStringCode}. */
    protected Integer assignStringCode (final Stat.Type type, final String value)
    {
        for (int ii = 0; ii < 10; ii++) {
            MaxStatCodeRecord maxRecord = load(
                MaxStatCodeRecord.class,
                new FromOverride(StringCodeRecord.class),
                new FieldDefinition(MaxStatCodeRecord.MAX_CODE, Funcs.max(StringCodeRecord.CODE)),
                new Where(StringCodeRecord.STAT_CODE, type.code()));

            int code = maxRecord != null ? maxRecord.maxCode + 1 : 1;

            // 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;

            } catch (DatabaseException pe) {
                // if this is not a duplicate row exception, something is booched and we just fail
                if (!(pe instanceof DuplicateKeyException)) {
                    throw pe;
                }

                // 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, StringCodeRecord.getKey(type.code(), value));
                if (record != null) {
                    log.info("Value collision assigning string code", "type", type, "value", value);
                    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);
            }
        }
        throw new DatabaseException(
            "Unable to assign code after 10 attempts [type=" + type + ", value=" + value + "]");
    }

    /** Helper function used at repository startup. */
    protected void loadStringCodes (Stat.Type type)
    {
        QueryClause[] clauses;
        if (type != null) {
            clauses = new QueryClause[] { new Where(StringCodeRecord.STAT_CODE, 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. */
    protected void mapStringCode (Stat.Type type, String value, int code)
    {
        Map fmap = _stringToCode.get(type);
        if (fmap == null) {
            _stringToCode.put(type, fmap = Maps.newHashMap());
        }
        fmap.put(value, code);
        IntMap rmap = _codeToString.get(type);
        if (rmap == null) {
            _codeToString.put(type, rmap = IntMaps.newHashIntMap());
        }
        rmap.put(code, value);
    }

    @Override // from DepotRepository
    protected void init ()
    {
        super.init();

        // load up our string set mappings
        loadStringCodes(null);
    }

    @Override // from DepotRepository
    protected void getManagedRecords (Set> classes)
    {
        classes.add(StatRecord.class);
        classes.add(StringCodeRecord.class);
    }

    protected Map> _stringToCode = Maps.newHashMap();
    protected Map> _codeToString = Maps.newHashMap();

    protected static final int MAX_UPDATE_TRIES = 5;
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy