AppEngineのHRDデータストアはMSデータストアとは異なり、データの整合性が保証されません。そのため、putした直後にqueryを投げると、putした要素を取得できないことがあります。これにより、AppEngine公式サイトの一番簡単なサンプルプログラムも動作しないという、衝撃的な状況となっています。

具体的に、サンプルプログラムでは、 greeting.put()した後にredirectしてgreetings = db.GqlQuery("SELECT * FROM Greeting ORDER BY date DESC LIMIT 10")を投げていますが、put()がインデックスに反映されるまでに、数十ms〜2sec程度かかるため、putしたデータは表示されません。リロードすると表示されます。

この問題を回避するために、Googleとしては、memcacheとの併用を勧めています。

上記の高レプリケーションのサンプル コードでは、ゲストブックごとに 1 つのエンティティ グループに書き込みを行います。このため、1 つのゲストブックに対するクエリには強い整合性がありますが、ゲストブックの変更は 1 秒あたり 1 件の書き込み(エンティティ グループに対してサポートされる制限)に制限されます。このため、頻繁な使用が予想される場合は、ゲストブックごとに 1 つのエンティティ グループに書き込むことはおすすめしません。アプリケーションで頻繁な書き込みが発生しそうな場合は別の方法を使用することを検討してください。たとえば、最近の投稿を期限付きで memcache に配置し、memcache 内の最近の投稿とデータストアから取得した投稿を合わせて表示することができます。

しかし、MSデータストアからの移行時に、既存のアプリケーションをすべて書き換えるのは現実的ではありません。

そこで、put_sync()的に、インデックスに反映するまでウエイトするようなメソッドがあるといいのですが、存在しません。ということで、対処療法ですが、put_sync的な動作をするメソッドを作りました。使い方としては、整合性を取りたいオブジェクトをobj.put()する代わりにSyncPut.put_sync(obj)とします。これで、putした後、インデックスに反映されるまで待機することができます。

実装としては、putした後、インデックスに反映されるまでqueryを投げ続けています。乱数とオブジェクトの型、現在の経過秒数からほぼユニークなsandとなる文字列を作っておき、sandでfilterすることで、最新の情報に更新されたかを確認しています。

#!-*- coding:utf-8 -*-
#!/usr/bin/env python

from google.appengine.ext import db

import random
import time

class SyncPut():
	@staticmethod
	def put_sync(obj):
		#ほぼユニークなsandを作成
		rand=random.randint(0, (1<<30))
		sand=""+str(type(obj))+"_"+str(time.time())+"_"+str(rand)
		
		#putする
		obj.sand=sand	#Modelにsand=db.StringProperty()を追加すること
		obj.put()
	
		#インデックスに反映されるまで待機
		for i in range(10):
			#objのオブジェクト全体からsandを持つオブジェクトの数を取得
			cnt=obj.all().filter("sand =",sand).count()
			if(cnt>=1):
				break
			
			#まだ反映されていなかったので待機
			time.sleep(1)

実際に運用に使っているコードは、https://github.com/abars/illustbook/blob/master/myapp/SyncPut.pyから確認頂けます。

これで、おおむね動作してはいるのですが、実際には1つのオブジェクトに対して複数のインデックスが対応するため、sandのインデックスは更新されたが、他のインデックスは更新されていない可能性があり、完全ではありません。

厳密な整合性が要求される用途では、db.getは強い整合性が保証されているので、db.keyのリストを持たせてQueryを使わない必要があるかと思います。また、Googleには、ぜひ、公式にput_sync的なメソッドを用意して欲しいと思います。公式のサンプルが動かないのはさすがに厳しいと思います。