Статьи

Платформа Play 2.0 и транзакции XA

Транзакции XA полезны и из коробки, Play 2.0 сегодня не имеет их поддержки. Здесь я покажу, как добавить эту поддержку:

Прежде всего, несколько примеров, когда XA полезен:

— JPA использует два физических соединения, если вы используете сущности из двух разных persistence.xml — эти два соединения, возможно, должны быть зафиксированы в одной транзакции, поэтому XA — ваш единственный вариант
— фиксация изменений в базе данных и одновременная фиксация сообщения в JMS. Например, вы хотите гарантировать, что электронное письмо будет отправлено после того, как вы успешно отправите заказ в базу данных, асинхронно. Есть и другие способы, но JMS предоставляет транзакционный способ сделать это с небольшими накладными расходами на необходимость думать о сбое.
— Запись в физически другую базу данных по любой из нескольких политических причин (устаревшая система, другой отдел, отвечающий за другой сервер базы данных / разные бюджеты).
— См. Http://docs.codehaus.org/display/BTM/FAQ#FAQ-WhywouldIneedatransactionmanager.

Так что, как я понимаю, XA — это то, что Play должен «поддерживать».

Добавить поддержку очень просто. Я создал игровой плагин, который основан на Bitronix. Ресурсы настраиваются в дереве JNDI Bitronix (в любом случае Play использует файл конфигурации, а не JNDI ?! в любом случае…). Вы запускаете транзакцию следующим образом: «withXaTransaction»:

01
02
03
04
05
06
07
08
09
10
    def someControllerMethod = Action {
                    withXaTransaction { ctx =>
                        TicketRepository.
 
addValidation(user.get, bookingRef, ctx)
                        ValidationRepository.addValidation(bookingRef, user.get, ctx)
                    }
                    val tickets = TicketRepository.getByEventUid(eventUid)
                    Ok(views.html.ticketsInEvent(eventUid, getTickets(eventUid), user, eventValidationForm))
    }

Объект ctx — это XAContext (мой собственный класс), который позволяет вам искать ресурсы как источник данных или устанавливать откат в случае сбоя. Таким образом, проверочное хранилище делает это, используя ScalaQuery (я использовал «withSession», а не «withTransaction!»):

1
2
3
4
5
6
def addValidation(bookingRef: String, validator: User, ctx: XAContext) = {
       val ds = ctx.lookupDS("jdbc/maxant/scalabook_admin")
       Database.forDataSource(ds) withSession { implicit db: Session =>
           Validations.insert(Validation(bookingRef, validator.email, new java.sql.Timestamp(now)))
       }
   }   

А репозиторий делает с JMS следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
def addValidation(user: User, bookingRef: String, ctx: XAContext) = {
 
       val xml =
            
               {bookingRef}
               {user.email}
            
 
       val qcf = ctx.lookupCF("jms/maxant/scalabook/ticketvalidations")
       val qc = qcf.createConnection("ticketValidation","password")
       val qs = qc.createSession(false, Session.AUTO_ACKNOWLEDGE)
       val q = qs.createQueue("ticketValidationQueue") //val q = ctx.lookup(QUEUE).asInstanceOf[Queue]
       val sender = qs.createProducer(q)
       val m = qs.createTextMessage(xml.toString)
       sender.send(m)
       sender.close
       qs.close
       qc.close
   }   

Я протестировал его с записью в MySQL и отправкой JMS-сообщения в JBoss (HornetQ), и, похоже, он работает хорошо (за исключением того, что заставить hornetQ играть с Bitronix было сукой — см. Здесь: https://community.jboss.org/ резьба / 206180? tstart = 0 ).

Scala-код для поддержки XA:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package ch.maxant.scalabook.play20.plugins.xasupport
 
import play.api.mvc.RequestHeader
import play.api.mvc.Results
import play.api.mvc.Request
import play.api.mvc.AnyContent
import play.api.mvc.Result
import play.api.mvc.Action
import play.api.mvc.Security
import play.api._
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import ch.maxant.scalabook.persistence.UserRepository
import bitronix.tm.TransactionManagerServices
import java.util.Hashtable
import javax.naming.Context._
import javax.naming.InitialContext
import javax.sql.DataSource
import bitronix.tm.BitronixTransaction
import java.io.File
import org.scalaquery.session.Database
import org.scalaquery.SQueryException
import scala.collection.mutable.ListBuffer
import java.sql.Connection
import java.sql.SQLException
import org.scalaquery.session.Session
import bitronix.tm.BitronixTransactionManager
import javax.jms.ConnectionFactory
 
class XAContext {
 
    private val env = new Hashtable[String, String]()
    env.put(INITIAL_CONTEXT_FACTORY, "bitronix.tm.jndi.BitronixInitialContextFactory")
    private val namingCtx = new InitialContext(env);
 
    var rollbackOnly = false
     
    def lookup(name: String) = {
        namingCtx.lookup(name)
    }
    def lookupDS(name: String) = {
        lookup(name).asInstanceOf[DataSource]
    }
    def lookupCF(name: String) = {
        lookup(name).asInstanceOf[ConnectionFactory]
    }
}
 
trait XASupport { self: Controller =>
 
    private lazy val tm = play.api.Play.current.plugin[XASupportPlugin] match {
      case Some(plugin) => plugin.tm
      case None => throw new Exception("There is no XASupport plugin registered. Make sure it is enabled. See play documentation. (Hint: add it to play.plugins)")
    }
 
    /**
     * Use this flow control to make resources used inside `f` commit with the XA protocol.
     * Conditions: get resources like drivers or connection factories out of the context passed to f.
     * Connections are opened and closed as normal, for example by the withSession flow control offered
     * by ScalaQuery / SLICK.
     */
    def withXaTransaction[T](f: XAContext => T): T = {
        tm.begin
 
        //get a ref to the transaction, in case when we want to commit we are no longer on the same thread and TLS has lost the TX.
        //we have no idea what happens inside f!  they might spawn new threads or send work to akka asyncly
        val t = tm.getCurrentTransaction
        Logger("XASupport").info("Started XA transaction " + t.getGtrid())
        val ctx = new XAContext()
        var completed = false
        try{
            val result = f(ctx)
            completed = true
            if(!ctx.rollbackOnly){
                Logger("XASupport").info("committing " + t.getGtrid() + "...")
                t.commit
                Logger("XASupport").info("committed " + t.getGtrid())
            }
            result
        }finally{
            if(!completed || ctx.rollbackOnly){
                //in case of exception, or in case of set rollbackOnly = true
                Logger("XASupport").warn("rolling back (completed=" + completed + "/ctx.rollbackOnly=" + ctx.rollbackOnly)
                t.rollback
            }
        }
    }
}
 
class XASupportPlugin(app: play.Application) extends Plugin {
 
    protected[plugins] var tm: BitronixTransactionManager = null
     
    override def onStart {
        //TODO how about getting config out of jar!
        val file = new File(".", "app/bitronix-default-config.properties").getAbsolutePath
        Logger("XASupport").info("Using Bitronix config at " + file)
        val prop = System.getProperty("bitronix.tm.configuration", file) //default
        System.setProperty("bitronix.tm.configuration", prop) //override with default, if not set
 
        //start the TM
        tm = TransactionManagerServices.getTransactionManager
         
        Logger("XASupport").info("Started TM with resource config " + TransactionManagerServices.getConfiguration.getResourceConfigurationFilename)
    }
 
    override def onStop {
        //on graceful shutdown, we want to shutdown the TM too
        Logger("XASupport").info("Shutting down TM")
        tm.shutdown
        Logger("XASupport").info("TM shut down")
    }
 
}

Используйте код по своему усмотрению, я раздаю его бесплатно 🙂 Только не жалуйтесь, если он не работает 😉

Было бы хорошо, если бы этот плагин был расширен и превращен в нечто более готовое к производству. Еще приятнее было бы, чтобы Play изначально поддерживал диспетчер транзакций, включая извлечение ресурсов из JNDI.

Приятного кодирования и не забудьте поделиться!

Ссылка: Play 2.0 Framework и транзакции XA от нашего партнера JCG Антона Кучера в блоге The Kitchen in the Zoo .