どうも、えいやです。
最近、会社で趣味としてGroovyで社内のデータとよそのニュース記事なんかを自然言語処理で関連付けて、その演算結果データをGrails3系を使って社内イントラとかで公開して遊んでいます。
そのデータのうち面白そうなものを人間が選択して会社の公式Twitterでつぶやく機能をつけていますが、その際にTweet時間を指定出来るようにしています。
今日は、そのような機能を実装する際に便利なGrailsのAsynchronousの話をします。Grailsの基本が理解できていることを前提とします。
Aysncの機能を覚えておくと、WebApplicationで出来ることの幅が広がるので覚えておくといいと思います。
今回の範囲ではタスクの予約実行などは含みませんが、ちょっと応用すればできるようになるはずです。
簡単に使えるAsync機能 Promiseの概要
Grailsで簡単に使えるAsync機能に、Promiseというものがあります。
以下の様なコードを書いてあげれば、taskに渡したクロージャが非同期的に処理されるという便利なものです。
package jp.eiya.aya.grails.sample
import static grails.async.Promises.*
class FooService{
def sayFoo(){
log.info 'start sayFoo'
task {
Thread.currentThread().sleep(5000L)
log.info 'task foo'
}
log.info 'end sayFoo'
}
}
もうちょっと詳しい説明に関しては、公式の説明を見てください。
なお、taskメソッドの返り値は、Promise型となっていて、onErrorやonCompleteなどのイベントが登録できます。
上記の公式でも説明が乗っていますが、僕がgrails 3.0.1で試してみた限りだとあまりまともに使えませんでした。
調べたところによれば、「getやwaitAllを使って待ち合わせないとイベントが発火しない」という情報もありますが、確かめる事はできませんでした。
Groovyであれば例外処理をきっちりやってさえ置けば問題なさそうなので、僕はonErrorやonCompleteは使わないことにしています。
Async中のThreadにおけるDomainClassのSessionについて
さて、Async機能を使うだけならば上記のtaskを呼ぶだけで話は済むのですが、大抵の場合では、AsyncThreadでDomainClassを扱いたいと思います。
そこで注意しなければならないのが、AsynThreadの中ではDomainClassはMainThreadのSessionが使えないということです。
具体的な例を挙げると、まず以下のコードが実行時にエラーになります。
まずサブキーを持つドメインクラスを適当に次のように用意します。
package jp.eiya.aya.grails.sample
class Hero {
Long id
String name
String masterName
Clazz clazz
String toString(){"$masterName x $clazz[$name]"}
static constraints = {
}
}
package jp.eiya.aya.grails.sample
class Clazz {
Long id
String name
String toString(){"$name($id)"}
static constraints = {
}
}
これらのドメインのデータを起動時に登録するため、BootStrapに次のように記述します。
package jp.eiya.aya.grails.sample.*
class BootStrap {
def init = { servletContext ->
def clz = new Clazz(id:1,name:'archer').save(flush:true)
new Hero(id:1,name:'Shiro Emiya',masterName:'Rin',clazz:clz).save(flush:true)
clz = new Clazz(id:2,name:'saber').save(flush:true)
new Hero(id:2,name:'Artoria Pendragon',masterName:'Shiro',clazz:clz).save(flush:true)
}
def destroy = {
}
}
サービスクラスは次のとおりです。
package jp.eiya.aya.grails.sample
import grails.transaction.Transactional
import static grails.async.Promises.*
@Transactional
class FooService{
def sayFoo(Hero h){
log.info 'start sayFoo'
task {
log.info 'start task foo'
Thread.currentThread().sleep(5000L)
try{
log.info ((h.clazz.name=='saber')?"Is dinner ready yet $h.masterName?":"$h") // error by no session
}catch(e){
log.error 'error',e
}
log.info 'end task foo'
}
log.info 'end sayFoo'
}
}
サービスを呼び出すコントローラを次のように適当に作ります。
package jp.eiya.aya.grails.sample
class AsyncController {
def fooService
def index() {
log.info 'start index'
fooService.sayFoo(Hero.get(params.id?:1))
log.info 'end index'
return 'foo'
}
}
no Sessionのため例外が発生します
./gradlew run
なりで起動したあとにhttp://localhost:8080/async/index/1
を表示して上記のコードを実行すると、ログに
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
というのが出ると思います。
(※ INFOレベルのログが出ない人はlogback.groovy
でroot(INFO, ['STDOUT'])
という設定をしてください)
このエラーはFooService.groovy
の以下のところで出ています。
task {
log.info 'start task foo'
Thread.currentThread().sleep(5000L)
try{
log.info ((h.clazz.name=='saber')?"Is dinner ready yet $h.masterName?":"$h") // error by no session
}catch(e){
log.error 'error',e // <--------------- 出してるのココね。
}
log.info 'end task foo'
}
なお、TryCatchをしてない場合、ExceptionはonErrorイベントに投げられるのでログは何も出ません。
また、データの登録をBootStrapではなく、Serviceやコントローラの中で行った場合、データオブジェクトのキャッシュの関係上、セッションを使用せずエラーにならないことがありますのでご注意ください。この場合、毎回必ずしもエラーにならない。という割りと嫌な状況になります。とくにデータの更新がupsert方針のときにそうなりやすいと思います。
no Sessionを防ぐためには
さて、no Sessionとなるのは、DomainObject h
が作られた時のSessionにアクセス出来ないからです。
なので、DomainObject h
がMainThreadのSessionにバインドされていると面倒なので、まずはそれを解除します。
DomainClassにdiscard()
というメソッドがありますのでこのメソッドを、MainThreadにいる間に呼び出します。
さらに、DomainClassにstaticなwithNewSessionとかwithNewTransactionというメソッドがありますのでそれを使います。
そして、DomainObject h
を新しいSessionにバインドし直します。これにはattach()
というメソッドを使います。
つまり、FooServiceは次のようになります。
package jp.eiya.aya.grails.sample
import grails.transaction.Transactional
import static grails.async.Promises.*
@Transactional
class FooService{
def sayFoo(Hero h){
log.info 'start sayFoo'
h.discard() // dettach from main thread's session
task {
log.info 'start task foo'
Hero.withNewSession{ // use withNewTransaction in case you need transaction.
h.attach() // attach with async thread's new session
Thread.currentThread().sleep(5000L)
try{
log.info ((h.clazz.name=='saber')?"Is dinner ready yet $h.masterName?":"I'm here. $h.clazz") // now OK
}catch(e){
log.error 'error',e
}
log.info 'end task foo'
}
log.info 'end sayFoo'
}
}
}
複数のデータソースを利用している場合は、それぞれのデータソースを代表するDomainClassのwithNewSessionやらをネストするようにしてください。
更新をためしましょう
時間経過でドメインの内容を書き換えて保存するメソッドをFooServiceに追加します。
package footest
import grails.transaction.Transactional
import static grails.async.Promises.*
@Transactional
class FooService{
def changeMaster(){
log.info 'start changeMaster'
task {
log.info 'start task changeMaster'
Hero.withNewSession{
def sclz = Clazz.findByName('saber')
def saber = Hero.findByClazz(sclz)
['Caster','Rin'].each{mstr->
Hero.withTransaction{
Thread.currentThread().sleep(15000L)
try{
saber.masterName = mstr
saber.save(flush:true)
log.info "saber's master changed to $mstr"
}catch(e){
log.error 'error',e
}
}
}
}
log.info 'end task changeMaster'
}
log.info 'end changeMaster'
}
def sayFoo(Hero h){
log.info 'start sayFoo'
h.discard()
task {
log.info 'start task foo'
Hero.withNewTransaction{
h.attach()
Thread.currentThread().sleep(5000L)
try{
log.info ((h.clazz.name=='saber')?"Is dinner ready yet $h.masterName?":"I'm here. $h.clazz") // error session failed
}catch(e){
log.error 'error',e
}
log.info 'end task foo'
}
log.info 'end sayFoo'
}
}
}
さて、起動時にこの追加したサービスのメソッドを実行するには、BootStrapで次のようにfooServiceを宣言して呼ぶだけでOKです。
import footest.*
class BootStrap {
def fooService
def init = { servletContext ->
def clz = new Clazz(id:1,name:'archer').save(flush:true)
new Hero(id:1,name:'Shiro Emiya',masterName:'Rin',clazz:clz).save(flush:true)
clz = new Clazz(id:2,name:'saber').save(flush:true)
new Hero(id:2,name:'Artoria Pendragon',masterName:'Shiro',clazz:clz).save(flush:true)
fooService.changeMaster()
}
def destroy = {
}
}
起動後に何度か間隔を置いてhttp://localhost:8080/async/index/2
にアクセスしてみると、表示が変更されることが分かるでしょう。
以上、Async機能のほんの触りになりますが、こういうことが出来れば作成可能なWebApplicationの幅が広がるでしょう。