Axon Spring Boot App with 2 data sources - One for domain events and one for Saga

Hi ,
I am new to Axon. We are trying to have a separate db for. saga store and event store using Postgres with. Axon 4.1.2

is this possible. Please advise…

Hi Annbuvel,

Yes, that is indeed doable.
The framework provides you the option to define distinct EntityManagers and TransactionManagers to any component which stores object.

You’ll likely have to set up a distinct EntityManager for the Saga Store and Event Store.
From here, you will have to put those in an EntityManagerProvider, which you in turn can set on the used SagaStore implementation and EventStorageEngine implementation.

Hope this helps!

Cheers,
Steven

Hello Steven,
Thank you for your response.
My config looks as below.


@Configuration
 class EventStoreConfig {
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    private DataSource dataSource() {
        return dataSourceProperties().initializeDataSourceBuilder().build();
    }

    @Bean("eventsEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean eventsEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dataSource())
                .persistenceUnit("events")
                .packages(DomainEventEntry.class,
                        SnapshotEventEntry.class)
                .build();

    }

    @Bean
    public EmbeddedEventStore eventStore(EventStorageEngine storageEngine, AxonConfiguration configuration) {
        return EmbeddedEventStore.builder()
                .storageEngine(storageEngine)
                .messageMonitor(configuration.messageMonitor(EventStore.class, "eventStore"))
                .build();
    }

    // The JpaEventStorageEngine stores events in a JPA-compatible data source
    @Bean
    public EventStorageEngine storageEngine(Serializer defaultSerializer,
                                            PersistenceExceptionResolver persistenceExceptionResolver,
                                            @Qualifier("eventSerializer") Serializer eventSerializer,
                                            AxonConfiguration configuration,
                                            @Qualifier("eventsEntityManagerFactory")LocalContainerEntityManagerFactoryBean eventsEntityManagerFactory,
                                            TransactionManager transactionManager) {
        ContainerManagedEntityManagerProvider containerManagedEntityManagerProvider =  new ContainerManagedEntityManagerProvider();
        containerManagedEntityManagerProvider.setEntityManager(eventsEntityManagerFactory.getObject().createEntityManager());
        return JpaEventStorageEngine.builder()
                .snapshotSerializer(defaultSerializer)
                .upcasterChain(configuration.upcasterChain())
                .persistenceExceptionResolver(persistenceExceptionResolver)
                .eventSerializer(eventSerializer)
                .entityManagerProvider(containerManagedEntityManagerProvider)
                .transactionManager(transactionManager)
                .build();
    }

    @Bean
    @Qualifier("eventsTx")
    PlatformTransactionManager  platformTransactionManager(@Qualifier("eventsEntityManagerFactory")LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean){
        return new JpaTransactionManager(localContainerEntityManagerFactoryBean.getObject());
    }
}

@Configuration
public class SagaStoreConfig {

    @Bean
    @ConfigurationProperties("sagastore.datasource")
    public DataSourceProperties dataSourceEsProperties() {
        return new DataSourceProperties();
    }

    private DataSource esDataSource() {
        return dataSourceEsProperties().initializeDataSourceBuilder().build();
    }

    @Bean("sagaEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean sagaEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(esDataSource())
                .persistenceUnit("saga")
                .packages(SagaEntry.class,
                        AssociationValueEntry.class , TokenEntry.class)
                .build();
    }

    @Bean
    @Qualifier("sagaTx")
    PlatformTransactionManager platformTransactionManager(@Qualifier("sagaEntityManagerFactory") LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean){
        return new JpaTransactionManager(localContainerEntityManagerFactoryBean.getObject());
    }

    @Bean("tokenStore")
    public TokenStore jpaTokenStore(@Qualifier("eventSerializer") Serializer eventSerializer , @Qualifier("sagaEntityManagerFactory")LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean)  {
        return  JpaTokenStore.builder()
                .entityManagerProvider(new SimpleEntityManagerProvider(localContainerEntityManagerFactoryBean.getObject().createEntityManager()))
                .serializer(eventSerializer)
                .build();
    }

    @Bean("sagaStore")
    public JpaSagaStore jpaSagaStore(@Qualifier("eventSerializer") Serializer eventSerializer, @Qualifier("sagaEntityManagerFactory")LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean){
        return JpaSagaStore.builder()
                .entityManagerProvider(new SimpleEntityManagerProvider(
                        localContainerEntityManagerFactoryBean.getObject().createEntityManager()))
                .serializer(eventSerializer)
                .build();
    }

}

My application.properties

server.port=8080
spring.cloud.discovery.enabled=true
spring.main.allow-bean-definition-overriding=true
spring.cloud.consul.host=localhost
spring.cloud.consul.port = 8500
spring.cloud.consul.discovery.port=${server.port}
axon.distributed.enabled=true
axon.distributed.spring-cloud.fallback-url=/message-routing-information
axon.distributed.spring-cloud.fallback-to-http-get=true
spring.jpa.hibernate.ddl-auto=create

sagastore.datasource.url=jdbc:postgresql://localhost:5432/eventstore
sagastore.datasource.username=admin
sagastore.datasource.password=admin123 
sagastore.datasource.driver-class-name=org.postgresql.Driver

spring.datasource.url=jdbc:postgresql://localhost:5432/orders
spring.datasource.username=admin
spring.datasource.password=admin123
spring.datasource.driver-class-name=org.postgresql.Driver

axon.serializer.general=jackson
axon.serializer.events=jackson
axon.serializer.messages=jackson
axon.amqp.transaction-mode=publisher-ack
spring.jpa.properties.hibernate.jdbc.lob.on_contextual_creation=true
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.EJB3NamingStrategy
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

My Pom file

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.1.3.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.progressivecoder.ordermanagement</groupId>
   <artifactId>order-service</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>order-service</name>
   <description>Order Service for E-Commerce Store</description>

   <properties>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-consul-discovery</artifactId>
         <version>2.1.2.RELEASE</version>
      </dependency>
      <!-- Axon -->
      <dependency>
         <groupId>org.axonframework</groupId>
         <artifactId>axon-spring-boot-starter</artifactId>
         <version>4.1.2</version>
         <exclusions>
            <exclusion>
               <groupId>org.axonframework</groupId>
               <artifactId>axon-server-connector</artifactId>
            </exclusion>
         </exclusions>
      </dependency>
      <dependency>
         <groupId>org.axonframework.extensions.springcloud</groupId>
         <artifactId>axon-springcloud-spring-boot-autoconfigure</artifactId>
         <version>4.1</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-amqp</artifactId>
         <version>2.1.2.RELEASE</version>
      </dependency>
      <dependency>
         <groupId>org.axonframework.extensions.amqp</groupId>
         <artifactId>axon-amqp</artifactId>
         <version>4.1</version>
      </dependency>

      <!--<dependency>
         <groupId>com.h2database</groupId>
         <artifactId>h2</artifactId>
         <scope>runtime</scope>
      </dependency>-->

      <!-- Swagger -->
      <dependency>
         <groupId>io.springfox</groupId>
         <artifactId>springfox-swagger2</artifactId>
         <version>2.9.2</version>
      </dependency>

      <dependency>
         <groupId>io.springfox</groupId>
         <artifactId>springfox-swagger-ui</artifactId>
         <version>2.9.2</version>
      </dependency>

      <dependency>
         <groupId>javax.inject</groupId>
         <artifactId>javax.inject</artifactId>
         <version>1</version>
      </dependency>

      <dependency>
         <groupId>com.progressivecoder.saga-pattern</groupId>
         <artifactId>core-apis</artifactId>
         <version>${project.version}</version>
      </dependency>

      <dependency>
         <groupId>org.postgresql</groupId>
         <artifactId>postgresql</artifactId>
         <version>42.2.5</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-autoconfigure</artifactId>
         <version>2.1.6.RELEASE</version>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

</project>

Getting the below error.

Hibernate: select tokenentry0_.segment as col_0_0_ from TokenEntry tokenentry0_ where tokenentry0_.processorName=? order by tokenentry0_.segment ASC
2019-08-13 22:08:54.087 DEBUG 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2019-08-13 22:08:54.087 DEBUG 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(1179762965)] for JPA transaction
2019-08-13 22:08:54.088 DEBUG 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1dc2a955]
2019-08-13 22:08:54.089 TRACE 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Triggering beforeCompletion synchronization
2019-08-13 22:08:54.089 DEBUG 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback
2019-08-13 22:08:54.089 DEBUG 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Rolling back JPA transaction on EntityManager [SessionImpl(1179762965)]
2019-08-13 22:08:54.090 TRACE 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Triggering afterCompletion synchronization
2019-08-13 22:08:54.090 DEBUG 46815 — [agaProcessor]-0] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1179762965)] after transaction
2019-08-13 22:08:54.091 INFO 46815 — [agaProcessor]-0] o.a.e.TrackingEventProcessor : An error occurred while attempting to claim a token for segment: 0. Will retry later…

javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.SessionImpl.checkTransactionNeeded(SessionImpl.java:3552) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3477) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3457) ~[hibernate-core-5.3.7.Final.jar:5.3.7.Final]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:350) ~[spring-orm-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at com.sun.proxy.$Proxy185.find(Unknown Source) ~[na:na]
at org.axonframework.eventhandling.tokenstore.jpa.JpaTokenStore.loadToken(JpaTokenStore.java:216) ~[axon-messaging-4.1.2.jar:4.1.2]
at org.axonframework.eventhandling.tokenstore.jpa.JpaTokenStore.fetchToken(JpaTokenStore.java:167) ~[axon-messaging-4.1.2.jar:4.1.2]
at org.axonframework.eventhandling.TrackingEventProcessor$WorkerLauncher.lambda$run$1(TrackingEventProcessor.java:1054) ~[axon-messaging-4.1.2.jar:4.1.2]
at org.axonframework.common.transaction.TransactionManager.executeInTransaction(TransactionManager.java:47) ~[axon-messaging-4.1.2.jar:4.1.2]
at org.axonframework.eventhandling.TrackingEventProcessor$WorkerLauncher.run(TrackingEventProcessor.java:1053) ~[axon-messaging-4.1.2.jar:4.1.2]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

Please guide .

Kind. Regards
Annbuvel

Hi Annbuvel,

In general, your config looks good, but your missing one thing here.
The Sagas are backed by an Event Processor, a TrackingEventProcessor.
A TrackingEventProcessor “keeps track of” which events it has handled, by storing this info in a Tracking Token.

It is thus advised to keep the Tracking Tokens which are used for your Sagas instances in the same database as those instances.
This also means, that you should define a specific TokenStore for your Sagas, where said TokenStore uses the same TransactionManager.

Resolving the above said set up should resolve the issue you’re encountering.

Aside from your Sagas, you are likely also storing view models which get updated through Event Handlers which are backed by TrackingEventProcessors.
I would suggest to set up a dedicated TokenStore for your view models apart from the TokenStore for your Saga instances.
Thus, you would also have a distinct database for your view models I’d argue.

Hope this helps you further Annbuvel!
Ow, and one note, if you would to use Axon Server, you would no longer have to define anything in regards to the Event Storage.
That would make your entire configuration, which is not bringing any business value to your product essentially, a lot simpler.

Cheers,
Steven