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();
        }        
    }
}

所感

文字列でクラス名やメソッド名を書いて,リフレクションで呼び出すという方法は,コンパイルチェックが効かなくなるのであまり好きじゃない.

でも,関数オブジェクトを作る方法でも,引数をObjectの配列で渡すという時点で,コンパイル時のチェックは,いずれにしても効かない.そう考えると,こんな方法でもいいのかな,と思ったり.