5 Tutorial - Reference Documentation
Authors: Burt Beckwith
Version: 1.0
5 Tutorial
First create a test application:$ grails create-app atomikostest $ cd atomikostest
$ grails install-plugin atomikos
5.1 JDBC
Convert the development H2 datasource to XA by adding anxaConfig
attribute:environments { development { dataSource { dbCreate = 'update' url = 'jdbc:h2:db/devDb;MVCC=TRUE' xaConfig = [ driverClassName: 'org.h2.jdbcx.JdbcDataSource', driverProperties: [ URL: 'jdbc:h2:db/devDb;MVCC=TRUE', user: 'sa', password: ''], minPoolSize: 1, maxPoolSize: 50 ] } } … }
dataSource_mysql { dbCreate = 'update' url = 'jdbc:mysql://localhost/xatest' driverClassName = 'com.mysql.jdbc.Driver' username = 'xatest' password = 'xatest' xaConfig = [ driverClassName: 'com.mysql.jdbc.jdbc2.optional.MysqlXADataSource', driverProperties: [ URL: 'jdbc:mysql://localhost/xatest', user: 'xatest', password: 'xatest', autoReconnect: true, autoReconnectForConnectionPools: true, autoReconnectForPools: true], minPoolSize: 1, maxPoolSize: 50 ] }
Domain classes
Create a domain class that will use the default H2 database:$ grails create-domain-class H2Thing
package atomikostestclass H2Thing { String name }
$ grails create-domain-class MySQLThing
package atomikostestclass MySQLThing { String name static mapping = { datasource 'mysql' } }
Dependencies
To use MySQL you'll need the JDBC driver, so add themavenLocal()
and mavenCentral()
repositories and the mysql-connector dependency to BuildConfig.groovy:repositories {
inherits true
grailsPlugins()
grailsHome()
grailsCentral() mavenLocal()
mavenCentral()
}dependencies {
runtime 'mysql:mysql-connector-java:5.1.16'
}
schema-export
You can verify that the domain classes are in separate datasources by running the schema-export script for each. Run the script for the default datasource:$ grails schema-export
target/ddl.sql
. Run it again for the other datasource:$ grails schema-export --datasource=mysql
target/ddl.sql
.
5.2 JDBC Testing
To test two-phase commit with the two databases, create a transactional service:$ grails create-service XaTest
package atomikostestclass XaTestService { void failAfterH2() { assert new MySQLThing(name: 'test MySQL').save(flush: true) assert new H2Thing(name: 'test H2').save(flush: true) throw new RuntimeException('forcing an auto rollback in failAfterH2') } void failAfterMySQL() { assert new H2Thing(name: 'test H2').save(flush: true) assert new MySQLThing(name: 'test MySQL').save(flush: true) throw new RuntimeException('forcing an auto rollback in failAfterH2') } void succeedBoth() { assert new H2Thing(name: 'test H2').save(flush: true) assert new MySQLThing(name: 'test MySQL').save(flush: true) } }
failAfterH2()
and failAfterMySQL()
. succeedBoth()
doesn't throw an exception, so it should successfully create both. You could create a controller to test these, but it's simpler to use the Grails console:$ grails console
failAfterH2()
:import atomikostest.H2Thing import atomikostest.MySQLThingint h2ThingCount = H2Thing.count() int mySQLCount = MySQLThing.count()def xaTestService = ctx.xaTestServicetry { xaTestService.failAfterH2() println "uh oh, no exception?" } catch (e) { println "caught exception: $e.message" }assert h2ThingCount == H2Thing.count() assert mySQLCount == MySQLThing.count()
failAfterMySQL()
:import atomikostest.H2Thing import atomikostest.MySQLThingint h2ThingCount = H2Thing.count() int mySQLCount = MySQLThing.count()def xaTestService = ctx.xaTestServicetry { xaTestService.failAfterMySQL() println "uh oh, no exception?" } catch (e) { println "caught exception: $e.message" }assert h2ThingCount == H2Thing.count() assert mySQLCount == MySQLThing.count()
import atomikostest.H2Thing import atomikostest.MySQLThingint h2ThingCount = H2Thing.count() int mySQLCount = MySQLThing.count()def xaTestService = ctx.xaTestServicexaTestService.succeedBoth()assert h2ThingCount + 1 == H2Thing.count() assert mySQLCount + 1 == MySQLThing.count()
5.3 JMS
Now let's add JMS to the mix. Install the jms plugin:$ grails install-plugin jms
grails-app/conf/BuildConfig.groovy
since the JMS plugin doesn't provide a JMS implementation:dependencies { runtime 'mysql:mysql-connector-java:5.1.16' compile('org.apache.activemq:activemq-core:5.5.0') { transitive = false } runtime('org.apache.activemq:kahadb:5.5.0') { transitive = false } }
grails-app/conf/spring/resources.groovy
:import org.apache.activemq.ActiveMQXAConnectionFactory import org.apache.activemq.command.ActiveMQQueuebeans = { jmsConnectionFactory(ActiveMQXAConnectionFactory) { brokerURL = 'vm://localhost' } jtaQueue(ActiveMQQueue) { physicalName = 'jtaQueue' } }
MessageTestService
Create a service that will act as our listener:$ grails create-service MessageTest
package atomikostestclass MessageTestService { static exposes = ['jms'] static destination = 'jtaQueue' // NOT THREAD SAFE - just for testing def mostRecentMessage void onMessage(Map message) { mostRecentMessage = message println "\n new message: $message\n" } void onMessage(String message) { mostRecentMessage = message println "\n new message: $message\n" } }
XaTestService
:void failAfterJms() { def thing = new MySQLThing(name: 'test JDBC').save(flush: true) assert thing def message = [thingId: thing.id, thingName: thing.name] jmsService.send 'jtaQueue', message throw new RuntimeException('forcing an auto rollback in failAfterJms') }void failAfterJdbc() { jmsService.send 'jtaQueue', "you won't get this" assert new MySQLThing(name: 'test JDBC').save(flush: true) throw new RuntimeException('forcing an auto rollback in failAfterJdbc') }void succeedJmsAndJdbc() { def thing = new MySQLThing(name: 'test JDBC').save(flush: true) assert thing def message = [thingId: thing.id, thingName: thing.name] jmsService.send 'jtaQueue', message }
Testing in the console
First tryfailAfterJms()
:import atomikostest.MySQLThingint mySQLCount = MySQLThing.count()def xaTestService = ctx.xaTestService def messageTestService = ctx.messageTestService messageTestService.mostRecentMessage = nulltry { xaTestService.failAfterJms() println "uh oh, no exception?" } catch (e) { println "caught exception: $e.message" }assert mySQLCount == MySQLThing.count() sleep 200 assert messageTestService.mostRecentMessage == null
failAfterJdbc()
:import atomikostest.MySQLThingint mySQLCount = MySQLThing.count()def xaTestService = ctx.xaTestService def messageTestService = ctx.messageTestService messageTestService.mostRecentMessage = nulltry { xaTestService.failAfterJdbc() println "uh oh, no exception?" } catch (e) { println "caught exception: $e.message" }assert mySQLCount == MySQLThing.count() sleep 200 assert messageTestService.mostRecentMessage == null
import atomikostest.MySQLThingint mySQLCount = MySQLThing.count()def xaTestService = ctx.xaTestService def messageTestService = ctx.messageTestService messageTestService.mostRecentMessage = nullxaTestService.succeedJmsAndJdbc()assert mySQLCount + 1 == MySQLThing.count() sleep 200 assert messageTestService.mostRecentMessage instanceof Map
mostRecentMessage
assertions can occasionally fail since JMS messages are sent and received asynchronously. The sleep()
call should be sufficient but if things are running slowly you can get a false failure.