Skip to content

Commit

Permalink
add multi pooled datasources with 2PC example
Browse files Browse the repository at this point in the history
  • Loading branch information
rinaldodev authored and Croway committed Sep 2, 2024
1 parent f1fcd42 commit cc1648b
Show file tree
Hide file tree
Showing 12 changed files with 657 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Number of Examples: 59 (0 deprecated)

| link:arangodb/README.adoc[Arangodb] (arangodb) | Database | An example showing the Camel ArangoDb component with Spring Boot

| link:multi-datasource-2pc/readme.adoc[Multi Datasource 2pc] (multi-datasource-2pc) | Database | An example showing how to work with Camel and Spring Boot using multiple pooled datasources with two-phase commit

| link:dynamic-router-eip/README.adoc[Dynamic Router Eip] (dynamic-router-eip) | EIP | Dynamic Router EIP component examples

| link:load-balancer-eip/README.adoc[Load Balancer Eip] (load-balancer-eip) | EIP | An example showing Load Balancer EIP with Camel and Spring Boot
Expand Down
2 changes: 2 additions & 0 deletions multi-datasource-2pc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
transaction-logs
test-transactions-logs
152 changes: 152 additions & 0 deletions multi-datasource-2pc/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.apache.camel.springboot.example</groupId>
<artifactId>examples</artifactId>
<version>4.8.0-SNAPSHOT</version>
</parent>

<artifactId>camel-example-spring-boot-muti-datasources-2pc</artifactId>
<name>Camel SB Examples :: Multiple pooled datasources with two-phase commit</name>
<description>An example showing how to work with Camel and Spring Boot using multiple pooled datasources with two-phase commit</description>

<properties>
<category>Database</category>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencyManagement>
<dependencies>
<!-- Camel BOM -->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-bom</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>

<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- Camel -->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-stream-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-jdbc-starter</artifactId>
</dependency>

<!-- Datasources -->
<dependency>
<groupId>dev.snowdrop</groupId>
<artifactId>narayana-spring-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.agroal</groupId>
<artifactId>agroal-spring-boot-starter</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>

<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-spring-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
75 changes: 75 additions & 0 deletions multi-datasource-2pc/readme.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
== Camel Spring Boot with multiple pooled datasources and 2PC

This example shows how to set up multiple pooled datasources with Camel Spring Boot and have them working with two-phase commit.

One of the databases has a non-unique `name` column, while the other has a unique one. When trying to insert repeated names in both databases at the same time, the second `prepare` should fail making the transaction rollback for both databases.

=== Classes and resources

* `MyCamelRouter`: where the camel routes are defined
* `DataSourcesConfig`: where the datasources' beans are created
* `MyCamelApplication`: the Spring Boot main class
* `application.properties`: configuration for the datasources and others
* `schema-ds1.sql` and `schema-ds2.sql`: initializers for the databases

=== How to run

. Run the database containers. Notice the commands have extra arguments to enable initialization and two-phase commit.
+
[source,console]
----
podman run --rm --name db1 -e POSTGRES_PASSWORD=password -p 5432:5432 -v ./src/main/resources/schema-ds1.sql:/docker-entrypoint-initdb.d/init.sql:Z docker.io/library/postgres:latest -c max_prepared_transactions=10
----
+
[source,console]
----
podman run --rm --name db2 -e POSTGRES_PASSWORD=password -p 5433:5432 -v ./src/main/resources/schema-ds2.sql:/docker-entrypoint-initdb.d/init.sql:Z docker.io/library/postgres:latest -c max_prepared_transactions=10
----

. Then run the example using
[source,console]
mvn spring-boot:run

. Every few seconds you can see in the logs that a new insert is created, but when it tries to insert a non-unique name then the transaction rollbacks and the names are not inserted in any of the databases.
+
[source,log]
----
[read #8 - Delay] info : Exchange[ExchangePattern: InOnly, BodyType: String, Body: There are 4 names in the ds2 database.]
[timer://runOnce] info : Exchange[Id: 0B8F2317CCE5D8D-000000000000000D, RouteGroup: null, RouteId: route2, ExchangePattern: InOnly, Properties: {CamelAggregationStrategy={split1=UseOriginalAggregationStrategy}, CamelCorrelationId=0B8F2317CCE5D8D-0000000000000000, CamelSplitComplete=false, CamelSplitIndex=4, CamelSplitSize=6, CamelStreamCacheUnitOfWork=DefaultUnitOfWork, CamelToEndpoint=log://info?showAll=true}, Headers: {}, BodyType: String, Body: Maria]
[timer://runOnce] route2 : insert into the first database (non-unique)
[timer://runOnce] route2 : insert into the second database (unique)
[timer://runOnce] com.arjuna.ats.jta : ARJUNA016041: prepare on < formatId=131077, gtrid_length=29, bqual_length=36, tx_uid=0:ffff0a057e34:aedb:66cc8122:39, node_name=1, branch_uid=0:ffff0a057e34:aedb:66cc8122:3f, subordinatenodename=null, eis_name=java:comp/env/jdbc/ds2 > (io.agroal.narayana.BaseXAResource@65fecc5) failed with exception XAException.XA_RBINTEGRITY
...
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "names_name_key"
...
[timer://runOnce] com.arjuna.ats.arjuna : ARJUNA012073: BasicAction.End() - prepare phase of action-id 0:ffff0a057e34:aedb:66cc8122:39 failed.
[timer://runOnce] com.arjuna.ats.arjuna : ARJUNA012075: Action Aborting
----

=== Cleanup

. Clear Narayana transaction logs:
[source,console]
rm -rf transaction-logs

. Stop the running containers

=== Running the tests

. Run the tests using
[source,console]
mvn clean test

=== Extra details

The `name` column in the second database is set up with `UNIQUE DEFERRABLE INITIALLY DEFERRED`. This configuration delays the constraint check to only be evaluated in the commit phase. Without this argument there would be an exception thrown immediately during the `INSERT` command, causing the transaction to be rolled-back immediately. This is only set up this way to make the example clearer.

=== Help and contributions

If you hit any problem using Camel or have some feedback, then please
https://camel.apache.org/support.html[let us know].

We also love contributors, so
https://camel.apache.org/contributing.html[get involved] :-)

The Camel riders!
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package sample.camel;

import io.agroal.springframework.boot.AgroalDataSource;
import io.agroal.springframework.boot.AgroalDataSourceAutoConfiguration;
import io.agroal.springframework.boot.jndi.AgroalDataSourceJndiBinder;
import org.jboss.tm.XAResourceRecoveryRegistry;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration;
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.sql.DataSource;

@Configuration
public class DataSourcesConfig {

// -- first data source config

@Bean("ds1properties")
@ConfigurationProperties("app.datasource.ds1")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}

@Bean("ds1")
@ConfigurationProperties("app.datasource.ds1.agroal")
public AgroalDataSource firstDataSource(
@Qualifier("ds1properties") DataSourceProperties properties,
JtaTransactionManager jtaPlatform,
XAResourceRecoveryRegistry xaResourceRecoveryRegistry,
ObjectProvider<AgroalDataSourceJndiBinder> jndiBinder) {

return new AgroalDataSourceAutoConfiguration(jtaPlatform, xaResourceRecoveryRegistry)
.dataSource(properties, false, false, jndiBinder);
}

@Bean("ds1jdbc")
public JdbcTemplate firstDataSourceJdbcTemplate(@Qualifier("ds1") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

// -- second data source config

@Bean("ds2properties")
@ConfigurationProperties("app.datasource.ds2")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}

@Bean("ds2init")
@ConfigurationProperties("app.datasource.ds2.sql.init")
public SqlInitializationProperties secondDataSourceInit() {
return new SqlInitializationProperties();
}

@Bean("ds2")
@ConfigurationProperties("app.datasource.ds2.agroal")
public AgroalDataSource secondDataSource(
@Qualifier("ds2properties") DataSourceProperties properties,
JtaTransactionManager jtaPlatform,
XAResourceRecoveryRegistry xaResourceRecoveryRegistry,
ObjectProvider<AgroalDataSourceJndiBinder> jndiBinder) {

return new AgroalDataSourceAutoConfiguration(jtaPlatform, xaResourceRecoveryRegistry)
.dataSource(properties, false, false, jndiBinder);
}

@Bean("ds2jdbc")
public JdbcTemplate secondDataSourceJdbcTemplate(@Qualifier("ds2") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.camel;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

//CHECKSTYLE:OFF
/**
* A sample Spring Boot application that starts the Camel routes.
*/
@SpringBootApplication
public class MyCamelApplication {

/**
* A main method to start this application.
*/
public static void main(String[] args) {
SpringApplication.run(MyCamelApplication.class, args);
}

}
//CHECKSTYLE:ON
Loading

0 comments on commit cc1648b

Please sign in to comment.