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されるようにした
さいごに
コードの改善案を提示してくれた方々に感謝。