├── src ├── main │ ├── resources │ │ ├── data.sql │ │ ├── application.properties │ │ ├── application-postgres.properties │ │ └── schema.sql │ └── java │ │ └── sample │ │ └── atomikos │ │ ├── AccountRepository.java │ │ ├── Messages.java │ │ ├── Account.java │ │ ├── AccountService.java │ │ └── SampleAtomikosApplication.java └── test │ └── java │ └── sample │ └── atomikos │ └── SampleAtomikosApplicationTests.java ├── .gitignore ├── README.md └── pom.xml /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO LOG_CLOUD_STARTUP_CONFIG (OWNING_RECOVERY_DOMAIN_NAME, PROPERTY_NAME, PROPERTY_VALUE) values ('logcloud', 'com.atomikos.icatch.max_timeout', '300000'); 2 | INSERT INTO LEADER (OWNING_RECOVERY_DOMAIN_NAME) select ('logcloud') WHERE NOT EXISTS (select 1 from LEADER); -- EXPIRY defaults to 0 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | /.project 14 | /transaction-logs/ 15 | /.settings/ 16 | /.classpath 17 | /logcloud.mv.db 18 | /logcloud.trace.db 19 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.artemis.embedded.queues=accounts 2 | spring.jpa.open-in-view=true 3 | spring.datasource.url=jdbc:h2:./logcloud 4 | 5 | spring.jta.atomikos.datasource.unique-resource-name=APP 6 | # allow DDL on starup 7 | spring.jta.atomikos.datasource.local-transaction-mode=true 8 | 9 | #all instances that connect to the same logCloud must use the same name (the one defined in data.sql) 10 | spring.jta.atomikos.properties.transaction-manager-unique-name=logcloud 11 | spring.datasource.continue-on-error=true 12 | 13 | atomikos.properties.transaction-manager-unique-name=logcloud 14 | -------------------------------------------------------------------------------- /src/main/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.artemis.embedded.queues=accounts 2 | spring.jpa.open-in-view=true 3 | 4 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 5 | spring.datasource.url=jdbc:postgresql://localhost:5432/atomikos 6 | spring.datasource.username=atomikos 7 | spring.datasource.password=atomikos 8 | spring.jpa.hibernate.ddl-auto=create-drop 9 | 10 | spring.jta.atomikos.datasource.supports-tm-join=false 11 | spring.jta.atomikos.datasource.unique-resource-name=APP 12 | # allow DDL on startup 13 | spring.jta.atomikos.datasource.local-transaction-mode=true 14 | 15 | #all instances that connect to the same logCloud must use the same name (the one defined in data.sql) 16 | spring.jta.atomikos.properties.transaction-manager-unique-name=logcloud 17 | spring.datasource.continue-on-error=true 18 | -------------------------------------------------------------------------------- /src/main/java/sample/atomikos/AccountRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample.atomikos; 18 | 19 | import org.springframework.data.jpa.repository.JpaRepository; 20 | 21 | public interface AccountRepository extends JpaRepository { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/sample/atomikos/Messages.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample.atomikos; 18 | 19 | import org.springframework.jms.annotation.JmsListener; 20 | import org.springframework.stereotype.Component; 21 | 22 | @Component 23 | public class Messages { 24 | 25 | @JmsListener(destination = "accounts") 26 | public void onMessage(String content) { 27 | System.out.println("----> " + content); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE COORDINATOR ( 2 | OWNING_RECOVERY_DOMAIN_NAME VARCHAR(45) NOT NULL, 3 | ID VARCHAR(36) NOT NULL, 4 | STATE VARCHAR(36) NOT NULL, 5 | EXPIRES BIGINT NOT NULL, 6 | SUPERIOR_ID VARCHAR(64), 7 | RECOVERY_DOMAIN_NAME VARCHAR(45) NOT NULL); 8 | 9 | CREATE TABLE LOG_CLOUD_STARTUP_CONFIG ( 10 | OWNING_RECOVERY_DOMAIN_NAME VARCHAR(45) NOT NULL, 11 | PROPERTY_NAME VARCHAR(2000) NOT NULL, 12 | PROPERTY_VALUE VARCHAR(2000) NOT NULL, 13 | CONSTRAINT LOG_CLOUD_STARTUP_CONFIG_PK PRIMARY KEY (OWNING_RECOVERY_DOMAIN_NAME, PROPERTY_NAME)); 14 | 15 | CREATE TABLE LEADER ( 16 | OWNING_RECOVERY_DOMAIN_NAME VARCHAR(45) NOT NULL, 17 | ACTIVE_JVM_ID VARCHAR(2000) NOT NULL default 'NONE', 18 | EXPIRY BIGINT NOT NULL default 0, 19 | CONSTRAINT ACTIVE_JVM_ID_PK PRIMARY KEY (OWNING_RECOVERY_DOMAIN_NAME, ACTIVE_JVM_ID)); 20 | 21 | CREATE TABLE REMOTE_PARTICIPANT ( 22 | OWNING_RECOVERY_DOMAIN_NAME VARCHAR(45) NOT NULL, 23 | COORDINATOR_ID VARCHAR(36) NOT NULL, 24 | REMOTE_PARTICIPANT_URI VARCHAR(64) NOT NULL); 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/sample/atomikos/Account.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample.atomikos; 18 | 19 | import javax.persistence.Entity; 20 | import javax.persistence.GeneratedValue; 21 | import javax.persistence.Id; 22 | 23 | @Entity 24 | public class Account { 25 | 26 | @Id 27 | @GeneratedValue 28 | private Long id; 29 | 30 | private String username; 31 | 32 | Account() { 33 | } 34 | 35 | public Account(String username) { 36 | this.username = username; 37 | } 38 | 39 | public String getUsername() { 40 | return this.username; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/sample/atomikos/AccountService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample.atomikos; 18 | 19 | import javax.transaction.Transactional; 20 | 21 | import org.springframework.jms.core.JmsTemplate; 22 | import org.springframework.stereotype.Service; 23 | 24 | @Service 25 | @Transactional 26 | public class AccountService { 27 | 28 | private final JmsTemplate jmsTemplate; 29 | 30 | private final AccountRepository accountRepository; 31 | 32 | public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) { 33 | this.jmsTemplate = jmsTemplate; 34 | this.accountRepository = accountRepository; 35 | } 36 | 37 | public void createAccountAndNotify(String username) { 38 | this.jmsTemplate.convertAndSend("accounts", username); 39 | this.accountRepository.save(new Account(username)); 40 | if ("error".equals(username)) { 41 | throw new RuntimeException("Simulated error"); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/sample/atomikos/SampleAtomikosApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample.atomikos; 18 | 19 | import java.io.Closeable; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | import org.springframework.context.ApplicationContext; 24 | import org.springframework.context.annotation.Import; 25 | 26 | import com.atomikos.springboot.autoconfigure.jdbc.logcloud.LogCloudDataSourceConfiguration; 27 | 28 | @SpringBootApplication 29 | @Import(LogCloudDataSourceConfiguration.class) 30 | public class SampleAtomikosApplication { 31 | 32 | public static void main(String[] args) throws Exception { 33 | ApplicationContext context = SpringApplication.run(SampleAtomikosApplication.class, args); 34 | AccountService service = context.getBean(AccountService.class); 35 | AccountRepository repository = context.getBean(AccountRepository.class); 36 | service.createAccountAndNotify("josh"); 37 | System.out.println("Count is " + repository.count()); 38 | try { 39 | service.createAccountAndNotify("error"); 40 | } 41 | catch (Exception ex) { 42 | System.out.println(ex.getMessage()); 43 | } 44 | System.out.println("Count is " + repository.count()); 45 | Thread.sleep(100); 46 | ((Closeable) context).close(); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-boot-sample-jta-atomikos-logcloud 2 | Sample project showing how easy it is to do XA with clustered, cloud-native transaction logging and recovery. You can run as many instances of this application as you like: scale up and down dynamically or kill any node with your chaos monkey and XA recovery simply works (as long as at least one node is up). This gives you self-healing XA transactions in the cloud... 3 | 4 | ## Highlights 5 | 6 | This example is based on the original Spring Boot / Atomikos example, but changed as follows: 7 | 8 | * Changed the H2 database from in-memory to persistent storage. 9 | * Updated the Atomikos version to 5.0.x with the LogCloud capabilities. 10 | * Added the transactions-logcloud dependency in the POM, so the LogCloud is activated automatically and overrides file-based logging. 11 | * Modified application.properties to configure the LogCloud to use the application's datasource for logging and recovery (so no 2 datasources are needed). 12 | * Added schema.sql and data.sql to initialize the LogCloud tables. 13 | 14 | ## How To Run 15 | 16 | * You need an ExtremeTransactions 5.0.x free trial to run this demo. 17 | * Run the SampleAtomikosApplication (a Spring Boot app). 18 | * Check the DBMS log by running **org.h2.tools.Console** (in the h2 jar) and connect to the database in your workspace (e.g., jdbc:h2:/path/to/spring-boot-sample-jta-atomikos-logcloud/logcloud). You can do this by importing the maven project in your IDE (which will set the classpath) and then launching **org.h2.tools.Console** as a main class. Make sure to exit before you run SampleAtomikosApplication again or you will get weird hibernate errors. 19 | 20 | ## Note 21 | 22 | You may see a pending COMMITTING record in the COORDINATOR table. This is normal, since recovery only cleans up at certain times. After a while, the record will be gone (at least under normal operating conditions and assuming that at least one node is still running). 23 | -------------------------------------------------------------------------------- /src/test/java/sample/atomikos/SampleAtomikosApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample.atomikos; 18 | 19 | import org.assertj.core.api.Condition; 20 | import org.junit.Rule; 21 | import org.junit.Test; 22 | 23 | import org.springframework.boot.test.system.OutputCaptureRule; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | /** 28 | * Basic integration tests for demo application. 29 | * 30 | * @author Phillip Webb 31 | */ 32 | public class SampleAtomikosApplicationTests { 33 | 34 | @Rule 35 | public OutputCaptureRule outputCapture = new OutputCaptureRule(); 36 | 37 | @Test 38 | public void testTransactionRollback() throws Exception { 39 | SampleAtomikosApplication.main(new String[] {}); 40 | String output = this.outputCapture.toString(); 41 | assertThat(output).has(substring(1, "---->")); 42 | assertThat(output).has(substring(1, "----> josh")); 43 | assertThat(output).has(substring(2, "Count is 1")); 44 | assertThat(output).has(substring(1, "Simulated error")); 45 | } 46 | 47 | private Condition substring(int times, String substring) { 48 | return new Condition("containing '" + substring + "' " + times + " times") { 49 | 50 | @Override 51 | public boolean matches(String value) { 52 | int i = 0; 53 | while (value.contains(substring)) { 54 | int beginIndex = value.indexOf(substring) + substring.length(); 55 | value = value.substring(beginIndex); 56 | i++; 57 | } 58 | return i == times; 59 | } 60 | 61 | }; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.4.RELEASE 10 | 11 | 12 | spring-boot-sample-jta-atomikos 13 | Spring Boot Atomikos JTA Sample 14 | Spring Boot Atomikos JTA Sample 15 | 16 | 5.0.108.EVAL 17 | 18 | 19 | 20 | 21 | org.springframework 22 | spring-jms 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-jpa 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-jta-atomikos 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-artemis 35 | 36 | 37 | org.apache.activemq 38 | artemis-jms-server 39 | 40 | 41 | geronimo-jms_2.0_spec 42 | org.apache.geronimo.specs 43 | 44 | 45 | 46 | 47 | com.h2database 48 | h2 49 | runtime 50 | 51 | 52 | org.postgresql 53 | postgresql 54 | runtime 55 | 56 | 57 | com.atomikos 58 | transactions-logcloud 59 | ${atomikos.version} 60 | 61 | 62 | 65 | 66 | 67 | 68 | com.atomikos 69 | transactions-spring-boot-starter 70 | ${atomikos.version} 71 | 72 | 73 | com.atomikos 74 | transactions-spring-boot 75 | ${atomikos.version} 76 | 77 | 78 | com.atomikos 79 | transactions-spring-boot-logcloud 80 | ${atomikos.version} 81 | 82 | 83 | javax.ws.rs 84 | javax.ws.rs-api 85 | 2.0.1 86 | provided 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-autoconfigure 92 | true 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-configuration-processor 97 | true 98 | 99 | 100 | 101 | 102 | org.springframework.boot 103 | spring-boot-starter-test 104 | test 105 | 106 | 107 | 108 | 109 | 110 | org.springframework.boot 111 | spring-boot-maven-plugin 112 | 113 | 114 | 115 | 116 | 117 | java9+ 118 | 119 | [9,) 120 | 121 | 122 | 123 | jakarta.xml.bind 124 | jakarta.xml.bind-api 125 | 2.3.2 126 | 127 | 128 | 129 | 130 | 131 | --------------------------------------------------------------------------------