App Engine deferred for Java
Task Queue
App Engineでは,一つのサーブレットは30秒しか実行出来ない上,スレッドを使うことができない.このため普通の方法では,長時間かかるようなタスクを実行することができない.これを補う機能としてTask Queueがある.
Task Queueでは,サーブレットとそれに渡す引数をタスクとして考える.このタスクをキューに積んでおくと,システムが自動的にサーブレットを引数をセットして呼び出してくれる.
defered for python
Task Queueは機能的には十分なのだがちょっと使いづらい.
これを解決するために,Python版では,deferredというライブラリが提供されている.これを使うと,こんな風に書くことができる.
from google.appengine.ext import deferred def do_something_expensive(a, b, c=None): logging.info("Doing something expensive!") # Do your work here # Somewhere else deferred.defer(do_something_expensive, "Hello, world!", 42, c=True)
関数と引数を指定して,deferred.defer を呼び出すと自動的に引数をシリアライズして,特殊なハンドラサーブレットに引き渡し,そこで実行してくれるという仕掛け.便利だ.
deferred Java 版
当然Java版が欲しいという話しになるのだけど,あんまり簡単にはいかない.そもそも関数と言う概念がないので,Runnableのような関数オブジェクトを作ることになるのだけど,クラスを定義しなければならない.フォーラムで議論されているのもこのタイプ.使い方としては,こんな感じ.
class TestDeferred implements Deferrable { void doTask(Object ... args) { // do something expensive } } .... TestDeferred testDeferred = new TestDeferred(); Deferred.defer( testDeferred, "one", "two", "three", 1, 2, 3 );
Deferrable.doTask(Object ... )を実装したクラスを作り,それを引数にする.もちろんインラインで無名クラスとして実装してもいいのだけど,余分なコードが入り込んで見通しが悪い.
リフレクションをつかったdefered
で,ちょっと別の方法を実装してみた.使い方はこんな感じ.
public static void func(int i, String str) { // do something expensive } .... Deferred2.defer("test.DeferredServlet", "func", 1, "string");
第一引数はクラス名,第二引数はそのクラスのstaticメソッド名,あとは引数.
データストアにEntityを作って,これらの情報を格納し,タスクのパラメータにはEntityのIDだけ格納してキューに積む.
タスクのハンドラでは,リフレクションを使って,クラスからメソッドを読み出し,引数の型にあったスタティックメソッドを選択して,それを呼び出す.この方法だと,メソッドの側では引数の型をObjectでなく,普通に書けるのがポイント.Objectで受けてキャストして,という方法よりは見通しがいいかな?
リフレクションを使うのは,Javaのお作法としてはあまり正しくないような気もするけど,多少見通しがよくなるということでどうか一つ.
実装
Entityはこんな感じ.JDOで書いている.ゲッタとセッタは長くなるので省略.下の方のexecute でメソッドを探して呼び出している.
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable ="true") public class Deferred2Entity { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Long id; @Persistent private String className; @Persistent private String methodName; @Persistent(serialized = "true") private List<Object> params; ..... SETTERS and GETTERS public void execute() throws ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, IOException { Class c = Class.forName(className); for (Method m: c.getMethods()){ if (checkAcceptable(m)) { m.invoke(null, params.toArray()); return; } } throw new IOException("cannot find matching method for " + className + "." + methodName); } @SuppressWarnings("unchecked") private boolean checkAcceptable(Method m) { if (! Modifier.isStatic(m.getModifiers())) return false; if (! m.getName().equals(methodName)) return false; Class [] types = m.getParameterTypes(); if (types.length != getParams().size()) return false; for (int i = 0; i < types.length; i++) { Class type = types[i]; Object o = params.get(i); if (! type.isAssignableFrom(o.getClass()) && ! isWrappingType(type, o.getClass())) return false; } return true; } private boolean isWrappingType(Class one, Class another) { return (one == Integer .TYPE && another == Integer .class) || (one == Long .TYPE && another == Long .class) || (one == Short .TYPE && another == Short .class) || (one == Character.TYPE && another == Character.class) || (one == Double .TYPE && another == Double .class) || (one == Float .TYPE && another == Float .class) || (one == Boolean .TYPE && another == Boolean .class); } }
メインとなるクラスはすごく単純で,エンティティを作ってデータストアに書き込み,あとはキューイングするだけ.
public class Deferred2 { public static void defer(String className, String methodName, Object... params) { Deferred2Entity entity = new Deferred2Entity(); entity.setClassName(className); entity.setMethodName(methodName); entity.setParams(new ArrayList<Object>(Arrays.asList(params))); PersistenceManager pm = null; try { pm = PMF.get().getPersistenceManager(); pm.makePersistent(entity); Queue queue = QueueFactory.getDefaultQueue(); queue.add(url("/deferred2Handler"). param("deferredId", ""+ entity.getId())); } finally { if (pm != null && !pm.isClosed()) pm.close(); } } }
タスクハンドラはこんな感じ.取りだして,executeして,デリートしている.
public class Deferred2Handler extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long id = Long.parseLong(req.getParameter("deferredId")); PersistenceManager pm = null; try { pm = PMF.get().getPersistenceManager(); Deferred2Entity entity = pm.getObjectById(Deferred2Entity.class, new Long(id)); pm.detachCopy(entity); entity.execute(); pm.deletePersistent(entity); } catch (IllegalArgumentException e) { throw new IOException(e); } catch (ClassNotFoundException e) { throw new IOException(e); } catch (IllegalAccessException e) { throw new IOException(e); } catch (InvocationTargetException e) { throw new IOException(e); } finally { if (pm != null && !pm.isClosed()) pm.close(); } } }