負荷テストその2(Grails 0.5)

プロファイル結果

昨日のHttpSessionがいっぱいになってしまう点については、大量アクセスが発生するサイトではよくあることだ。大量アクセスが短期に発生するようなページで無駄にHttpSessionを使用するとTomcatの場合、OutOfMemoryErrorが発生し、発生したスレッドによってはそのままハングしてしまうという事象はTomcat 5.0 ではよくおきていた。その後の5.5、6.0では検証していないので分からない。ただ、昨日の負荷テストの結果を見る限り同じ?なのではないかと思われる。
まあ、これに対してはよくあることなので独自のHttpSessionの実装が実は用意してある。フィルタをかましてHttpServletRequestをラップして独自のHttpSessionを返す、というやつだ。これをGrailsプラグインでweb.xmlにちょちょいと追加するようなプラグインを作ってインストールした。
ちなみにセッションの永続化についてはehcacheを利用している。テストで使用した際にはehcacheについてはレプリケーション等の設定は行っていない。
再度、同じように負荷をかけてみた。そのときの結果はこれ(GSPなし)。

$ ab -c 300 -n 30000 http://localhost:8080/pagetest-0.1-c/test01
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 3000 requests
Completed 6000 requests
Completed 9000 requests
Completed 12000 requests
Completed 15000 requests
Completed 18000 requests
Completed 21000 requests
Completed 24000 requests
Completed 27000 requests
Finished 30000 requests


Server Software:        Apache-Coyote/1.1
Server Hostname:        localhost
Server Port:            8080

Document Path:          /pagetest-0.1-c/test01
Document Length:        90 bytes

Concurrency Level:      300
Time taken for tests:   73.788111 seconds
Complete requests:      30000
Failed requests:        0
Write errors:           0
Total transferred:      8098725 bytes
HTML transferred:       2700270 bytes
Requests per second:    406.57 [#/sec] (mean)
Time per request:       737.881 [ms] (mean)
Time per request:       2.460 [ms] (mean, across all concurrent requests)
Transfer rate:          107.17 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0  401 2053.1      1   45024
Processing:     7  314 276.2    253    9336
Waiting:        6  299 264.1    245    9238
Total:         74  716 2091.4    263   45322

Percentage of the requests served within a certain time (ms)
  50%    263
  66%    301
  75%    345
  80%    394
  90%    885
  95%   3264
  98%   4140
  99%   9270
 100%  45322 (longest request)

若干スループットが上がってるかも。誤差の範囲ともいえるが。プロファイラの結果で気が付くのは、

  1. コントローラの生成が遅い
  2. コントローラのgetProperty関連処理が遅い

ような気がする。

負荷テスト(Grails 0.5)

以前、やろうとしていたスループットのテストをやってみる。groovy-all-1.1-BEATA-1.jarをgroovy-all-1.1-BETA-2-SNAPSHOT.jarに入れ替えて実行した。実行したアプリケーションはこんな感じ。
まず、コントローラ

class Test01Controller {

    def index = {
        render """
<html>
	<head>
		<title>Hello</title>
	</head>
	<body>
		Hello World		
	</body>	
</html>
"""
	}
    def view = {
        render(view:'hello')
    }
    def layout = {
        render(view:'hellolayout')
    }
}

views/test01/hello.gspはこんな感じ

<html>
	<head>
		<title>Hello</title>
	</head>
	<body>
		Hello World		
	</body>	
</html>

views/test01/hellolayout.gspはこんな感じ

<html>
<head>
<meta name="layout" content="simple" />
</head>
<body>Hello World</body>
</html>

views/layouts/simple.gspはこんな感じだ。

<html>
	<head>
		<title>Hello</title>
		<g:layoutHead />
	</head>
	<body>
		<g:layoutBody />		
	</body>	
</html>

何がやりたいかと言うと、

  1. コントローラのrenderでHTML出力した場合
  2. GSPでHTML出力した場合(レイアウトなし)
  3. GSPでHTML出力した場合(レイアウトあり)

でどの程度の違いがあるのか、ということだ。

テストは、grails war でWARファイルを作成して、Tomcat 6.0.13にデプロイし、最初に対象URLをブラウザで表示して、その後ApacheBenchで負荷をかけることとした。
最初に、コントローラのrenderで出力した場合のテスト結果はこんな感じだった。
第1回目

$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)


Server Software:        Apache-Coyote/1.1
Server Hostname:        localhost
Server Port:            8080

Document Path:          /pagetest-0.1/test01
Document Length:        90 bytes

Concurrency Level:      100
Time taken for tests:   26.640011 seconds
Complete requests:      10000
Failed requests:        1
   (Connect: 0, Length: 1, Exceptions: 0)
Write errors:           0
Non-2xx responses:      1
Total transferred:      2891115 bytes
HTML transferred:       901010 bytes
Requests per second:    375.38 [#/sec] (mean)
Time per request:       266.400 [ms] (mean)
Time per request:       2.664 [ms] (mean, across all concurrent requests)
Transfer rate:          105.97 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    6  17.4      1     301
Processing:    12  256 155.7    210    1394
Waiting:        7  248 154.9    204    1392
Total:         36  263 154.6    215    1419

Percentage of the requests served within a certain time (ms)
  50%    215
  66%    264
  75%    304
  80%    335
  90%    459
  95%    559
  98%    752
  99%    918
 100%   1419 (longest request)

続けて2度目

$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Total of 6052 requests completed

むむむ、タイムアウトしてしまった。Tomcatの方はというと、OutOfMemoryErrorが出ている。ま、原因は予想がつくのだが。

Tomcatを再起動して、GSP(レイアウトなし)のテスト。
1回目

$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01/view
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)


Server Software:        Apache-Coyote/1.1
Server Hostname:        localhost
Server Port:            8080

Document Path:          /pagetest-0.1/test01/view
Document Length:        97 bytes

Concurrency Level:      100
Time taken for tests:   59.736206 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      3600000 bytes
HTML transferred:       970000 bytes
Requests per second:    167.40 [#/sec] (mean)
Time per request:       597.362 [ms] (mean)
Time per request:       5.974 [ms] (mean, across all concurrent requests)
Transfer rate:          58.84 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   5.4      0     128
Processing:    95  593 340.9    480    3809
Waiting:       77  590 339.4    477    3808
Total:        130  594 341.0    480    3812

Percentage of the requests served within a certain time (ms)
  50%    480
  66%    584
  75%    677
  80%    748
  90%    967
  95%   1227
  98%   1588
  99%   1920
 100%   3812 (longest request)

2回目はハングして返ってこなかった(ように見えた)ので強制終了した。
再度Tomcatを再起動(停止中にOutOfMemoryError発生)してGSP(レイアウトあり)のテスト。
1回目

$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01/layout
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)


Server Software:        Apache-Coyote/1.1
Server Hostname:        localhost
Server Port:            8080

Document Path:          /pagetest-0.1/test01/layout
Document Length:        140 bytes

Concurrency Level:      100
Time taken for tests:   169.166464 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      4040000 bytes
HTML transferred:       1400000 bytes
Requests per second:    59.11 [#/sec] (mean)
Time per request:       1691.665 [ms] (mean)
Time per request:       16.917 [ms] (mean, across all concurrent requests)
Transfer rate:          23.32 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   3.4      0      55
Processing:    28 1681 7113.9    632   72432
Waiting:       27 1678 7112.7    630   72432
Total:         71 1682 7113.8    633   72432

Percentage of the requests served within a certain time (ms)
  50%    633
  66%    970
  75%   1277
  80%   1546
  90%   2174
  95%   3014
  98%   5148
  99%  71387
 100%  72432 (longest request)

2回目はやはりハングしたように見えるほど遅かったので強制終了した。同様にOutOfMemoryErrorが発生した。
回数を重ねると遅く、メモリを消費していくのは多分HttpSessionのせいだろう。
しかし、Cookieが使えないクライアントから毎秒1000以上のリクエストが来るようなサイトでGrailsが果たして使えるのか?ということが実は最大の関心事なのだけれども。
一応、最初のテスト(コントローラのrenderでHTMLを出力)をプロファイラで調べてみた。やはりメモリを圧迫しているのはHttpSessionのインスタンスのようだ。1セッションで1800バイトくらい使っているようだ。確かにデフォルトのメモリ設定だと、20000セッションは厳しいかもしれない。
次に、CPU時間の結果を見てみた。全体の63.8%がSimpleGrailsControllerHelper#handleURI()で消費されている。そのうち、SimpleGrailsControllerHelper#getControllerInstance()で実に39.1%を消費している。SimpleGrailsControllerHelper#handleAction()が9.1%の消費となっているので、コントローラの実行の実に4倍のCPU時間を消費していることになる。まあ、今回のコントローラは殆ど何もしていないので、こんな結果になったのではないかと思われる。なお、SimpleGrailsControllerHelper#getControllerInstance()の消費時間はクロージャの数に比例するようだ。

次にGSPでHTML出力した場合(レイアウトなし)の場合のプロファイルを取る。
ここで新たに登場してくるのが、全体の40.2%のCPU時間を消費しているGrailsLayoutDecoratorMapper#getNamedDecorator()だ。これが増えたおかげで先ほどのSimpleGrailsControllerHelper#handleURI()の消費時間が26.6%まで落ちた。このメソッドは結果をキャッシュできるような気がするなぁ(少なくともProductionでは)。
ソースを見てみると、

	public Decorator getNamedDecorator(HttpServletRequest request, String name) {
		if(StringUtils.isBlank(name))return null;
		
		if(decoratorMap.containsKey(name)) {
			return (Decorator)decoratorMap.get(name);
		}
		else {
			String decoratorName = name;
			if(!name.matches("(.+)(\\.)(\\w{2}|\\w{3})")) {
				name += DEFAULT_VIEW_TYPE;
			}
			String decoratorPage = DEFAULT_DECORATOR_PATH + '/' + name;

            ResourceLoader resourceLoader = establishResourceLoader();


            Resource res = resourceLoader.getResource(decoratorPage);
            if(!res.exists()) {

                PathMatchingResourcePatternResolver matcher = new PathMatchingResourcePatternResolver(resourceLoader);
                String pattern = GrailsResourceUtils.WEB_INF + "/plugins/*/grails-app/views/layouts/" + name;

                if(LOG.isDebugEnabled())
                    LOG.debug("No decorator found at " + decoratorPage+". Trying plug-ins with pattern: " + pattern);

                try {
                    Resource[] layouts = matcher.getResources(pattern);
                    if(layouts.length>0) {
                        if(layouts.length>1) {
                            LOG.warn("Multiple matching layouts found in plug-ins for name ["+name+"] using first from ["+ ArrayUtils.toString(layouts) +"]");
                        }
                        String url = layouts[0].getURL().toString();
                        url = GrailsResourceUtils.WEB_INF + url.substring(url.indexOf("/plugins"),url.length());
                        Decorator d = new DefaultDecorator(name, request.getRequestURI(), url, Collections.EMPTY_MAP);
                        decoratorMap.put(decoratorName, d);
                        return d;
                    }
                } catch (IOException e) {
                    // ignore, if there was a problem here its going to be a FNFE which is ok
                }
                return null;
            }
            else {
                if(LOG.isDebugEnabled())
                    LOG.debug("Using decorator " + decoratorPage);

                Decorator d = new DefaultDecorator(decoratorName,request.getRequestURI(),decoratorPage, Collections.EMPTY_MAP);
                decoratorMap.put(decoratorName,d);
                return d;
            }
		}
	}

をを、キャッシュしているぞ。ただし、見つかったものだけだな。return nullのものに関してはキャッシュしていないということか。確かに妥当な気もする。nameとして与えられるパラメタの内容が有限ならばネガティブキャッシュしてもよさそうだが。

最後にGSPでHTML出力した場合(レイアウトあり)の場合。
新たに登場するのは、GrailsPageFilter#applyDecorator()だ(31.7%)。これはレイアウトGSPページを実行するのだから当然といえば当然か。逆にGrailsLayoutDecoratorMapper#getNamedDecorator()は姿を消した。おそらく、レイアウトが指定されているので何も検索しないのだろう。ちなみに、SimpleGrailsControllerHelper#handleURI()は、32.2%のCPU消費時間となっている。
むぅ、劇的なパフォーマンスアップは望めないかも。。。

ハングしちゃうんですけど!を調べた

昨日の日記でGrailsがハングすることを書きましたが調べてみました。スタックトレースを見ると、Groovyで書かれたクラス(コントローラ)のClass#newInstance()で止まっているみたいで、怖くて使えないです。もう、Grailsの話題じゃないです。
スタックトレースを見る限りでは、org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMapが怪しい。Grails 0.5に付属のGroovy(1.1 BETA-1)のこのクラスのソースはここ。その中で、wait()したままの箇所はここ。

    private void waitForWriteState() {
        synchronized (writeQueue) {
            while (concurrentReads!=0) {
                try {
                    writeQueue.wait();
                } catch (InterruptedException e) {}
            }
        }
    }

MemoryAwareConcurrentReadMapって言うのはいわゆるReaderWriterLock機能を持ったMapなのだろう。とすると、concurrentReadsはその名のとおり現在getしているスレッド数で、put時にはgetしているやつがいなくなるまで待つのだろうと思われる。で、concurrentReadsの定義はこれ、

    private volatile long concurrentReads = 0;

volatile宣言してあってもマルチスレッド環境では、synchronizedが必要。Java初心者の方は調べてくださいね。ここでは説明しませんけれど。とすると、synchronizedされているのはどのオブジェクト?writeQueue?他のconcurrentReadsをアクセスしているところはというと、

    private void lockWrite() {
        synchronized (writeLock) {
            concurrentReads++;
        }
    }
    
    private void unlockWrite(){
        synchronized (writeLock) {
            concurrentReads--;
        }
        synchronized (writeQueue) {
            writeQueue.notify();
        }
    }

    public Object get(Object key) {
        int hash = key.hashCode();
        lockWrite();        
        try {
            int index = index(hash,table.length);

            for (Entry current = table[index]; current!=null; current=current.next) {
                if (hash!=current.hash) continue;
                Object oldKey = current.getKey();
                Object oldValue = current.getValue();
                if (!current.isValid()) continue;
                if (key==oldKey || key.equals(oldKey)) {
                    return oldValue;
                }
            }
        } finally {
            unlockWrite();
        }
        return null;
    }

おやおや、writeLockで排他しているみたいだ。それなら、waitForWriteState()の呼出元はどうなっているんだろう?いくつかあるが、例えばput()

    public void put(Object key, Object value) {
        if (value == null) {
            remove(key);
            return;
        }

        synchronized(writeLock) {
            waitForWriteState();
            putNonBlocking(key,value,false);
        }
    }

呼び出す前にsynchronized(writeLock)してるのね。でも....
なるほど、ハイ、わかりました原因。バグですね。それじゃあ、また来週!!
ちなみに最新のソースでは直っているみたい。入れ替えた方がいいかも。

ハングしちゃうんですけど!(Grails 0.5+オリジナルパッチ)

コントローラ中でrender "hogehoe"したときと、GSPに飛ばしたとき(レイアウトがあるとき、ないとき)とでどれくらいスループットが変わるかをテストすることにした。
Windowsだとそもそもソケット周りが耐え切れないと思って、Linux(CentOS) on VMWare(仮想CPU数2)上のTomcatでアプリケーションを実行し、ApacheBench(ab)で -c 100 -n 10000 とかのパラメタで実行。
ところが、どうもハングしたみたいで、いつまでたっても返ってこない。
JProfilerを使用して、再度Tomcatを実行してApacheBenchで負荷をかけてみた。
2103秒間ブロックされてるって(スクリーンショットを参照のこと。見えないと思うけど)!
待っているスレッドのスタックトレースは以下のとおり。

org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.lockWrite()
org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.get(java.lang.Object)
org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.getMetaClass(java.lang.Class)
groovy.lang.MetaClassRegistry.getMetaClass(java.lang.Class)
org.codehaus.groovy.runtime.Invoker.getMetaClass(java.lang.Object)
org.codehaus.groovy.runtime.InvokerHelper.getMetaClass(java.lang.Object)
groovy.lang.Closure.<init>(java.lang.Object, java.lang.Object)
Test01Controller$_closure2.<init>(java.lang.Object, java.lang.Object)
Test01Controller.<init>()
java.lang.Class.newInstance()
org.codehaus.groovy.grails.commons.AbstractGrailsClass.newInstance()
java.lang.reflect.InvocationHandler.invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[ ])
$Proxy3.newInstance()
org.springframework.context.ApplicationContext.getBean(java.lang.String)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.getControllerInstance(org.codehaus.groovy.grails.commons.GrailsControllerClass)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest, java.util.Map)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsController.handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
org.springframework.web.servlet.HandlerAdapter.handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object)
org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
URL: /pagetest-0.1/grails/test01.dispatch
javax.servlet.http.HttpServlet.service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
org.codehaus.groovy.grails.web.mapping.filter.UrlMappingsFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
org.apache.tomcat.util.net.JIoEndpoint$Worker.run()

ブロックして(待たせて)いるほうのスレッドのスタックトレースは以下のとおり。

java.lang.Object.wait()
org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.waitForWriteState()
org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.put(java.lang.Object, java.lang.Object)
org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.getMetaClass(java.lang.Class)
groovy.lang.MetaClassRegistry.getMetaClass(java.lang.Class)
org.codehaus.groovy.runtime.Invoker.invokeStaticMethod(java.lang.Class, java.lang.String, java.lang.Object)
org.codehaus.groovy.runtime.InvokerHelper.invokeStaticMethod(java.lang.Class, java.lang.String, java.lang.Object)
org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeStaticMethodN(java.lang.Class, java.lang.Class, java.lang.String, java.lang.Object[ ])
Test01Controller.<init>()
java.lang.Class.newInstance()
org.codehaus.groovy.grails.commons.AbstractGrailsClass.newInstance()
java.lang.reflect.InvocationHandler.invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[ ])
$Proxy3.newInstance()
org.springframework.context.ApplicationContext.getBean(java.lang.String)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.getControllerInstance(org.codehaus.groovy.grails.commons.GrailsControllerClass)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest, java.util.Map)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest)
org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsController.handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
org.springframework.web.servlet.HandlerAdapter.handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object)
org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
URL: /pagetest-0.1/grails/test01.dispatch
javax.servlet.http.HttpServlet.service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
org.codehaus.groovy.grails.web.mapping.filter.UrlMappingsFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
org.apache.tomcat.util.net.JIoEndpoint$Worker.run()

これって、Groovyかな。pageEncodingパッチのせいじゃないよねぇ? http://jira.codehaus.org/browse/GRAILS みても、issueが多くて既に登録されているのか良く分からない...

GSPで定義タグって使えるの?(Grails 0.5+オリジナルパッチ)

こんな感じのGSPを作ってみた。

<%!
def square = { x -> x * x }
%>
<html>
	<head>
		<title>Test</title>
	</head>
	<body>
		3 ^ 2 = <%= square(3) %>
	</body>	
</html>

これ、コンパイルできないみたい。Parse.javaとかみると、<%! %>の処理はあるみたいなんだけど..未実装?それともGSPでは使えない?それともバグ?
ParseTest.javaに以下のようなテストを追加して実行してみた。

 	public void testParseWithDeclare() throws Exception {
		String output = parseCode("myTest",
				"<%! def hoge = 'hoge' %>" +
				"<div>Hello</div>");
		System.out.println(output);
 	}

結果として表示されたのは、

import org.codehaus.groovy.grails.web.pages.GroovyPage
import org.codehaus.groovy.grails.web.taglib.*

def hoge = 'hoge'


class myTest extends GroovyPage {
public Object run() {
out.print('<div>Hello</div>')
}
}

だった。def hogeの文がmyTestクラスの中にあるのが正しいのかな。

T.Yamamotoさんのブログで紹介されてた!

JProfilerの画面

T.Yamamotoさんのブログでこのブログが紹介されていました。Grailsもどんどんメジャーになっていって欲しいですね。
そうですか、濃いですか...。どちらかというと特殊な環境での使用を考えているのでいろいろと検証中なのです、ハイ。その名のとおり備忘録的な内容なので読んでいて楽しい&役に立つ内容じゃないかもです。

プラグインでコマンドを追加(Grails 0.5)

grails hoge-hoge とかすると、何かやってくれるようなコマンドをプラグインに追加したくって。例えば、grails configure-hogehoge とかすると、対話式にプラグインの設定ができるとか。
そりゃあ、多分できるのでしょう。なんといっても、開発者ガイドには、「コマンドラインインターフェイスからランタイムコンフィグレーションエンジンにいたるどんな拡張も可能にする方法を提供します」ですから。
さて、やってみる。まずはプラグインプロジェクトを。

grails create-plugin hoge

作成したプロジェクトのscriptsディレクトリに以下のHogeHoge.groovyを作成。

task ('default': "Print hoge message.") {
	println 'Hello from Grails plugin hoge'
}

これで完成。次にプラグインのZipファイルを作成。

cd hoge
grails package-plugin

パッケージ作成完了。あとは適当なGrailsプロジェクトでこのプラグインをインストールして、コマンドラインから

grails hoge-hoge

とすればOK。Hello from Grails plugin hoge って出力されます。なんて簡単なんだ。
でも、同じ名前があったらどうなるんだろう。AプラグインとBプラグインで同じコマンドがあった場合とか。
作ってみるのが手っ取り早いんだろうが、それも何なので、ソースをみた。grails.batで起動されているのは、org.codehaus.groovy.grails.cli.GrailsScriptRunnerだ。mainメソッド中に以下のような箇所が。

try {      
	callPluginOrGrailsScript(scriptName)
}
catch(Throwable t) {
	println "Error executing script ${scriptName}: ${t.message}"
	t.printStackTrace(System.out)
}

そのものずばりの名前。で、callPluginOrGrailsScript()はというと、

private static callPluginOrGrailsScript(scriptName) {
	def potentialScripts = [] 

	def userHome = ANT.antProject.properties."user.home"
		
	def scriptLocations = ["${baseDir.absolutePath}/scripts", "${grailsHome}/scripts", "${userHome}/.grails/scripts"]
	scriptLocations.each {
		def scriptFile = new File("${it}/${scriptName}.groovy")
		if(scriptFile.exists()) {
			potentialScripts << scriptFile
		}
	}
		                       
	try {
		def pluginScripts = RESOLVER.getResources("file:${baseDir.absolutePath}/plugins/**/scripts/${scriptName}.groovy")
		potentialScripts += pluginScripts.collect { it.file }			
	}
	catch(Exception e) {
		println "Note: No plugin scripts found"
	} 
	if(potentialScripts.size()>0) {  
		potentialScripts = potentialScripts.unique()
		if(potentialScripts.size() == 1) {
			println "Running script ${potentialScripts[0].absolutePath}"
                                                               
				Gant.main(["-c","-d","${userHome}/.grails/${version}/scriptCache","-f", potentialScripts[0].absolutePath] as String[])																		
		}                                      
		else {
			println "Multiple options please select:"  
			def validArgs = []
			potentialScripts.eachWithIndex { f, i ->
				println "[${i+1}] $f "
				validArgs << i+1
			}               
			ANT.input(message: "Enter # ",validargs:validArgs.join(","), addproperty:"grails.script.number")
                def number = ANT.antProject.properties."grails.script.number".toInteger()        

			println "Running script ${potentialScripts[number-1].absolutePath}"				
			Gant.main(["-f", potentialScripts[number-1].absolutePath] as String[])																						
		}
	}
	else {
		println "Script $scriptName not found."
	}    		
}

ほほう、複数あるときは、「Multiple options please select:」とかでて選択することになるっぽい。
これならどんな名前でも心配しなくていい。