sobota, maja 09, 2009

Google App Engine i DatastoreTimeoutException

Pisząc dziś servlet, który siedzi w cron'ie mojej aplikacji w Google App Engine [wersja Java'owa] natknąłem się na problem z wyjątkiem:
com.google.appengine.api.datastore.DatastoreTimeoutException: datastore timeout: operation took too long.


Mój kod, który powodował problem wyglądał mniej więcej tak:
PersistenceManager pm = PMF.get().getPersistenceManager();
String query = "select from TABLE order by date";
List list = (List) pm.newQuery(query).execute();
for (Record record:list) {
// do smth with record
}

Wyjątek wylatywał z "podświetlonej" linii.

Kod w tej linii wykonywał się do 15 sekund po czym był brutalnie przerywany przez serwer... związane to było z tym, że w bazie znajdowało się wtedy grubo ponad 20 tysięcy rekordów... a Google App Engine jest na tyle miłe, że dane z bazy pobierane są dopiero wtedy gdy są naprawdę konieczne :-) [z jednym "ale"]

Linia:
for (Record record:list) {

jest równoważna takiemu kodowi:
for(Iterator iterator = list.iterator(); iterator.hasNext();) {
Record record = (Record)iterator.next();


I wygląda na to, że w trakcie tworzenia iteratora Google App Engine pobiera dane z bazy.... a zebranie 1000 rekordów zajmuje zbyt wiele czasu i stąd wyjątek.

Jak z nim walczyć?

Prosto :-)
Po pierwsze zamiast pętli for-each użyć należy zwykłej pętli, ale jako warunku stopu nie powinniśmy używać warunku index<list.size(), bo rozmiar danych poznamy dopiero po pobraniu wszystkich danych z bazy.
Ja użyłem wartości 1000 jako maksymalnej ilości iteracji [ponieważ z każdego query możemy dostać maksymalnie do 1000 rekordów], zabezpieczyłem się też w bardzo brzydki sposób przed IndexOutOfBoundsException po prostu go łapiąc ;-)
Po drugie trzeba w pętli pobierającej dane z bazy wstawić watchdoga, który w razie potrzeby wyjdzie z niej.
long start = System.currentTimeMillis();
PersistenceManager pm = PMF.get().getPersistenceManager();
String query = "select from TABLE order by date";
List list = (List) pm.newQuery(query).execute();
for (int idx=0; idx<1000; idx++) {
Record record = null;
try {
record = list.get(idx);
} catch (IndexOutOfBoundsException ioobe) {
// ugly
break;
}
// do smth with record
if ((System.currentTimeMillis()-start)>=2000) break;
}


I to działa :-)
Dodatkowo dodałem jeszcze ograniczenie do operowania na maksymalnie 100 rekordach ponieważ później je jeszcze kasuje i tworze nowe, ale to już w transakcjach :-)


Podobne postybeta
Wymiana obiektów między PC a Androidem... - użyj serializacji Luke ;-)
Zdradliwa Java i 8 królowych ;-)
Google App Engine - pierwsze wrażenia
GAE zmienia ceny i darmową quota'ę... a ja zmieniam kod ;-)
IE suxx ;-)