LoginSignup
7
7

More than 5 years have passed since last update.

Groovyのちょっとしたこと「Grails3のAsyncを試してみる」

Last updated at Posted at 2015-07-06

どうも、えいやです。

最近、会社で趣味としてGroovyで社内のデータとよそのニュース記事なんかを自然言語処理で関連付けて、その演算結果データをGrails3系を使って社内イントラとかで公開して遊んでいます。
そのデータのうち面白そうなものを人間が選択して会社の公式Twitterでつぶやく機能をつけていますが、その際にTweet時間を指定出来るようにしています。

今日は、そのような機能を実装する際に便利なGrailsのAsynchronousの話をします。Grailsの基本が理解できていることを前提とします。

Aysncの機能を覚えておくと、WebApplicationで出来ることの幅が広がるので覚えておくといいと思います。

今回の範囲ではタスクの予約実行などは含みませんが、ちょっと応用すればできるようになるはずです。

簡単に使えるAsync機能 Promiseの概要

Grailsで簡単に使えるAsync機能に、Promiseというものがあります。

以下の様なコードを書いてあげれば、taskに渡したクロージャが非同期的に処理されるという便利なものです。

FooService.groovy
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型となっていて、onErroronCompleteなどのイベントが登録できます。

上記の公式でも説明が乗っていますが、僕がgrails 3.0.1で試してみた限りだとあまりまともに使えませんでした

調べたところによれば、「getやwaitAllを使って待ち合わせないとイベントが発火しない」という情報もありますが、確かめる事はできませんでした。

Groovyであれば例外処理をきっちりやってさえ置けば問題なさそうなので、僕はonErrorやonCompleteは使わないことにしています

Async中のThreadにおけるDomainClassのSessionについて

さて、Async機能を使うだけならば上記のtaskを呼ぶだけで話は済むのですが、大抵の場合では、AsyncThreadでDomainClassを扱いたいと思います。

そこで注意しなければならないのが、AsynThreadの中ではDomainClassはMainThreadのSessionが使えないということです。

具体的な例を挙げると、まず以下のコードが実行時にエラーになります。

まずサブキーを持つドメインクラスを適当に次のように用意します。

Hero.groovy
package jp.eiya.aya.grails.sample

class Hero {
    Long id
    String name
    String masterName
    Clazz clazz
    String toString(){"$masterName x $clazz[$name]"}
    static constraints = {
    }
}
Clazz.groovy
package jp.eiya.aya.grails.sample

class Clazz {
    Long id
    String name
    String toString(){"$name($id)"}
    static constraints = {
    }
}

これらのドメインのデータを起動時に登録するため、BootStrapに次のように記述します。

BootStrap.groovy
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 = {
    }
}

サービスクラスは次のとおりです。

FooService.groovy
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'
  }
}

サービスを呼び出すコントローラを次のように適当に作ります。

AsyncController
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.groovyroot(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は次のようになります。

FooService.groovy
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です。

BootStrap.groovy
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の幅が広がるでしょう。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7