AppEngineでCounterを実装する(2) #appengine #slim3

前回のエントリーはバグってました。修正版をアップします。

単純なカウンター

import java.io.Serializable;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Datastore;
import org.slim3.datastore.EntityNotFoundRuntimeException;
import org.slim3.datastore.Model;
import org.slim3.util.DateUtil;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

@Model
public class Counter implements Serializable {

    private static final long serialVersionUID = 1L;

    @Attribute(primaryKey = true)
    private Key key;

    private Long seq;

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public Long getSeq() {
        return seq;
    }

    public void setSeq(Long seq) {
        this.seq = seq;
    }

    private static Logger logger = Logger.getLogger(Counter.class.getName());
    private static final int TRY_MAX = 10;

    private static Key createKey(Date date) {
        String keyName = DateUtil.toString(date, DateUtil.ISO_DATE_PATTERN);
        return Datastore.createKey(Counter.class, keyName);
    }

    public static Long increment() {
        return increment(new Date());
    }

    public static Long increment(Date date) {
        Key key = createKey(date);
        for (int i = 0; i < TRY_MAX; i++) {
            try {
                return createOrUpdate(key);
            } catch (ConcurrentModificationException e) {
                logger.log(Level.WARNING, "Try(" + (i + 1) + "): " + e.getMessage(), e);
                if (i + 1 == TRY_MAX) {
                    throw e;
                }
            }
        }
        return null;
    }

    private static Long createOrUpdate(Key key) throws ConcurrentModificationException {
        Counter counter = null;
        Transaction tx = Datastore.beginTransaction();
        try {
            counter = Datastore.get(tx, Counter.class, key);
            counter.setSeq(counter.getSeq() + 1);
            Datastore.put(tx, counter);
            Datastore.commit(tx);
        } catch (EntityNotFoundRuntimeException e) {
            counter = new Counter();
            counter.setKey(key);
            counter.setSeq(new Long(1));
            Datastore.put(tx, counter);
            Datastore.commit(tx);
        } finally {
            // Datastore.rollback内でtx.isActive()をチェックしているので不要→slim3のr977にてDatastore.rollback仕様変更によりtx.isActive()は必要になりました。
            if (tx.isActive()) {
                Datastore.rollback(tx);
            }
        }
        return counter.getSeq();
    }

    public static void clear() {
        clear(new Date());
    }

    public static void clear(Date date) {
        Datastore.delete(createKey(date));
    }

}

ShardingCounter

import java.io.Serializable;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Datastore;
import org.slim3.datastore.EntityNotFoundRuntimeException;
import org.slim3.datastore.Model;
import org.slim3.util.DateUtil;

import com.blank.meta.ShardingCounterMeta;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

@Model
public class ShardingCounter implements Serializable {

    private static Logger logger = Logger.getLogger(ShardingCounter.class.getName());

    private static final long serialVersionUID = 1L;

    private static final int KIND_COUNT = 10;

    private static final int TRY_MAX = 10;

    private static final ShardingCounterMeta meta = ShardingCounterMeta.get();

    @Attribute(primaryKey = true)
    private Key key;

    private Long seq;

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public Long getSeq() {
        return seq;
    }

    public void setSeq(Long seq) {
        this.seq = seq;
    }

    private static Key createKey(String kind, Date date) {
        String keyName = DateUtil.toString(date, DateUtil.ISO_DATE_PATTERN);
        return Datastore.createKey(kind, keyName);
    }

    private static Key createKey(Date date) {
        String kind =
            meta.getKind() + "$" + (((int) (Math.random() * (KIND_COUNT))) + 1);
        return createKey(kind, date);
    }

    public static long increment() {
        return increment(new Date());
    }

    public static long increment(Date date) {
        Key key = createKey(date);
        for (int i = 0; i < TRY_MAX; i++) {
            try {
                createOrUpdate(key);
                break;
            } catch (ConcurrentModificationException e) {
                logger.log(Level.WARNING, "Try(" + (i + 1) + "): " + e.getMessage(), e);
                if (i + 1 == TRY_MAX) {
                    throw e;
                }
            }
        }
        return getTotalCount(date);
    }

    private static void createOrUpdate(Key key) throws ConcurrentModificationException {
        Transaction tx = Datastore.beginTransaction();
        try {
            Entity entity = Datastore.get(tx, key);
            entity.setProperty(meta.seq.getName(), (Long) entity.getProperty(meta.seq.getName()) + 1);
            Datastore.put(tx, entity);
            Datastore.commit(tx);
        } catch (EntityNotFoundRuntimeException e) {
            Entity entity = new Entity(key);
            entity.setProperty(meta.seq.getName(), new Long(1));
            Datastore.put(tx, entity);
            Datastore.commit(tx);
        } finally {
            // Datastore.rollback内でtx.isActive()をチェックしているので不要→slim3のr977にてDatastore.rollback仕様変更によりtx.isActive()は必要になりました。
            if (tx.isActive()) {
                Datastore.rollback(tx);
            }
        }
    }

    private static List<Key> getKeyList(Date date) {
        List<Key> keys = new ArrayList<Key>();
        for (int i = 0; i < KIND_COUNT; i++) {
            keys.add(createKey(meta.getKind() + "$" + (i + 1), date));
        }
        return keys;
    }

    private static long getTotalCount(Date date) {
        long result = 0;
        Map<Key, Entity> entityMap = Datastore.getAsMap(getKeyList(date)); // parallel get
        for (Entity e : entityMap.values()) {
            result += (Long)e.getProperty(meta.seq.getName());
        }
        return result;
    }

    public static void clear() {
        clear(new Date());
    }

    public static void clear(Date date) {
        Datastore.delete(getKeyList(date)); // parallel delete
    }

}

修正しました。

修正点は下記

  • (バグ)ConcurrentModificationExceptionが発生した場合は再度beginTransactionしなければならないがしていなかったのを修正
  • Transactionのbegin,commit,rollbackをcreateOrUpdateメソッド内で完結する
  • clearはseqを0にするのではなくdeleteする
  • ShardingCounterでseqの取得/更新をEntityクラスで操作するようにした(もはや@Modelにするまでもなくなってしまった・・。)
  • ShardingCounterのgetTotalCountはparallel getされるようにした

さいごに

コードの改善案を提示してくれた方々に感謝。