diff --git a/.github/Atomikos_Logo_Background.png b/.github/Atomikos_Logo_Background.png new file mode 100644 index 000000000..4f396cf59 Binary files /dev/null and b/.github/Atomikos_Logo_Background.png differ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..6f48ba968 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve our next community release! + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + + 1. What you were doing (or trying to do) + 1. What happened + 1. What you expected to happen instead + 1. What TransactionsEssentials version you are using + +**Additional context** +Add any other context about the problem here. + +_I understand that this project is not intended for support - because bug reports may or may not be considered for inclusion some day (in a future release). If this is issue is important to me then I can go to https://www.atomikos.com/Main/SupportOverview and arrange a paid support subscription._ diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..066b2d920 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/atomikos_horizontal_logo.jpg b/.github/atomikos_horizontal_logo.jpg new file mode 100644 index 000000000..6952ce799 Binary files /dev/null and b/.github/atomikos_horizontal_logo.jpg differ diff --git a/.github/inconsistency.jpg b/.github/inconsistency.jpg new file mode 100644 index 000000000..9f895b294 Binary files /dev/null and b/.github/inconsistency.jpg differ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..2ce979175 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,3 @@ +_Write your description here. BTW we're really honoured to receive your help, but please understand that some pull requests may be rejected for several reasons - in particular because it might conflict with the commercial offerings of Atomikos. Without making money on something, we would not have been able to give TransactionsEssentials for free in the first place! Thanks for understanding..._ + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..759bbe274 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +private/ +specs/ +target/ +.hg/ +.settings/ +.classpath +.hgignore +.hgtags +.project diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..9d98a14b1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: java + +jdk: + - openjdk6 + +install: mvn install -DskipTests=true -Popensource + +script: mvn clean test -Popensource + + +notifications: + email: + - guy@atomikos.com + - pascal.leclercq@gmail.com \ No newline at end of file diff --git a/CODE-OF-CONDUCT.MD b/CODE-OF-CONDUCT.MD new file mode 100644 index 000000000..2db907eda --- /dev/null +++ b/CODE-OF-CONDUCT.MD @@ -0,0 +1,12 @@ +# Our Code of Conduct + +We try to manage this community with 2 simple rules: + + 1. Focus on improving TransactionsEssentials and its user-friendliness. + 1. Intend no harm towards fellow members. + +Let these rules guide everything you say and do here. + +## Reporting Violations + +If you suspect any violations then let us know by emailing to webmaster at atomikos dot com. \ No newline at end of file diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD new file mode 100644 index 000000000..f2fb03314 --- /dev/null +++ b/CONTRIBUTING.MD @@ -0,0 +1,62 @@ +# Contributing to Atomikos TransactionsEssentials + +Thank you very much for considering a contribution to our project! + +Please follow the guidelines on this page to make things go smooth... + +## Most Wanted + +We're happy with all the help we can get (and you want to give), but we think that the following would be the most useful contributions: + + - Vote on existing feature requests you need most - so we know which ones are priority for the community. + - Give feedback on planned features before we build them - so we can avoid implementing useless things. + - Report on missing features so we can make our product better for you. Better still: send us a PR with the implementation. + - Did you already fork and make changes for yourself? Why not send us a PR to check if we can use this, too. + - Report bugs - so we can maximize stability (or even better if you can: send us a PR with a fix). + - Create some visibility by starring the project, blogging about it, ... + +## How to Request a Feature + +You can request a new feature by opening an issue and choosing the feature request template. + +## How to Report a Bug + +You can report bugs by opening an issue and choosing the bug report template. + +## How to Contribute Code (Features or Fixes) + + 1. If you need inspiration: check our open issues with contribution labels (i.e., ["good first issue"](https://github.com/atomikos/transactions-essentials/labels/good%20first%20issue) or ["help wanted"](https://github.com/atomikos/transactions-essentials/labels/help%20wanted))... + 1. Implement a minimal set of changes in your fork of our sources. + 1. Create a pull request for us to merge. + 1. As part of the pull request, you will be asked to accept our contributor license terms. + + +## Other Ways to Contribute + + 1. Vote on issues you find useful - this will help us prioritize work for the future releases! + 1. Give feedback on planned features before we build them - so we can do them the right way. + 1. Help us grow this community by [recommending us](https://www.atomikos.com/Main/RecommendUs) or starring this repository. + 1. Feel free to share bug reports, usage problems or clarity issues... + 1. Help us learn: [share your story](https://www.surveymonkey.com/r/XMS5987) so we can use your insight to improve the project. + 1. Share configuration examples (or other useful documentation) on our [documentation wiki](https://www.atomikos.com/Documentation). + 1. Write a blog post about how you use this project. + 1. Request a new feature if you find that useful functionality is missing. + 1. Consider getting a paid subscription so we (Atomikos) can continue working full-time on this technology :-) + +## How Pull Requests Can Get Accepted + + - We review every pull request, although it may take a while. + - Please respond to any questions why might have - so we can accept your pull request. + - Issues for which we had questions but receive no answer within 2 weeks will likely be closed without being incorporated. + - Accepted pull requests require post-processing on our end, because we typically add the required tests to our test suites. We'll probably also change the code here and there to match our design style. + +## What If... My Pull Request Is Rejected? + +It happens, don't take it personally: + + - Our open source work is only sustainable because of the money we make on commercial support and power features, so we may reject pull requests that we believe to be in conflict with our commercial distribution. Otherwise, there would not be this open source code base to contribute to in the first place... + - We try to keep the product scope focused so we may also reject pull requests that we feel are not aligned with our long-term vision. + +You can also look at it in a positive way: here on github it is easy to fork and modify whatever you like, so the project fits your specific needs. In some cases, those needs overlap with our vision and that's the perfect pull request. In other cases, there is not much overlap - but then you still have your fork with your custom changes. That's a lot better than in the past, when we did not have a public repository at all. + +_Thanks for your understanding!_ diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..f893d6c08 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,17 @@ +pipeline { + agent any + tools { + maven 'maven-3.6.0' + } +// Use the pomfix tool to validate that bundle dependencies are properly declared + stage('Build') { + steps { + retry(3) { + checkout scm + } + withMaven(maven: 'maven-3.6.0', jdk: 'jdk8-latest', mavenOpts: '-Xmx1024m -Xms512m') { + sh 'mvn clean install' + } + } + } +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..cfebf9b15 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,4 @@ +LICENSE CONDITIONS + +See http://www.atomikos.com/Main/WhichLicenseApplies for details. + diff --git a/README.md b/README.md new file mode 100644 index 000000000..2663afed2 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +

Atomikos TransactionsEssentials: light-weight distributed transactions +

+ + +

Atomikos Logo

+ +

Community development "mirror" of atomikos.com/Main/TransactionsEssentials.

+ +

New contributors welcome - help us shape transactions for the cloud!

+ +

Please star this project to help us grow. Thanks in advance!

+ + + + +## Highlights + +* **Distributed transaction management for Java** - so your Java application gets **instant reliability** without design efforts in your code. +* **Microservices support** - so you can have one global transaction spanning several services. +* **JEE compatible** - so it integrates effortlessly in your **Spring** or **Tomcat** configuration. +* **Javax and JakartaEE compatible** - so you can use the latest greatest ecosystem if you want to. +* **Light-weight** - so your **microservices** can use it, too. +* **Embeddable in your code** - so you can **test everything in the IDE** and avoid late integration issues at deployment time. +* **OSGi support** - so you can use OSGi containers also. +* **Connection pooling for JDBC and JMS** - so you get maximum **performance**. +* **Built-in support for Hibernate and JPA** - so you can use your favorite persistence framework. +* **Automatic crash / restart recovery** - so your incomplete **distributed transactions are cleaned up** and your data returns to a consistent state. +* **Cloud-native design** - so your applications are ready for **deployment to your cloud**. +* **Commercial support available** - if you're serious about transactions in your business. + +## NEW: JakartaEE support + +Relevant jars are transformed into jakarta equivalents so we support both Javax and Jakarta. + +## NEW: Microservice Support + +With microservices, the typical monolith ACID transaction becomes split into 2 or more local transactions, one in each microservice. This can quickly lead to inconsistencies - for instance if there are network timeouts or other failures: + +

Inconsistent Microservices

+ +Some concrete examples of possible inconsistencies: + + * For network timeouts, microservice 1 does not see the difference between a failure/rollback of microservice 2 and a commit at microservice 2. The global system state (and consistency) is undefined. + * For retries (when microservice 1 calls microservice 2 a number of times until it does succeed) we depend on microservice 2 offering a correct implementation of an idempotent consumer. This is hard and error-prone. + * Also for retries, microservice 1 has to remember that it needs to retry at microservice 2. This adds technical complexity to an otherwise simple scenario. + * For failures when invoking microservice 2 there is no easy alternative but to retry (due to the above). This means you can't have microservice 1 forget about microservice 2 and try an equivalent "competing" implementation. Why? Because it just might be that the invocation of microservice 2 did actually commit. + +We allow 1 global transaction, even across separate microservices and you don't have to code anything for that. In the example shown, a failed call means rollback at microservice 2, and if you want then you can safely try an alternative microservice instead (even within the same global transaction). + +Working samples are included in our official download. This enables safer retries (if you do want to retry - that is your choice) plus the notion of a global commit. + +For more information on how to fine-tune microservice transactions: check out our online course. + +## Using TransactionsEssentials + +### Getting Started + +See [Getting Started](http://www.atomikos.com/Documentation/GettingStarted) for general documentation. + +### Documentation + +See documentation at [www.atomikos.com/Documentation](http://www.atomikos.com/Documentation/) + +### Code Samples + +Register and download from [www.atomikos.com](http://www.atomikos.com/) + +### Releases + +Register and download from [www.atomikos.com](http://www.atomikos.com/) and get documentation plus working sample applications + +Or check [Maven Central (without the samples or documentation)](http://search.maven.org) + + +## Governance & Participating + +### Joining + +See our [Community Page](http://www.atomikos.com/Main/AtomikosCommunity) for how to join us. + +### Building From Source + +Pull latest from repo `git pull origin master` and try `mvn clean install -Popensource`. + +### Contributing + +The fastest way to contribute is by starring this project. Thank you :-) + +See our [contributor guidelines](CONTRIBUTING.MD) for inspiration and guidance. + +### Code of conduct + +See our [code of conduct](CODE-OF-CONDUCT.MD) for details and how to report violations. + +### About This Repository + +This GitHub project is merged with - and updated regularly from - our internal development repository to work towards our next open source release (note: stable maintenance releases and our commercial power features are managed outside GitHub). + +IMPORTANT: we (Atomikos) don't develop on GitHub ourselves (yet) so you won't see a lot of our commits here - only refreshes when we merge + push to GitHub. That is because at least initially, the sole purpose of this project is to allow interested GitHub community members to fork and contribute useful features to what we have. + +Activities here are probably higher when: + + - we prepare a new community release (milestone), and + - the 3 months of stabilization period after that (during which we publish lots of bug fixes) + - (check the milestones page with due dates to get an impression...) + +After that, we are busy upgrading our customers and on-boarding new customers so you will see less activity here. That's because most customer work is done in our private repositories. + +## Special Thanks / Featured Contributors + +You know how it goes, one always forgets to mention someone - but the following fine people have all delivered memorable technical contributions to this project in one way or another. + +So: **thank you!** (and apologies to the superstars not mentioned here): + + - [@pascalleclercq](https://github.com/pascalleclercq) + - [@tkrah](https://github.com/tkrah) + - [@dpocock](https://github.com/dpocock) + - [@lorban](https://github.com/lorban) + - [@dandiep](https://github.com/dandiep) + - [@gregw](https://github.com/gregw) + - [@janbartel](https://github.com/janbartel) + - [@Fungrim](https://github.com/Fungrim) + + +## License + +See our [license policy page](http://www.atomikos.com/Main/WhichLicenseApplies). + +## Feedback Wanted! + +Do you think something's missing on this page? Please open an issue to let us know! + +_Copyright (c) 2000-2023, Atomikos - all rights reserved. Visit [www.atomikos.com](http://www.atomikos.com/) for more..._ diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..1b697c643 --- /dev/null +++ b/pom.xml @@ -0,0 +1,360 @@ + + + 4.0.0 + com.atomikos + atomikos-parent + 6.0.1-SNAPSHOT + + pom + Atomikos All POM + http://www.atomikos.com/ + Reliability through Atomicity: manage your distributed transactions and protect your mission critical data + + + Atomikos Multiple Licensing Scheme + http://www.atomikos.com/Main/WhichLicenseApplies + repo + + + + + + + UTF-8 + 1.8 + 1.8 + + + + Atomikos + info@atomikos.com + Atomikos + https://www.atomikos.com + + + + scm:hg:http://atomikos.repositoryhosting.com/hg/atomikos/development + scm:hg:http://atomikos.repositoryhosting.com/hg/atomikos/development + http://www.atomikos.com/ + HEAD + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + false + -Xdoclint:none + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + + org.apache.maven.plugins + maven-assembly-plugin + 2.4 + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + + org.apache.maven.plugins + maven-surefire-plugin + 2.14 + + + com.atlassian.maven.plugins + maven-clover2-plugin + 3.0.1 + + + com.agilejava.docbkx + docbkx-maven-plugin + 2.0.11 + + + org.apache.maven.plugins + maven-jar-plugin + 2.3.1 + + + com.mycila.maven-license-plugin + maven-license-plugin + 1.8.0 + + + org.apache.felix + maven-bundle-plugin + 2.5.4 + + + org.ops4j.pax.exam + maven-paxexam-plugin + 1.2.2 + + + org.apache.maven.plugins + maven-release-plugin + 2.4.1 + + + org.eclipse.transformer + transformer-maven-plugin + 0.5.0 + + + + + + + org.eclipse.transformer + transformer-maven-plugin + true + + jakarta + + true + + + + + default-jar + + jar + + + + ${project.groupId} + ${project.artifactId} + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + -Xmx1024m + + **/*TestJUnit.java + + + ${project.build.testOutputDirectory} + + + + maven-javadoc-plugin + + com.atomikos + + + + + org.apache.maven.plugins + maven-release-plugin + + true + + clean package javadoc:aggregate-jar install + deploy + -DskipTests -Passemble + ${releaseVersion} + @{project.version} + + + + + + + org.apache.maven.wagon + wagon-ftp + 1.0-beta-6 + + + + + + + + org.mockito + mockito-core + 4.4.0 + test + + + junit + junit + 4.11 + test + + + org.assertj + assertj-core + 3.17.2 + test + + + javax.interceptor + javax.interceptor-api + 1.2.2 + provided + + + + + + + local + + + + maven-release-plugin + + false + true + + + + maven-deploy-plugin + + true + + + + + + + public + private + specs + + + + all + + true + + + + atomikos.website.maven.repo + ftp://www.atomikos.com + + + internal.snapshot.repo + Atomikos Internal Snapshot Repository + file://${user.home}/.m2/repository + + + + + + public + private + specs + + + + + opensource + + false + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + false + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + + + + public + + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + release-sign-artifacts + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + + + + org.codehaus.mojo + jdepend-maven-plugin + 2.0 + + + + + diff --git a/public/header.txt b/public/header.txt new file mode 100644 index 000000000..1424b4cd9 --- /dev/null +++ b/public/header.txt @@ -0,0 +1,5 @@ +Copyright (C) 2000-2024 Atomikos + +LICENSE CONDITIONS + +See http://www.atomikos.com/Main/WhichLicenseApplies for details. diff --git a/public/pom.xml b/public/pom.xml new file mode 100644 index 000000000..387ddc462 --- /dev/null +++ b/public/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + com.atomikos + atomikos-parent + 6.0.1-SNAPSHOT + + ate + pom + Atomikos Transactions Essentials + + + 2000-2024 + true + + + + util + transactions-api + transactions + transactions-jta + transactions-jdbc + transactions-jms + transactions-osgi + transactions-hibernate2 + transactions-hibernate3 + transactions-hibernate4 + transactions-eclipselink + transactions-jndi-provider + transactions-essentials + transactions-essentials-jakarta + transactions-remoting + spring-boot2 + spring-boot3 + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + true + + + attach-sources + package + + jar-no-fork + + + + + + + + + false + com.mycila.maven-license-plugin + maven-license-plugin + + +
header.txt
+ false + true + false + + src/** + + + src/test/** + target/** + .clover/** + private/** + axt/** + + true + + true + + ${project.inceptionYear} + Atomikos + info@atomikos.com + + UTF-8 + + +
+ + + + check + + + +
+ + +
+
+
diff --git a/public/spring-boot2/pom.xml b/public/spring-boot2/pom.xml new file mode 100644 index 000000000..60c1e7a5a --- /dev/null +++ b/public/spring-boot2/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + spring-boot2 + pom + + 2.3.4.RELEASE + + + transactions-spring-boot + transactions-spring-boot-starter + transactions-spring-boot-integration-tests + + + + + org.springframework.boot + spring-boot-dependencies + pom + import + ${boot.version} + + + + + + org.mockito + mockito-core + 4.4.0 + test + + + diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/.gitignore b/public/spring-boot2/transactions-spring-boot-integration-tests/.gitignore new file mode 100644 index 000000000..8fbc30c96 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/.gitignore @@ -0,0 +1 @@ +transaction-logs diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/pom.xml b/public/spring-boot2/transactions-spring-boot-integration-tests/pom.xml new file mode 100644 index 000000000..8a7ff9964 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com.atomikos + spring-boot2 + 6.0.1-SNAPSHOT + + transactions-spring-boot-integration-tests + Transactions Spring Boot Integration Tests + + + com.atomikos + transactions-spring-boot-starter + 6.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-artemis + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework + spring-jms + + + com.h2database + h2 + + + org.apache.activemq + artemis-jms-server + + + org.apache.geronimo.specs + geronimo-jms_2.0_spec + + + + + org.springframework.boot + spring-boot-starter-test + + + + + + org.springframework.boot + spring-boot-dependencies + 2.3.4.RELEASE + pom + import + + + + diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Account.java b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Account.java new file mode 100644 index 000000000..2ca6d45ca --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Account.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Account { + + @Id + @GeneratedValue + private Long id; + + private String username; + + Account() { + } + + public Account(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountRepository.java b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountRepository.java new file mode 100644 index 000000000..a850470ac --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountRepository.java @@ -0,0 +1,15 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccountRepository extends JpaRepository { + +} diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountService.java b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountService.java new file mode 100644 index 000000000..3a90fcf30 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountService.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import javax.transaction.Transactional; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class AccountService { + + private final JmsTemplate jmsTemplate; + + private final AccountRepository accountRepository; + + public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) { + this.jmsTemplate = jmsTemplate; + this.accountRepository = accountRepository; + } + + public void createAccountAndNotify(String username) { + this.jmsTemplate.convertAndSend("accounts", username); + this.accountRepository.save(new Account(username)); + if ("error".equals(username)) { + throw new RuntimeException("Simulated error"); + } + } + +} diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Messages.java b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Messages.java new file mode 100644 index 000000000..96e114c96 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Messages.java @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class Messages { + + @JmsListener(destination = "accounts") + public void onMessage(String content) { + System.out.println("----> " + content); + } + +} diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/SampleAtomikosApplication.java b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/SampleAtomikosApplication.java new file mode 100644 index 000000000..b442d2c5a --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/java/com/atomikos/spring/integrationtest/SampleAtomikosApplication.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import java.io.Closeable; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +@SpringBootApplication +public class SampleAtomikosApplication { + + public static void main(String... args) throws Exception { + ApplicationContext context = SpringApplication.run(SampleAtomikosApplication.class, args); + AccountService service = context.getBean(AccountService.class); + AccountRepository repository = context.getBean(AccountRepository.class); + service.createAccountAndNotify("josh"); + System.out.println("Count is " + repository.count()); + try { + service.createAccountAndNotify("error"); + } + catch (Exception ex) { + System.out.println(ex.getMessage()); + } + System.out.println("Count is " + repository.count()); + Thread.sleep(100); + ((Closeable) context).close(); + } + +} diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/resources/application.properties b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/resources/application.properties new file mode 100644 index 000000000..62ba22b88 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/main/resources/application.properties @@ -0,0 +1,11 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +logging.level.com.atomikos=DEBUG +spring.artemis.embedded.queues=accounts +spring.jpa.open-in-view=true diff --git a/public/spring-boot2/transactions-spring-boot-integration-tests/src/test/java/com/atomikos/spring/integrationtest/SampleAtomikosApplicationTests.java b/public/spring-boot2/transactions-spring-boot-integration-tests/src/test/java/com/atomikos/spring/integrationtest/SampleAtomikosApplicationTests.java new file mode 100644 index 000000000..583b098c1 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-integration-tests/src/test/java/com/atomikos/spring/integrationtest/SampleAtomikosApplicationTests.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.util.StringUtils; + +/** + * Basic integration tests for demo application. + */ +@ExtendWith(OutputCaptureExtension.class) +class SampleAtomikosApplicationTests { + + @Test + void testTransactionRollback(CapturedOutput output) throws Exception { + SampleAtomikosApplication.main(); + assertThat(output).satisfies(numberOfOccurrences("---->", 1)); + assertThat(output).satisfies(numberOfOccurrences("----> josh", 1)); + assertThat(output).satisfies(numberOfOccurrences("Count is 1", 2)); + assertThat(output).satisfies(numberOfOccurrences("Simulated error", 1)); + } + + @Test + void testTransactionRollbackWithoutSpringBoot23JtaAutoConfiguration(CapturedOutput output) throws Exception { + SampleAtomikosApplication.main("--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration"); + assertThat(output).satisfies(numberOfOccurrences("---->", 1)); + assertThat(output).satisfies(numberOfOccurrences("----> josh", 1)); + assertThat(output).satisfies(numberOfOccurrences("Count is 1", 2)); + assertThat(output).satisfies(numberOfOccurrences("Simulated error", 1)); + } + + private Consumer numberOfOccurrences(String substring, int expectedCount) { + return (charSequence) -> { + int count = StringUtils.countOccurrencesOf(charSequence.toString(), substring); + assertThat(count).isEqualTo(expectedCount); + }; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot-starter/pom.xml b/public/spring-boot2/transactions-spring-boot-starter/pom.xml new file mode 100644 index 000000000..056f34a12 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot-starter/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + com.atomikos + spring-boot2 + 6.0.1-SNAPSHOT + + transactions-spring-boot-starter + Transactions Spring Boot Starter + + + com.atomikos + transactions-spring-boot + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + + + + + + org.apache.maven.plugins + maven-source-plugin + + true + + + + + diff --git a/public/spring-boot2/transactions-spring-boot/.gitignore b/public/spring-boot2/transactions-spring-boot/.gitignore new file mode 100644 index 000000000..8fbc30c96 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/.gitignore @@ -0,0 +1 @@ +transaction-logs diff --git a/public/spring-boot2/transactions-spring-boot/pom.xml b/public/spring-boot2/transactions-spring-boot/pom.xml new file mode 100644 index 000000000..6919530ed --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + com.atomikos + spring-boot2 + 6.0.1-SNAPSHOT + + transactions-spring-boot + Transactions Spring Boot + + + jakarta.jms + jakarta.jms-api + provided + + + jakarta.transaction + jakarta.transaction-api + provided + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + + + org.springframework + spring-tx + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.boot + spring-boot-configuration-processor + 2.3.4.RELEASE + true + + + diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosAutoConfiguration.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosAutoConfiguration.java new file mode 100644 index 000000000..88fa9c302 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosAutoConfiguration.java @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.util.Properties; + +import javax.jms.Message; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jdbc.XADataSourceWrapper; +import org.springframework.boot.jms.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.jta.JtaTransactionManager; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.config.UserTransactionServiceImp; +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Atomikos JTA. + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({ SpringJtaAtomikosProperties.class, AtomikosProperties.class}) +@ConditionalOnClass({ JtaTransactionManager.class, UserTransactionManager.class }) +@ConditionalOnMissingBean(org.springframework.transaction.TransactionManager.class) +@AutoConfigureBefore( + value = { XADataSourceAutoConfiguration.class, ActiveMQAutoConfiguration.class, ArtemisAutoConfiguration.class, HibernateJpaAutoConfiguration.class }, + name = "org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration") +class AtomikosAutoConfiguration { + + @Bean(initMethod = "init", destroyMethod = "shutdownWait") + @ConditionalOnMissingBean(UserTransactionService.class) + UserTransactionServiceImp userTransactionService(SpringJtaAtomikosProperties springJtaAtomikosProperties, AtomikosProperties atomikosProperties) { + Properties properties = new Properties(); + properties.putAll(springJtaAtomikosProperties.asProperties()); + properties.putAll(atomikosProperties.asProperties()); + return new UserTransactionServiceImp(properties); + } + + + @Bean(initMethod = "init", destroyMethod = "close") + @ConditionalOnMissingBean(TransactionManager.class) + UserTransactionManager atomikosTransactionManager(UserTransactionService userTransactionService) throws Exception { + UserTransactionManager manager = new UserTransactionManager(); + manager.setStartupTransactionService(false); + manager.setForceShutdown(true); + return manager; + } + + @Bean + @ConditionalOnMissingBean(XADataSourceWrapper.class) + AtomikosXADataSourceWrapper xaDataSourceWrapper() { + return new AtomikosXADataSourceWrapper(); + } + + @Bean + @ConditionalOnMissingBean + static AtomikosDependsOnBeanFactoryPostProcessor atomikosDependsOnBeanFactoryPostProcessor() { + return new AtomikosDependsOnBeanFactoryPostProcessor(); + } + + @Bean + JtaTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager, + ObjectProvider transactionManagerCustomizers) { + JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, transactionManager); + transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(jtaTransactionManager)); + return jtaTransactionManager; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Message.class) + static class AtomikosJtaJmsConfiguration { + + @Bean + @ConditionalOnMissingBean(XAConnectionFactoryWrapper.class) + AtomikosXAConnectionFactoryWrapper xaConnectionFactoryWrapper() { + return new AtomikosXAConnectionFactoryWrapper(); + } + + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosConnectionFactoryBean.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosConnectionFactoryBean.java new file mode 100644 index 000000000..a1af32944 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosConnectionFactoryBean.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Spring friendly version of {@link com.atomikos.jms.AtomikosConnectionFactoryBean}. + */ +@SuppressWarnings("serial") +@ConfigurationProperties(prefix = "spring.jta.atomikos.connectionfactory") +public class AtomikosConnectionFactoryBean extends com.atomikos.jms.AtomikosConnectionFactoryBean + implements BeanNameAware, InitializingBean, DisposableBean { + + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueResourceName())) { + setUniqueResourceName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourceBean.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourceBean.java new file mode 100644 index 000000000..01ed82c65 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourceBean.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Spring friendly version of {@link com.atomikos.jdbc.AtomikosDataSourceBean}. + */ +@SuppressWarnings("serial") +@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource") +public class AtomikosDataSourceBean extends com.atomikos.jdbc.AtomikosDataSourceBean + implements BeanNameAware, InitializingBean, DisposableBean { + + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueResourceName())) { + setUniqueResourceName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourceBeanMetadata.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourceBeanMetadata.java new file mode 100644 index 000000000..d145b55e4 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourceBeanMetadata.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.boot.jdbc.metadata.AbstractDataSourcePoolMetadata; + +import com.atomikos.jdbc.AtomikosDataSourceBean; + +public class AtomikosDataSourceBeanMetadata extends AbstractDataSourcePoolMetadata { + + + public AtomikosDataSourceBeanMetadata(AtomikosDataSourceBean dataSource) { + super(dataSource); + } + + + @Override + public Integer getActive() { + return getDataSource().poolTotalSize() - getDataSource().poolAvailableSize(); + } + + @Override + public Integer getMax() { + return getDataSource().getMaxPoolSize(); + } + + @Override + public Integer getMin() { + return getDataSource().getMinPoolSize(); + } + + @Override + public String getValidationQuery() { + return getDataSource().getTestQuery(); + } + + @Override + public Boolean getDefaultAutoCommit() { + return false; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourcePoolMetadataProvidersConfiguration.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourcePoolMetadataProvidersConfiguration.java new file mode 100644 index 000000000..b05592386 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDataSourcePoolMetadataProvidersConfiguration.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.jdbc.DataSourceUnwrapper; +import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.atomikos.jdbc.AtomikosDataSourceBean; +import com.atomikos.jdbc.AtomikosNonXADataSourceBean; + +/** + * Register the {@link AtomikosDataSourcePoolMetadataProvidersConfiguration} for the AtomikosDataSourceBeans. For use in the Spring Boot actuator: + * https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-jdbc. + * This configuration must be explicitly imported: + *

+ * {@code @Import({ AtomikosDataSourcePoolMetadataProvidersConfiguration.class}) } + *

+ */ +@Configuration +public class AtomikosDataSourcePoolMetadataProvidersConfiguration { + + @Configuration + @ConditionalOnClass(AtomikosDataSourceBean.class) + static class AtomikosDataSourceBeanMetadataProviderConfiguration { + + @Bean + public DataSourcePoolMetadataProvider atomikosDataSourceBeanMetadataProvider() { + + return (dataSource) -> { + AtomikosDataSourceBean atomikosDataSource = DataSourceUnwrapper.unwrap(dataSource, AtomikosDataSourceBean.class); + if (atomikosDataSource != null) { + return new AtomikosDataSourceBeanMetadata(atomikosDataSource); + } + return null; + }; + } + } + + @Configuration + @ConditionalOnClass(AtomikosNonXADataSourceBean.class) + static class AtomikosNonXADataSourceBeanMetadataProviderConfiguration { + + @Bean + public DataSourcePoolMetadataProvider atomikosNonXADataSourceBeanMetadataProvider() { + + return (dataSource) -> { + AtomikosNonXADataSourceBean atomikosDataSource = DataSourceUnwrapper.unwrap(dataSource, AtomikosNonXADataSourceBean.class); + if (atomikosDataSource != null) { + return new AtomikosNonXADataSourceBeanMetadata(atomikosDataSource); + } + return null; + }; + } + } +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessor.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessor.java new file mode 100644 index 000000000..5b88478d4 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessor.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.atomikos.icatch.jta.UserTransactionManager; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; + +/** + * {@link BeanFactoryPostProcessor} to automatically setup the recommended + * {@link BeanDefinition#setDependsOn(String[]) dependsOn} settings for + * correct Atomikos + * ordering. + */ +public class AtomikosDependsOnBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered { + + private static final String[] NO_BEANS = {}; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + String[] transactionManagers = beanFactory.getBeanNamesForType(UserTransactionManager.class, true, false); + for (String transactionManager : transactionManagers) { + addTransactionManagerDependencies(beanFactory, transactionManager); + } + addMessageDrivenContainerDependencies(beanFactory, transactionManagers); + } + + private void addTransactionManagerDependencies(ConfigurableListableBeanFactory beanFactory, + String transactionManager) { + BeanDefinition bean = beanFactory.getBeanDefinition(transactionManager); + Set dependsOn = new LinkedHashSet<>(asList(bean.getDependsOn())); + int initialSize = dependsOn.size(); + addDependencies(beanFactory, "javax.jms.ConnectionFactory", dependsOn); + addDependencies(beanFactory, "javax.sql.DataSource", dependsOn); + if (dependsOn.size() != initialSize) { + bean.setDependsOn(StringUtils.toStringArray(dependsOn)); + } + } + + private void addMessageDrivenContainerDependencies(ConfigurableListableBeanFactory beanFactory, + String[] transactionManagers) { + String[] messageDrivenContainers = getBeanNamesForType(beanFactory, + "com.atomikos.jms.extra.MessageDrivenContainer"); + for (String messageDrivenContainer : messageDrivenContainers) { + BeanDefinition bean = beanFactory.getBeanDefinition(messageDrivenContainer); + Set dependsOn = new LinkedHashSet<>(asList(bean.getDependsOn())); + dependsOn.addAll(asList(transactionManagers)); + bean.setDependsOn(StringUtils.toStringArray(dependsOn)); + } + } + + private void addDependencies(ConfigurableListableBeanFactory beanFactory, String type, Set dependsOn) { + dependsOn.addAll(asList(getBeanNamesForType(beanFactory, type))); + } + + private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory, String type) { + try { + return beanFactory.getBeanNamesForType(Class.forName(type), true, false); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + // Ignore + } + return NO_BEANS; + } + + private List asList(String[] array) { + return (array != null) ? Arrays.asList(array) : Collections.emptyList(); + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosNonXADataSourceBeanMetadata.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosNonXADataSourceBeanMetadata.java new file mode 100644 index 000000000..8412286f2 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosNonXADataSourceBeanMetadata.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.boot.jdbc.metadata.AbstractDataSourcePoolMetadata; + +import com.atomikos.jdbc.AtomikosNonXADataSourceBean; + +public class AtomikosNonXADataSourceBeanMetadata extends AbstractDataSourcePoolMetadata { + + + public AtomikosNonXADataSourceBeanMetadata(AtomikosNonXADataSourceBean dataSource) { + super(dataSource); + } + + + @Override + public Integer getActive() { + return getDataSource().poolTotalSize() - getDataSource().poolAvailableSize(); + } + + @Override + public Integer getMax() { + return getDataSource().getMaxPoolSize(); + } + + @Override + public Integer getMin() { + return getDataSource().getMinPoolSize(); + } + + @Override + public String getValidationQuery() { + return getDataSource().getTestQuery(); + } + + @Override + public Boolean getDefaultAutoCommit() { + return false; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosProperties.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosProperties.java new file mode 100644 index 000000000..eadfba2d5 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosProperties.java @@ -0,0 +1,411 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.time.Duration; +import java.util.Properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Bean friendly variant of + * Atomikos configuration + * properties. Allows for setter based configuration and is amiable to relaxed data + * binding. + * @see #asProperties() + */ +@ConfigurationProperties(prefix = "atomikos.properties") +public class AtomikosProperties { + + /** + * Transaction manager implementation that should be started. + */ + private String service; + + /** + * Maximum timeout that can be allowed for transactions. + */ + private Duration maxTimeout; + + /** + * Default timeout for JTA transactions. + */ + private Duration defaultJtaTimeout; + + /** + * Maximum number of active transactions. + */ + private Integer maxActives; + + /** + * Whether to enable disk logging. + */ + private Boolean enableLogging; + + /** + * The transaction manager's unique name. Defaults to the machine's IP address. If you + * plan to run more than one transaction manager against one database you must set + * this property to a unique value. + */ + private String transactionManagerUniqueName; + + /** + * Whether sub-transactions should be joined when possible. + */ + private Boolean serialJtaTransactions; + + /** + * Specify whether sub-transactions are allowed. + */ + private Boolean allowSubTransactions; + + /** + * Whether a VM shutdown should trigger forced shutdown of the transaction core. + */ + private Boolean forceShutdownOnVmExit; + + /** + * How long should normal shutdown (no-force) wait for transactions to complete. + */ + private Long defaultMaxWaitTimeOnShutdown; + + /** + * Transactions log file base name. + */ + private String logBaseName; + + /** + * Directory in which the log files should be stored. Defaults to the current working + * directory. + */ + private String logBaseDir; + + /** + * Interval between checkpoints, expressed as the number of log writes between two + * checkpoints. A checkpoint reduces the log file size at the expense of adding some + * overhead in the runtime. + */ + private Long checkpointInterval; + + private Boolean throwOnHeuristic; + + + private final Recovery recovery = new Recovery(); + + /** + * Specifies the transaction manager implementation that should be started. There is + * no default value and this must be set. Generally, + * {@literal com.atomikos.icatch.standalone.UserTransactionServiceFactory} is the + * value you should set. + * @param service the service + */ + public void setService(String service) { + this.service = service; + } + + public String getService() { + return this.service; + } + + /** + * Specifies the maximum timeout that can be allowed for transactions. Defaults to + * {@literal 300000}. This means that calls to UserTransaction.setTransactionTimeout() + * with a value higher than configured here will be max'ed to this value. + * @param maxTimeout the max timeout + */ + public void setMaxTimeout(Duration maxTimeout) { + this.maxTimeout = maxTimeout; + } + + public Duration getMaxTimeout() { + return this.maxTimeout; + } + + /** + * The default timeout for JTA transactions (optional, defaults to {@literal 10000} + * ms). + * @param defaultJtaTimeout the default JTA timeout + */ + public void setDefaultJtaTimeout(Duration defaultJtaTimeout) { + this.defaultJtaTimeout = defaultJtaTimeout; + } + + public Duration getDefaultJtaTimeout() { + return this.defaultJtaTimeout; + } + + /** + * Specifies the maximum number of active transactions. Defaults to {@literal 50}. A + * negative value means infinite amount. You will get an {@code IllegalStateException} + * with error message "Max number of active transactions reached" if you call + * {@code UserTransaction.begin()} while there are already n concurrent transactions + * running, n being this value. + * @param maxActives the max activities + */ + public void setMaxActives(Integer maxActives) { + this.maxActives = maxActives; + } + + public Integer getMaxActives() { + return this.maxActives; + } + + /** + * Specifies if disk logging should be enabled or not. Defaults to true. It is useful + * for JUnit testing, or to profile code without seeing the transaction manager's + * activity as a hot spot but this should never be disabled on production or data + * integrity cannot be guaranteed. + * @param enableLogging if logging is enabled + */ + public void setEnableLogging(Boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public Boolean isEnableLogging() { + return this.enableLogging; + } + + /** + * Specifies the transaction manager's unique name. Defaults to the machine's IP + * address. If you plan to run more than one transaction manager against one database + * you must set this property to a unique value or you might run into duplicate + * transaction ID (XID) problems that can be quite subtle (example: + * {@literal https://fogbugz.atomikos.com/default.asp?community.6.2225.7}). If + * multiple instances need to use the same properties file then the easiest way to + * ensure uniqueness for this property is by referencing a system property specified + * at VM startup. + * @param uniqueName the unique name + */ + public void setTransactionManagerUniqueName(String uniqueName) { + this.transactionManagerUniqueName = uniqueName; + } + + public String getTransactionManagerUniqueName() { + return this.transactionManagerUniqueName; + } + + /** + * Specifies if subtransactions should be joined when possible. Defaults to true. When + * false, no attempt to call {@code XAResource.start(TM_JOIN)} will be made for + * different but related subtransactions. This setting has no effect on resource + * access within one and the same transaction. If you don't use subtransactions then + * this setting can be ignored. + * @param serialJtaTransactions if serial JTA transactions are supported + */ + public void setSerialJtaTransactions(Boolean serialJtaTransactions) { + this.serialJtaTransactions = serialJtaTransactions; + } + + public Boolean isSerialJtaTransactions() { + return this.serialJtaTransactions; + } + + public void setAllowSubTransactions(Boolean allowSubTransactions) { + this.allowSubTransactions = allowSubTransactions; + } + + public Boolean isAllowSubTransactions() { + return this.allowSubTransactions; + } + + /** + * Specifies whether VM shutdown should trigger forced shutdown of the transaction + * core. Defaults to false. + * @param forceShutdownOnVmExit if VM shutdown should be forced + */ + public void setForceShutdownOnVmExit(Boolean forceShutdownOnVmExit) { + this.forceShutdownOnVmExit = forceShutdownOnVmExit; + } + + public Boolean isForceShutdownOnVmExit() { + return this.forceShutdownOnVmExit; + } + + /** + * Specifies how long should a normal shutdown (no-force) wait for transactions to + * complete. Defaults to {@literal Long.MAX_VALUE}. + * @param defaultMaxWaitTimeOnShutdown the default max wait time on shutdown + */ + public void setDefaultMaxWaitTimeOnShutdown(Long defaultMaxWaitTimeOnShutdown) { + this.defaultMaxWaitTimeOnShutdown = defaultMaxWaitTimeOnShutdown; + } + + public Long getDefaultMaxWaitTimeOnShutdown() { + return this.defaultMaxWaitTimeOnShutdown; + } + + /** + * Specifies the transactions log file base name. Defaults to {@literal tmlog}. The + * transactions logs are stored in files using this name appended with a number and + * the extension {@literal .log}. At checkpoint, a new transactions log file is + * created and the number is incremented. + * @param logBaseName the log base name + */ + public void setLogBaseName(String logBaseName) { + this.logBaseName = logBaseName; + } + + public String getLogBaseName() { + return this.logBaseName; + } + + /** + * Specifies the directory in which the log files should be stored. Defaults to the + * current working directory. This directory should be a stable storage like a SAN, + * RAID or at least backed up location. The transactions logs files are as important + * as the data themselves to guarantee consistency in case of failures. + * @param logBaseDir the log base dir + */ + public void setLogBaseDir(String logBaseDir) { + this.logBaseDir = logBaseDir; + } + + public String getLogBaseDir() { + return this.logBaseDir; + } + + /** + * Specifies the interval between checkpoints. A checkpoint reduces the log file size + * at the expense of adding some overhead in the runtime. Defaults to {@literal 500}. + * @param checkpointInterval the checkpoint interval + */ + public void setCheckpointInterval(Long checkpointInterval) { + this.checkpointInterval = checkpointInterval; + } + + public Long getCheckpointInterval() { + return this.checkpointInterval; + } + + + public Recovery getRecovery() { + return this.recovery; + } + + public void setThrowOnHeuristic(Boolean throwOnHeuristic) { + this.throwOnHeuristic = throwOnHeuristic; + } + + public Boolean isThrowOnHeuristic() { + return this.throwOnHeuristic; + } + + /** + * Returns the properties as a {@link Properties} object that can be used with + * Atomikos. + * @return the properties + */ + public Properties asProperties() { + Properties properties = new Properties(); + set(properties, "service", getService()); + set(properties, "max_timeout", getMaxTimeout()); + set(properties, "default_jta_timeout", getDefaultJtaTimeout()); + set(properties, "max_actives", getMaxActives()); + set(properties, "enable_logging", isEnableLogging()); + set(properties, "tm_unique_name", getTransactionManagerUniqueName()); + set(properties, "serial_jta_transactions", isSerialJtaTransactions()); + set(properties, "allow_subtransactions", isAllowSubTransactions()); + set(properties, "force_shutdown_on_vm_exit", isForceShutdownOnVmExit()); + set(properties, "default_max_wait_time_on_shutdown", getDefaultMaxWaitTimeOnShutdown()); + set(properties, "log_base_name", getLogBaseName()); + set(properties, "log_base_dir", getOrDeduceLogBaseDir()); + set(properties, "checkpoint_interval", getCheckpointInterval()); + set(properties, "forget_orphaned_log_entries_delay", recovery.getForgetOrphanedLogEntriesDelay()); + set(properties, "recovery_delay", recovery.getDelay()); + set(properties, "oltp_max_retries", recovery.getMaxRetries()); + set(properties, "oltp_retry_interval", recovery.getRetryInterval()); + set(properties, "throw_on_heuristic", isThrowOnHeuristic()); + return properties; + } + + private String getOrDeduceLogBaseDir() { + if(StringUtils.hasText(this.logBaseDir)) { + return this.logBaseDir; + } + return null; + + } + + private void set(Properties properties, String key, Object value) { + String id = "com.atomikos.icatch." + key; + if (value != null && !properties.containsKey(id)) { + properties.setProperty(id, asString(value)); + } + } + + private String asString(Object value) { + if (value instanceof Duration) { + return String.valueOf(((Duration) value).toMillis()); + } + return value.toString(); + } + + /** + * Recovery specific settings. + */ + public static class Recovery { + + /** + * Delay after which recovery can cleanup pending ('orphaned') log entries. + */ + private Duration forgetOrphanedLogEntriesDelay; + + /** + * Delay between two recovery scans. + */ + private Duration delay; + + /** + * Number of retry attempts to commit the transaction before throwing an + * exception. + */ + private Integer maxRetries; + + /** + * Delay between retry attempts. + */ + private Duration retryInterval; + + public Duration getForgetOrphanedLogEntriesDelay() { + return this.forgetOrphanedLogEntriesDelay; + } + + public void setForgetOrphanedLogEntriesDelay(Duration forgetOrphanedLogEntriesDelay) { + this.forgetOrphanedLogEntriesDelay = forgetOrphanedLogEntriesDelay; + } + + public Duration getDelay() { + return this.delay; + } + + public void setDelay(Duration delay) { + this.delay = delay; + } + + public Integer getMaxRetries() { + return this.maxRetries; + } + + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + } + + public Duration getRetryInterval() { + return this.retryInterval; + } + + public void setRetryInterval(Duration retryInterval) { + this.retryInterval = retryInterval; + } + + + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapper.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapper.java new file mode 100644 index 000000000..b01e357a4 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapper.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.jms.ConnectionFactory; +import javax.jms.XAConnectionFactory; + +import org.springframework.boot.jms.XAConnectionFactoryWrapper; + +/** + * {@link XAConnectionFactoryWrapper} that uses an {@link AtomikosConnectionFactoryBean} + * to wrap a {@link XAConnectionFactory}. + */ +public class AtomikosXAConnectionFactoryWrapper implements XAConnectionFactoryWrapper { + + @Override + public ConnectionFactory wrapConnectionFactory(XAConnectionFactory connectionFactory) { + AtomikosConnectionFactoryBean bean = new AtomikosConnectionFactoryBean(); + bean.setXaConnectionFactory(connectionFactory); + return bean; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosXADataSourceWrapper.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosXADataSourceWrapper.java new file mode 100644 index 000000000..7d2c02c4f --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/AtomikosXADataSourceWrapper.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.sql.XADataSource; + +import org.springframework.boot.jdbc.XADataSourceWrapper; + +/** + * {@link XADataSourceWrapper} that uses an {@link AtomikosDataSourceBean} to wrap a + * {@link XADataSource}. + */ +public class AtomikosXADataSourceWrapper implements XADataSourceWrapper { + + @Override + public AtomikosDataSourceBean wrapDataSource(XADataSource dataSource) throws Exception { + AtomikosDataSourceBean bean = new AtomikosDataSourceBean(); + bean.setXaDataSource(dataSource); + return bean; + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/SpringJtaAtomikosProperties.java b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/SpringJtaAtomikosProperties.java new file mode 100644 index 000000000..aa11902c5 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/java/com/atomikos/spring/SpringJtaAtomikosProperties.java @@ -0,0 +1,412 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.time.Duration; +import java.util.Properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Bean friendly variant of + * Atomikos configuration + * properties. Allows for setter based configuration and is amiable to relaxed data + * binding. + * @see #asProperties() + */ +@ConfigurationProperties(prefix = "spring.jta.atomikos.properties") +public class SpringJtaAtomikosProperties { + + /** + * Transaction manager implementation that should be started. + */ + private String service; + + /** + * Maximum timeout that can be allowed for transactions. + */ + private Duration maxTimeout; + + /** + * Default timeout for JTA transactions. + */ + private Duration defaultJtaTimeout; + + /** + * Maximum number of active transactions. + */ + private Integer maxActives; + + /** + * Whether to enable disk logging. + */ + private Boolean enableLogging; + + /** + * The transaction manager's unique name. Defaults to the machine's IP address. If you + * plan to run more than one transaction manager against one database you must set + * this property to a unique value. + */ + private String transactionManagerUniqueName; + + /** + * Whether sub-transactions should be joined when possible. + */ + private Boolean serialJtaTransactions; + + /** + * Specify whether sub-transactions are allowed. + */ + private Boolean allowSubTransactions; + + /** + * Whether a VM shutdown should trigger forced shutdown of the transaction core. + */ + private Boolean forceShutdownOnVmExit; + + /** + * How long should normal shutdown (no-force) wait for transactions to complete. + */ + private Long defaultMaxWaitTimeOnShutdown; + + /** + * Transactions log file base name. + */ + private String logBaseName; + + /** + * Directory in which the log files should be stored. Defaults to the current working + * directory. + */ + private String logBaseDir; + + /** + * Interval between checkpoints, expressed as the number of log writes between two + * checkpoints. A checkpoint reduces the log file size at the expense of adding some + * overhead in the runtime. + */ + private Long checkpointInterval; + + private Boolean throwOnHeuristic; + + + private final Recovery recovery = new Recovery(); + + /** + * Specifies the transaction manager implementation that should be started. There is + * no default value and this must be set. Generally, + * {@literal com.atomikos.icatch.standalone.UserTransactionServiceFactory} is the + * value you should set. + * @param service the service + */ + public void setService(String service) { + this.service = service; + } + + public String getService() { + return this.service; + } + + /** + * Specifies the maximum timeout that can be allowed for transactions. Defaults to + * {@literal 300000}. This means that calls to UserTransaction.setTransactionTimeout() + * with a value higher than configured here will be max'ed to this value. + * @param maxTimeout the max timeout + */ + public void setMaxTimeout(Duration maxTimeout) { + this.maxTimeout = maxTimeout; + } + + public Duration getMaxTimeout() { + return this.maxTimeout; + } + + /** + * The default timeout for JTA transactions (optional, defaults to {@literal 10000} + * ms). + * @param defaultJtaTimeout the default JTA timeout + */ + public void setDefaultJtaTimeout(Duration defaultJtaTimeout) { + this.defaultJtaTimeout = defaultJtaTimeout; + } + + public Duration getDefaultJtaTimeout() { + return this.defaultJtaTimeout; + } + + /** + * Specifies the maximum number of active transactions. Defaults to {@literal 50}. A + * negative value means infinite amount. You will get an {@code IllegalStateException} + * with error message "Max number of active transactions reached" if you call + * {@code UserTransaction.begin()} while there are already n concurrent transactions + * running, n being this value. + * @param maxActives the max activities + */ + public void setMaxActives(Integer maxActives) { + this.maxActives = maxActives; + } + + public Integer getMaxActives() { + return this.maxActives; + } + + /** + * Specifies if disk logging should be enabled or not. Defaults to true. It is useful + * for JUnit testing, or to profile code without seeing the transaction manager's + * activity as a hot spot but this should never be disabled on production or data + * integrity cannot be guaranteed. + * @param enableLogging if logging is enabled + */ + public void setEnableLogging(Boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public Boolean isEnableLogging() { + return this.enableLogging; + } + + /** + * Specifies the transaction manager's unique name. Defaults to the machine's IP + * address. If you plan to run more than one transaction manager against one database + * you must set this property to a unique value or you might run into duplicate + * transaction ID (XID) problems that can be quite subtle (example: + * {@literal https://fogbugz.atomikos.com/default.asp?community.6.2225.7}). If + * multiple instances need to use the same properties file then the easiest way to + * ensure uniqueness for this property is by referencing a system property specified + * at VM startup. + * @param uniqueName the unique name + */ + public void setTransactionManagerUniqueName(String uniqueName) { + this.transactionManagerUniqueName = uniqueName; + } + + public String getTransactionManagerUniqueName() { + return this.transactionManagerUniqueName; + } + + /** + * Specifies if subtransactions should be joined when possible. Defaults to true. When + * false, no attempt to call {@code XAResource.start(TM_JOIN)} will be made for + * different but related subtransactions. This setting has no effect on resource + * access within one and the same transaction. If you don't use subtransactions then + * this setting can be ignored. + * @param serialJtaTransactions if serial JTA transactions are supported + */ + public void setSerialJtaTransactions(Boolean serialJtaTransactions) { + this.serialJtaTransactions = serialJtaTransactions; + } + + public Boolean isSerialJtaTransactions() { + return this.serialJtaTransactions; + } + + public void setAllowSubTransactions(Boolean allowSubTransactions) { + this.allowSubTransactions = allowSubTransactions; + } + + public Boolean isAllowSubTransactions() { + return this.allowSubTransactions; + } + + /** + * Specifies whether VM shutdown should trigger forced shutdown of the transaction + * core. Defaults to false. + * @param forceShutdownOnVmExit if VM shutdown should be forced + */ + public void setForceShutdownOnVmExit(Boolean forceShutdownOnVmExit) { + this.forceShutdownOnVmExit = forceShutdownOnVmExit; + } + + public Boolean isForceShutdownOnVmExit() { + return this.forceShutdownOnVmExit; + } + + /** + * Specifies how long should a normal shutdown (no-force) wait for transactions to + * complete. Defaults to {@literal Long.MAX_VALUE}. + * @param defaultMaxWaitTimeOnShutdown the default max wait time on shutdown + */ + public void setDefaultMaxWaitTimeOnShutdown(Long defaultMaxWaitTimeOnShutdown) { + this.defaultMaxWaitTimeOnShutdown = defaultMaxWaitTimeOnShutdown; + } + + public Long getDefaultMaxWaitTimeOnShutdown() { + return this.defaultMaxWaitTimeOnShutdown; + } + + /** + * Specifies the transactions log file base name. Defaults to {@literal tmlog}. The + * transactions logs are stored in files using this name appended with a number and + * the extension {@literal .log}. At checkpoint, a new transactions log file is + * created and the number is incremented. + * @param logBaseName the log base name + */ + public void setLogBaseName(String logBaseName) { + this.logBaseName = logBaseName; + } + + public String getLogBaseName() { + return this.logBaseName; + } + + /** + * Specifies the directory in which the log files should be stored. Defaults to the + * current working directory. This directory should be a stable storage like a SAN, + * RAID or at least backed up location. The transactions logs files are as important + * as the data themselves to guarantee consistency in case of failures. + * @param logBaseDir the log base dir + */ + public void setLogBaseDir(String logBaseDir) { + this.logBaseDir = logBaseDir; + } + + public String getLogBaseDir() { + return this.logBaseDir; + } + + /** + * Specifies the interval between checkpoints. A checkpoint reduces the log file size + * at the expense of adding some overhead in the runtime. Defaults to {@literal 500}. + * @param checkpointInterval the checkpoint interval + */ + public void setCheckpointInterval(Long checkpointInterval) { + this.checkpointInterval = checkpointInterval; + } + + public Long getCheckpointInterval() { + return this.checkpointInterval; + } + + + public Recovery getRecovery() { + return this.recovery; + } + + public void setThrowOnHeuristic(Boolean throwOnHeuristic) { + this.throwOnHeuristic = throwOnHeuristic; + } + + public Boolean isThrowOnHeuristic() { + return this.throwOnHeuristic; + } + + /** + * Returns the properties as a {@link Properties} object that can be used with + * Atomikos. + * @return the properties + */ + public Properties asProperties() { + Properties properties = new Properties(); + set(properties, "service", getService()); + set(properties, "max_timeout", getMaxTimeout()); + set(properties, "default_jta_timeout", getDefaultJtaTimeout()); + set(properties, "max_actives", getMaxActives()); + set(properties, "enable_logging", isEnableLogging()); + set(properties, "tm_unique_name", getTransactionManagerUniqueName()); + set(properties, "serial_jta_transactions", isSerialJtaTransactions()); + set(properties, "allow_subtransactions", isAllowSubTransactions()); + set(properties, "force_shutdown_on_vm_exit", isForceShutdownOnVmExit()); + set(properties, "default_max_wait_time_on_shutdown", getDefaultMaxWaitTimeOnShutdown()); + set(properties, "log_base_name", getLogBaseName()); + set(properties, "log_base_dir", getOrDeduceLogBaseDir()); + set(properties, "checkpoint_interval", getCheckpointInterval()); + set(properties, "forget_orphaned_log_entries_delay", recovery.getForgetOrphanedLogEntriesDelay()); + set(properties, "recovery_delay", recovery.getDelay()); + set(properties, "oltp_max_retries", recovery.getMaxRetries()); + set(properties, "oltp_retry_interval", recovery.getRetryInterval()); + set(properties, "throw_on_heuristic", isThrowOnHeuristic()); + return properties; + } + + private String getOrDeduceLogBaseDir() { + if(StringUtils.hasText(this.logBaseDir)) { + return this.logBaseDir; + } + return null; + + } + + private void set(Properties properties, String key, Object value) { + String id = "com.atomikos.icatch." + key; + if (value != null && !properties.containsKey(id)) { + properties.setProperty(id, asString(value)); + } + } + + private String asString(Object value) { + if (value instanceof Duration) { + return String.valueOf(((Duration) value).toMillis()); + } + return value.toString(); + } + + /** + * Recovery specific settings. + */ + public static class Recovery { + + /** + * Delay after which recovery can cleanup pending ('orphaned') log entries. + */ + private Duration forgetOrphanedLogEntriesDelay; + + /** + * Delay between two recovery scans. + */ + private Duration delay; + + /** + * Number of retry attempts to commit the transaction before throwing an + * exception. + */ + private Integer maxRetries; + + /** + * Delay between retry attempts. + */ + private Duration retryInterval; + + public Duration getForgetOrphanedLogEntriesDelay() { + return this.forgetOrphanedLogEntriesDelay; + } + + public void setForgetOrphanedLogEntriesDelay(Duration forgetOrphanedLogEntriesDelay) { + this.forgetOrphanedLogEntriesDelay = forgetOrphanedLogEntriesDelay; + } + + public Duration getDelay() { + return this.delay; + } + + public void setDelay(Duration delay) { + this.delay = delay; + } + + public Integer getMaxRetries() { + return this.maxRetries; + } + + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + } + + public Duration getRetryInterval() { + return this.retryInterval; + } + + public void setRetryInterval(Duration retryInterval) { + this.retryInterval = retryInterval; + } + + + } + + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/main/resources/META-INF/spring.factories b/public/spring-boot2/transactions-spring-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..de088c501 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.atomikos.spring.AtomikosAutoConfiguration diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosAutoConfigurationJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosAutoConfigurationJUnit.java new file mode 100644 index 000000000..1210a42a2 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosAutoConfigurationJUnit.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.transaction.UserTransaction; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.transaction.jta.JtaProperties; +import org.springframework.boot.jdbc.XADataSourceWrapper; +import org.springframework.boot.jms.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.transaction.jta.JtaTransactionManager; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * Tests for {@link AtomikosAutoConfiguration}. + */ +public class AtomikosAutoConfigurationJUnit { + + @Test + public void sanityCheck() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JtaProperties.class, + AtomikosAutoConfiguration.class)) { + context.getBean(AtomikosProperties.class); + context.getBean(UserTransactionService.class); + context.getBean(UserTransactionManager.class); + context.getBean(UserTransaction.class); + context.getBean(XADataSourceWrapper.class); + context.getBean(XAConnectionFactoryWrapper.class); + context.getBean(AtomikosDependsOnBeanFactoryPostProcessor.class); + context.getBean(JtaTransactionManager.class); + } + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosConnectionFactoryBeanTestJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosConnectionFactoryBeanTestJUnit.java new file mode 100644 index 000000000..b02cb0c91 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosConnectionFactoryBeanTestJUnit.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AtomikosConnectionFactoryBean}. + */ +public class AtomikosConnectionFactoryBeanTestJUnit { + + @Test + public void beanMethods() throws Exception { + MockAtomikosConnectionFactoryBean bean = spy(new MockAtomikosConnectionFactoryBean()); + bean.setBeanName("bean"); + bean.afterPropertiesSet(); + assertThat(bean.getUniqueResourceName()).isEqualTo("bean"); + verify(bean).init(); + verify(bean, never()).close(); + bean.destroy(); + verify(bean).close(); + } + + @SuppressWarnings("serial") + static class MockAtomikosConnectionFactoryBean extends AtomikosConnectionFactoryBean { + + @Override + public synchronized void init() { + } + + @Override + public synchronized void close() { + } + + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosDataSourceBeanTestJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosDataSourceBeanTestJUnit.java new file mode 100644 index 000000000..c03bc79ac --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosDataSourceBeanTestJUnit.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AtomikosDataSourceBean}. + */ +public class AtomikosDataSourceBeanTestJUnit { + + @Test + public void beanMethods() throws Exception { + MockAtomikosDataSourceBean bean = spy(new MockAtomikosDataSourceBean()); + bean.setBeanName("bean"); + bean.afterPropertiesSet(); + assertThat(bean.getUniqueResourceName()).isEqualTo("bean"); + verify(bean).init(); + verify(bean, never()).close(); + bean.destroy(); + verify(bean).close(); + } + + @SuppressWarnings("serial") + static class MockAtomikosDataSourceBean extends AtomikosDataSourceBean { + + @Override + public synchronized void init() { + } + + @Override + public void close() { + } + + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessorTestJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessorTestJUnit.java new file mode 100644 index 000000000..53bb3740e --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessorTestJUnit.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.util.Arrays; +import java.util.HashSet; + +import javax.jms.ConnectionFactory; +import javax.sql.DataSource; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jms.extra.MessageDrivenContainer; +import org.junit.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosDependsOnBeanFactoryPostProcessor}. + */ +public class AtomikosDependsOnBeanFactoryPostProcessorTestJUnit { + + private AnnotationConfigApplicationContext context; + + @Test + public void setsDependsOn() { + this.context = new AnnotationConfigApplicationContext(Config.class); + assertDependsOn("dataSource"); + assertDependsOn("connectionFactory"); + assertDependsOn("userTransactionManager", "dataSource", "connectionFactory"); + assertDependsOn("messageDrivenContainer", "userTransactionManager"); + this.context.close(); + } + + private void assertDependsOn(String bean, String... expected) { + BeanDefinition definition = this.context.getBeanDefinition(bean); + if (definition.getDependsOn() == null) { + assertThat(expected).as("No dependsOn expected for " + bean).isEmpty(); + return; + } + HashSet dependsOn = new HashSet<>(Arrays.asList(definition.getDependsOn())); + assertThat(dependsOn).isEqualTo(new HashSet<>(Arrays.asList(expected))); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + DataSource dataSource() { + return mock(DataSource.class); + } + + @Bean + ConnectionFactory connectionFactory() { + return mock(ConnectionFactory.class); + } + + @Bean + UserTransactionManager userTransactionManager() { + return mock(UserTransactionManager.class); + } + + @Bean + MessageDrivenContainer messageDrivenContainer() { + return mock(MessageDrivenContainer.class); + } + + @Bean + static AtomikosDependsOnBeanFactoryPostProcessor atomikosPostProcessor() { + return new AtomikosDependsOnBeanFactoryPostProcessor(); + } + + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosPropertiesTestJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosPropertiesTestJUnit.java new file mode 100644 index 000000000..6add27e6f --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosPropertiesTestJUnit.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.junit.Test; + +/** + * Tests for {@link AtomikosProperties}. + */ +public class AtomikosPropertiesTestJUnit { + + private AtomikosProperties properties = new AtomikosProperties(); + + @Test + public void testProperties() { + this.properties.setService("service"); + this.properties.setMaxTimeout(Duration.ofMillis(1)); + this.properties.setDefaultJtaTimeout(Duration.ofMillis(2)); + this.properties.setMaxActives(3); + this.properties.setEnableLogging(true); + this.properties.setTransactionManagerUniqueName("uniqueName"); + this.properties.setSerialJtaTransactions(true); + this.properties.setAllowSubTransactions(false); + this.properties.setForceShutdownOnVmExit(true); + this.properties.setDefaultMaxWaitTimeOnShutdown(20l); + this.properties.setLogBaseName("logBaseName"); + this.properties.setLogBaseDir("logBaseDir"); + this.properties.setCheckpointInterval(4l); + this.properties.getRecovery().setForgetOrphanedLogEntriesDelay(Duration.ofMillis(2000)); + this.properties.getRecovery().setDelay(Duration.ofMillis(3000)); + this.properties.getRecovery().setMaxRetries(10); + this.properties.getRecovery().setRetryInterval(Duration.ofMillis(4000)); + this.properties.setThrowOnHeuristic(true); + assertThat(this.properties.asProperties().size()).isEqualTo(18); + assertProperty("com.atomikos.icatch.service", "service"); + assertProperty("com.atomikos.icatch.max_timeout", "1"); + assertProperty("com.atomikos.icatch.default_jta_timeout", "2"); + assertProperty("com.atomikos.icatch.max_actives", "3"); + assertProperty("com.atomikos.icatch.enable_logging", "true"); + assertProperty("com.atomikos.icatch.tm_unique_name", "uniqueName"); + assertProperty("com.atomikos.icatch.serial_jta_transactions", "true"); + assertProperty("com.atomikos.icatch.allow_subtransactions", "false"); + assertProperty("com.atomikos.icatch.force_shutdown_on_vm_exit", "true"); + assertProperty("com.atomikos.icatch.default_max_wait_time_on_shutdown", "20"); + assertProperty("com.atomikos.icatch.log_base_name", "logBaseName"); + assertProperty("com.atomikos.icatch.log_base_dir", "logBaseDir"); + assertProperty("com.atomikos.icatch.checkpoint_interval", "4"); + assertProperty("com.atomikos.icatch.forget_orphaned_log_entries_delay", "2000"); + assertProperty("com.atomikos.icatch.recovery_delay", "3000"); + assertProperty("com.atomikos.icatch.oltp_max_retries", "10"); + assertProperty("com.atomikos.icatch.oltp_retry_interval", "4000"); + assertProperty("com.atomikos.icatch.throw_on_heuristic", "true"); + } + + @Test + public void testDefaultProperties() { + Properties properties = this.properties.asProperties(); + List keys = new ArrayList<>(); + keys.add("com.atomikos.icatch.max_timeout"); + keys.add("com.atomikos.icatch.default_jta_timeout"); + keys.add("com.atomikos.icatch.max_actives"); + keys.add("com.atomikos.icatch.enable_logging"); + keys.add("com.atomikos.icatch.serial_jta_transactions"); + keys.add("com.atomikos.icatch.allow_subtransactions"); + keys.add("com.atomikos.icatch.force_shutdown_on_vm_exit"); + keys.add("com.atomikos.icatch.default_max_wait_time_on_shutdown"); + keys.add("com.atomikos.icatch.log_base_name"); + keys.add("com.atomikos.icatch.checkpoint_interval"); + keys.add("com.atomikos.icatch.forget_orphaned_log_entries_delay"); + keys.add("com.atomikos.icatch.oltp_max_retries"); + keys.add("com.atomikos.icatch.oltp_retry_interval"); + keys.add("com.atomikos.icatch.throw_on_heuristic"); + assertThat(properties).isEmpty(); + } + + private void assertProperty(String key, String value) { + assertThat(this.properties.asProperties().getProperty(key)).isEqualTo(value); + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapperTestJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapperTestJUnit.java new file mode 100644 index 000000000..b9563b2fd --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapperTestJUnit.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.jms.ConnectionFactory; +import javax.jms.XAConnectionFactory; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosXAConnectionFactoryWrapper}. + */ +public class AtomikosXAConnectionFactoryWrapperTestJUnit { + + @Test + public void wrap() { + XAConnectionFactory connectionFactory = mock(XAConnectionFactory.class); + AtomikosXAConnectionFactoryWrapper wrapper = new AtomikosXAConnectionFactoryWrapper(); + ConnectionFactory wrapped = wrapper.wrapConnectionFactory(connectionFactory); + assertThat(wrapped).isInstanceOf(AtomikosConnectionFactoryBean.class); + assertThat(((AtomikosConnectionFactoryBean) wrapped).getXaConnectionFactory()).isSameAs(connectionFactory); + } + +} diff --git a/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosXADataSourceWrapperTestJUnit.java b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosXADataSourceWrapperTestJUnit.java new file mode 100644 index 000000000..bff614b58 --- /dev/null +++ b/public/spring-boot2/transactions-spring-boot/src/test/java/com/atomikos/spring/AtomikosXADataSourceWrapperTestJUnit.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.sql.DataSource; +import javax.sql.XADataSource; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosXADataSourceWrapper}. + */ +public class AtomikosXADataSourceWrapperTestJUnit { + + @Test + public void wrap() throws Exception { + XADataSource dataSource = mock(XADataSource.class); + AtomikosXADataSourceWrapper wrapper = new AtomikosXADataSourceWrapper(); + DataSource wrapped = wrapper.wrapDataSource(dataSource); + assertThat(wrapped).isInstanceOf(AtomikosDataSourceBean.class); + assertThat(((AtomikosDataSourceBean) wrapped).getXaDataSource()).isSameAs(dataSource); + } + +} diff --git a/public/spring-boot3/pom.xml b/public/spring-boot3/pom.xml new file mode 100644 index 000000000..3d983ec88 --- /dev/null +++ b/public/spring-boot3/pom.xml @@ -0,0 +1,62 @@ + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + spring-boot3 + pom + + 17 + 17 + 6.0.3 + 3.0.1 + + + transactions-spring-boot3 + transactions-spring-boot3-starter + transactions-spring-boot3-integration-tests + + + + + org.apache.maven.plugins + maven-toolchains-plugin + + + + toolchain + + + + + 17 + + + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + pom + import + ${boot.version} + + + + + + org.mockito + mockito-core + 4.4.0 + test + + + diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/.gitignore b/public/spring-boot3/transactions-spring-boot3-integration-tests/.gitignore new file mode 100644 index 000000000..8fbc30c96 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/.gitignore @@ -0,0 +1 @@ +transaction-logs diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/pom.xml b/public/spring-boot3/transactions-spring-boot3-integration-tests/pom.xml new file mode 100644 index 000000000..ec9e3f02c --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.atomikos + spring-boot3 + 6.0.1-SNAPSHOT + + + 17 + 17 + true + + transactions-spring-boot3-integration-tests + Transactions Spring Boot Integration Tests + + + com.atomikos + transactions-spring-boot3-starter + 6.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-artemis + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework + spring-jms + + + com.h2database + h2 + + + org.apache.activemq + artemis-jakarta-server + + + org.springframework.boot + spring-boot-starter-test + + + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.1.0 + + + + toolchain + + + + + 17 + + + + + + + + + diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/run.sh b/public/spring-boot3/transactions-spring-boot3-integration-tests/run.sh new file mode 100644 index 000000000..3be47977a --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/run.sh @@ -0,0 +1,9 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + + mvn exec:java -Dexec.mainClass="com.atomikos.spring.integrationtest.SampleAtomikosApplication" diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Account.java b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Account.java new file mode 100644 index 000000000..956a12a8f --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Account.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Account { + + @Id + @GeneratedValue + private Long id; + + private String username; + + Account() { + } + + public Account(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountRepository.java b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountRepository.java new file mode 100644 index 000000000..a850470ac --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountRepository.java @@ -0,0 +1,15 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccountRepository extends JpaRepository { + +} diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountService.java b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountService.java new file mode 100644 index 000000000..5dcc6146e --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/AccountService.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import jakarta.transaction.Transactional; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class AccountService { + + private final JmsTemplate jmsTemplate; + + private final AccountRepository accountRepository; + + public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) { + this.jmsTemplate = jmsTemplate; + this.accountRepository = accountRepository; + } + + public void createAccountAndNotify(String username) { + this.jmsTemplate.convertAndSend("accounts", username); + this.accountRepository.save(new Account(username)); + if ("error".equals(username)) { + throw new RuntimeException("Simulated error"); + } + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Messages.java b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Messages.java new file mode 100644 index 000000000..96e114c96 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/Messages.java @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class Messages { + + @JmsListener(destination = "accounts") + public void onMessage(String content) { + System.out.println("----> " + content); + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/SampleAtomikosApplication.java b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/SampleAtomikosApplication.java new file mode 100644 index 000000000..b442d2c5a --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/java/com/atomikos/spring/integrationtest/SampleAtomikosApplication.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import java.io.Closeable; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +@SpringBootApplication +public class SampleAtomikosApplication { + + public static void main(String... args) throws Exception { + ApplicationContext context = SpringApplication.run(SampleAtomikosApplication.class, args); + AccountService service = context.getBean(AccountService.class); + AccountRepository repository = context.getBean(AccountRepository.class); + service.createAccountAndNotify("josh"); + System.out.println("Count is " + repository.count()); + try { + service.createAccountAndNotify("error"); + } + catch (Exception ex) { + System.out.println(ex.getMessage()); + } + System.out.println("Count is " + repository.count()); + Thread.sleep(100); + ((Closeable) context).close(); + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/resources/application.properties b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/resources/application.properties new file mode 100644 index 000000000..62ba22b88 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/main/resources/application.properties @@ -0,0 +1,11 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +logging.level.com.atomikos=DEBUG +spring.artemis.embedded.queues=accounts +spring.jpa.open-in-view=true diff --git a/public/spring-boot3/transactions-spring-boot3-integration-tests/src/test/java/com/atomikos/spring/integrationtest/SampleAtomikosApplicationTests.java b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/test/java/com/atomikos/spring/integrationtest/SampleAtomikosApplicationTests.java new file mode 100644 index 000000000..f763a0baf --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-integration-tests/src/test/java/com/atomikos/spring/integrationtest/SampleAtomikosApplicationTests.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring.integrationtest; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.util.StringUtils; + +/** + * Basic integration tests for demo application. + */ +@ExtendWith(OutputCaptureExtension.class) +class SampleAtomikosApplicationTests { + + @Test + void testTransactionRollback(CapturedOutput output) throws Exception { + SampleAtomikosApplication.main(); + assertThat(output).satisfies(numberOfOccurrences("---->", 1)); + assertThat(output).satisfies(numberOfOccurrences("----> josh", 1)); + assertThat(output).satisfies(numberOfOccurrences("Count is 1", 2)); + assertThat(output).satisfies(numberOfOccurrences("Simulated error", 1)); + } + + @Test + void testTransactionRollbackWithoutSpringBoot23JtaAutoConfiguration(CapturedOutput output) throws Exception { + SampleAtomikosApplication.main("--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration"); + assertThat(output).satisfies(numberOfOccurrences("---->", 2)); + assertThat(output).satisfies(numberOfOccurrences("----> josh", 1)); + assertThat(output).satisfies(numberOfOccurrences("Count is 1", 2)); + assertThat(output).satisfies(numberOfOccurrences("Simulated error", 1)); + } + + private Consumer numberOfOccurrences(String substring, int expectedCount) { + return (charSequence) -> { + int count = StringUtils.countOccurrencesOf(charSequence.toString(), substring); + assertThat(count).isEqualTo(expectedCount); + }; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3-starter/pom.xml b/public/spring-boot3/transactions-spring-boot3-starter/pom.xml new file mode 100644 index 000000000..1ecc91419 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3-starter/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + com.atomikos + spring-boot3 + 6.0.1-SNAPSHOT + + transactions-spring-boot3-starter + Transactions Spring Boot Starter + + + + com.atomikos + transactions-spring-boot3 + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + jakarta + + + + + + org.apache.maven.plugins + maven-source-plugin + + true + + + + org.apache.maven.plugins + maven-toolchains-plugin + + + + toolchain + + + + + 17 + + + + + + + + + diff --git a/public/spring-boot3/transactions-spring-boot3/.gitignore b/public/spring-boot3/transactions-spring-boot3/.gitignore new file mode 100644 index 000000000..8fbc30c96 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/.gitignore @@ -0,0 +1 @@ +transaction-logs diff --git a/public/spring-boot3/transactions-spring-boot3/pom.xml b/public/spring-boot3/transactions-spring-boot3/pom.xml new file mode 100644 index 000000000..152746cf5 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + com.atomikos + spring-boot3 + 6.0.1-SNAPSHOT + + + 17 + 17 + + transactions-spring-boot3 + Transactions Spring Boot + + + jakarta.jms + jakarta.jms-api + + + + jakarta.transaction + jakarta.transaction-api + + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + jakarta + + + org.springframework + spring-tx + ${spring.version} + + + org.springframework.boot + spring-boot + ${boot.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${boot.version} + true + + + org.springframework.boot + spring-boot-configuration-processor + ${boot.version} + true + + + + + + org.apache.maven.plugins + maven-toolchains-plugin + + + + toolchain + + + + + 17 + + + + + + + + + diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosAutoConfiguration.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosAutoConfiguration.java new file mode 100644 index 000000000..bbbaf1e41 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosAutoConfiguration.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.util.Properties; + +import jakarta.jms.Message; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.UserTransaction; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jdbc.XADataSourceWrapper; +import org.springframework.boot.jms.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.jta.JtaTransactionManager; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.config.UserTransactionServiceImp; +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Atomikos JTA. + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({ SpringJtaAtomikosProperties.class, AtomikosProperties.class}) +@ConditionalOnClass({ JtaTransactionManager.class, UserTransactionManager.class }) +@ConditionalOnMissingBean(org.springframework.transaction.TransactionManager.class) +@AutoConfigureBefore( + value = { XADataSourceAutoConfiguration.class, ArtemisAutoConfiguration.class, HibernateJpaAutoConfiguration.class }, + name = "org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration") +public class AtomikosAutoConfiguration { + + @Bean(initMethod = "init", destroyMethod = "shutdownWait") + @ConditionalOnMissingBean(UserTransactionService.class) + UserTransactionServiceImp userTransactionService(SpringJtaAtomikosProperties springJtaAtomikosProperties, AtomikosProperties atomikosProperties) { + Properties properties = new Properties(); + properties.putAll(springJtaAtomikosProperties.asProperties()); + properties.putAll(atomikosProperties.asProperties()); + return new UserTransactionServiceImp(properties); + } + + + @Bean(initMethod = "init", destroyMethod = "close") + @ConditionalOnMissingBean(TransactionManager.class) + UserTransactionManager atomikosTransactionManager(UserTransactionService userTransactionService) throws Exception { + UserTransactionManager manager = new UserTransactionManager(); + manager.setStartupTransactionService(false); + manager.setForceShutdown(true); + return manager; + } + + @Bean + @ConditionalOnMissingBean(XADataSourceWrapper.class) + AtomikosXADataSourceWrapper xaDataSourceWrapper() { + return new AtomikosXADataSourceWrapper(); + } + + @Bean + @ConditionalOnMissingBean + static AtomikosDependsOnBeanFactoryPostProcessor atomikosDependsOnBeanFactoryPostProcessor() { + return new AtomikosDependsOnBeanFactoryPostProcessor(); + } + + @Bean + JtaTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager, + ObjectProvider transactionManagerCustomizers) { + JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, transactionManager); + transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(jtaTransactionManager)); + return jtaTransactionManager; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Message.class) + static class AtomikosJtaJmsConfiguration { + + @Bean + @ConditionalOnMissingBean(XAConnectionFactoryWrapper.class) + AtomikosXAConnectionFactoryWrapper xaConnectionFactoryWrapper() { + return new AtomikosXAConnectionFactoryWrapper(); + } + + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosConnectionFactoryBean.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosConnectionFactoryBean.java new file mode 100644 index 000000000..a1af32944 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosConnectionFactoryBean.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Spring friendly version of {@link com.atomikos.jms.AtomikosConnectionFactoryBean}. + */ +@SuppressWarnings("serial") +@ConfigurationProperties(prefix = "spring.jta.atomikos.connectionfactory") +public class AtomikosConnectionFactoryBean extends com.atomikos.jms.AtomikosConnectionFactoryBean + implements BeanNameAware, InitializingBean, DisposableBean { + + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueResourceName())) { + setUniqueResourceName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourceBean.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourceBean.java new file mode 100644 index 000000000..01ed82c65 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourceBean.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Spring friendly version of {@link com.atomikos.jdbc.AtomikosDataSourceBean}. + */ +@SuppressWarnings("serial") +@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource") +public class AtomikosDataSourceBean extends com.atomikos.jdbc.AtomikosDataSourceBean + implements BeanNameAware, InitializingBean, DisposableBean { + + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueResourceName())) { + setUniqueResourceName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourceBeanMetadata.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourceBeanMetadata.java new file mode 100644 index 000000000..d145b55e4 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourceBeanMetadata.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.boot.jdbc.metadata.AbstractDataSourcePoolMetadata; + +import com.atomikos.jdbc.AtomikosDataSourceBean; + +public class AtomikosDataSourceBeanMetadata extends AbstractDataSourcePoolMetadata { + + + public AtomikosDataSourceBeanMetadata(AtomikosDataSourceBean dataSource) { + super(dataSource); + } + + + @Override + public Integer getActive() { + return getDataSource().poolTotalSize() - getDataSource().poolAvailableSize(); + } + + @Override + public Integer getMax() { + return getDataSource().getMaxPoolSize(); + } + + @Override + public Integer getMin() { + return getDataSource().getMinPoolSize(); + } + + @Override + public String getValidationQuery() { + return getDataSource().getTestQuery(); + } + + @Override + public Boolean getDefaultAutoCommit() { + return false; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourcePoolMetadataProvidersConfiguration.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourcePoolMetadataProvidersConfiguration.java new file mode 100644 index 000000000..b05592386 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDataSourcePoolMetadataProvidersConfiguration.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.jdbc.DataSourceUnwrapper; +import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.atomikos.jdbc.AtomikosDataSourceBean; +import com.atomikos.jdbc.AtomikosNonXADataSourceBean; + +/** + * Register the {@link AtomikosDataSourcePoolMetadataProvidersConfiguration} for the AtomikosDataSourceBeans. For use in the Spring Boot actuator: + * https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-jdbc. + * This configuration must be explicitly imported: + *

+ * {@code @Import({ AtomikosDataSourcePoolMetadataProvidersConfiguration.class}) } + *

+ */ +@Configuration +public class AtomikosDataSourcePoolMetadataProvidersConfiguration { + + @Configuration + @ConditionalOnClass(AtomikosDataSourceBean.class) + static class AtomikosDataSourceBeanMetadataProviderConfiguration { + + @Bean + public DataSourcePoolMetadataProvider atomikosDataSourceBeanMetadataProvider() { + + return (dataSource) -> { + AtomikosDataSourceBean atomikosDataSource = DataSourceUnwrapper.unwrap(dataSource, AtomikosDataSourceBean.class); + if (atomikosDataSource != null) { + return new AtomikosDataSourceBeanMetadata(atomikosDataSource); + } + return null; + }; + } + } + + @Configuration + @ConditionalOnClass(AtomikosNonXADataSourceBean.class) + static class AtomikosNonXADataSourceBeanMetadataProviderConfiguration { + + @Bean + public DataSourcePoolMetadataProvider atomikosNonXADataSourceBeanMetadataProvider() { + + return (dataSource) -> { + AtomikosNonXADataSourceBean atomikosDataSource = DataSourceUnwrapper.unwrap(dataSource, AtomikosNonXADataSourceBean.class); + if (atomikosDataSource != null) { + return new AtomikosNonXADataSourceBeanMetadata(atomikosDataSource); + } + return null; + }; + } + } +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessor.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessor.java new file mode 100644 index 000000000..70d5579f5 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessor.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.atomikos.icatch.jta.UserTransactionManager; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; + +/** + * {@link BeanFactoryPostProcessor} to automatically setup the recommended + * {@link BeanDefinition#setDependsOn(String[]) dependsOn} settings for + * correct Atomikos + * ordering. + */ +public class AtomikosDependsOnBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered { + + private static final String[] NO_BEANS = {}; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + String[] transactionManagers = beanFactory.getBeanNamesForType(UserTransactionManager.class, true, false); + for (String transactionManager : transactionManagers) { + addTransactionManagerDependencies(beanFactory, transactionManager); + } + addMessageDrivenContainerDependencies(beanFactory, transactionManagers); + } + + private void addTransactionManagerDependencies(ConfigurableListableBeanFactory beanFactory, + String transactionManager) { + BeanDefinition bean = beanFactory.getBeanDefinition(transactionManager); + Set dependsOn = new LinkedHashSet<>(asList(bean.getDependsOn())); + int initialSize = dependsOn.size(); + addDependencies(beanFactory, "jakarta.jms.ConnectionFactory", dependsOn); + addDependencies(beanFactory, "javax.sql.DataSource", dependsOn); + if (dependsOn.size() != initialSize) { + bean.setDependsOn(StringUtils.toStringArray(dependsOn)); + } + } + + private void addMessageDrivenContainerDependencies(ConfigurableListableBeanFactory beanFactory, + String[] transactionManagers) { + String[] messageDrivenContainers = getBeanNamesForType(beanFactory, + "com.atomikos.jms.extra.MessageDrivenContainer"); + for (String messageDrivenContainer : messageDrivenContainers) { + BeanDefinition bean = beanFactory.getBeanDefinition(messageDrivenContainer); + Set dependsOn = new LinkedHashSet<>(asList(bean.getDependsOn())); + dependsOn.addAll(asList(transactionManagers)); + bean.setDependsOn(StringUtils.toStringArray(dependsOn)); + } + } + + private void addDependencies(ConfigurableListableBeanFactory beanFactory, String type, Set dependsOn) { + dependsOn.addAll(asList(getBeanNamesForType(beanFactory, type))); + } + + private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory, String type) { + try { + return beanFactory.getBeanNamesForType(Class.forName(type), true, false); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + // Ignore + } + return NO_BEANS; + } + + private List asList(String[] array) { + return (array != null) ? Arrays.asList(array) : Collections.emptyList(); + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosNonXADataSourceBeanMetadata.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosNonXADataSourceBeanMetadata.java new file mode 100644 index 000000000..8412286f2 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosNonXADataSourceBeanMetadata.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.springframework.boot.jdbc.metadata.AbstractDataSourcePoolMetadata; + +import com.atomikos.jdbc.AtomikosNonXADataSourceBean; + +public class AtomikosNonXADataSourceBeanMetadata extends AbstractDataSourcePoolMetadata { + + + public AtomikosNonXADataSourceBeanMetadata(AtomikosNonXADataSourceBean dataSource) { + super(dataSource); + } + + + @Override + public Integer getActive() { + return getDataSource().poolTotalSize() - getDataSource().poolAvailableSize(); + } + + @Override + public Integer getMax() { + return getDataSource().getMaxPoolSize(); + } + + @Override + public Integer getMin() { + return getDataSource().getMinPoolSize(); + } + + @Override + public String getValidationQuery() { + return getDataSource().getTestQuery(); + } + + @Override + public Boolean getDefaultAutoCommit() { + return false; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosProperties.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosProperties.java new file mode 100644 index 000000000..eadfba2d5 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosProperties.java @@ -0,0 +1,411 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.time.Duration; +import java.util.Properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Bean friendly variant of + * Atomikos configuration + * properties. Allows for setter based configuration and is amiable to relaxed data + * binding. + * @see #asProperties() + */ +@ConfigurationProperties(prefix = "atomikos.properties") +public class AtomikosProperties { + + /** + * Transaction manager implementation that should be started. + */ + private String service; + + /** + * Maximum timeout that can be allowed for transactions. + */ + private Duration maxTimeout; + + /** + * Default timeout for JTA transactions. + */ + private Duration defaultJtaTimeout; + + /** + * Maximum number of active transactions. + */ + private Integer maxActives; + + /** + * Whether to enable disk logging. + */ + private Boolean enableLogging; + + /** + * The transaction manager's unique name. Defaults to the machine's IP address. If you + * plan to run more than one transaction manager against one database you must set + * this property to a unique value. + */ + private String transactionManagerUniqueName; + + /** + * Whether sub-transactions should be joined when possible. + */ + private Boolean serialJtaTransactions; + + /** + * Specify whether sub-transactions are allowed. + */ + private Boolean allowSubTransactions; + + /** + * Whether a VM shutdown should trigger forced shutdown of the transaction core. + */ + private Boolean forceShutdownOnVmExit; + + /** + * How long should normal shutdown (no-force) wait for transactions to complete. + */ + private Long defaultMaxWaitTimeOnShutdown; + + /** + * Transactions log file base name. + */ + private String logBaseName; + + /** + * Directory in which the log files should be stored. Defaults to the current working + * directory. + */ + private String logBaseDir; + + /** + * Interval between checkpoints, expressed as the number of log writes between two + * checkpoints. A checkpoint reduces the log file size at the expense of adding some + * overhead in the runtime. + */ + private Long checkpointInterval; + + private Boolean throwOnHeuristic; + + + private final Recovery recovery = new Recovery(); + + /** + * Specifies the transaction manager implementation that should be started. There is + * no default value and this must be set. Generally, + * {@literal com.atomikos.icatch.standalone.UserTransactionServiceFactory} is the + * value you should set. + * @param service the service + */ + public void setService(String service) { + this.service = service; + } + + public String getService() { + return this.service; + } + + /** + * Specifies the maximum timeout that can be allowed for transactions. Defaults to + * {@literal 300000}. This means that calls to UserTransaction.setTransactionTimeout() + * with a value higher than configured here will be max'ed to this value. + * @param maxTimeout the max timeout + */ + public void setMaxTimeout(Duration maxTimeout) { + this.maxTimeout = maxTimeout; + } + + public Duration getMaxTimeout() { + return this.maxTimeout; + } + + /** + * The default timeout for JTA transactions (optional, defaults to {@literal 10000} + * ms). + * @param defaultJtaTimeout the default JTA timeout + */ + public void setDefaultJtaTimeout(Duration defaultJtaTimeout) { + this.defaultJtaTimeout = defaultJtaTimeout; + } + + public Duration getDefaultJtaTimeout() { + return this.defaultJtaTimeout; + } + + /** + * Specifies the maximum number of active transactions. Defaults to {@literal 50}. A + * negative value means infinite amount. You will get an {@code IllegalStateException} + * with error message "Max number of active transactions reached" if you call + * {@code UserTransaction.begin()} while there are already n concurrent transactions + * running, n being this value. + * @param maxActives the max activities + */ + public void setMaxActives(Integer maxActives) { + this.maxActives = maxActives; + } + + public Integer getMaxActives() { + return this.maxActives; + } + + /** + * Specifies if disk logging should be enabled or not. Defaults to true. It is useful + * for JUnit testing, or to profile code without seeing the transaction manager's + * activity as a hot spot but this should never be disabled on production or data + * integrity cannot be guaranteed. + * @param enableLogging if logging is enabled + */ + public void setEnableLogging(Boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public Boolean isEnableLogging() { + return this.enableLogging; + } + + /** + * Specifies the transaction manager's unique name. Defaults to the machine's IP + * address. If you plan to run more than one transaction manager against one database + * you must set this property to a unique value or you might run into duplicate + * transaction ID (XID) problems that can be quite subtle (example: + * {@literal https://fogbugz.atomikos.com/default.asp?community.6.2225.7}). If + * multiple instances need to use the same properties file then the easiest way to + * ensure uniqueness for this property is by referencing a system property specified + * at VM startup. + * @param uniqueName the unique name + */ + public void setTransactionManagerUniqueName(String uniqueName) { + this.transactionManagerUniqueName = uniqueName; + } + + public String getTransactionManagerUniqueName() { + return this.transactionManagerUniqueName; + } + + /** + * Specifies if subtransactions should be joined when possible. Defaults to true. When + * false, no attempt to call {@code XAResource.start(TM_JOIN)} will be made for + * different but related subtransactions. This setting has no effect on resource + * access within one and the same transaction. If you don't use subtransactions then + * this setting can be ignored. + * @param serialJtaTransactions if serial JTA transactions are supported + */ + public void setSerialJtaTransactions(Boolean serialJtaTransactions) { + this.serialJtaTransactions = serialJtaTransactions; + } + + public Boolean isSerialJtaTransactions() { + return this.serialJtaTransactions; + } + + public void setAllowSubTransactions(Boolean allowSubTransactions) { + this.allowSubTransactions = allowSubTransactions; + } + + public Boolean isAllowSubTransactions() { + return this.allowSubTransactions; + } + + /** + * Specifies whether VM shutdown should trigger forced shutdown of the transaction + * core. Defaults to false. + * @param forceShutdownOnVmExit if VM shutdown should be forced + */ + public void setForceShutdownOnVmExit(Boolean forceShutdownOnVmExit) { + this.forceShutdownOnVmExit = forceShutdownOnVmExit; + } + + public Boolean isForceShutdownOnVmExit() { + return this.forceShutdownOnVmExit; + } + + /** + * Specifies how long should a normal shutdown (no-force) wait for transactions to + * complete. Defaults to {@literal Long.MAX_VALUE}. + * @param defaultMaxWaitTimeOnShutdown the default max wait time on shutdown + */ + public void setDefaultMaxWaitTimeOnShutdown(Long defaultMaxWaitTimeOnShutdown) { + this.defaultMaxWaitTimeOnShutdown = defaultMaxWaitTimeOnShutdown; + } + + public Long getDefaultMaxWaitTimeOnShutdown() { + return this.defaultMaxWaitTimeOnShutdown; + } + + /** + * Specifies the transactions log file base name. Defaults to {@literal tmlog}. The + * transactions logs are stored in files using this name appended with a number and + * the extension {@literal .log}. At checkpoint, a new transactions log file is + * created and the number is incremented. + * @param logBaseName the log base name + */ + public void setLogBaseName(String logBaseName) { + this.logBaseName = logBaseName; + } + + public String getLogBaseName() { + return this.logBaseName; + } + + /** + * Specifies the directory in which the log files should be stored. Defaults to the + * current working directory. This directory should be a stable storage like a SAN, + * RAID or at least backed up location. The transactions logs files are as important + * as the data themselves to guarantee consistency in case of failures. + * @param logBaseDir the log base dir + */ + public void setLogBaseDir(String logBaseDir) { + this.logBaseDir = logBaseDir; + } + + public String getLogBaseDir() { + return this.logBaseDir; + } + + /** + * Specifies the interval between checkpoints. A checkpoint reduces the log file size + * at the expense of adding some overhead in the runtime. Defaults to {@literal 500}. + * @param checkpointInterval the checkpoint interval + */ + public void setCheckpointInterval(Long checkpointInterval) { + this.checkpointInterval = checkpointInterval; + } + + public Long getCheckpointInterval() { + return this.checkpointInterval; + } + + + public Recovery getRecovery() { + return this.recovery; + } + + public void setThrowOnHeuristic(Boolean throwOnHeuristic) { + this.throwOnHeuristic = throwOnHeuristic; + } + + public Boolean isThrowOnHeuristic() { + return this.throwOnHeuristic; + } + + /** + * Returns the properties as a {@link Properties} object that can be used with + * Atomikos. + * @return the properties + */ + public Properties asProperties() { + Properties properties = new Properties(); + set(properties, "service", getService()); + set(properties, "max_timeout", getMaxTimeout()); + set(properties, "default_jta_timeout", getDefaultJtaTimeout()); + set(properties, "max_actives", getMaxActives()); + set(properties, "enable_logging", isEnableLogging()); + set(properties, "tm_unique_name", getTransactionManagerUniqueName()); + set(properties, "serial_jta_transactions", isSerialJtaTransactions()); + set(properties, "allow_subtransactions", isAllowSubTransactions()); + set(properties, "force_shutdown_on_vm_exit", isForceShutdownOnVmExit()); + set(properties, "default_max_wait_time_on_shutdown", getDefaultMaxWaitTimeOnShutdown()); + set(properties, "log_base_name", getLogBaseName()); + set(properties, "log_base_dir", getOrDeduceLogBaseDir()); + set(properties, "checkpoint_interval", getCheckpointInterval()); + set(properties, "forget_orphaned_log_entries_delay", recovery.getForgetOrphanedLogEntriesDelay()); + set(properties, "recovery_delay", recovery.getDelay()); + set(properties, "oltp_max_retries", recovery.getMaxRetries()); + set(properties, "oltp_retry_interval", recovery.getRetryInterval()); + set(properties, "throw_on_heuristic", isThrowOnHeuristic()); + return properties; + } + + private String getOrDeduceLogBaseDir() { + if(StringUtils.hasText(this.logBaseDir)) { + return this.logBaseDir; + } + return null; + + } + + private void set(Properties properties, String key, Object value) { + String id = "com.atomikos.icatch." + key; + if (value != null && !properties.containsKey(id)) { + properties.setProperty(id, asString(value)); + } + } + + private String asString(Object value) { + if (value instanceof Duration) { + return String.valueOf(((Duration) value).toMillis()); + } + return value.toString(); + } + + /** + * Recovery specific settings. + */ + public static class Recovery { + + /** + * Delay after which recovery can cleanup pending ('orphaned') log entries. + */ + private Duration forgetOrphanedLogEntriesDelay; + + /** + * Delay between two recovery scans. + */ + private Duration delay; + + /** + * Number of retry attempts to commit the transaction before throwing an + * exception. + */ + private Integer maxRetries; + + /** + * Delay between retry attempts. + */ + private Duration retryInterval; + + public Duration getForgetOrphanedLogEntriesDelay() { + return this.forgetOrphanedLogEntriesDelay; + } + + public void setForgetOrphanedLogEntriesDelay(Duration forgetOrphanedLogEntriesDelay) { + this.forgetOrphanedLogEntriesDelay = forgetOrphanedLogEntriesDelay; + } + + public Duration getDelay() { + return this.delay; + } + + public void setDelay(Duration delay) { + this.delay = delay; + } + + public Integer getMaxRetries() { + return this.maxRetries; + } + + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + } + + public Duration getRetryInterval() { + return this.retryInterval; + } + + public void setRetryInterval(Duration retryInterval) { + this.retryInterval = retryInterval; + } + + + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapper.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapper.java new file mode 100644 index 000000000..6b5431c2c --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapper.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.XAConnectionFactory; + +import org.springframework.boot.jms.XAConnectionFactoryWrapper; + +/** + * {@link XAConnectionFactoryWrapper} that uses an {@link AtomikosConnectionFactoryBean} + * to wrap a {@link XAConnectionFactory}. + */ +public class AtomikosXAConnectionFactoryWrapper implements XAConnectionFactoryWrapper { + + @Override + public ConnectionFactory wrapConnectionFactory(XAConnectionFactory connectionFactory) { + AtomikosConnectionFactoryBean bean = new AtomikosConnectionFactoryBean(); + bean.setXaConnectionFactory(connectionFactory); + return bean; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosXADataSourceWrapper.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosXADataSourceWrapper.java new file mode 100644 index 000000000..7d2c02c4f --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/AtomikosXADataSourceWrapper.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.sql.XADataSource; + +import org.springframework.boot.jdbc.XADataSourceWrapper; + +/** + * {@link XADataSourceWrapper} that uses an {@link AtomikosDataSourceBean} to wrap a + * {@link XADataSource}. + */ +public class AtomikosXADataSourceWrapper implements XADataSourceWrapper { + + @Override + public AtomikosDataSourceBean wrapDataSource(XADataSource dataSource) throws Exception { + AtomikosDataSourceBean bean = new AtomikosDataSourceBean(); + bean.setXaDataSource(dataSource); + return bean; + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/SpringJtaAtomikosProperties.java b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/SpringJtaAtomikosProperties.java new file mode 100644 index 000000000..aa11902c5 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/java/com/atomikos/spring/SpringJtaAtomikosProperties.java @@ -0,0 +1,412 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.time.Duration; +import java.util.Properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Bean friendly variant of + * Atomikos configuration + * properties. Allows for setter based configuration and is amiable to relaxed data + * binding. + * @see #asProperties() + */ +@ConfigurationProperties(prefix = "spring.jta.atomikos.properties") +public class SpringJtaAtomikosProperties { + + /** + * Transaction manager implementation that should be started. + */ + private String service; + + /** + * Maximum timeout that can be allowed for transactions. + */ + private Duration maxTimeout; + + /** + * Default timeout for JTA transactions. + */ + private Duration defaultJtaTimeout; + + /** + * Maximum number of active transactions. + */ + private Integer maxActives; + + /** + * Whether to enable disk logging. + */ + private Boolean enableLogging; + + /** + * The transaction manager's unique name. Defaults to the machine's IP address. If you + * plan to run more than one transaction manager against one database you must set + * this property to a unique value. + */ + private String transactionManagerUniqueName; + + /** + * Whether sub-transactions should be joined when possible. + */ + private Boolean serialJtaTransactions; + + /** + * Specify whether sub-transactions are allowed. + */ + private Boolean allowSubTransactions; + + /** + * Whether a VM shutdown should trigger forced shutdown of the transaction core. + */ + private Boolean forceShutdownOnVmExit; + + /** + * How long should normal shutdown (no-force) wait for transactions to complete. + */ + private Long defaultMaxWaitTimeOnShutdown; + + /** + * Transactions log file base name. + */ + private String logBaseName; + + /** + * Directory in which the log files should be stored. Defaults to the current working + * directory. + */ + private String logBaseDir; + + /** + * Interval between checkpoints, expressed as the number of log writes between two + * checkpoints. A checkpoint reduces the log file size at the expense of adding some + * overhead in the runtime. + */ + private Long checkpointInterval; + + private Boolean throwOnHeuristic; + + + private final Recovery recovery = new Recovery(); + + /** + * Specifies the transaction manager implementation that should be started. There is + * no default value and this must be set. Generally, + * {@literal com.atomikos.icatch.standalone.UserTransactionServiceFactory} is the + * value you should set. + * @param service the service + */ + public void setService(String service) { + this.service = service; + } + + public String getService() { + return this.service; + } + + /** + * Specifies the maximum timeout that can be allowed for transactions. Defaults to + * {@literal 300000}. This means that calls to UserTransaction.setTransactionTimeout() + * with a value higher than configured here will be max'ed to this value. + * @param maxTimeout the max timeout + */ + public void setMaxTimeout(Duration maxTimeout) { + this.maxTimeout = maxTimeout; + } + + public Duration getMaxTimeout() { + return this.maxTimeout; + } + + /** + * The default timeout for JTA transactions (optional, defaults to {@literal 10000} + * ms). + * @param defaultJtaTimeout the default JTA timeout + */ + public void setDefaultJtaTimeout(Duration defaultJtaTimeout) { + this.defaultJtaTimeout = defaultJtaTimeout; + } + + public Duration getDefaultJtaTimeout() { + return this.defaultJtaTimeout; + } + + /** + * Specifies the maximum number of active transactions. Defaults to {@literal 50}. A + * negative value means infinite amount. You will get an {@code IllegalStateException} + * with error message "Max number of active transactions reached" if you call + * {@code UserTransaction.begin()} while there are already n concurrent transactions + * running, n being this value. + * @param maxActives the max activities + */ + public void setMaxActives(Integer maxActives) { + this.maxActives = maxActives; + } + + public Integer getMaxActives() { + return this.maxActives; + } + + /** + * Specifies if disk logging should be enabled or not. Defaults to true. It is useful + * for JUnit testing, or to profile code without seeing the transaction manager's + * activity as a hot spot but this should never be disabled on production or data + * integrity cannot be guaranteed. + * @param enableLogging if logging is enabled + */ + public void setEnableLogging(Boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public Boolean isEnableLogging() { + return this.enableLogging; + } + + /** + * Specifies the transaction manager's unique name. Defaults to the machine's IP + * address. If you plan to run more than one transaction manager against one database + * you must set this property to a unique value or you might run into duplicate + * transaction ID (XID) problems that can be quite subtle (example: + * {@literal https://fogbugz.atomikos.com/default.asp?community.6.2225.7}). If + * multiple instances need to use the same properties file then the easiest way to + * ensure uniqueness for this property is by referencing a system property specified + * at VM startup. + * @param uniqueName the unique name + */ + public void setTransactionManagerUniqueName(String uniqueName) { + this.transactionManagerUniqueName = uniqueName; + } + + public String getTransactionManagerUniqueName() { + return this.transactionManagerUniqueName; + } + + /** + * Specifies if subtransactions should be joined when possible. Defaults to true. When + * false, no attempt to call {@code XAResource.start(TM_JOIN)} will be made for + * different but related subtransactions. This setting has no effect on resource + * access within one and the same transaction. If you don't use subtransactions then + * this setting can be ignored. + * @param serialJtaTransactions if serial JTA transactions are supported + */ + public void setSerialJtaTransactions(Boolean serialJtaTransactions) { + this.serialJtaTransactions = serialJtaTransactions; + } + + public Boolean isSerialJtaTransactions() { + return this.serialJtaTransactions; + } + + public void setAllowSubTransactions(Boolean allowSubTransactions) { + this.allowSubTransactions = allowSubTransactions; + } + + public Boolean isAllowSubTransactions() { + return this.allowSubTransactions; + } + + /** + * Specifies whether VM shutdown should trigger forced shutdown of the transaction + * core. Defaults to false. + * @param forceShutdownOnVmExit if VM shutdown should be forced + */ + public void setForceShutdownOnVmExit(Boolean forceShutdownOnVmExit) { + this.forceShutdownOnVmExit = forceShutdownOnVmExit; + } + + public Boolean isForceShutdownOnVmExit() { + return this.forceShutdownOnVmExit; + } + + /** + * Specifies how long should a normal shutdown (no-force) wait for transactions to + * complete. Defaults to {@literal Long.MAX_VALUE}. + * @param defaultMaxWaitTimeOnShutdown the default max wait time on shutdown + */ + public void setDefaultMaxWaitTimeOnShutdown(Long defaultMaxWaitTimeOnShutdown) { + this.defaultMaxWaitTimeOnShutdown = defaultMaxWaitTimeOnShutdown; + } + + public Long getDefaultMaxWaitTimeOnShutdown() { + return this.defaultMaxWaitTimeOnShutdown; + } + + /** + * Specifies the transactions log file base name. Defaults to {@literal tmlog}. The + * transactions logs are stored in files using this name appended with a number and + * the extension {@literal .log}. At checkpoint, a new transactions log file is + * created and the number is incremented. + * @param logBaseName the log base name + */ + public void setLogBaseName(String logBaseName) { + this.logBaseName = logBaseName; + } + + public String getLogBaseName() { + return this.logBaseName; + } + + /** + * Specifies the directory in which the log files should be stored. Defaults to the + * current working directory. This directory should be a stable storage like a SAN, + * RAID or at least backed up location. The transactions logs files are as important + * as the data themselves to guarantee consistency in case of failures. + * @param logBaseDir the log base dir + */ + public void setLogBaseDir(String logBaseDir) { + this.logBaseDir = logBaseDir; + } + + public String getLogBaseDir() { + return this.logBaseDir; + } + + /** + * Specifies the interval between checkpoints. A checkpoint reduces the log file size + * at the expense of adding some overhead in the runtime. Defaults to {@literal 500}. + * @param checkpointInterval the checkpoint interval + */ + public void setCheckpointInterval(Long checkpointInterval) { + this.checkpointInterval = checkpointInterval; + } + + public Long getCheckpointInterval() { + return this.checkpointInterval; + } + + + public Recovery getRecovery() { + return this.recovery; + } + + public void setThrowOnHeuristic(Boolean throwOnHeuristic) { + this.throwOnHeuristic = throwOnHeuristic; + } + + public Boolean isThrowOnHeuristic() { + return this.throwOnHeuristic; + } + + /** + * Returns the properties as a {@link Properties} object that can be used with + * Atomikos. + * @return the properties + */ + public Properties asProperties() { + Properties properties = new Properties(); + set(properties, "service", getService()); + set(properties, "max_timeout", getMaxTimeout()); + set(properties, "default_jta_timeout", getDefaultJtaTimeout()); + set(properties, "max_actives", getMaxActives()); + set(properties, "enable_logging", isEnableLogging()); + set(properties, "tm_unique_name", getTransactionManagerUniqueName()); + set(properties, "serial_jta_transactions", isSerialJtaTransactions()); + set(properties, "allow_subtransactions", isAllowSubTransactions()); + set(properties, "force_shutdown_on_vm_exit", isForceShutdownOnVmExit()); + set(properties, "default_max_wait_time_on_shutdown", getDefaultMaxWaitTimeOnShutdown()); + set(properties, "log_base_name", getLogBaseName()); + set(properties, "log_base_dir", getOrDeduceLogBaseDir()); + set(properties, "checkpoint_interval", getCheckpointInterval()); + set(properties, "forget_orphaned_log_entries_delay", recovery.getForgetOrphanedLogEntriesDelay()); + set(properties, "recovery_delay", recovery.getDelay()); + set(properties, "oltp_max_retries", recovery.getMaxRetries()); + set(properties, "oltp_retry_interval", recovery.getRetryInterval()); + set(properties, "throw_on_heuristic", isThrowOnHeuristic()); + return properties; + } + + private String getOrDeduceLogBaseDir() { + if(StringUtils.hasText(this.logBaseDir)) { + return this.logBaseDir; + } + return null; + + } + + private void set(Properties properties, String key, Object value) { + String id = "com.atomikos.icatch." + key; + if (value != null && !properties.containsKey(id)) { + properties.setProperty(id, asString(value)); + } + } + + private String asString(Object value) { + if (value instanceof Duration) { + return String.valueOf(((Duration) value).toMillis()); + } + return value.toString(); + } + + /** + * Recovery specific settings. + */ + public static class Recovery { + + /** + * Delay after which recovery can cleanup pending ('orphaned') log entries. + */ + private Duration forgetOrphanedLogEntriesDelay; + + /** + * Delay between two recovery scans. + */ + private Duration delay; + + /** + * Number of retry attempts to commit the transaction before throwing an + * exception. + */ + private Integer maxRetries; + + /** + * Delay between retry attempts. + */ + private Duration retryInterval; + + public Duration getForgetOrphanedLogEntriesDelay() { + return this.forgetOrphanedLogEntriesDelay; + } + + public void setForgetOrphanedLogEntriesDelay(Duration forgetOrphanedLogEntriesDelay) { + this.forgetOrphanedLogEntriesDelay = forgetOrphanedLogEntriesDelay; + } + + public Duration getDelay() { + return this.delay; + } + + public void setDelay(Duration delay) { + this.delay = delay; + } + + public Integer getMaxRetries() { + return this.maxRetries; + } + + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + } + + public Duration getRetryInterval() { + return this.retryInterval; + } + + public void setRetryInterval(Duration retryInterval) { + this.retryInterval = retryInterval; + } + + + } + + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/public/spring-boot3/transactions-spring-boot3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..479dc84ea --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.atomikos.spring.AtomikosAutoConfiguration \ No newline at end of file diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosAutoConfigurationJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosAutoConfigurationJUnit.java new file mode 100644 index 000000000..21ff58e37 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosAutoConfigurationJUnit.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import jakarta.transaction.UserTransaction; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.transaction.TransactionProperties; +import org.springframework.boot.jdbc.XADataSourceWrapper; +import org.springframework.boot.jms.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.transaction.jta.JtaTransactionManager; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * Tests for {@link AtomikosAutoConfiguration}. + */ +public class AtomikosAutoConfigurationJUnit { + + @Test + public void sanityCheck() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TransactionProperties.class, + AtomikosAutoConfiguration.class)) { + context.getBean(AtomikosProperties.class); + context.getBean(UserTransactionService.class); + context.getBean(UserTransactionManager.class); + context.getBean(UserTransaction.class); + context.getBean(XADataSourceWrapper.class); + context.getBean(XAConnectionFactoryWrapper.class); + context.getBean(AtomikosDependsOnBeanFactoryPostProcessor.class); + context.getBean(JtaTransactionManager.class); + } + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosConnectionFactoryBeanTestJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosConnectionFactoryBeanTestJUnit.java new file mode 100644 index 000000000..b02cb0c91 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosConnectionFactoryBeanTestJUnit.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AtomikosConnectionFactoryBean}. + */ +public class AtomikosConnectionFactoryBeanTestJUnit { + + @Test + public void beanMethods() throws Exception { + MockAtomikosConnectionFactoryBean bean = spy(new MockAtomikosConnectionFactoryBean()); + bean.setBeanName("bean"); + bean.afterPropertiesSet(); + assertThat(bean.getUniqueResourceName()).isEqualTo("bean"); + verify(bean).init(); + verify(bean, never()).close(); + bean.destroy(); + verify(bean).close(); + } + + @SuppressWarnings("serial") + static class MockAtomikosConnectionFactoryBean extends AtomikosConnectionFactoryBean { + + @Override + public synchronized void init() { + } + + @Override + public synchronized void close() { + } + + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosDataSourceBeanTestJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosDataSourceBeanTestJUnit.java new file mode 100644 index 000000000..c03bc79ac --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosDataSourceBeanTestJUnit.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AtomikosDataSourceBean}. + */ +public class AtomikosDataSourceBeanTestJUnit { + + @Test + public void beanMethods() throws Exception { + MockAtomikosDataSourceBean bean = spy(new MockAtomikosDataSourceBean()); + bean.setBeanName("bean"); + bean.afterPropertiesSet(); + assertThat(bean.getUniqueResourceName()).isEqualTo("bean"); + verify(bean).init(); + verify(bean, never()).close(); + bean.destroy(); + verify(bean).close(); + } + + @SuppressWarnings("serial") + static class MockAtomikosDataSourceBean extends AtomikosDataSourceBean { + + @Override + public synchronized void init() { + } + + @Override + public void close() { + } + + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessorTestJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessorTestJUnit.java new file mode 100644 index 000000000..bf2523688 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosDependsOnBeanFactoryPostProcessorTestJUnit.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import java.util.Arrays; +import java.util.HashSet; + +import jakarta.jms.ConnectionFactory; +import javax.sql.DataSource; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jms.extra.MessageDrivenContainer; +import org.junit.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosDependsOnBeanFactoryPostProcessor}. + */ +public class AtomikosDependsOnBeanFactoryPostProcessorTestJUnit { + + private AnnotationConfigApplicationContext context; + + @Test + public void setsDependsOn() { + this.context = new AnnotationConfigApplicationContext(Config.class); + assertDependsOn("dataSource"); + assertDependsOn("connectionFactory"); + assertDependsOn("userTransactionManager", "dataSource", "connectionFactory"); + assertDependsOn("messageDrivenContainer", "userTransactionManager"); + this.context.close(); + } + + private void assertDependsOn(String bean, String... expected) { + BeanDefinition definition = this.context.getBeanDefinition(bean); + if (definition.getDependsOn() == null) { + assertThat(expected).as("No dependsOn expected for " + bean).isEmpty(); + return; + } + HashSet dependsOn = new HashSet<>(Arrays.asList(definition.getDependsOn())); + assertThat(dependsOn).isEqualTo(new HashSet<>(Arrays.asList(expected))); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + DataSource dataSource() { + return mock(DataSource.class); + } + + @Bean + ConnectionFactory connectionFactory() { + return mock(ConnectionFactory.class); + } + + @Bean + UserTransactionManager userTransactionManager() { + return mock(UserTransactionManager.class); + } + + @Bean + MessageDrivenContainer messageDrivenContainer() { + return mock(MessageDrivenContainer.class); + } + + @Bean + static AtomikosDependsOnBeanFactoryPostProcessor atomikosPostProcessor() { + return new AtomikosDependsOnBeanFactoryPostProcessor(); + } + + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosPropertiesTestJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosPropertiesTestJUnit.java new file mode 100644 index 000000000..6add27e6f --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosPropertiesTestJUnit.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.junit.Test; + +/** + * Tests for {@link AtomikosProperties}. + */ +public class AtomikosPropertiesTestJUnit { + + private AtomikosProperties properties = new AtomikosProperties(); + + @Test + public void testProperties() { + this.properties.setService("service"); + this.properties.setMaxTimeout(Duration.ofMillis(1)); + this.properties.setDefaultJtaTimeout(Duration.ofMillis(2)); + this.properties.setMaxActives(3); + this.properties.setEnableLogging(true); + this.properties.setTransactionManagerUniqueName("uniqueName"); + this.properties.setSerialJtaTransactions(true); + this.properties.setAllowSubTransactions(false); + this.properties.setForceShutdownOnVmExit(true); + this.properties.setDefaultMaxWaitTimeOnShutdown(20l); + this.properties.setLogBaseName("logBaseName"); + this.properties.setLogBaseDir("logBaseDir"); + this.properties.setCheckpointInterval(4l); + this.properties.getRecovery().setForgetOrphanedLogEntriesDelay(Duration.ofMillis(2000)); + this.properties.getRecovery().setDelay(Duration.ofMillis(3000)); + this.properties.getRecovery().setMaxRetries(10); + this.properties.getRecovery().setRetryInterval(Duration.ofMillis(4000)); + this.properties.setThrowOnHeuristic(true); + assertThat(this.properties.asProperties().size()).isEqualTo(18); + assertProperty("com.atomikos.icatch.service", "service"); + assertProperty("com.atomikos.icatch.max_timeout", "1"); + assertProperty("com.atomikos.icatch.default_jta_timeout", "2"); + assertProperty("com.atomikos.icatch.max_actives", "3"); + assertProperty("com.atomikos.icatch.enable_logging", "true"); + assertProperty("com.atomikos.icatch.tm_unique_name", "uniqueName"); + assertProperty("com.atomikos.icatch.serial_jta_transactions", "true"); + assertProperty("com.atomikos.icatch.allow_subtransactions", "false"); + assertProperty("com.atomikos.icatch.force_shutdown_on_vm_exit", "true"); + assertProperty("com.atomikos.icatch.default_max_wait_time_on_shutdown", "20"); + assertProperty("com.atomikos.icatch.log_base_name", "logBaseName"); + assertProperty("com.atomikos.icatch.log_base_dir", "logBaseDir"); + assertProperty("com.atomikos.icatch.checkpoint_interval", "4"); + assertProperty("com.atomikos.icatch.forget_orphaned_log_entries_delay", "2000"); + assertProperty("com.atomikos.icatch.recovery_delay", "3000"); + assertProperty("com.atomikos.icatch.oltp_max_retries", "10"); + assertProperty("com.atomikos.icatch.oltp_retry_interval", "4000"); + assertProperty("com.atomikos.icatch.throw_on_heuristic", "true"); + } + + @Test + public void testDefaultProperties() { + Properties properties = this.properties.asProperties(); + List keys = new ArrayList<>(); + keys.add("com.atomikos.icatch.max_timeout"); + keys.add("com.atomikos.icatch.default_jta_timeout"); + keys.add("com.atomikos.icatch.max_actives"); + keys.add("com.atomikos.icatch.enable_logging"); + keys.add("com.atomikos.icatch.serial_jta_transactions"); + keys.add("com.atomikos.icatch.allow_subtransactions"); + keys.add("com.atomikos.icatch.force_shutdown_on_vm_exit"); + keys.add("com.atomikos.icatch.default_max_wait_time_on_shutdown"); + keys.add("com.atomikos.icatch.log_base_name"); + keys.add("com.atomikos.icatch.checkpoint_interval"); + keys.add("com.atomikos.icatch.forget_orphaned_log_entries_delay"); + keys.add("com.atomikos.icatch.oltp_max_retries"); + keys.add("com.atomikos.icatch.oltp_retry_interval"); + keys.add("com.atomikos.icatch.throw_on_heuristic"); + assertThat(properties).isEmpty(); + } + + private void assertProperty(String key, String value) { + assertThat(this.properties.asProperties().getProperty(key)).isEqualTo(value); + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapperTestJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapperTestJUnit.java new file mode 100644 index 000000000..00422f957 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosXAConnectionFactoryWrapperTestJUnit.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.XAConnectionFactory; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosXAConnectionFactoryWrapper}. + */ +public class AtomikosXAConnectionFactoryWrapperTestJUnit { + + @Test + public void wrap() { + XAConnectionFactory connectionFactory = mock(XAConnectionFactory.class); + AtomikosXAConnectionFactoryWrapper wrapper = new AtomikosXAConnectionFactoryWrapper(); + ConnectionFactory wrapped = wrapper.wrapConnectionFactory(connectionFactory); + assertThat(wrapped).isInstanceOf(AtomikosConnectionFactoryBean.class); + assertThat(((AtomikosConnectionFactoryBean) wrapped).getXaConnectionFactory()).isSameAs(connectionFactory); + } + +} diff --git a/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosXADataSourceWrapperTestJUnit.java b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosXADataSourceWrapperTestJUnit.java new file mode 100644 index 000000000..bff614b58 --- /dev/null +++ b/public/spring-boot3/transactions-spring-boot3/src/test/java/com/atomikos/spring/AtomikosXADataSourceWrapperTestJUnit.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.spring; + +import javax.sql.DataSource; +import javax.sql.XADataSource; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosXADataSourceWrapper}. + */ +public class AtomikosXADataSourceWrapperTestJUnit { + + @Test + public void wrap() throws Exception { + XADataSource dataSource = mock(XADataSource.class); + AtomikosXADataSourceWrapper wrapper = new AtomikosXADataSourceWrapper(); + DataSource wrapped = wrapper.wrapDataSource(dataSource); + assertThat(wrapped).isInstanceOf(AtomikosDataSourceBean.class); + assertThat(((AtomikosDataSourceBean) wrapped).getXaDataSource()).isSameAs(dataSource); + } + +} diff --git a/public/transactions-api/pom.xml b/public/transactions-api/pom.xml new file mode 100644 index 000000000..9d65032ae --- /dev/null +++ b/public/transactions-api/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-api + Transactions API + + + + jdepend + jdepend + 2.9.1 + test + + + + + + diff --git a/public/transactions-api/src/main/java/com/atomikos/datasource/RecoverableResource.java b/public/transactions-api/src/main/java/com/atomikos/datasource/RecoverableResource.java new file mode 100644 index 000000000..81f8b4161 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/datasource/RecoverableResource.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource; + +import java.util.Collection; + +import com.atomikos.icatch.RecoveryService; +import com.atomikos.recovery.PendingTransactionRecord; + + + /** + * A Recoverable Resource is the abstraction of a resource + * that supports recoverable work (i.e., that supports Participant + * instances). A recoverable resource is invoked at recovery time by the transaction core. + */ + +public interface RecoverableResource +{ + + /** + * Initializes this resource with the recovery service. + * This method is called by the transaction service during + * intialization of the transaction service or when the + * resource is added, whichever comes last. If the + * resource wants to recover, it should subsequently + * ask the recoveryService + * to do so. + * @param recoveryService The recovery service. This instance + * can be used by the resource to ask recovery from the + * transaction engine. + * @throws ResourceException On errors. + */ + + void setRecoveryService ( RecoveryService recoveryService ) + throws ResourceException; + + + /** + * Closes the resource for shutdown. + * This notifies the resource that it is no longer needed. + */ + + void close() throws ResourceException; + + /** + * Gets the name of the resource. Names should be unique + * within one TM domain. + * @return String The name. + */ + + String getName(); + + /** + * Tests if a resource is the same as another one. + */ + + boolean isSameRM(RecoverableResource res) + throws ResourceException; + + /** + * Tests if the resource is closed. + * @return boolean True if the resource is closed. + */ + boolean isClosed(); + + + /** + * Instructs the resource to recover. + * + * @param startOfRecoveryScan + * @param expiredCommittingCoordinators + * @param indoubtForeignCoordinatorsToKeep + * @return True if this resource has no more participants for the supplied expiredCommittingCoordinators. + */ + boolean recover(long startOfRecoveryScan, Collection expiredCommittingCoordinators, Collection indoubtForeignCoordinatorsToKeep); + + /** + * + * @return True if this resource has pending participants from the last recovery scan. + */ + boolean hasPendingParticipantsFromLastRecoveryScan(); + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/datasource/ResourceException.java b/public/transactions-api/src/main/java/com/atomikos/datasource/ResourceException.java new file mode 100644 index 000000000..0ce545c7a --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/datasource/ResourceException.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource; + + +/** +* Exception on the level of the resource manager. +* Contains more detailed info of actual underlying exception. +*/ + +public class ResourceException extends com.atomikos.icatch.SysException +{ + + public ResourceException(String msg){ + super(msg); + + } + + public ResourceException(String msg, Throwable cause) { + super(msg, cause); + } + +} + diff --git a/public/transactions-api/src/main/java/com/atomikos/datasource/ResourceTransaction.java b/public/transactions-api/src/main/java/com/atomikos/datasource/ResourceTransaction.java new file mode 100644 index 000000000..2783c45dc --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/datasource/ResourceTransaction.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource; + + +/** + * The notion of a local transaction executed on a resource. + * Serves as a handle towards the transaction management module. + */ + +public interface ResourceTransaction +{ + + + /** + * Suspends the work, so that underlying resources can + * be used for a next (sibling) invocation. + * + */ + + void suspend() throws IllegalStateException,ResourceException; + + /** + * Resumes a previously suspended tx. + * + */ + + void resume() throws IllegalStateException,ResourceException; + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/datasource/TransactionalResource.java b/public/transactions-api/src/main/java/com/atomikos/datasource/TransactionalResource.java new file mode 100644 index 000000000..225ef343f --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/datasource/TransactionalResource.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource; + +import com.atomikos.icatch.CompositeTransaction; + +/** + * Represents the abstraction of a data source that + * supports transactions and recovery. + */ + +public interface TransactionalResource extends RecoverableResource +{ + /** + * Gets or creates a ResourceTransaction. This instructs the resource + * to internally start a context for a new transaction. + * If the resource decides to return a new instance, it should + * also make sure that before returning, the new resource + * transaction is registered as a participant for the supplied + * composite transaction. + * + */ + + ResourceTransaction + getResourceTransaction ( CompositeTransaction compositeTransaction ) + throws IllegalStateException, ResourceException; + + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeCoordinator.java b/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeCoordinator.java new file mode 100644 index 000000000..5ebded6bb --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeCoordinator.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * Represents the per-server work done + * as part of the same global (root) transaction scope. + */ + +public interface CompositeCoordinator +{ + + /** + * @return The coordinatorId. + */ + + String getCoordinatorId(); + + /** + * + * @return The top-level root's coordinatorId. + */ + String getRootId(); + + + /** + * + *@return RecoveryCoordinator. + */ + + RecoveryCoordinator getRecoveryCoordinator(); + +} + + + + + + + diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeTransaction.java b/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeTransaction.java new file mode 100644 index 000000000..7173da5ba --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeTransaction.java @@ -0,0 +1,263 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + +package com.atomikos.icatch; + +import java.util.Properties; +import java.util.Stack; + +import com.atomikos.recovery.TxState; + +/** + * Represents a nested part of a global + * composite transaction. Each invocation of a server is + * represented by an instance of this type. For transaction + * proxies (instances that represent non-local transactions), + * all non-trivial methods are allowed to generate + * an exception of type UnsupportedOperationException. The only + * methods that always work for all instances are the simple + * getters and test methods. + * + * + * + */ + +public interface CompositeTransaction +{ + + /** + * Gets the current state. + * @return Object One of the state constants. + * @see TxState + */ + TxState getState(); + + /** + * + * @return boolean True if this is the root transaction, + * i.e. the first transaction created in a (possibly distributed) hierarchy. + */ + + boolean isRoot(); + + /** + * @return Stack A stack of ancestors, bottom one is the root. + */ + + Stack getLineage(); + + + /** + * + * @return String The tid for the tx. + */ + + String getTid(); + + /** + * + * @param otherCompositeTransaction + * + * @return boolean True if this instance is an ancestor of the supplied transaction. + */ + + boolean isAncestorOf( CompositeTransaction otherCompositeTransaction ); + + /** + * @param otherCompositeTransaction + * + * @return boolean True if this instance is a descendant of the other instance. + */ + + boolean isDescendantOf( CompositeTransaction otherCompositeTransaction ); + + + /** + * @param otherCompositeTransaction + * + * @return True if related. That is: if both share the same root transaction. + */ + + boolean isRelatedTransaction ( CompositeTransaction otherCompositeTransaction ); + + + /** + * @param otherCompositeTransaction + * @return True if both are the same. + */ + + boolean isSameTransaction ( CompositeTransaction otherCompositeTransaction ); + + /** + * @return CompositeCoordinator + */ + + CompositeCoordinator getCompositeCoordinator() + throws SysException; + + + + /** + * @param participant + * + * @return RecoveryCoordinator Whom to ask for indoubt timeout resolution. + */ + + RecoveryCoordinator addParticipant ( Participant participant ) + throws SysException, + java.lang.IllegalStateException; + + + /** + * + * @param sync + * @throws IllegalStateException + * @throws SysException + */ + + void registerSynchronization(Synchronization sync) + throws + IllegalStateException, + SysException; + + + + /** + * Resources that support lock inheritance can use this feature + * to be notified whenever a lock should be inherited. + * + * @param subtxaware + * @throws SysException + * @throws java.lang.IllegalStateException + */ + + void addSubTxAwareParticipant( SubTxAwareParticipant subtxaware ) + throws SysException, + java.lang.IllegalStateException; + + + /** + * Serial mode is an optimized way for lock inheritance: + * no locks among related transactions are necessary if all related + * transactions are executed serially with respect to each other. + * The serial property is set by the root transaction and is + * propagated to all its subtransactions. + * + * @return + */ + boolean isSerial(); + + /** + * + * @return boolean True if started in the local VM. + * For imported transactions, this is false. + */ + + boolean isLocal (); + + + /** + * + * @return The extent. + */ + + Extent getExtent(); + + + /** + * @return long The transaction timeout in millis. + */ + + long getTimeout(); + + /** + * Marks the transaction so that the only possible + * termination is rollback. + * + */ + + void setRollbackOnly(); + + /** + * Commits the composite transaction. + * + * @exception HeurMixedException On heuristic mixed outcome. + * @exception SysException For unexpected failures. + * @exception SecurityException If calling thread does not have + * right to commit. + * @exception HeurHazardException In case of heuristic hazard. + * @exception RollbackException If the transaction was rolled back. + */ + + void commit() + throws + HeurMixedException, + HeurHazardException, + SysException,java.lang.SecurityException, + RollbackException; + + + + /** + * Rollback of the current transaction. + * @exception IllegalStateException If prepared or inactive. + * @exception SysException If unexpected error. + */ + + void rollback() + throws IllegalStateException, SysException; + + + /** + * Sets metadata property information on the transaction object. + * Once set, metadata properties can't be overwritten. Properties + * are inherited by all subtransactions created after the set. + * Properties may or may not be propagated along with + * exported transactions (depending on the protocol). + * + * @param name + * @param value + */ + + void setProperty ( String name , String value ); + + /** + * Gets the specified metadata property. + * @param name The name of the property. + * @return The property, or null if not set. + */ + + String getProperty ( String name ); + + /** + * Gets all properties of this instance. + * @return The (cloned) properties of this transaction. + */ + + Properties getProperties(); + + CompositeTransaction createSubTransaction(); + + /** + *Set serial mode for root. + *This only works on the root itself, and can not be undone. + *After this, no parallel calls are allowed in any descendant. + *@exception IllegalStateException If called for non-root tx. + *@exception SysException For unexpected errors. + */ + + void setSerial() throws IllegalStateException, SysException; + +} + + + + + + diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeTransactionManager.java b/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeTransactionManager.java new file mode 100644 index 000000000..7bc530c71 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/CompositeTransactionManager.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * This interface outlines the API for managing composite transactions + * in the local VM. + */ + +public interface CompositeTransactionManager +{ + /** + * Starts a new (sub)transaction (not an activity) for the current thread. + * Associates the current thread with that instance. + *
+ * NOTE: subtransactions should not be mixed: either each subtransaction is + * an activity, or not (default). Use suspend/resume if mixed models are necessary: + * for instance, if you want to create a normal transaction within an activity, then + * suspend the activity first before starting the transaction. Afterwards, resume the + * activity. + * + * @param Timeout (in millis) for the transaction. + * + * @return CompositeTransaction The new instance. + * @exception SysException Unexpected error. + * @exception IllegalStateException If there is an existing transaction that is + * an activity instead of a classical transaction. + */ + + CompositeTransaction createCompositeTransaction ( long timeout ) + throws SysException, IllegalStateException; + + /** + * @return CompositeTransaction The instance for the current thread, null if none. + * + * @exception SysException On unexpected failure. + */ + + CompositeTransaction getCompositeTransaction () throws SysException; + + /** + * Gets the composite transaction with the given id. + * This method is useful e.g. for retrieving a suspended + * transaction by its id. + * + * @param tid The id of the transaction. + * @return CompositeTransaction The transaction with the given id, + * or null if not found. + * @exception SysException Unexpected failure. + */ + + CompositeTransaction getCompositeTransaction ( String tid ) + throws SysException; + + /** + * Re-maps the calling thread to the given transaction. + * + * @param compositeTransaction + * @exception IllegalStateException If this thread has a transaction context already. + * @exception SysException + */ + + void resume ( CompositeTransaction compositeTransaction ) + throws IllegalStateException, SysException; + + /** + * Suspends the transaction context for the current thread. + * This method suspends the entire transaction tree, including any parent transactions. + * + * @return CompositeTransaction The transaction for the current thread. + * + * @exception SysException + */ + + CompositeTransaction suspend() throws SysException ; + + /** + * Recreate a composite transaction based on an imported context. Needed by + * the application's communication layer. + * + * @param context + * The propagationcontext. + * + * @return CompositeTransaction The recreated local instance. + * @exception SysException + * Failure. + */ + + CompositeTransaction recreateCompositeTransaction(Propagation propagation); + + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/ExportingTransactionManager.java b/public/transactions-api/src/main/java/com/atomikos/icatch/ExportingTransactionManager.java new file mode 100644 index 000000000..b2af58c67 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/ExportingTransactionManager.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * An interface for a TM that allows outgoing remote calls to be transactional. + */ + + public interface ExportingTransactionManager + { + /** + * Gets the propagation info of the transaction for the calling thread. + * Should be called before doing the remote call. + * + * @return Propagation The propagation for the current thread's transaction. + * + * @throws IllegalStateException If no such transaction exists, e.g. after a prior rollback. + * + */ + + Propagation getPropagation() throws SysException, IllegalStateException; + + /** + * Should be called after call returns successfully: + * adds the extent of the call to the current transaction. + * + * If a remote call has failed, this method should NOT be called. + * + * @param extent The extent of the call. + * + * @throws IllegalArgumentException If the format of the supplied extent is not recognized. + * @throws RollbackException If the current transaction has already rolled back. + */ + + + void addExtent(Extent extent) throws SysException, IllegalArgumentException, RollbackException; + } diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/Extent.java b/public/transactions-api/src/main/java/com/atomikos/icatch/Extent.java new file mode 100644 index 000000000..d65364c1f --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/Extent.java @@ -0,0 +1,206 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +/** + * + * + * The extent carries the information about the 'size' of a propagation + * after it returns: the directly and indirectly invoked servers, and the orphan + * detection information for those. + * + * This interface is a system interface; it should not be handled by application + * level code (besides shipping it around via toString()). + * + * This class (and its parsing) represent the minimum information required for + * transactions to work across remoting calls. All values are required. + * + * In addition, we are liberal in parsing: any additional, unknown values are ignored - + * so future releases can add extra, optional properties and still work with + * installations of this release. + */ + +public class Extent { + + /** + * Major version indicator. Only change this for future releases + * that add incompatible changes such as adding/removing required properties + * and/or changing the semantics of existing properties. As long as optional properties + * are added/removed, the version can stay the same. + */ + + public static final String VERSION ="2019"; + + private final Map participants = new Hashtable(); + private boolean queried = false; + private final Stack directs = new Stack(); + private String parentTransactionId; //null for root + + public Extent() { + } + + public Extent (String parentTransactionId) { + this.parentTransactionId = parentTransactionId; + } + + /** + * Creates a new extent with the supplied extent's participants as indirect participants only. + * Intended for use at the importing / server end, when returning an extent to the client. + * + * The resulting extent must then still be completed with a direct participant representing this JVM for 2-phase commit. + * + * @param extent + */ + public Extent(Extent extent) { + this.parentTransactionId = extent.getParentTransactionId(); + addRemoteParticipants(extent.participants); + } + + public String getParentTransactionId() { + return parentTransactionId; + } + + public void addRemoteParticipants(Map participants) + throws IllegalStateException, SysException { + if (participants == null) { + return; + } + Set parts = participants.keySet(); + for (String participant : parts) { + Integer count = this.participants.get(participant); + if (count == null) { + count = 0; + } + + Integer cnt = participants.get(participant); + count = count.intValue() + cnt.intValue(); + + this.participants.put(participant, count); + // NOTE: this will replace the old participant, and if + // it is a proxy then the buffered heuristic msgs will + // also be replaced. This loses info if multiple PARALLEL calls + // went to the same FIRST-ORDER server (i.e., directly invoked). + // Never mind, though: it is considered bad practice + // to execute parallel calls if they might act on the same + // data. This is the case if they go to the same directly + // invoked server. + } + } + + /** + * @return Map Mapping URIs of remote participants (directly or indirectly invoked) + * to Integer counts that represent the number of invocations detected by each participant. + */ + + public Map getRemoteParticipants() + { + queried = true; + return new HashMap(participants); + } + + + /** + * + * @return Stack A stack of direct participants. Direct participants + * are those that need to be added to the client TM's two-phase + * commit set. + * + * NOTE: If a participant occurs in the direct participant set, + * it will also be part of the remote set. + */ + + + @SuppressWarnings("unchecked") + public Stack getParticipants () + { + queried = true; + return (Stack) directs.clone(); + } + + /** + * Adds a participant to the extent. + * This method is called at the server side, in order to add the work done + * to the two-phase commit set of the calling (client) side, as well as to + * make sure that orphan information is propagated through the system. + * + * @param participant This instance will + *be added to the indirect as well as to the direct participant set. + * + * @param count The number of invocations detected by the adding client. + * @throws IllegalStateException If no longer allowed. + * @throws SysException + */ + + public synchronized void add(Participant participant, int count) + throws SysException, IllegalStateException { + Hashtable table = new Hashtable(); + table.put(participant.getURI(), count); + addRemoteParticipants(table); + directs.push(participant); + } + + /** + * Merges another extent into this one. + * + *@param extent The extent to add. + * + *@throws IllegalStateException If no longer allowed. + *@throws SysException + */ + + public synchronized void add ( Extent extent ) + throws IllegalStateException, SysException + { + if (queried) throw new IllegalStateException("Adding extent no longer allowed"); + addRemoteParticipants(extent.getRemoteParticipants()); + Enumeration enumm = extent.getParticipants().elements(); + while (enumm.hasMoreElements()) { + Participant part = enumm.nextElement(); + directs.push(part); + } + } + + @Override + public String toString() { + StringBuffer ret = new StringBuffer(); + ret.append("version=").append(VERSION).append(","); + String delimiter = ""; + if (parentTransactionId != null) { //should not be null but we never know + ret.append("parent=").append(parentTransactionId); + delimiter = ","; + } + Set alreadyAdded = new HashSet(); + for (Participant p : directs) { + ret.append(delimiter).append("uri=").append(p.getURI()).append(","). + append("responseCount=").append(participants.get(p.getURI())).append(","). + append("direct=").append("true"); + delimiter = ","; + alreadyAdded.add(p.getURI()); + } + for (String pUri : participants.keySet()) { + if (!alreadyAdded.contains(pUri)) { + ret.append(delimiter). + append("uri=").append(pUri).append(","). + append("responseCount=").append(participants.get(pUri)).append(","). + append("direct=").append("false"); + } + } + return ret.toString(); + } + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/HeurCommitException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurCommitException.java new file mode 100644 index 000000000..28631b04f --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurCommitException.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +/** + * Exception signaling heuristic commit. + */ + +public class HeurCommitException extends HeuristicException { + + private static final long serialVersionUID = 1L; + + public HeurCommitException() { + super("Heuristic Commit Exception"); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/HeurHazardException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurHazardException.java new file mode 100644 index 000000000..17d3a05c7 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurHazardException.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +/** + * Exception signaling that two-phase commit was not acknowledged by some + * participants. + */ + +public class HeurHazardException extends HeuristicException { + + private static final long serialVersionUID = 1L; + + public HeurHazardException() { + super("Heuristic Hazard Exception"); + } + + public HeurHazardException(String msg) { + super(msg); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/HeurMixedException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurMixedException.java new file mode 100644 index 000000000..abcd6579c --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurMixedException.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + +// +//Revision 1.2 2001/03/01 19:26:57 pardon +//Added more. +// +//Revision 1.1 2001/02/21 19:51:23 pardon +//Redesign! +// + +package com.atomikos.icatch; + + + +/** + * An exception signaling that some participants + * have committed whereas others performed a rollback. + */ + +public class HeurMixedException extends HeuristicException +{ + private static final long serialVersionUID = 1L; + + public HeurMixedException () + { + super("Heuristic Mixed Exception"); + } +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/HeurRollbackException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurRollbackException.java new file mode 100644 index 000000000..56fb650ea --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/HeurRollbackException.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +/** + * An exception signaling that the transaction's work has been rolled back + * heuristically. + */ + +public class HeurRollbackException extends HeuristicException { + + private static final long serialVersionUID = 1L; + + public HeurRollbackException() { + super("Heuristic Rollback Exception"); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/HeuristicException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/HeuristicException.java new file mode 100644 index 000000000..1f5429574 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/HeuristicException.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + /** + * Common superclass for heuristics. + */ + +public class HeuristicException extends Exception { + + private static final long serialVersionUID = 1L; + + public HeuristicException(String message) { + super(message); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/ImportingTransactionManager.java b/public/transactions-api/src/main/java/com/atomikos/icatch/ImportingTransactionManager.java new file mode 100644 index 000000000..fdb80138a --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/ImportingTransactionManager.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * Represents a handle to import a transaction from an incoming request, + * so that the work in this VM becomes part of the request's commit or rollback. + */ + + public interface ImportingTransactionManager + { + /** + * Imports the transaction propagation obtained from an incoming request. + * + * @param propagation The propagation (transaction context). + * + * @return CompositeTransaction The locally created transaction instance that takes part in the global commit/rollback. + * This instance will also be mapped to the calling thread. + * + * @throws IllegalArgumentException If the supplied propagation cannot be understood. + */ + + CompositeTransaction importTransaction(Propagation propagation) throws IllegalArgumentException, SysException; + + + + /** + * Signals that the incoming request is done processing, in order to + * terminate the transaction context for the calling thread. + * + * @param commit True if the invocation had no errors: commit the local transaction + * but make its final outcome subject to the request's commit/rollback. + * + * @return Extent The extent to return to remote client (i.e., the participant information for two-phase commit) + * along with the parent's ID for asynchronous calls. + * + * + * @throws RollbackException If no transaction exists, e.g. if it has been rolled back already. + * + * + * + */ + + Extent terminated(boolean commit) throws SysException, RollbackException; + + + + + + } diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/OrderedLifecycleComponent.java b/public/transactions-api/src/main/java/com/atomikos/icatch/OrderedLifecycleComponent.java new file mode 100644 index 000000000..128901d8d --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/OrderedLifecycleComponent.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + /** + * Marker interface for system components whose order of init + * and close is important for correct behavior of the + * transaction system. The knowledge of ordering is supposed to + * be present elsewhere - in the system configuration. + */ + +public interface OrderedLifecycleComponent { + + /** + * + * @throws Exception Implementations are free to narrow the exception + * or even not throw anything. + */ + + void init() throws Exception; + + /** + * + * @throws Exception Implementations are free to narrow the exception + * or even not throw anything. + */ + + void close() throws Exception; +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/Participant.java b/public/transactions-api/src/main/java/com/atomikos/icatch/Participant.java new file mode 100644 index 000000000..00ac03152 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/Participant.java @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import java.util.Map; + + + + +/** + * A participant for (distributed) two-phase commit of composite transactions. + * + * Implementations can be added as a 2PC participant in the icatch kernel. + * + * VERY IMPORTANT: implementations should also override the + * default equals + * and hashCode methods, in order for two-phase commit to work properly! + * + */ + +public interface Participant +{ + /** + * Indicates that no commit/rollback is needed after prepare. + */ + + static final int READ_ONLY=0x00; + + /** + * @return String The unique URI for this remote participant. + */ + + String getURI(); + + /** + * For cascading 2PC, this method sets the information needed + * to cascade. This method is relevant only for + * transaction monitors; leave empty otherwise! + * + * @param allParticipants The information needed by + * the transaction monitor. + * + * @exception SysException + */ + + void setCascadeList(Map allParticipants) + throws SysException; + + /** + * Set by the root coordinator: the total no of siblings detected. + * This method is relevant only for + * transaction monitors; leave empty otherwise! + * + *@param count The global count. + */ + + void setGlobalSiblingCount(int count); + + /** + * Prepares the participant. + * Any locks for this participant's work should be + * recoverable (either saved to an external file OR be part + * of the instance's non-transient state so that they can be + * flushed into the transaction manager's log). + * + * @return int READ_ONLY if no second round is needed. + * Participants that return this value on prepare will not be + * called by commit or rollback afterwards. + * + * @exception RollbackException For a NO vote. + * This indicates that the participant has already rolled back + * (or marked for rollback) the work on behalf of this participant. + * + * @exception HeurHazardException On possible conflicts. + * This happens for remote participants instances, in case of + * communication failures. + * + * @exception HeurMixedException If some subordinate + * participants voted YES, timed out and committed heuristically + * whereas afterwards some NO votes where received. + * + * @exception SysException + */ + + int prepare() + throws RollbackException, + HeurHazardException, + HeurMixedException, + SysException; + + + /** + * Commits the participant's work. + * NOTE: custom participant implementations should + * preferably be made for the server's local VM + * (e.g., it is better not to do this over RMI). + * Also, they should rely on the transaction manager + * for heuristic timeout (and NOT decide to terminate + * heuristically themselves). + * In that case, implementations never need to throw any of the heuristic exceptions + * of this method. + * + * + * @param onePhase If true, one-phase commit is being started. + * If the participant has received a prepare call earlier, + * then it should throw a SysException here. + * + * @exception HeuristicRollbackException If the participant has rolled back. + * + * @exception HeuristicMixedException If part of it was rolled back. + * + * @exception HeurHazardException On possible conflicts. + * + * @exception RollbackException In case of one-phase commit, + * and the transaction has been rolled back at the time + * commit is called. + * + * @exception SysException + */ + + void commit ( boolean onePhase ) + throws HeurRollbackException, + HeurHazardException, + HeurMixedException, + RollbackException, + SysException; + + /** + * Rollback of the participant's work. + * + * NOTE: custom participant implementations should + * preferably be made for the server's local VM + * (e.g., it is better not to do this over RMI). + * Also, they should rely on the icatch transaction manager + * for heuristic timeout (and NOT decide to terminate + * heuristically themselves). + * In that case, implementations never need to throw any of the heuristic exceptions + * of this method. + * + * @exception HeurCommitException If the participant committed. + * @exception HeurHazardException If the participant's final state + * is unsure. + * @exception HeurMixedException If part of the work was rolled back. + * + * @exception SysException + */ + + void rollback() + throws HeurCommitException, + HeurMixedException, + HeurHazardException, + SysException; + + + + + /** + * Indicates that a heuristic participant can forget about its work. + * + * If implementations rely on the transaction manager to + * decide when to do heuristics (rather then deciding + * in the participant implementation), then + * leave this method empty. + */ + + void forget(); + + + + /** + * @return The (unique) name of the recoverable resource as known in the configuration. Null if not relevant. + */ + String getResourceName(); +} + diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/Propagation.java b/public/transactions-api/src/main/java/com/atomikos/icatch/Propagation.java new file mode 100644 index 000000000..765bc3635 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/Propagation.java @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import java.util.Properties; +import java.util.Stack; + +/** + * Information about the transaction context that can be + * shipped along with a remote request (via toString()), to make the other side + * participate in the transaction present for the current thread in this VM. + * + * This class (and its parsing) represent the minimum information required for + * transactions to work across remoting calls. All values are required. + * + * In addition, we are liberal in parsing: any additional, unknown values are ignored - + * so future releases can add extra, optional properties and still work with + * installations of this release. + * + */ + +public class Propagation { + + /** + * Major version indicator. Only change this for future releases + * that add incompatible changes such as adding/removing required properties + * and/or changing the semantics of existing properties. As long as optional properties + * are added/removed, the version can stay the same. + */ + public static final String VERSION ="2019"; + + private final Stack lineage; + private final boolean serial; + private final long timeout; + private final String recoveryDomainName; + private final String recoveryCoordinatorUri; + + public Propagation (String recoveryDomainName,CompositeTransaction rootTransaction, CompositeTransaction parentTransaction, boolean serial, long timeout) { + this(recoveryDomainName, rootTransaction, parentTransaction, serial, timeout, null); + } + + public Propagation (String recoveryDomainName,CompositeTransaction rootTransaction, CompositeTransaction parentTransaction, boolean serial, long timeout, String recoveryCoordinatorUri) { + if (rootTransaction == null) { + throw new IllegalArgumentException("rootTransaction cannot be null"); + } + if (parentTransaction == null) { + throw new IllegalArgumentException("parentTransaction cannot be null"); + } + if (recoveryCoordinatorUri == null) { + //default to parent coordinator ID + recoveryCoordinatorUri = parentTransaction.getCompositeCoordinator().getCoordinatorId(); + } + this.timeout = timeout; + this.serial = serial; + this.recoveryDomainName = recoveryDomainName; + this.lineage = new Stack(); + this.lineage.push(rootTransaction); + this.lineage.push(parentTransaction); + this.recoveryCoordinatorUri = recoveryCoordinatorUri; + } + + public String getRecoveryDomainName() { + return recoveryDomainName; + } + + public CompositeTransaction getRootTransaction() { + return this.getLineage().firstElement(); + } + + + public CompositeTransaction getParentTransaction() { + return this.lineage.lastElement(); + } + + /** + * @return A stack of ancestors, bottom one is the root. + */ + + public Stack getLineage() { + return lineage; + } + + + public boolean isSerial() { + return serial; + } + + public long getTimeout() { + return timeout; + } + + public String getRecoveryCoordinatorURI() { + return recoveryCoordinatorUri; + } + + @Override + public String toString() { + StringBuffer ret = new StringBuffer(); + ret.append("version=").append(VERSION).append(","); + ret.append("domain=").append(recoveryDomainName).append(","); + ret.append("timeout=").append(getTimeout()).append(","); + ret.append("serial=").append(isSerial()).append(","); + ret.append("recoveryCoordinatorURI=").append(recoveryCoordinatorUri).append(","); + addParent(getRootTransaction(), ret); + if (!getRootTransaction().isSameTransaction(getParentTransaction())) { + ret.append(","); + addParent(getParentTransaction(), ret); + } + return ret.toString(); + } + + private void addParent(CompositeTransaction parent, StringBuffer buf) { + buf.append("parent=").append(parent.getTid()); + Properties p = parent.getProperties(); + for (String key : p.stringPropertyNames()) { + buf.append(","). + append("property.").append(key). //use prefix to avoid name collisions between property keys and propagation attributes + append("=").append(p.getProperty(key)); + } + } +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/RecoveryCoordinator.java b/public/transactions-api/src/main/java/com/atomikos/icatch/RecoveryCoordinator.java new file mode 100644 index 000000000..265ea63d7 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/RecoveryCoordinator.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * The coordinator who knows the outcome for recovery purposes. + */ + +public interface RecoveryCoordinator +{ + + + /** + * Gets the URI identifier for this coordinator. + * @return String The URI identifier. + */ + + String getURI(); + + /** + * + * @return The recovery domain of this coordinator; a different one indicates a foreign transaction + * (i.e., one whose commit decision is unknown in our logs). + */ + String getRecoveryDomainName(); + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/RecoveryService.java b/public/transactions-api/src/main/java/com/atomikos/icatch/RecoveryService.java new file mode 100644 index 000000000..a8a72be21 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/RecoveryService.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import com.atomikos.recovery.RecoveryLog; + + +/** + * A handle to the TM that resources can use to recover. + */ + +public interface RecoveryService +{ + + /** + * @return String The unique name of the TM. Resources can use this name to determine what resource + * transactions need to be considered for recovery by this + * transaction service. + */ + + String getName(); + + + RecoveryLog getRecoveryLog(); + + /** + * Instructs the core to do a full recovery cycle. + * + * @return False if no recovery was done, + * for instance if this node is not responsible for recovery + * or if there was a concurrent shutdown. + */ + boolean performRecovery(); + + /** + * Asks the core to do a full recovery cycle. + * + * @param lax True to allow for lax optimisation + * so the actual overhead of recovery can be avoided in + * some cases. Depending on your deployment, lax mode + * may be accurate (or not). + * + * @return False if no recovery was done. + */ + boolean performRecovery(boolean lax); +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/RollbackException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/RollbackException.java new file mode 100644 index 000000000..3ccfa30c5 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/RollbackException.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + + +/** + * An exception indicating that a transaction has already been rolled back. + */ + + public class RollbackException extends Exception + { + + private static final long serialVersionUID = 1L; + + public RollbackException(String msg) + { + super (msg); + } + + public RollbackException(String msg, Throwable cause) { + super(msg,cause); + } + + public RollbackException() + { + super(); + } + } diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/SubTxAwareParticipant.java b/public/transactions-api/src/main/java/com/atomikos/icatch/SubTxAwareParticipant.java new file mode 100644 index 000000000..c1b091210 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/SubTxAwareParticipant.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * A participant that wants to be notified of local termination of a node in a + * nested transaction tree. + */ + +public interface SubTxAwareParticipant +{ + /** + * Notification of termination. + * + * @param transaction The composite transaction that has terminated + * locally at its node. + */ + + void committed ( CompositeTransaction transaction ); + + /** + * Notification that some transaction has been rolledback. + * + * @param parent The transaction that has rolled back at its node. + */ + + void rolledback ( CompositeTransaction transaction ); +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/Synchronization.java b/public/transactions-api/src/main/java/com/atomikos/icatch/Synchronization.java new file mode 100644 index 000000000..e57b38109 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/Synchronization.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import com.atomikos.recovery.TxState; + + + + /** + * A synchronization inferface for transaction termination callbacks. + * Instances are volatile, i.e. not recovered after a crash/restart. + */ + + public interface Synchronization { + /** + * Called before prepare decision is made. + */ + + void beforeCompletion (); + + /** + * Called after the overall outcome is known. + * + * @param txstate The state of the coordinator after preparing. + * Equals either null ( readonly ), TxState.COMMITTING or TxState.ABORTING. + */ + + void afterCompletion ( TxState txstate ); + } diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/SysException.java b/public/transactions-api/src/main/java/com/atomikos/icatch/SysException.java new file mode 100644 index 000000000..5e4c44a39 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/SysException.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + +/** + * An exception for unexpected system errors with nested information. + */ + + +public class SysException extends RuntimeException +{ + + private static final long serialVersionUID = -9183281406145817016L; + + public SysException (String msg) + { + super(msg); + } + + public SysException(String msg, Throwable cause) { + super(msg,cause); + } + + public SysException(Throwable cause) { + super(cause); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/TransactionService.java b/public/transactions-api/src/main/java/com/atomikos/icatch/TransactionService.java new file mode 100644 index 000000000..b7d142866 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/TransactionService.java @@ -0,0 +1,136 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + + /** + * This internal interface is the base interface for creating transactions. + * It also acts as a container for existing transactions. + * Each transaction manager has a transaction service to take care + * of recovery, and for creating transactions and subtransactions. + * + * Application code should not use this interface directly. + */ + +public interface TransactionService +{ + /** + * Adds a listener to the transaction service. + * This method should be called before init! + * If called afterwards, only the init (false) + * callback will happen in the listener. + * + * @param listener The listener. + */ + + void addTSListener ( TransactionServicePlugin listener ); + + /** + * Removes a listener from the transaction service. + * @param listener The listener. + */ + + void removeTSListener ( TransactionServicePlugin listener ); + + + /** + * Gets the composite transaction with the given tid. + * @param tid The transaction identifier. + * @return CompositeTransaction The transaction, or null if none. + */ + + CompositeTransaction getCompositeTransaction ( String tid ); + + + + /** + * Starts a new transaction. + * + * + * @timeout Timeout (in millis) after which heuristics are done for indoubts. + * + * + * @return CompositeTransaction The new instance. + * + * @exception SysException + * + */ + + CompositeTransaction createCompositeTransaction ( + long timeout ) + throws SysException; + + + /** + * Recreates a composite transaction based on an imported context. + * Needed by the application's communication layer. + * + * @param context The propagation context. Any interposition + * actions should already have taken place, so that the propagation + * is ready to be used by the local transaction service. + * + * @param timeout Time in millis after which heur_commit is applied. + * + * @return CompositeTransaction The recreated local instance. + * throws SysException + */ + + + CompositeTransaction + recreateCompositeTransaction ( Propagation context) + throws SysException; + + /** + * Shuts down the server in a clean way. + * + * @param force If true, shutdown will not wait + * for possibly indoubt txs to finish. + * Calling shutdown with force being true implies that + * shutdown will not fail, but there may be remaining timer + * threads that stay asleep until there timeouts expire. + * Such remaining active transactions will NOT be able to finish, + * because the recovery manager will be shutdown by that time. + * New transactions will not be allowed. + * + * @exception IllegalStateException If active transactions exist, and not force. + * @exception SysException + */ + + void shutdown ( boolean force ) + throws SysException, IllegalStateException; + + /** + * Gets a participant handle for the given root. + * This method is for subordinated coordinators: in those + * cases, a participant role is fulfilled w.r.t. parent + * coordinators. + * @param root The root identifier. + * @return Participant The participant instance. + * @exception SysException On failure, or if the given root is not known. + */ + + Participant getParticipant ( String root ) + throws SysException; + + /** + * Gets a composite coordinator for the given root. + * Needed to allow TM to swap out instances after + * heuristic termination. + * If a commit, abort, forget or replay request comes in, + * this method should be called first to revive the instance. + * + * @param root The root in case. + * @return The composite coordinator for this root. + * @exception SysException + */ + + CompositeCoordinator getCompositeCoordinator ( String root ) + throws SysException; + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/TransactionServicePlugin.java b/public/transactions-api/src/main/java/com/atomikos/icatch/TransactionServicePlugin.java new file mode 100644 index 000000000..1393c5cc4 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/TransactionServicePlugin.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + + + /** + * A plugin interface for transaction service extension modules. + * Instances can register themselves via the ServiceLoader mechanism + * in order to be notified about startup and shutdown events. + */ + +public interface TransactionServicePlugin +{ + /** + * Called before initialization of the transaction core. + * + * + * DISCLAIMER: only implementations that register with the ServiceLoader + * mechanism are sure of receiving this notification. Other implementations + * should be aware that the transaction core may already be running by the + * time they register - in which case there will be no callback. + * + */ + + void beforeInit(); + + /** + * Called after initialization of the transaction core. + */ + + void afterInit(); + + /** + * Called after shutdown of the transaction core. + */ + + void afterShutdown(); +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/config/Configuration.java b/public/transactions-api/src/main/java/com/atomikos/icatch/config/Configuration.java new file mode 100644 index 000000000..da1574f75 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/config/Configuration.java @@ -0,0 +1,441 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + +package com.atomikos.icatch.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Vector; + +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.datasource.ResourceException; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.RecoveryService; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionService; +import com.atomikos.icatch.TransactionServicePlugin; +import com.atomikos.icatch.provider.Assembler; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.icatch.provider.TransactionServiceProvider; +import com.atomikos.recovery.RecoveryLog; + +/** + * Configuration is a facade for the transaction management core. + * Allows the application code to find the transaction manager, even if + * the actual implementation varies over time. + */ + +@SuppressWarnings("all") +public final class Configuration +{ + + + private static CompositeTransactionManager ctxmgr_ = null; + // the tm for the virtual machine instance + + private static Hashtable resources_ = new Hashtable (); + // filled on startup, contains all resources managed by the + // transaction manager. + + private static Vector resourceList_ = new Vector (); + // keep resources in a list, to enable ordered search of XAResource + // this way, an AcceptAllXATransactionalResource can be added at the end + + + private static RecoveryService recoveryService_; + // needed for addResource to do recovery + + private static TransactionServiceProvider service_; + // the transaction service for this VM. + + private static Vector tsListenersList_ = new Vector (); + + private static List shutdownHooks_ = new ArrayList(); + + private static Assembler assembler; + + private static ConfigProperties configProperties; + + + private static void purgeResources () + { + Collection resources = getResources (); + for(RecoverableResource res : resources) { + if (res.isClosed()) { + removeResource(res.getName()); + } + } + } + + /** + * Construction not allowed. + * + */ + private Configuration () + { + } + + private static void addAllTransactionServicePluginServicesFromClasspath() { + ServiceLoader loader = ServiceLoader.load(TransactionServicePlugin.class,Configuration.class.getClassLoader()); + for (TransactionServicePlugin l : loader ) { + registerTransactionServicePlugin(l); + } + } + + /** + * Adds a shutdown hook to the configuration. + * Shutdown hooks are managed here, since regular shutdown + * of the transaction core should remove hooks + * (cf case 21519). + * + * @param hook + */ + private static synchronized void addShutdownHook ( Thread hook ) + { + if ( shutdownHooks_.contains ( hook ) ) return; + + shutdownHooks_.add ( hook ); + try { + Runtime.getRuntime().addShutdownHook ( hook ); + } + catch ( IllegalStateException alreadyShuttingDownVm ) { + //ignore: this happens when the VM exits and this method + //is called as part of one of the shutdown hooks executing + } + } + + + /** + * Removes all shutdown hooks from the system. + * This method should be called on shutdown of the core. + */ + + private static synchronized void removeShutdownHooks() + { + Iterator it = shutdownHooks_.iterator(); + + //first check if we are not already doing a VM exit; + //don't remove the hooks if so + boolean vmShutdown = false; + while ( it.hasNext() ) { + Thread t = ( Thread ) it.next(); + if ( t.equals ( Thread.currentThread() ) ) vmShutdown = true; + } + + it = shutdownHooks_.iterator(); + while ( !vmShutdown && it.hasNext() ) { + Thread hook = ( Thread ) it.next(); + it.remove(); + try { + Runtime.getRuntime().removeShutdownHook ( hook ); + } + catch ( IllegalStateException alreadyShuttingDownVm ) { + //ignore: this happens when the VM exits and this method + //is called as part of one of the shutdown hooks executing + } + } + } + + /** + * Retrieves the transaction service being used. + * + * @return TransactionService The transaction service. + */ + + public static TransactionService getTransactionService () + { + return service_; + } + + /** + * Add a transaction service listener. + * + * @param l + * The listener. + */ + public static synchronized void registerTransactionServicePlugin ( TransactionServicePlugin l ) + { + + if ( service_ != null ) { + service_.addTSListener ( l ); + } + tsListenersList_.add ( l ); + } + + /** + * Remove a transaction service listener. + * + * @param l + * The listener. + */ + public static synchronized void unregisterTransactionServicePlugin ( TransactionServicePlugin l ) + { + if ( service_ != null ) { + service_.removeTSListener ( l ); + } + tsListenersList_.remove ( l ); + } + + /** + * Installs a composite transaction manager as a Singleton. + * + * @param compositeTransactionManager + * The instance to install. + */ + + public static synchronized void installCompositeTransactionManager ( + CompositeTransactionManager compositeTransactionManager ) + { + + ctxmgr_ = compositeTransactionManager; + } + + /** + * Get the composite transaction manager. + * + * @return CompositeTransactionManager The instance, or null if none. + */ + + public static CompositeTransactionManager getCompositeTransactionManager () + { + return ctxmgr_; + } + + + + /** + * Add a resource to the transaction manager domain. Should be called for + * all resources that have to be recovered, BEFORE initializing the + * transaction manager! The purpose of registering resources is mainly to be + * able the recovery the ResourceTransaction context for each prepared + * ResourceTransction. This is needed for those ResourceTransaction + * instances that do not encapsulate the full state themselves, as in the + * XAResource case. + * + * @param resource + * The resource to add. + * + * @exception IllegalStateException + * If the name of the resource is already in use. + */ + + public static synchronized void addResource ( RecoverableResource resource ) + throws IllegalStateException + { + // ADDED with new recovery: temporary resources: + // memory overflow can only happen upon addition of resources + // so before each add, first purge closed resources to make room + purgeResources (); + + if ( resources_.containsKey ( resource.getName () ) ) + throw new IllegalStateException ( "Another resource already exists with name " + resource.getName() + " - pick a different name"); + + + //FIRST add init resource, only then add it - cf case 142795 + resource.setRecoveryService ( recoveryService_ ); + resources_.put ( resource.getName (), resource ); + resourceList_.add ( resource ); + } + + /** + * Removes a resource from the config. + * + * @param name + * The resource's name. + * @return RecoverableResource The removed object. + */ + + public static RecoverableResource removeResource ( String name ) + { + RecoverableResource ret = null; + if ( name != null ) { + ret = (RecoverableResource) resources_.remove ( name ); + if ( ret != null ) resourceList_.remove ( ret ); + + } + return ret; + } + + /** + * Get the resource with the given name. + * + * @return RecoverableResource The resource. + * @param name + * The name to find. + */ + + public static RecoverableResource getResource ( String name ) + { + RecoverableResource res = null; + if ( name != null ) res = (RecoverableResource) resources_.get ( name ); + return res; + } + + /** + * Get all resources added so far, in the order that they were added. + * + */ + + public static Collection getResources () + { + // clone to avoid concurrency problems with + // add/removeResource (new recovery makes this possible) + Vector ret = (Vector) resourceList_.clone (); + return ret; + } + + protected static synchronized Assembler getAssembler() { + if (assembler == null) loadAssembler(); + return assembler; + } + + private static void loadAssembler() { + ServiceLoader loader = ServiceLoader.load(Assembler.class,Configuration.class.getClassLoader()); + Iterator it = loader.iterator(); + if (it.hasNext()) { + assembler = it.next(); + } else { + throw new SysException("No Assembler service found - please make sure that the right jars are in your classpath"); + } + } + + public static synchronized ConfigProperties getConfigProperties() { + if (configProperties == null) { + configProperties = getAssembler().initializeProperties(); + } + return configProperties; + } + + static synchronized void resetConfigProperties() { + configProperties = null; + } + + public static synchronized void shutdown(boolean force) { + if (service_ != null) { + service_.shutdown(force); + notifyAfterShutdown(); + removeShutdownHooks(); + removeAndCloseResources(force); + clearSystemComponents(); + } + } + + private static void clearSystemComponents() { + service_ = null; + recoveryService_ = null; + ctxmgr_ = null; + tsListenersList_.clear(); + resetConfigProperties(); + assembler = null; + } + + private static void notifyAfterShutdown() { + for (TransactionServicePlugin p : tsListenersList_) { + p.afterShutdown(); + } + } + + private static void removeAndCloseResources(boolean force) { + Collection resources = Configuration.getResources(); + for (RecoverableResource res : resources) { + Configuration.removeResource ( res.getName () ); + try { + res.close (); + } catch ( ResourceException re ) { + //Issue 10038: + //Ignore errors in force mode: force is most likely + //during VM exit; in that case interleaving of shutdown hooks + //means that resource connectors may have closed already + //by the time the TM hook runs. We don't want useless + //reports in that case. + //NOTE: any invalid states will be detected during the next + //(re)init so they can be ignored here (if force mode) + + if ( !force ) { + re.printStackTrace(); + } + } + } + } + + /** + * + * @return False if already running. + */ + public static synchronized boolean init() { + boolean startupInitiated = false; + if (service_ == null) { + startupInitiated = true; + addAllTransactionServicePluginServicesFromClasspath(); + notifyBeforeInit(); + assembleSystemComponents(); + initializeSystemComponents(); + notifyAfterInit(); + if (configProperties.getForceShutdownOnVmExit()) { + addShutdownHook(new ForceShutdownHook()); + } + } + return startupInitiated; + } + + private static void notifyAfterInit() { + for (TransactionServicePlugin p : tsListenersList_) { + p.afterInit(); + } + for (RecoverableResource r : resourceList_ ) { + r.setRecoveryService(recoveryService_); + } + + } + + private static void initializeSystemComponents() { + service_.init(getConfigProperties().getCompletedProperties()); + } + + private static void notifyBeforeInit() { + for (TransactionServicePlugin p : tsListenersList_) { + p.beforeInit(); + } + } + + private static void assembleSystemComponents() { + Assembler assembler = getAssembler(); + service_ = assembler.assembleTransactionService(); + recoveryService_ = service_.getRecoveryService(); + ctxmgr_ = assembler.assembleCompositeTransactionManager(); + } + + public static RecoveryLog getRecoveryLog() { + return recoveryService_.getRecoveryLog(); + } + + public static RecoveryService getRecoveryService() { + return recoveryService_; + } + + private static class ForceShutdownHook extends Thread + { + + + private ForceShutdownHook() + { + super(); + } + + public void run() { + Configuration.shutdown(true); + } + } + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/config/UserTransactionService.java b/public/transactions-api/src/main/java/com/atomikos/icatch/config/UserTransactionService.java new file mode 100644 index 000000000..76c03773e --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/config/UserTransactionService.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.config; +import java.util.Properties; + +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionServicePlugin; + + /** + * + * The user's (client program) view of the transaction manager's configuration, + * with all the information the client program needs. + * + */ + +public interface UserTransactionService +{ + void shutdown(boolean force) throws IllegalStateException; + + void registerResource ( RecoverableResource resource ); + + void removeResource ( RecoverableResource res ); + + void registerTransactionServicePlugin ( TransactionServicePlugin listener ); + + void removeTransactionServicePlugin ( TransactionServicePlugin listener ); + + void init ( Properties properties ) throws SysException; + + void init() throws SysException; + + CompositeTransactionManager getCompositeTransactionManager(); + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/config/UserTransactionServiceImp.java b/public/transactions-api/src/main/java/com/atomikos/icatch/config/UserTransactionServiceImp.java new file mode 100644 index 000000000..9909245fc --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/config/UserTransactionServiceImp.java @@ -0,0 +1,207 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + + +package com.atomikos.icatch.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionServicePlugin; +import com.atomikos.icatch.provider.ConfigProperties; + +/** + * This is the main class for creating a UserTransactionService instance. + * This class is the client's main entry point into the transaction engine. + * + * The creation of a user transaction happens via a UserTransactionServiceFactory + * which is looked up by this object behind the scenes. + * Instances can be serialized to disk and re-read at a later time. + * Note: deserialization will only work in the SAME configuration as the + * one that did the streaming out. In particular, if no identical Atomikos + * transaction service is present in the target VM then the process + * of deserialization will be erroneous. + */ + +public final class UserTransactionServiceImp + implements java.io.Serializable , UserTransactionService, OrderedLifecycleComponent +{ + + + private static final long serialVersionUID = -3374591336514451887L; + + private Properties properties_; + + private List tsListeners_; + private List resources_; + + /** + * Default constructor. + * + */ + + public UserTransactionServiceImp () + { + tsListeners_ = new ArrayList(); + resources_ = new ArrayList(); + properties_ = new Properties(); + + } + + /** + * Constructs a new instance and initializes it with the given properties. + * If this constructor is called, then file-based initialization is overridden. + * In particular, the given properties will take precedence over the file-based + * properties (if found). + * + * @param properties The properties. + */ + + public UserTransactionServiceImp ( Properties properties ) + { + this(); + properties_ = properties; + } + + /** + * + * @see UserTransactionService + */ + + public void shutdown ( boolean force ) + throws IllegalStateException + { + Configuration.shutdown(force); + } + + + + + private void initialize() { + for (RecoverableResource resource : resources_) { + Configuration.addResource ( resource ); + } + for (TransactionServicePlugin nxt : tsListeners_) { + Configuration.registerTransactionServicePlugin ( nxt ); + } + ConfigProperties configProps = Configuration.getConfigProperties(); + configProps.applyUserSpecificProperties(properties_); + Configuration.init(); + } + + /** + *@see UserTransactionService + */ + + public CompositeTransactionManager + getCompositeTransactionManager () + { + return Configuration.getCompositeTransactionManager(); + } + + + /** + * @see com.atomikos.icatch.UserTransactionService#registerResource(com.atomikos.datasource.RecoverableResource) + */ + public void registerResource(RecoverableResource res) + { + Configuration.addResource(res); + + } + + public void removeResource ( RecoverableResource res ) + { + Configuration.removeResource(res.getName()); + + } + + public void registerTransactionServicePlugin ( TransactionServicePlugin listener ) + { + Configuration.registerTransactionServicePlugin(listener); + } + + public void removeTransactionServicePlugin ( TransactionServicePlugin listener ) + { + Configuration.unregisterTransactionServicePlugin(listener); + } + + + /** + * Convenience shutdown method for DI containers like Spring. + * + */ + + public void shutdownForce() + { + shutdown ( true ); + + } + + /** + * Convenience shutdown method for DI containers like Spring. + * + */ + + public void shutdownWait() + { + shutdown ( false ); + + } + + /** + * Dependency injection of all resources to be added during init. + * + * @param resources + */ + public void setInitialRecoverableResources ( List resources ) + { + resources_ = resources; + + } + + /** + * Dependency injection of extra plugins to be added during init. + * @param listeners + */ + public void setInitialTransactionServicePlugins ( List listeners ) + { + tsListeners_ = listeners; + + } + + /** + * Convenience init method for DI containers like Spring. + * + */ + public void init() + { + initialize(); + } + + + /** + * Initializes with given properties. + */ + public void init ( Properties properties ) throws SysException { + properties_ = properties; + initialize(); + } + + @Override + public void close() throws Exception { + shutdownWait(); + } + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/event/Event.java b/public/transactions-api/src/main/java/com/atomikos/icatch/event/Event.java new file mode 100644 index 000000000..0c5380152 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/event/Event.java @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.event; + +/** + * Significant core events that are communicated to plugged-in listeners. + */ +public abstract class Event { + + public final long eventCreationTimestamp; + + protected Event() { + this.eventCreationTimestamp = System.currentTimeMillis(); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/event/EventListener.java b/public/transactions-api/src/main/java/com/atomikos/icatch/event/EventListener.java new file mode 100644 index 000000000..78425ea26 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/event/EventListener.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.event; + + +/** + * Observer interface for transaction-related domain events. + * + * External applications/modules can implement this functionality to + * be notified of significant events. Implementations are registered + * via the JDK 6+ ServiceLoader mechanism. + * + * CAUTION: event notification is synchronous, so registering listeners + * may impact performance of the core! + */ +public interface EventListener { + + void eventOccurred(Event event); + +} \ No newline at end of file diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/ParticipantHeuristicEvent.java b/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/ParticipantHeuristicEvent.java new file mode 100644 index 000000000..a7e90fe18 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/ParticipantHeuristicEvent.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.event.transaction; + +import com.atomikos.recovery.TxState; + +/** + * Signals heuristic outcome on behalf of a participant. + */ +public class ParticipantHeuristicEvent extends TransactionEvent { + + public final String participantUri; + public final TxState state; + + public ParticipantHeuristicEvent(String transactionId, String participantUri, TxState state) { + super(transactionId); + this.participantUri = participantUri; + this.state = state; + } + + @Override + public String toString() { + StringBuffer ret = new StringBuffer(); + ret.append("Heuristic state detected: ").append(state). + append(" for participant ").append(participantUri). + append(" in transaction: ").append(transactionId). + append(" (HINT: check https://www.atomikos.com/Documentation/HowToHandleHeuristics to learn more on how to handle heuristics...)"); + + return ret.toString(); + } +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/TransactionEvent.java b/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/TransactionEvent.java new file mode 100644 index 000000000..5c1fb9b7a --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/TransactionEvent.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.event.transaction; + +import com.atomikos.icatch.event.Event; + + +/** + * Domain event raised whenever something significant happens in the transaction life cycle. + * + */ + +public abstract class TransactionEvent extends Event { + + public final String transactionId; + + protected TransactionEvent(String transactionId) { + this.transactionId = transactionId; + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/TransactionHeuristicEvent.java b/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/TransactionHeuristicEvent.java new file mode 100644 index 000000000..c11e3ecce --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/event/transaction/TransactionHeuristicEvent.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.event.transaction; + +import com.atomikos.recovery.TxState; + + /** + * Event to signal a heuristic outcome. + * + */ + +public class TransactionHeuristicEvent extends TransactionEvent { + + public final String parentTransactionId; + public final TxState state; + + public TransactionHeuristicEvent(String transactionId, String parentTransactionId, TxState state) { + super(transactionId); + this.parentTransactionId = parentTransactionId; + this.state = state; + } + + @Override + public String toString() { + StringBuffer ret = new StringBuffer(); + ret.append("Detected state: ").append(state). + append(" for transaction ").append(transactionId); + if (parentTransactionId != null) { + ret.append(" with parent transaction ").append(parentTransactionId); + } + if (state.isHeuristic()) { + ret.append(" (HINT: check https://www.atomikos.com/Documentation/HowToHandleHeuristics to learn more on how to handle heuristics...)"); + } + + return ret.toString(); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/provider/Assembler.java b/public/transactions-api/src/main/java/com/atomikos/icatch/provider/Assembler.java new file mode 100644 index 000000000..9085f00d7 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/provider/Assembler.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.provider; + +import com.atomikos.icatch.CompositeTransactionManager; + + /** + * Abstraction of how the API is instantiated. + * Instances are found by the Configuration class, + * via the ServiceLoader mechanism of the JDK. + */ + +public interface Assembler { + + ConfigProperties initializeProperties(); + + TransactionServiceProvider assembleTransactionService(); + + CompositeTransactionManager assembleCompositeTransactionManager(); + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/provider/ConfigProperties.java b/public/transactions-api/src/main/java/com/atomikos/icatch/provider/ConfigProperties.java new file mode 100644 index 000000000..3a61392d6 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/provider/ConfigProperties.java @@ -0,0 +1,277 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.provider; + +import java.lang.management.ManagementFactory; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.Properties; + +public final class ConfigProperties { + + + public static final String TM_UNIQUE_NAME_PROPERTY_NAME = "com.atomikos.icatch.tm_unique_name"; + public static final String LOG_BASE_DIR_PROPERTY_NAME = "com.atomikos.icatch.log_base_dir"; + public static final String LOG_BASE_NAME_PROPERTY_NAME = "com.atomikos.icatch.log_base_name"; + public static final String ENABLE_LOGGING_PROPERTY_NAME = "com.atomikos.icatch.enable_logging"; + public static final String MAX_TIMEOUT_PROPERTY_NAME = "com.atomikos.icatch.max_timeout"; + public static final String MAX_ACTIVES_PROPERTY_NAME = "com.atomikos.icatch.max_actives"; + public static final String FORCE_SHUTDOWN_ON_VM_EXIT_PROPERTY_NAME = "com.atomikos.icatch.force_shutdown_on_vm_exit"; + public static final String FILE_PATH_PROPERTY_NAME = "com.atomikos.icatch.file"; + public static final String CHECKPOINT_INTERVAL_PROPERTY_NAME = "com.atomikos.icatch.checkpoint_interval"; + + public static final String FORGET_ORPHANED_LOG_ENTRIES_DELAY_PROPERTY_NAME = "com.atomikos.icatch.forget_orphaned_log_entries_delay"; + public static final String OLTP_MAX_RETRIES_PROPERTY_NAME = "com.atomikos.icatch.oltp_max_retries"; + public static final String OLTP_RETRY_INTERVAL_PROPERTY_NAME = "com.atomikos.icatch.oltp_retry_interval"; + public static final String RECOVERY_DELAY_PROPERTY_NAME = "com.atomikos.icatch.recovery_delay"; + + public static final String ALLOW_SUBTRANSACTIONS_PROPERTY_NAME = "com.atomikos.icatch.allow_subtransactions"; + public static final String THROW_ON_HEURISTIC_PROPERTY_NAME = "com.atomikos.icatch.throw_on_heuristic"; + public static final String JVM_ID_PROPERTY_NAME = "com.atomikos.icatch.jvm_id"; + + public static final String LOG_LOCK_ACQUISITION_MAX_ATTEMPTS = "com.atomikos.icatch.log_lock_acquisition_max_attempts"; + public static final String LOG_LOCK_ACQUISITION_RETRY_DELAY = "com.atomikos.icatch.log_lock_acquisition_retry_delay"; + + + /** + * Replace ${...} sequence with the referenced value from the given properties or + * (if not found) the system properties - + * contributed through Marian Kelc (marian.kelc@eplus.de) + * E-Plus Mobilfunk GmbH & Co. KG, Germany + */ + private static String evaluateReference ( String value , Properties properties ) + { + String result = value; + //by default, the value as-is is returned + + int startIndex = value.indexOf ( '$' ); + if ( startIndex > -1 && value.charAt ( startIndex +1 ) == '{') { + //at least one reference is found + int endIndex = value.indexOf ( '}' ); + if ( startIndex + 2 == endIndex ) + throw new IllegalArgumentException ( "property ref cannot refer to an empty name: ${}" ); + if ( endIndex == -1 ) + throw new IllegalArgumentException ( "unclosed property ref: ${" + value.substring ( startIndex + 2 ) ); + + //strip-off reference characters -> get the referenced property name + String subPropertyKey = value.substring ( startIndex + 2, endIndex ); + //the properties take precedence -> try them first + String subPropertyValue = properties.getProperty ( subPropertyKey ); + if ( subPropertyValue == null ) { + //not found in properties -> try system property + subPropertyValue = System.getProperty ( subPropertyKey ); + } + + if ( subPropertyValue != null ) { + //in-line refs supported - result is prefix + value + suffix !!! + result = result.substring ( 0, startIndex ) + subPropertyValue + result.substring ( endIndex +1 ); + //two or more refs supported - evaluate any remaining references in the value + result = evaluateReference ( result , properties ); + } + else { + //referenced value not found -> ignore any other references and return value as-is + //NOTE: trying to resolve further references would lead to infinite recursion + } + + } + + return result; + } + + private static String getDefaultName () + { + + String ret = "tm"; + try { + ret = java.net.InetAddress.getLocalHost ().getHostAddress () + + ".tm"; + } catch ( UnknownHostException e ) { + // ignore: use short default + } + + return ret; + } + + + private Properties properties; + + + public ConfigProperties(Properties properties) { + if (properties == null) throw new IllegalArgumentException("Properties should not be null"); + this.properties = properties; + } + + private void setDefaultTmUniqueName() { + if (properties.getProperty(TM_UNIQUE_NAME_PROPERTY_NAME) == null ) { + properties.setProperty(TM_UNIQUE_NAME_PROPERTY_NAME, getDefaultName()); + } + } + + private void setDefaultJvmId() { + if (properties.getProperty(JVM_ID_PROPERTY_NAME) == null ) { + properties.setProperty(JVM_ID_PROPERTY_NAME, getDefaultJvmId()); + } + } + + private String getDefaultJvmId() { + return ManagementFactory.getRuntimeMXBean().getName(); + } + + private void applySystemProperties() { + Properties systemProperties = System.getProperties(); + Enumeration propertyNames = systemProperties.propertyNames(); + while (propertyNames.hasMoreElements()) { + String name = (String) propertyNames.nextElement(); + if (name.startsWith("com.atomikos")) { + properties.setProperty(name, systemProperties.getProperty(name)); + } + } + } + + private void substitutePlaceHolderValues() { + //resolve referenced values with ant-like ${...} syntax + Enumeration allProps= properties.propertyNames(); + while ( allProps.hasMoreElements() ) { + String key = ( String ) allProps.nextElement(); + String raw = properties.getProperty ( key ); + String value= evaluateReference ( raw , properties ); + if ( !raw.equals ( value ) ) { + properties.setProperty ( key, value ); + } + } + } + + public String getProperty(String name) { + completeProperties(); + String ret = properties.getProperty(name); + if (ret == null) { + throw new IllegalArgumentException("Missing required property: " + name); + } + ret = ret.trim(); + return ret; + } + + public void setProperty(String name, String value) { + properties.setProperty(name, value); + } + + public boolean getAsBoolean(String name) { + boolean ret = false; + String retAsString = getProperty(name); + ret = Boolean.valueOf(retAsString); + return ret; + } + + public int getAsInt(String name) { + String retAsString = getProperty(name); + return Integer.valueOf(retAsString); + } + + public long getAsLong(String name) { + String retAsString = getProperty(name); + return Long.valueOf(retAsString); + } + + public String getTmUniqueName() { + return getProperty(TM_UNIQUE_NAME_PROPERTY_NAME); + } + + public String getLogBaseDir() { + return getProperty(LOG_BASE_DIR_PROPERTY_NAME); + } + + public String getLogBaseName() { + return getProperty(LOG_BASE_NAME_PROPERTY_NAME); + } + + public boolean getEnableLogging() { + return getAsBoolean(ENABLE_LOGGING_PROPERTY_NAME); + } + + public long getMaxTimeout() { + return getAsLong(MAX_TIMEOUT_PROPERTY_NAME); + } + + public int getMaxActives() { + return getAsInt(MAX_ACTIVES_PROPERTY_NAME); + } + + public long getCheckpointInterval(){ + return getAsLong(CHECKPOINT_INTERVAL_PROPERTY_NAME); + } + + public void applyUserSpecificProperties(Properties userSpecificProperties) { + Enumeration names = userSpecificProperties.propertyNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + properties.setProperty(name, userSpecificProperties.getProperty(name)); + } + } + + public Properties getCompletedProperties() { + Properties ret = new Properties(); + completeProperties(); + Enumeration propertyNames = properties.propertyNames(); + while (propertyNames.hasMoreElements()) { + String name = (String) propertyNames.nextElement(); + ret.setProperty(name, getProperty(name)); + } + return ret; + } + + private void completeProperties() { + applySystemProperties(); + substitutePlaceHolderValues(); + setDefaultTmUniqueName(); + setDefaultJvmId(); + } + + public boolean getForceShutdownOnVmExit() { + return getAsBoolean(FORCE_SHUTDOWN_ON_VM_EXIT_PROPERTY_NAME); + } + + public long getForgetOrphanedLogEntriesDelay() { + return getAsLong(FORGET_ORPHANED_LOG_ENTRIES_DELAY_PROPERTY_NAME); + } + + public int getOltpMaxRetries() { + return getAsInt(OLTP_MAX_RETRIES_PROPERTY_NAME); + } + + public long getOltpRetryInterval() { + return getAsInt(OLTP_RETRY_INTERVAL_PROPERTY_NAME); + } + + public long getRecoveryDelay() { + return getAsLong(RECOVERY_DELAY_PROPERTY_NAME); + } + + public boolean getAllowSubTransactions() { + return getAsBoolean(ALLOW_SUBTRANSACTIONS_PROPERTY_NAME); + } + + public boolean getThrowOnHeuristic() { + return getAsBoolean(THROW_ON_HEURISTIC_PROPERTY_NAME); + } + + public int getLockAcquisitionMaxAttempts(){ + return getAsInt(LOG_LOCK_ACQUISITION_MAX_ATTEMPTS); + } + + public long getLockAcquisitionRetryDelay(){ + return getAsLong(LOG_LOCK_ACQUISITION_RETRY_DELAY); + } + + public String getJvmId() { + return getProperty(JVM_ID_PROPERTY_NAME); + + } + + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/icatch/provider/TransactionServiceProvider.java b/public/transactions-api/src/main/java/com/atomikos/icatch/provider/TransactionServiceProvider.java new file mode 100644 index 000000000..59b1d923c --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/icatch/provider/TransactionServiceProvider.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.provider; + +import java.util.Properties; + +import com.atomikos.icatch.RecoveryService; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionService; + +public interface TransactionServiceProvider extends TransactionService { + + void init(Properties properties) throws SysException; + + RecoveryService getRecoveryService(); + + void shutdown(boolean force); + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/LogException.java b/public/transactions-api/src/main/java/com/atomikos/recovery/LogException.java new file mode 100644 index 000000000..2835757ea --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/LogException.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + +public class LogException extends Exception { + + private static final long serialVersionUID = 3259337218182873867L; + + public LogException() { + super(); + } + + public LogException(String message) { + super(message); + } + + public LogException(Throwable cause) { + super(cause); + } +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/LogReadException.java b/public/transactions-api/src/main/java/com/atomikos/recovery/LogReadException.java new file mode 100644 index 000000000..9ecc037bb --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/LogReadException.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + +public class LogReadException extends LogException { + + private static final long serialVersionUID = -4835268355879075429L; + + public LogReadException() { + super(); + } + + public LogReadException(Throwable cause) { + super(cause); + } + + public LogReadException(String message) { + super(message); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/LogWriteException.java b/public/transactions-api/src/main/java/com/atomikos/recovery/LogWriteException.java new file mode 100644 index 000000000..2cd8ed29c --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/LogWriteException.java @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + +public class LogWriteException extends LogException { + + private static final long serialVersionUID = 5648208124041649641L; + + public LogWriteException() { + super(); + } + public LogWriteException(Throwable cause) { + super(cause); + } + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/OltpLog.java b/public/transactions-api/src/main/java/com/atomikos/recovery/OltpLog.java new file mode 100644 index 000000000..a7c553759 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/OltpLog.java @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + + + /** + * Handle to the transaction logs for writing during transaction processing. + */ +public interface OltpLog { + + void write(PendingTransactionRecord pendingTransactionRecord) throws LogWriteException; + + void close(); +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/OltpLogFactory.java b/public/transactions-api/src/main/java/com/atomikos/recovery/OltpLogFactory.java new file mode 100644 index 000000000..867342586 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/OltpLogFactory.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + + + /** + * Non-default OLTP logging can be enabled by registering an instance + * of this interface via the ServiceLoader mechanism of the JDK. + * At most one instance is allowed. If none is found, then default logging will be used. + */ + +public interface OltpLogFactory { + + /** + * @param properties The init properties picked up . + */ + public OltpLog createOltpLog(); + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/PendingTransactionRecord.java b/public/transactions-api/src/main/java/com/atomikos/recovery/PendingTransactionRecord.java new file mode 100644 index 000000000..80a621441 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/PendingTransactionRecord.java @@ -0,0 +1,204 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +public class PendingTransactionRecord { + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + private static final String COLUMN_SEPARATOR = "|"; + + public final String id; + + public final TxState state; + + public final long expires; + + public final String superiorId; + + /** + * For imported transactions, this will be a foreign recovery domain. + */ + public final String recoveryDomainName; + + + + public PendingTransactionRecord(String id, TxState state, long expires, String recoveryDomainName) { + this(id, state, expires, recoveryDomainName, null); + } + + public PendingTransactionRecord(String id, TxState state, long expires, String recoveryDomainName, String superiorId) { + super(); + this.id = id; + this.state = state; + this.expires = expires; + this.superiorId = superiorId; + this.recoveryDomainName = recoveryDomainName; + } + + public String toRecord() { + StringBuilder sb = new StringBuilder(); + sb.append(id) + .append(COLUMN_SEPARATOR) + .append(state.name()) + .append(COLUMN_SEPARATOR) + .append(expires) + .append(COLUMN_SEPARATOR) + .append(recoveryDomainName) + .append(COLUMN_SEPARATOR) + .append(superiorId==null?"":superiorId) + .append(LINE_SEPARATOR); + return sb.toString(); + } + + /** + * + * @throws IllegalArgumentException If the supplied value cannot be parsed. + */ + public static PendingTransactionRecord fromRecord(String record) { + String[] properties = record.split("\\|"); + if (properties.length < 4) { + throw new IllegalArgumentException("Invalid record value supplied: " + record); + } + String id = properties[0]; + TxState state = TxState.valueOf(properties[1]); + Long expires = Long.valueOf(properties[2]); + String recoveryDomainName = String.valueOf(properties[3]); + String superiorId = null; + if(properties.length > 4) { + superiorId = properties[4]; + } + + return new PendingTransactionRecord(id, state, expires, recoveryDomainName, superiorId); + } + + public static Collection findAllDescendants(PendingTransactionRecord entry, Collection collection) { + return collectLineages( + (PendingTransactionRecord r)-> entry.id.equals(r.superiorId), + collection); + } + + public static void removeAllDescendants(PendingTransactionRecord entry, Collection allCoordinatorLogEntries) { + Collection descendants = findAllDescendants(entry, allCoordinatorLogEntries); + for (PendingTransactionRecord descendant : descendants) { + allCoordinatorLogEntries.remove(descendant); + } + } + + /** + * + * @param predicate + * @param collection + * @return A collection of all descendants of records that match the given predicate, including the matching records. + */ + public static Collection collectLineages(AncestorPredicate predicate, Collection collection) { + Collection results = new HashSet<>(); + Map map = map(collection); + for (PendingTransactionRecord record : collection) { + if (!predicate.holdsFor(record)) { + if (record.superiorId != null) { //look for ancestor that matches + Collection ret = new HashSet<>(); + collectAncestors(ret, record.superiorId, predicate, map); + if(!ret.isEmpty()) { + ret.add(record); + results.addAll(ret); + } + } + } else { //match found already + results.add(record); + } + } + return results; + } + + private static Map map(Collection collection) { + Map ret = new HashMap<>(); + for (PendingTransactionRecord record : collection) { + ret.put(record.id, record); + } + return ret; + } + + private static void collectAncestors(Collection collector, String superiorId, AncestorPredicate predicate, Map map) { + PendingTransactionRecord superior = map.get(superiorId); + if (superior != null) { + if (predicate.holdsFor(superior)) { + collector.add(superior); + } else if (superior.superiorId != null) { + collectAncestors(collector, superior.superiorId, predicate, map); + } + } + } + + public PendingTransactionRecord markAsTerminated() { + return new PendingTransactionRecord(id, TxState.TERMINATED, expires, recoveryDomainName, superiorId); + } + + public PendingTransactionRecord markAsCommitting() { + return new PendingTransactionRecord(id, TxState.COMMITTING, expires, recoveryDomainName, superiorId); + } + + @Override + public String toString() { + return toRecord(); + } + + /** + * + * @param recoveryDomainName + * @return True iff this is a foreign record in the given domain. + */ + public boolean isForeignInDomain(String recoveryDomainName) { + return !this.recoveryDomainName.equals(recoveryDomainName); + } + + public boolean isRecoveredByDomain(String recoveryDomainName) { + boolean ret = true; + if (isForeignInDomain(recoveryDomainName) && superiorId!= null && superiorId.startsWith("http")) { + // foreign record with remote recovery available => + // this record is recovered by remote recovery in the foreign domain + ret = false; + } + return ret; + } + + public boolean isLocalRoot(String recoveryDomainName) { + return isForeignInDomain(recoveryDomainName) || superiorId == null; + } + + public boolean allowsHeuristicTermination(String recoveryDomainName) { + boolean ret = false; + if (isForeignInDomain(recoveryDomainName) && + isRecoveredByDomain(recoveryDomainName) && + state.equals(TxState.IN_DOUBT)) { + ret = true; + } + return ret; + } + + @FunctionalInterface + public static interface AncestorPredicate { + + boolean holdsFor(PendingTransactionRecord record); + } + + public static Collection extractCoordinatorIds(Collection collection, TxState... statesToFilterOn) { + HashSet ret = new HashSet<>(); + for (PendingTransactionRecord entry : collection) { + if (entry.state.isOneOf(statesToFilterOn)) { + ret.add(entry.id); + } + } + return ret; + } +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/RecoveryLog.java b/public/transactions-api/src/main/java/com/atomikos/recovery/RecoveryLog.java new file mode 100644 index 000000000..4698d9a89 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/RecoveryLog.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + +import java.util.Collection; + + /** + * Handle to the transaction logs for recovery purposes. + */ + +public interface RecoveryLog { + + /** + * @return False if we don't have to do recovery because another instance is doing it. + */ + boolean isActive(); + + /** + * Notification of JVM shutdown - allows another instance to take over. + */ + void closing(); + + Collection getIndoubtTransactionRecords() throws LogReadException; + + Collection getExpiredPendingCommittingTransactionRecordsAt(long time) throws LogReadException; + + void forgetTransactionRecords(Collection coordinators); + + /** + * Mark the given transaction as committing. + * @param coordinatorId The transaction, previously logged as IN_DOUBT. + * For retries, the IN_DOUBT may no longer exist. + * @throws LogException + */ + void recordAsCommitting(String coordinatorId) throws LogException; + + void forget(String coordinatorId); + + PendingTransactionRecord get(String coordinatorId) throws LogReadException; + + Collection getPendingTransactionRecords() throws LogReadException; + + void closed(); + +} diff --git a/public/transactions-api/src/main/java/com/atomikos/recovery/TxState.java b/public/transactions-api/src/main/java/com/atomikos/recovery/TxState.java new file mode 100644 index 000000000..aefaa9090 --- /dev/null +++ b/public/transactions-api/src/main/java/com/atomikos/recovery/TxState.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; +/** + * The states for a distributed transaction system. + */ +//@formatter:off +public enum TxState { + //OLTP States + MARKED_ABORT (false, false), + LOCALLY_DONE (false, false), + COMMITTED (false, false), + ABORTED (false, false), + ABANDONED (false, false), + + TERMINATED (true, true), + HEUR_HAZARD (false, false, TERMINATED, ABANDONED), + //Recoverable States + HEUR_COMMITTED (false, false, TERMINATED, ABANDONED), + HEUR_ABORTED (false, false, TERMINATED, ABANDONED), + HEUR_MIXED (false, false, TERMINATED, ABANDONED), + COMMITTING (true, false, HEUR_ABORTED, HEUR_COMMITTED, HEUR_HAZARD, HEUR_MIXED, TERMINATED, ABANDONED), + ABORTING (false, false, HEUR_ABORTED, HEUR_COMMITTED, HEUR_HAZARD, HEUR_MIXED, TERMINATED, ABANDONED), + IN_DOUBT (true, false, ABORTING, COMMITTING, ABANDONED, TERMINATED), + PREPARING (false, false, IN_DOUBT, ABORTING, TERMINATED, ABANDONED), + ACTIVE (false, false, ABORTING, COMMITTING, PREPARING); + + private boolean recoverableState; + + private boolean finalState; + + private TxState[] legalNextStates; + + TxState (boolean recoverableState, boolean finalState, TxState... legalNextStates) { + this.finalState=finalState; + this.recoverableState=recoverableState; + this.legalNextStates=legalNextStates; + } + + + public boolean isFinalState() { + return finalState; + } + + + public boolean isFinalStateForOltp() { + return isFinalState() || this == ABANDONED; + } + + public boolean isRecoverableState() { + return recoverableState; + } + + public boolean transitionAllowedTo(TxState nextState) { + //transition to the same state... + if(nextState == this) { + return true; + } + + for (TxState txState : legalNextStates) { + if(txState == nextState) { + return true; + } + } + return false; + } + + public boolean isOneOf(TxState... state) { + for (int i = 0; i < state.length; i++) { + if(this==state[i]) + return true; + } + return false; + } + + public boolean isHeuristic() { + return isOneOf(HEUR_ABORTED, HEUR_COMMITTED, HEUR_HAZARD, HEUR_MIXED); + } + +} diff --git a/public/transactions-api/src/test/java/com/atomikos/icatch/ExtentTestJUnit.java b/public/transactions-api/src/test/java/com/atomikos/icatch/ExtentTestJUnit.java new file mode 100644 index 000000000..47c3498db --- /dev/null +++ b/public/transactions-api/src/test/java/com/atomikos/icatch/ExtentTestJUnit.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class ExtentTestJUnit { + + private Extent extent; + private Participant p1; + private Map map; + + @Before + public void setUp() throws Exception { + p1 = Mockito.mock(Participant.class); + Mockito.when(p1.getURI()).thenReturn("p1"); + extent = new Extent(); + map = new HashMap(); + map.put("p2", 2); + + } + + @Test + public void testToStringWithParticipants() { + extent.add(p1, 1); + extent.addRemoteParticipants(map); + final String expected="version=2019,uri=p1,responseCount=1,direct=true,uri=p2,responseCount=2,direct=false"; + assertEquals(expected,extent.toString()); + } + + @Test + public void testToStringWithOneDirectParticipant() { + extent.add(p1, 1); + final String expected="version=2019,uri=p1,responseCount=1,direct=true"; + assertEquals(expected,extent.toString()); + } + + @Test + public void testToStringWithOneRemoteParticipant() { + extent.addRemoteParticipants(map); + final String expected="version=2019,uri=p2,responseCount=2,direct=false"; + assertEquals(expected,extent.toString()); + } + + @Test + public void testToStringWithParentTransactionId() { + extent = new Extent("parentId"); + final String expected = "version=2019,parent=parentId"; + assertEquals(expected, extent.toString()); + } + + @Test + public void testMergeDoesNotContainDirectParticipants() { + extent.add(p1, 1); + assertFalse(extent.getParticipants().isEmpty()); + Extent merged = new Extent(extent); + assertTrue(merged.getParticipants().isEmpty()); + assertTrue(merged.getRemoteParticipants().containsKey(p1.getURI())); + } + + @Test + public void testMergePreservesParentTransactionId() { + extent = new Extent("parentId"); + extent.add(p1, 1); + assertNotNull(extent.getParentTransactionId()); + Extent merged = new Extent(extent); + assertEquals(extent.getParentTransactionId(), merged.getParentTransactionId()); + } + +} diff --git a/public/transactions-api/src/test/java/com/atomikos/icatch/JDependTestJunit.java b/public/transactions-api/src/test/java/com/atomikos/icatch/JDependTestJunit.java new file mode 100644 index 000000000..a75c0900d --- /dev/null +++ b/public/transactions-api/src/test/java/com/atomikos/icatch/JDependTestJunit.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import jdepend.framework.JDepend; + +public class JDependTestJunit { + + @Test + public void test() throws IOException { + JDepend jdepend = new JDepend(); + jdepend.addDirectory("target/classes"); + jdepend.analyze(); + Assert.assertFalse(jdepend.containsCycles()); + } + +} diff --git a/public/transactions-api/src/test/java/com/atomikos/icatch/PropagationTestJUnit.java b/public/transactions-api/src/test/java/com/atomikos/icatch/PropagationTestJUnit.java new file mode 100644 index 000000000..cfa50dc7d --- /dev/null +++ b/public/transactions-api/src/test/java/com/atomikos/icatch/PropagationTestJUnit.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; +import static org.junit.Assert.assertEquals; + +import java.util.Properties; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class PropagationTestJUnit { + + private static final String PARENT_ID = "parent"; + private static final String ROOT_ID = "root"; + private static final long TIMEOUT = 100l; + private static final boolean SERIAL = true; + private static final String DOMAIN = "domain"; + private static final String PROPERTY_KEY ="key"; + private static final String PROPERTY_VALUE ="value"; + private static final String COORDINATOR_ID = "coordinator"; + + private final String EXPECTED_TO_STRING = + "version=2019,domain="+DOMAIN+",timeout="+TIMEOUT+",serial="+SERIAL+",recoveryCoordinatorURI="+COORDINATOR_ID+ + ",parent="+ROOT_ID+ + ",parent="+PARENT_ID+","+"property."+ + PROPERTY_KEY+"="+PROPERTY_VALUE; + + private Propagation propagation; + private CompositeTransaction parent; + private CompositeTransaction root; + + @Before + public void setUp() throws Exception { + parent = Mockito.mock(CompositeTransaction.class); + Mockito.when(parent.getTid()).thenReturn(PARENT_ID); + CompositeCoordinator coordinator = Mockito.mock(CompositeCoordinator.class); + Mockito.when(coordinator.getCoordinatorId()).thenReturn(COORDINATOR_ID); + Mockito.when(parent.getCompositeCoordinator()).thenReturn(coordinator); + Properties p = new Properties(); + p.setProperty(PROPERTY_KEY, PROPERTY_VALUE); + Mockito.when(parent.getProperties()).thenReturn(p); + root = Mockito.mock(CompositeTransaction.class); + Mockito.when(root.getTid()).thenReturn(ROOT_ID); + Mockito.when(root.getProperties()).thenReturn(new Properties()); + propagation = new Propagation(DOMAIN, root, parent, SERIAL, TIMEOUT); + } + + @Test + public void testToString() { + assertEquals(EXPECTED_TO_STRING, propagation.toString()); + } + +} diff --git a/public/transactions-api/src/test/java/com/atomikos/icatch/TxStateTestJUnit.java b/public/transactions-api/src/test/java/com/atomikos/icatch/TxStateTestJUnit.java new file mode 100644 index 000000000..365ac66eb --- /dev/null +++ b/public/transactions-api/src/test/java/com/atomikos/icatch/TxStateTestJUnit.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch; + +import static com.atomikos.recovery.TxState.ABANDONED; +import static com.atomikos.recovery.TxState.COMMITTING; +import static com.atomikos.recovery.TxState.IN_DOUBT; +import static com.atomikos.recovery.TxState.TERMINATED; +import static org.junit.Assert.assertEquals; + +import java.util.EnumSet; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import com.atomikos.recovery.TxState; + +public class TxStateTestJUnit { + @Test + public void testCountStates() { + TxState[] states = TxState.values(); + assertEquals(15, states.length); + } + + @Test + public void testRecoverableStates() throws Exception { + Set recoverableStates = EnumSet.of(IN_DOUBT, COMMITTING, TERMINATED); + TxState[] states = TxState.values(); + for (TxState txState : states) { + if (recoverableStates.contains(txState)) { + Assert.assertTrue(txState.isRecoverableState()); + } else { + Assert.assertFalse(txState.isRecoverableState()); + } + } + } + + + @Test + public void testFinalStates() { + Set finalStates = EnumSet.of(TERMINATED); + TxState[] states = TxState.values(); + for (TxState txState : states) { + if (finalStates.contains(txState)) { + Assert.assertTrue(txState.isFinalState()); + } else { + Assert.assertFalse(txState.isFinalState()); + } + } + + } + + @Test + public void testFinalStatesForOltp() { + Set finalStates = EnumSet.of(TERMINATED, ABANDONED); + TxState[] states = TxState.values(); + for (TxState txState : states) { + if (finalStates.contains(txState)) { + Assert.assertTrue(txState.isFinalStateForOltp()); + } else { + Assert.assertFalse(txState.isFinalStateForOltp()); + } + } + } +} diff --git a/public/transactions-api/src/test/java/com/atomikos/icatch/provider/ConfigPropertiesTestJUnit.java b/public/transactions-api/src/test/java/com/atomikos/icatch/provider/ConfigPropertiesTestJUnit.java new file mode 100644 index 000000000..6eb1aa2e5 --- /dev/null +++ b/public/transactions-api/src/test/java/com/atomikos/icatch/provider/ConfigPropertiesTestJUnit.java @@ -0,0 +1,182 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.provider; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ConfigPropertiesTestJUnit { + + private static final String CUSTOM_PROPERTY_NAME = "bla"; + private ConfigProperties props; + + @Before + public void setUp() throws Exception { + props = new ConfigProperties(new Properties()); + } + + @Test(expected=IllegalArgumentException.class) + public void testGetPropertyThrowsIfNotSet() { + props.getProperty("bla"); + } + + @Test + public void testGetAsBoolean() { + props.setProperty(CUSTOM_PROPERTY_NAME,"true"); + Assert.assertTrue(props.getAsBoolean(CUSTOM_PROPERTY_NAME)); + } + + @Test + public void testGetAsInt() { + props.setProperty(CUSTOM_PROPERTY_NAME, "10"); + Assert.assertEquals(10, props.getAsInt(CUSTOM_PROPERTY_NAME)); + } + + @Test + public void testGetAsLong() { + props.setProperty(CUSTOM_PROPERTY_NAME, "10"); + Assert.assertEquals(10, props.getAsLong(CUSTOM_PROPERTY_NAME)); + } + + @Test + public void testGetPropertyTrimsSpaces() { + props.setProperty(CUSTOM_PROPERTY_NAME, " bla "); + assertEquals("bla", props.getProperty(CUSTOM_PROPERTY_NAME)); + } + + @Test + public void testGetLogBaseDir() { + props.setProperty("com.atomikos.icatch.log_base_dir", "bla"); + assertEquals("bla", props.getLogBaseDir()); + } + + @Test + public void testGetLogBaseName() { + props.setProperty("com.atomikos.icatch.log_base_name", "bla"); + assertEquals("bla", props.getLogBaseName()); + } + + @Test + public void testGetEnableLogging() { + props.setProperty("com.atomikos.icatch.enable_logging", "true"); + assertTrue(props.getEnableLogging()); + } + + @Test + public void testMaxTimeout() { + props.setProperty("com.atomikos.icatch.max_timeout", "30000"); + assertEquals(30000, props.getMaxTimeout()); + } + + @Test + public void testMaxActives() { + props.setProperty("com.atomikos.icatch.max_actives", "100"); + assertEquals(100, props.getMaxActives()); + } + + @Test + public void testMergeProperties() { + props.setProperty(CUSTOM_PROPERTY_NAME, "bla"); + Properties userSpecificProperties = new Properties(); + userSpecificProperties.setProperty("userSpecific", "userBla"); + props.applyUserSpecificProperties(userSpecificProperties); + assertEquals("bla" , props.getProperty(CUSTOM_PROPERTY_NAME)); + assertEquals("userBla", props.getProperty("userSpecific")); + } + + @Test + public void testDefaultTmUniqueName() { + assertNotNull(props.getTmUniqueName()); + } + + @Test + public void testDefaultJvmId() { + assertNotNull(props.getJvmId()); + } + + @Test + public void testDefaultJvmIdDoesNotOverrideAnyCustomSetting() { + final String NAME = "bla"; + Properties p = createDefaultProperties(ConfigProperties.JVM_ID_PROPERTY_NAME, NAME); + props = new ConfigProperties(p); + assertEquals(NAME, props.getJvmId()); + } + + @Test + public void testGetPropertiesIncludesSystemProperties() { + final String name = "com.atomikos.icatch.testGetPropertiesIncludesSystemProperties"; + System.setProperty(name, "bla"); + assertNotNull(props.getCompletedProperties().getProperty(name)); + } + + @Test + public void testForceShutdownOnVmExit() { + props.setProperty("com.atomikos.icatch.force_shutdown_on_vm_exit", "true"); + Assert.assertTrue(props.getForceShutdownOnVmExit()); + props.setProperty("com.atomikos.icatch.force_shutdown_on_vm_exit", "false"); + Assert.assertFalse(props.getForceShutdownOnVmExit()); + } + + + private Properties createDefaultProperties(String name, String value) { + Properties ret = new Properties(); + ret.setProperty(name, value); + return ret; + } + + @Test + public void testDefaultTmUniqueNameDoesNotOverrideAnyCustomSetting() { + final String NAME = "bla"; + Properties p = createDefaultProperties(ConfigProperties.TM_UNIQUE_NAME_PROPERTY_NAME, NAME); + props = new ConfigProperties(p); + assertEquals(NAME, props.getTmUniqueName()); + } + @Test + public void testForgetOrphanedLogEntriesDelay() throws Exception { + props.setProperty("com.atomikos.icatch.forget_orphaned_log_entries_delay", "1800000"); + assertEquals(TimeUnit.MINUTES.toMillis(30), props.getForgetOrphanedLogEntriesDelay()); + } + + @Test + public void testRecoveryDelay() throws Exception { + final long VALUE = 12345L; + props.setProperty("com.atomikos.icatch.recovery_delay", Long.toString(VALUE)); + assertEquals(VALUE, props.getRecoveryDelay()); + } + + @Test + public void testOltpMaxRetries() throws Exception { + final int VALUE = 123; + props.setProperty("com.atomikos.icatch.oltp_max_retries", Integer.toString(VALUE)); + assertEquals(VALUE, props.getOltpMaxRetries()); + } + + @Test + public void testOltpRetryInterval() throws Exception { + final long VALUE = 2345l; + props.setProperty("com.atomikos.icatch.oltp_retry_interval", Long.toString(VALUE)); + assertEquals(VALUE, props.getOltpRetryInterval()); + } + + @Test + public void testAllowSubTransactions() throws Exception { + final boolean VALUE = false; + props.setProperty("com.atomikos.icatch.allow_subtransactions", "false"); + assertEquals(VALUE, props.getAllowSubTransactions()); + } +} + diff --git a/public/transactions-api/src/test/java/com/atomikos/recovery/PendingTransactionRecordTestJUnit.java b/public/transactions-api/src/test/java/com/atomikos/recovery/PendingTransactionRecordTestJUnit.java new file mode 100644 index 000000000..d5de714dd --- /dev/null +++ b/public/transactions-api/src/test/java/com/atomikos/recovery/PendingTransactionRecordTestJUnit.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; + +public class PendingTransactionRecordTestJUnit { + + PendingTransactionRecord given; + @Test + public void testToRecord() throws Exception { + String id = "id1"; + TxState state = TxState.COMMITTING; + long expires = 100l; + given = new PendingTransactionRecord(id, state, expires, "domain"); + String record = given.toRecord(); + assertEquals("id1|COMMITTING|100|domain|"+System.lineSeparator(), record); + } + @Test + public void testFromRecord() throws Exception { + String givenRecord = "id1|COMMITTING|100|domain|123"; + PendingTransactionRecord pendingTransactionRecord = PendingTransactionRecord.fromRecord(givenRecord); + assertEquals("id1", pendingTransactionRecord.id); + assertEquals(TxState.COMMITTING, pendingTransactionRecord.state); + assertEquals(100, pendingTransactionRecord.expires); + assertEquals("123", pendingTransactionRecord.superiorId); + } + + @Test(expected=IllegalArgumentException.class) + public void testFromPartialRecordThrows() { + String givenRecord = "id1|COMMITTING|"; + PendingTransactionRecord.fromRecord(givenRecord); + } + + @Test(expected=IllegalArgumentException.class) + public void testFromInvalidRecordThrows() { + String givenRecord = "id1-COMMITTING-"; + PendingTransactionRecord.fromRecord(givenRecord); + } + + @Test + public void testRootIsNativeInItsOwnDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain"); + assertFalse(given.isForeignInDomain("domain")); + } + + @Test + public void testLocalSubtransactionIsNativeInItsOwnDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "localSuperiorId"); + assertFalse(given.isForeignInDomain("domain")); + } + + @Test + public void testImportedTransactionIsForeignInImportingDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "localSuperiorId"); + assertTrue(given.isForeignInDomain("importingDomain")); + } + + @Test + public void testRootIsRecoveredByItsOwnDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain"); + assertTrue(given.isRecoveredByDomain("domain")); + + } + + @Test + public void testImportedTransactionWithoutSuperiorUrlIsRecoveredByImportingDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "foreignDomain", "superiorId"); + assertTrue(given.isRecoveredByDomain("importingDomain")); + } + + @Test + public void testImportedTransactionWithSuperiorUrlIsNotRecoveredByImportingDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "foreignDomain", "https://superiorId"); + assertFalse(given.isRecoveredByDomain("importingDomain")); + } + + + @Test + public void testLocalSubtransactionIsRecoveredByItsOwnDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "localSuperiorId"); + assertTrue(given.isRecoveredByDomain("domain")); + } + + @Test + public void testRootDoesNotAllowHeuristicAbort() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain"); + assertFalse(given.allowsHeuristicTermination("domain")); + } + + @Test + public void testLocalSubtransactionDoesNotAllowHeuristicAbort() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "localSuperiorId"); + assertFalse(given.allowsHeuristicTermination("domain")); + } + + @Test + public void testImportedTransactionWithoutSuperiorUrlAllowsHeuristicAbortByImportingDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "foreignDomain", "superiorId"); + assertTrue(given.allowsHeuristicTermination("importingDomain")); + } + + @Test + public void testImportedTransactionWithSuperiorUrlDoesNotAllowHeuristicAbortByImportingDomain() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "foreignDomain", "http://superiorId"); + assertFalse(given.allowsHeuristicTermination("importingDomain")); + } + + @Test + public void testImportedTransactionFromSameDomainDoesNotAllowheuristicAbort() { + given = new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "http://superiorId/"); + assertFalse(given.allowsHeuristicTermination("domain")); + } + + @Test + public void testFindAllDescendants() { + given = new PendingTransactionRecord("rootId", TxState.IN_DOUBT, 0, "domain"); + Collection set = new HashSet(); + set.add(new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "superiorId")); + set.add(new PendingTransactionRecord("superiorId", TxState.IN_DOUBT, 0, "domain", "rootId")); + set.add(given); + Collection result = PendingTransactionRecord.findAllDescendants(given, set); + assertEquals(2, result.size()); + } + + @Test + public void testRemoveAllDescendants() { + given = new PendingTransactionRecord("rootId", TxState.IN_DOUBT, 0, "domain"); + Collection set = new HashSet(); + set.add(new PendingTransactionRecord("id", TxState.IN_DOUBT, 0, "domain", "superiorId")); + set.add(new PendingTransactionRecord("superiorId", TxState.IN_DOUBT, 0, "domain", "rootId")); + set.add(given); + PendingTransactionRecord.removeAllDescendants(given, set); + assertEquals(1, set.size()); + } + + @Test + public void testExtractCoordinatorIds() { + given = new PendingTransactionRecord("rootId", TxState.IN_DOUBT, 0, "domain"); + Collection result = PendingTransactionRecord.extractCoordinatorIds(Collections.singleton(given), TxState.IN_DOUBT); + assertFalse(result.isEmpty()); + result = PendingTransactionRecord.extractCoordinatorIds(Collections.singleton(given), TxState.COMMITTING); + assertTrue(result.isEmpty()); + } +} diff --git a/public/transactions-eclipselink/pom.xml b/public/transactions-eclipselink/pom.xml new file mode 100644 index 000000000..9d4bb2aa1 --- /dev/null +++ b/public/transactions-eclipselink/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + ate + com.atomikos + 6.0.1-SNAPSHOT + + transactions-eclipselink + + + org.eclipse.persistence + eclipselink + 2.7.12 + provided + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + \ No newline at end of file diff --git a/public/transactions-eclipselink/src/main/java/com/atomikos/eclipselink/platform/AtomikosPlatform.java b/public/transactions-eclipselink/src/main/java/com/atomikos/eclipselink/platform/AtomikosPlatform.java new file mode 100644 index 000000000..2db20d2fe --- /dev/null +++ b/public/transactions-eclipselink/src/main/java/com/atomikos/eclipselink/platform/AtomikosPlatform.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.eclipselink.platform; + +import org.eclipse.persistence.platform.server.ServerPlatformBase; +import org.eclipse.persistence.sessions.DatabaseSession; + +import com.atomikos.util.Atomikos; + +public class AtomikosPlatform extends ServerPlatformBase { + + public AtomikosPlatform(DatabaseSession newDatabaseSession) { + super(newDatabaseSession); + disableRuntimeServices(); + } + + @Override + public Class getExternalTransactionControllerClass() { + + return AtomikosTransactionController.class; + } + + @Override + protected void initializeServerNameAndVersion() { + this.serverNameAndVersion="Atomikos: "+Atomikos.VERSION; + } +} diff --git a/public/transactions-eclipselink/src/main/java/com/atomikos/eclipselink/platform/AtomikosTransactionController.java b/public/transactions-eclipselink/src/main/java/com/atomikos/eclipselink/platform/AtomikosTransactionController.java new file mode 100644 index 000000000..5f64821d5 --- /dev/null +++ b/public/transactions-eclipselink/src/main/java/com/atomikos/eclipselink/platform/AtomikosTransactionController.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.eclipselink.platform; + +import javax.transaction.TransactionManager; + +import org.eclipse.persistence.transaction.JTATransactionController; + +import com.atomikos.icatch.jta.UserTransactionManager; + +public class AtomikosTransactionController extends JTATransactionController { + + private UserTransactionManager utm; + + public AtomikosTransactionController() { + utm = new UserTransactionManager(); + } + /** + * INTERNAL: Obtain and return the JTA TransactionManager on this platform + */ + protected TransactionManager acquireTransactionManager() throws Exception { + return utm; + } + + @Override + public TransactionManager getTransactionManager() { + + return utm; + } + +} diff --git a/public/transactions-essentials-jakarta/pom.xml b/public/transactions-essentials-jakarta/pom.xml new file mode 100644 index 000000000..aa943aa51 --- /dev/null +++ b/public/transactions-essentials-jakarta/pom.xml @@ -0,0 +1,94 @@ + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-essentials-jakarta + pom + transactions-essentials-jakarta + Atomikos TransactionsEssentials Jakarta BOM + https://www.atomikos.com/Main/TransactionsEssentials + + + https://www.atomikos.com/Main/WhichLicenseApplies + + + + + Atomikos + info@atomikos.com + Atomikos + https://www.atomikos.com + + + + + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-hibernate4 + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-jndi-provider + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-api + 6.0.1-SNAPSHOT + + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-osgi + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + atomikos-util + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-remoting + 6.0.1-SNAPSHOT + jakarta + + + com.atomikos + transactions-spring-boot3-starter + 6.0.1-SNAPSHOT + + + + diff --git a/public/transactions-essentials/pom.xml b/public/transactions-essentials/pom.xml new file mode 100644 index 000000000..c160d4d39 --- /dev/null +++ b/public/transactions-essentials/pom.xml @@ -0,0 +1,101 @@ + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-essentials + pom + transactions-essentials + Atomikos TransactionsEssentials BOM + https://www.atomikos.com/Main/TransactionsEssentials + + + https://www.atomikos.com/Main/WhichLicenseApplies + + + + + Atomikos + info@atomikos.com + Atomikos + https://www.atomikos.com + + + + + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-hibernate3 + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-hibernate4 + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-hibernate2 + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-eclipselink + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jndi-provider + 6.0.1-SNAPSHOT + + + com.atomikos + transactions + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-api + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-osgi + 6.0.1-SNAPSHOT + + + com.atomikos + atomikos-util + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-remoting + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-spring-boot-starter + 6.0.1-SNAPSHOT + + + + diff --git a/public/transactions-hibernate2/pom.xml b/public/transactions-hibernate2/pom.xml new file mode 100644 index 000000000..a3481b968 --- /dev/null +++ b/public/transactions-hibernate2/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-hibernate2 + Transactions Hibernate2 + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + + + net.sf.hibernate + hibernate + 2.1.8 + provided + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + diff --git a/public/transactions-hibernate2/src/main/java/com/atomikos/icatch/jta/hibernate/TransactionManagerLookup.java b/public/transactions-hibernate2/src/main/java/com/atomikos/icatch/jta/hibernate/TransactionManagerLookup.java new file mode 100644 index 000000000..0c1c13eda --- /dev/null +++ b/public/transactions-hibernate2/src/main/java/com/atomikos/icatch/jta/hibernate/TransactionManagerLookup.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate; + +import java.util.Properties; + +import javax.transaction.TransactionManager; + +import com.atomikos.icatch.jta.UserTransactionManager; + +import net.sf.hibernate.HibernateException; + +/** + * + * + * + * This class is provided for Hibernate integration. + * To use Atomikos as the Hibernate JTA transaction manager, + * specify this class as the value of the + * hibernate.transaction.manager_lookup_class of the + * hibernate configuration properties. + * + */ +public class TransactionManagerLookup + implements net.sf.hibernate.transaction.TransactionManagerLookup +{ + + UserTransactionManager utm; + + public TransactionManagerLookup() + { + utm = new UserTransactionManager(); + } + + + /* (non-Javadoc) + * @see net.sf.hibernate.transaction.TransactionManagerLookup#getTransactionManager(java.util.Properties) + */ + public TransactionManager getTransactionManager(Properties arg0) + throws HibernateException + { + return utm; + } + + /* (non-Javadoc) + * @see net.sf.hibernate.transaction.TransactionManagerLookup#getUserTransactionName() + */ + public String getUserTransactionName() + { + + return null; + } + +} diff --git a/public/transactions-hibernate2/src/test/java/com/atomikos/icatch/jta/hibernate/TransactionManagerLookupTestJUnit.java b/public/transactions-hibernate2/src/test/java/com/atomikos/icatch/jta/hibernate/TransactionManagerLookupTestJUnit.java new file mode 100644 index 000000000..077695c98 --- /dev/null +++ b/public/transactions-hibernate2/src/test/java/com/atomikos/icatch/jta/hibernate/TransactionManagerLookupTestJUnit.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate; + +import junit.framework.TestCase; +import net.sf.hibernate.HibernateException; + +public class TransactionManagerLookupTestJUnit extends TestCase { + + private TransactionManagerLookup lookup; + + protected void setUp() throws Exception { + super.setUp(); + lookup = new TransactionManagerLookup(); + } + + public void testTransactionManager() throws HibernateException { + assertNotNull ( lookup.getTransactionManager( null) ); + } + + public void testName() throws Exception + { + assertNull ( lookup.getUserTransactionName() ); + } + +} diff --git a/public/transactions-hibernate3/pom.xml b/public/transactions-hibernate3/pom.xml new file mode 100644 index 000000000..66e7f549a --- /dev/null +++ b/public/transactions-hibernate3/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-hibernate3 + Transactions Hibernate3 + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + + + org.hibernate + hibernate + 3.2.5.ga + provided + + + javax.transaction + jta + + + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + org.slf4j + slf4j-api + 1.4.3 + provided + + + org.slf4j + slf4j-simple + 1.4.3 + test + + + diff --git a/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/AtomikosConnectionProvider.java b/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/AtomikosConnectionProvider.java new file mode 100644 index 000000000..d68f38a4c --- /dev/null +++ b/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/AtomikosConnectionProvider.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate3; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Properties; + +import org.hibernate.HibernateException; +import org.hibernate.connection.ConnectionProvider; + +import com.atomikos.beans.PropertyException; +import com.atomikos.beans.PropertyUtils; +import com.atomikos.jdbc.AtomikosDataSourceBean; +import com.atomikos.jdbc.AtomikosNonXADataSourceBean; +import com.atomikos.jdbc.internal.AbstractDataSourceBean; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * Atomikos-specific ConnectionProvider implementation that can + * create Atomikos connection pools directly from the Hibernate + * configuration. + * + *

To use the AtomikosConnectionProvider specify this class as the + * value of the hibernate.connection.provider_class of the + * hibernate configuration properties. + * You then have to configure the {@link AtomikosDataSourceBean} properties + * by prefixing them with hibernate.connection.atomikos. + * Eg: hibernate.connection.atomikos.uniqueResourceName + * hibernate.connection.atomikos.xaDataSourceClassName + * hibernate.connection.atomikos.xaProperties.databaseName + * ... + * Add a hibernate.connection.atomikos.nonxa=true property if you want + * to configure a {@link AtomikosNonXADataSourceBean} instead. + *

+ * + *

+ * NOTE: if you use the Hibernate XML config mechanism, + * then the prefix should be connection.atomikos instead (without hibernate prefix). + *

+ * + * @author Ludovic Orban + */ +public class AtomikosConnectionProvider implements ConnectionProvider { + /** + * Logger for this class + */ + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosConnectionProvider.class); + + + private static final String PROPERTIES_PREFIX = "hibernate.connection.atomikos."; + private static final String PROPERTY_NONXA = "hibernate.connection.atomikos.nonxa"; + + + private AbstractDataSourceBean dataSource = null; + + + public void close() throws HibernateException { + if (dataSource != null) + dataSource.close(); + dataSource = null; + } + + public void closeConnection(Connection connection) throws SQLException { + connection.close(); + } + + public void configure(Properties props) throws HibernateException { + if (dataSource != null) { + return; + } + + if ("true".equalsIgnoreCase(props.getProperty(PROPERTY_NONXA))) { + dataSource = new AtomikosNonXADataSourceBean(); + } + else { + dataSource = new AtomikosDataSourceBean(); + } + + Properties atomikosProperties = filterOutHibernateProperties(props); + LOGGER.logInfo("configuring AtomikosConnectionProvider with properties: " + atomikosProperties); + + try { + PropertyUtils.setProperties(dataSource, atomikosProperties); + } catch (PropertyException ex) { + throw new HibernateException("cannot create Atomikos DataSource", ex); + } + + try { + dataSource.init(); + } catch (SQLException ex) { + throw new HibernateException("cannot initialize Atomikos DataSource", ex); + } + } + + private Properties filterOutHibernateProperties(Properties props) { + Properties atomikosProperties = new Properties(); + + Iterator> it = props.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + String key = (String) entry.getKey(); + Object value = entry.getValue(); + + if (key.startsWith(PROPERTIES_PREFIX) && !key.equals(PROPERTY_NONXA)) { + atomikosProperties.put(key.substring(PROPERTIES_PREFIX.length()), value); + } + } + return atomikosProperties; + } + + public Connection getConnection() throws SQLException { + if (dataSource == null) + throw new HibernateException("datasource is not configured"); + return dataSource.getConnection(); + } + + public boolean supportsAggressiveRelease() { + return true; + } + +} diff --git a/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/AtomikosJTATransactionFactory.java b/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/AtomikosJTATransactionFactory.java new file mode 100644 index 000000000..93355955d --- /dev/null +++ b/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/AtomikosJTATransactionFactory.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate3; + +import java.util.Properties; + +import javax.transaction.UserTransaction; + +import org.hibernate.HibernateException; +import org.hibernate.transaction.JTATransactionFactory; + +import com.atomikos.icatch.jta.UserTransactionImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * Atomikos-specific JTATransactionFactory implementation that does not + * rely on JNDI for standalone (JNDI-less) deployments. + * + *

To use Atomikos as the Hibernate JTA transaction manager, + * specify this class as the value of the + * hibernate.transaction.factory_class of the + * hibernate configuration properties.

+ * + * + * @author Les Hazlewood + * @author Ludovic Orban + */ +public class AtomikosJTATransactionFactory extends JTATransactionFactory { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJTATransactionFactory.class); + + private UserTransaction userTransaction; + + public void configure ( Properties props ) throws HibernateException { + + try { + //fix for case 32252: hibernate config init - required for Hibernate 3.2.6 or lower!!! + super.configure ( props ); + } catch ( Exception e ) { + //fix for case 58114: exceptions here for Hibernate 3.2.7 and higher + String msg = "Hibernate: error during config - ignore for hibernate 3.2.7 or higher"; + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( msg , e ); + } + } + + protected UserTransaction getUserTransaction() { + if (this.userTransaction == null) { + this.userTransaction = new UserTransactionImp(); + } + return this.userTransaction; + } +} diff --git a/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/TransactionManagerLookup.java b/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/TransactionManagerLookup.java new file mode 100644 index 000000000..2161d92d2 --- /dev/null +++ b/public/transactions-hibernate3/src/main/java/com/atomikos/icatch/jta/hibernate3/TransactionManagerLookup.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate3; + +import java.util.Properties; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import org.hibernate.HibernateException; + +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * + * + * + * This class is provided for Hibernate3 integration. + * To use Atomikos as the Hibernate JTA transaction manager, + * specify this class as the value of the + * hibernate.transaction.manager_lookup_class of the + * hibernate configuration properties. + * + */ +public class TransactionManagerLookup implements org.hibernate.transaction.TransactionManagerLookup +{ + + private UserTransactionManager utm; + + public TransactionManagerLookup() + { + utm = new UserTransactionManager(); + } + + + + public TransactionManager getTransactionManager(Properties props) throws HibernateException + { + return utm; + } + + public String getUserTransactionName() + { + return null; + } + + + // new in Hibernate 3.3 + public Object getTransactionIdentifier(Transaction transaction) + { + return transaction; + } + +} diff --git a/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/AtomikosJTATransactionFactoryTestJUnit.java b/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/AtomikosJTATransactionFactoryTestJUnit.java new file mode 100644 index 000000000..ac853bd32 --- /dev/null +++ b/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/AtomikosJTATransactionFactoryTestJUnit.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate3; + +import junit.framework.TestCase; + +public class AtomikosJTATransactionFactoryTestJUnit extends TestCase +{ + + private AtomikosJTATransactionFactory hibernate; + + protected void setUp() throws Exception + { + super.setUp(); + hibernate = new AtomikosJTATransactionFactory(); + } + + public void testIssue58114() + { + try { + hibernate.configure ( null ); + } catch ( NullPointerException bug58114 ) { + fail ( "Calling configure should not throw exceptions or Hibernate-JTA will not work!" ); + } + + testUserTransaction(); + } + + public void testUserTransaction() + { + assertNotNull ( hibernate.getUserTransaction() ); + } +} diff --git a/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/ConnectionProviderTestJUnit.java b/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/ConnectionProviderTestJUnit.java new file mode 100644 index 000000000..92cc64bae --- /dev/null +++ b/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/ConnectionProviderTestJUnit.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate3; + +import java.sql.Connection; +import java.util.Properties; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; + +import org.hibernate.HibernateException; +import org.mockito.Mockito; + +import com.atomikos.beans.PropertyException; + +import junit.framework.TestCase; + +public class ConnectionProviderTestJUnit extends TestCase { + + public void testConnectionProviderXaDataSource() throws Exception { + XADataSource xaDataSource= Mockito.mock(XADataSource.class); + XAConnection mockedXAConnection = Mockito.mock(XAConnection.class); + Mockito.when(xaDataSource.getXAConnection()).thenReturn(mockedXAConnection); + Connection mockedConnection = Mockito.mock(Connection.class); + Mockito.when(mockedXAConnection.getConnection()).thenReturn(mockedConnection); + Mockito.when(mockedConnection.isValid(Mockito.anyInt())).thenReturn(true); + Properties props = new Properties(); + props.setProperty("hibernate.connection.atomikos.uniqueResourceName", "aaa"); + //assert that XADataSource-specific properties can be set! + //cf case 30961 + props.setProperty("hibernate.connection.atomikos.xaProperties.lastUser" , "scott" ); + //normally, hibernate.properties file would contain hibernate.connection.atomikos.xaDataSourceClassName + props.put("hibernate.connection.atomikos.xaDataSource", xaDataSource); + + AtomikosConnectionProvider provider = new AtomikosConnectionProvider(); + provider.configure(props); + + Connection conn = provider.getConnection(); + assertNotNull(conn); + + conn.close(); + } + + public void testConnectionProviderNonXaDataSource() throws Exception { + XADataSource xaDataSource= Mockito.mock(XADataSource.class); + Properties props = new Properties(); + props.setProperty("hibernate.connection.atomikos.nonxa", "true"); + props.setProperty("hibernate.connection.atomikos.uniqueResourceName", "aaa"); + //normally, hibernate.properties file would contain hibernate.connection.atomikos.xaDataSourceClassName + props.put("hibernate.connection.atomikos.xaDataSource", xaDataSource); + + AtomikosConnectionProvider provider = new AtomikosConnectionProvider(); + try { + provider.configure(props); + fail("non-xa datasource creation should have failed"); + } catch (HibernateException ex) { + PropertyException pex = (PropertyException) ex.getCause(); + assertEquals("no writeable property 'xaDataSource' in class 'com.atomikos.jdbc.AtomikosNonXADataSourceBean'", pex.getMessage()); + } + } + +} diff --git a/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/TransactionManagerLookupTestJUnit.java b/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/TransactionManagerLookupTestJUnit.java new file mode 100644 index 000000000..5787f96c6 --- /dev/null +++ b/public/transactions-hibernate3/src/test/java/com/atomikos/icatch/jta/hibernate3/TransactionManagerLookupTestJUnit.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate3; + +import org.hibernate.HibernateException; + +import junit.framework.TestCase; + +public class TransactionManagerLookupTestJUnit extends TestCase { + + private TransactionManagerLookup lookup; + + protected void setUp() throws Exception { + super.setUp(); + lookup = new TransactionManagerLookup(); + } + + public void testTransactionManager() throws HibernateException { + assertNotNull ( lookup.getTransactionManager( null) ); + } + + public void testName() throws Exception + { + assertNull ( lookup.getUserTransactionName() ); + } + +} diff --git a/public/transactions-hibernate4/pom.xml b/public/transactions-hibernate4/pom.xml new file mode 100644 index 000000000..6bd63a5c5 --- /dev/null +++ b/public/transactions-hibernate4/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-hibernate4 + Transactions Hibernate4 + + false + + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + provided + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + provided + + + org.hibernate + hibernate-entitymanager + 4.3.1.Final + provided + + + javax.transaction + jta + + + + + com.h2database + h2 + 1.3.175 + test + + + org.springframework + spring-context + 4.0.2.RELEASE + test + + + org.springframework + spring-tx + 4.0.2.RELEASE + test + + + org.springframework + spring-aop + 4.0.2.RELEASE + test + + + org.springframework + spring-orm + 4.0.2.RELEASE + test + + + org.springframework.data + spring-data-jpa + 1.4.4.RELEASE + test + + + org.aspectj + aspectjrt + 1.7.4 + test + + + org.aspectj + aspectjweaver + 1.7.4 + test + + + org.springframework + spring-test + 4.0.2.RELEASE + test + + + org.slf4j + slf4j-simple + 1.4.3 + test + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + org.slf4j + slf4j-api + 1.4.3 + provided + + + diff --git a/public/transactions-hibernate4/src/main/java/com/atomikos/icatch/jta/hibernate4/AtomikosJ2eePlatform.java b/public/transactions-hibernate4/src/main/java/com/atomikos/icatch/jta/hibernate4/AtomikosJ2eePlatform.java new file mode 100644 index 000000000..2c67baa4f --- /dev/null +++ b/public/transactions-hibernate4/src/main/java/com/atomikos/icatch/jta/hibernate4/AtomikosJ2eePlatform.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate4; + +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.hibernate.TransactionException; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatformException; + +import com.atomikos.icatch.jta.J2eeTransactionManager; +import com.atomikos.icatch.jta.J2eeUserTransaction; + +/** + * + * Hibernate4 JTA Platform which is using the J2eeTransactionManager and the + * J2eeUserTransaction. + * + * Use this one, if you want to integrate Hibernate4 and Atomikos is used in a + * J2EE container environment or externally managed via Spring. + * + */ +public class AtomikosJ2eePlatform implements JtaPlatform { + + private static final long serialVersionUID = 1L; + + private final TransactionManager txMgr; + + private final UserTransaction userTx; + + public AtomikosJ2eePlatform() { + super(); + this.txMgr = new J2eeTransactionManager(); + this.userTx = new J2eeUserTransaction(); + } + + @Override + public Object getTransactionIdentifier(Transaction transaction) { + // generally we use the transaction itself. + return transaction; + } + + @Override + public void registerSynchronization(Synchronization synchronization) { + try { + this.txMgr.getTransaction() + .registerSynchronization(synchronization); + } catch (Exception e) { + throw new JtaPlatformException( + "Could not access JTA Transaction to register synchronization", + e); + } + } + + @Override + public boolean canRegisterSynchronization() { + try { + if (this.txMgr.getTransaction() != null) { + return this.txMgr.getTransaction().getStatus() == Status.STATUS_ACTIVE; + } + } catch (SystemException se) { + throw new TransactionException( + "Could not determine transaction status", se); + } + return false; + } + + @Override + public int getCurrentStatus() throws SystemException { + return retrieveTransactionManager().getStatus(); + } + + @Override + public UserTransaction retrieveUserTransaction() { + return this.userTx; + } + + @Override + public TransactionManager retrieveTransactionManager() { + return this.txMgr; + } +} diff --git a/public/transactions-hibernate4/src/main/java/com/atomikos/icatch/jta/hibernate4/AtomikosPlatform.java b/public/transactions-hibernate4/src/main/java/com/atomikos/icatch/jta/hibernate4/AtomikosPlatform.java new file mode 100644 index 000000000..a944556ca --- /dev/null +++ b/public/transactions-hibernate4/src/main/java/com/atomikos/icatch/jta/hibernate4/AtomikosPlatform.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.hibernate4; + +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.hibernate.TransactionException; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatformException; + +import com.atomikos.icatch.jta.UserTransactionImp; +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * + * Hibernate4 JTA Platform which is using the standard UserTransactionManager + * and the standard UserTransactionImp object. + * + * If you are looking for J2EE container managed transactions or using an + * external infrastructure like Spring to setup them, use the + * {@link AtomikosJ2eePlatform} to integrate Hibernate4. + * + */ +public class AtomikosPlatform implements JtaPlatform { + + private static final long serialVersionUID = 1L; + + private final TransactionManager txMgr; + + private final UserTransaction userTx; + + public AtomikosPlatform() { + super(); + this.txMgr = new UserTransactionManager(); + this.userTx = new UserTransactionImp(); + } + + @Override + public Object getTransactionIdentifier(Transaction transaction) { + // generally we use the transaction itself. + return transaction; + } + + @Override + public void registerSynchronization(Synchronization synchronization) { + try { + this.txMgr.getTransaction() + .registerSynchronization(synchronization); + } catch (Exception e) { + throw new JtaPlatformException( + "Could not access JTA Transaction to register synchronization", + e); + } + } + + @Override + public boolean canRegisterSynchronization() { + try { + if (this.txMgr.getTransaction() != null) { + return this.txMgr.getTransaction().getStatus() == Status.STATUS_ACTIVE; + } + } catch (SystemException se) { + throw new TransactionException( "Could not determine transaction status", se ); + } + return false; + } + + @Override + public int getCurrentStatus() throws SystemException { + return retrieveTransactionManager().getStatus(); + } + + @Override + public UserTransaction retrieveUserTransaction() { + return this.userTx; + } + + @Override + public TransactionManager retrieveTransactionManager() { + return this.txMgr; + } + +} diff --git a/public/transactions-jdbc/pom.xml b/public/transactions-jdbc/pom.xml new file mode 100644 index 000000000..e916fed04 --- /dev/null +++ b/public/transactions-jdbc/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-jdbc + Transactions JDBC + + false + + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + provided + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/AtomikosDataSourceBean.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/AtomikosDataSourceBean.java new file mode 100644 index 000000000..9537a99f0 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/AtomikosDataSourceBean.java @@ -0,0 +1,254 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; + +import com.atomikos.beans.PropertyUtils; +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.datasource.pool.ConnectionFactory; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.datasource.pool.XPooledConnection; +import com.atomikos.datasource.xa.jdbc.JdbcTransactionalResource; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.jdbc.internal.AbstractDataSourceBean; +import com.atomikos.jdbc.internal.AtomikosSQLException; +import com.atomikos.jdbc.internal.AtomikosXAPooledConnection; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.ClassLoadingHelper; + + /** + * The preferred class for using Atomikos connection pooling. Use an instance of + * this class if you want to use Atomikos JTA-enabled connection pooling. All + * you need to do is construct an instance and set the required properties as + * outlined below. The resulting bean will automatically register with the + * transaction service (for recovery) and take part in active transactions. + * All SQL done over connections (gotten from this class) will participate in JTA transactions. + */ + +public class AtomikosDataSourceBean +extends AbstractDataSourceBean +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosDataSourceBean.class); + + + private static final long serialVersionUID = 1L; + + private Properties xaProperties = new Properties(); + private String xaDataSourceClassName; + private transient XADataSource xaDataSource; + private boolean localTransactionMode = true; + + public AtomikosDataSourceBean() { + } + + /** + * Gets the properties used to + * configure the XADataSource. + */ + + public Properties getXaProperties() + { + return xaProperties; + } + + /** + * Sets the properties (name,value pairs) used to + * configure the XADataSource. Required, unless you call setXaDataSource directly. + * + * @param xaProperties + * + * + */ + public void setXaProperties ( Properties xaProperties ) + { + this.xaProperties = xaProperties; + } + + /** + * Get the XADataSource class name. + */ + public String getXaDataSourceClassName() + { + return xaDataSourceClassName; + } + + /** + * Sets the fully qualified underlying XADataSource class name. Required, unless you + * call setXaDataSource directly. + * + * @param xaDataSourceClassName + */ + public void setXaDataSourceClassName ( String xaDataSourceClassName ) + { + this.xaDataSourceClassName = xaDataSourceClassName; + } + + /** + * Gets the configured XADataSource (if any). + * @return The instance, or null if none. + */ + + public XADataSource getXaDataSource() + { + return xaDataSource; + } + + /** + * Sets the XADataSource directly - instead of providing the xaDataSourceClassName and xaProperties. + * @param xaDataSource + */ + public void setXaDataSource(XADataSource xaDataSource) + { + this.xaDataSource = xaDataSource; + } + + @Override + public boolean getLocalTransactionMode() { + return localTransactionMode; + } + + /** + * Sets localTransactionMode. Optional, defaults to true. + * + * @param localTransactionMode If true, then (for historical reasons) this + * datasource supports "hybrid" behaviour: if a JTA transaction is present then + * XA will be used, if not then a regular JDBC connection with connection-level + * commit/rollback will be returned. + * + * For safety, this property is best left to false: that way, there is no + * doubt about the transactional nature of your JDBC work. + */ + public void setLocalTransactionMode(boolean localTransactionMode) { + this.localTransactionMode = localTransactionMode; + } + + protected com.atomikos.datasource.pool.ConnectionFactory doInit() throws Exception + { + if (xaDataSource == null) + { + if (xaDataSourceClassName == null) + throwAtomikosSQLException("Property 'xaDataSourceClassName' cannot be null"); + if (xaProperties == null) + throwAtomikosSQLException("Property 'xaProperties' cannot be null"); + } + + + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo( + this + ": initializing with [" + + " xaDataSourceClassName=" + xaDataSourceClassName + "," + + " uniqueResourceName=" + getUniqueResourceName() + "," + + " maxPoolSize=" + getMaxPoolSize() + "," + + " minPoolSize=" + getMinPoolSize() + "," + + " borrowConnectionTimeout=" + getBorrowConnectionTimeout() + "," + + " maxIdleTime=" + getMaxIdleTime() + "," + + " maintenanceInterval=" + getMaintenanceInterval() + "," + + " testQuery=" + getTestQuery() + "," + + " xaProperties=" + PropertyUtils.toString(xaProperties) + "," + + " loginTimeout=" + getLoginTimeout() + "," + + " maxLifetime=" + getMaxLifetime() + "," + + " localTransactionMode=" + getLocalTransactionMode() + + "]" + ); + + + if (xaDataSource == null) + { + try { + Class xadsClass = ClassLoadingHelper.loadClass ( getXaDataSourceClassName() ); + xaDataSource = xadsClass.newInstance(); + + } catch ( ClassNotFoundException nf ) { + AtomikosSQLException.throwAtomikosSQLException ( "The class '" + getXaDataSourceClassName() + + "' specified by property 'xaDataSourceClassName' could not be found in the classpath. Please make sure the spelling is correct, and that the required jar(s) are in the classpath." , nf ); + } catch (ClassCastException cce) { + AtomikosSQLException.throwAtomikosSQLException ( + "The class '" + getXaDataSourceClassName() + + "' specified by property 'xaDataSourceClassName' does not implement the required interface javax.jdbc.XADataSource. Please make sure the spelling is correct, and check your JDBC driver vendor's documentation."); + } + xaDataSource.setLoginTimeout ( getLoginTimeout() ); + xaDataSource.setLogWriter ( getLogWriter() ); + PropertyUtils.setProperties(xaDataSource, xaProperties ); + + } + + JdbcTransactionalResource tr = new JdbcTransactionalResource(getUniqueResourceName() , xaDataSource); + ConnectionFactory cf = new AtomikosXAConnectionFactory(xaDataSource, tr, this); + Configuration.addResource ( tr ); + + return cf; + } + + protected void doClose() + { + RecoverableResource res = Configuration.getResource ( getUniqueResourceName() ); + if ( res != null ) { + Configuration.removeResource ( getUniqueResourceName() ); + //fix for case 26005 + res.close(); + } + } + + protected boolean isAssignableFromWrappedVendorClass(Class iface) { + boolean ret = false; + if (xaDataSource != null ) { + ret = iface.isAssignableFrom(xaDataSource.getClass()); + } + return ret; + } + + @Override + protected Object unwrapVendorInstance() { + return xaDataSource; + } + + private static class AtomikosXAConnectionFactory implements ConnectionFactory + { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosXAConnectionFactory.class); + + private final JdbcTransactionalResource jdbcTransactionalResource; + private final XADataSource xaDataSource; + private final ConnectionPoolProperties props; + + + private AtomikosXAConnectionFactory ( XADataSource xaDataSource, JdbcTransactionalResource jdbcTransactionalResource, ConnectionPoolProperties props ) + { + this.xaDataSource = xaDataSource; + this.jdbcTransactionalResource = jdbcTransactionalResource; + this.props = props; + } + + public XPooledConnection createPooledConnection() throws CreateConnectionException + { + try { + XAConnection xaConnection = xaDataSource.getXAConnection(); + return new AtomikosXAPooledConnection ( xaConnection, jdbcTransactionalResource, props ); + } catch ( SQLException e ) { + String msg = "XAConnectionFactory: failed to create pooled connection - DBMS down or unreachable?"; + LOGGER.logWarning ( msg , e ); + throw new CreateConnectionException ( msg , e ); + } + } + + } + + @Override + public boolean getIgnoreJtaTransactions() { + return false; + } + + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/AtomikosNonXADataSourceBean.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/AtomikosNonXADataSourceBean.java new file mode 100644 index 000000000..749ab7e5c --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/AtomikosNonXADataSourceBean.java @@ -0,0 +1,335 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +import com.atomikos.datasource.pool.ConnectionFactory; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.datasource.pool.XPooledConnection; +import com.atomikos.jdbc.internal.AbstractDataSourceBean; +import com.atomikos.jdbc.internal.AtomikosNonXAPooledConnection; +import com.atomikos.jdbc.internal.AtomikosSQLException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.ClassLoadingHelper; + + /** + * + * A Bean class for DataSource access to non-XA JDBC implementations. + * Instances are JTA transaction-aware and can rollback the work done + * over multiple connections (provided that all work was done in one and the same thread). + * + * + */ +public class AtomikosNonXADataSourceBean extends AbstractDataSourceBean +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosNonXADataSourceBean.class); + + private static final long serialVersionUID = 1L; + + private String url; + + private String user; + + private String password; + + private String driverClassName; + + private boolean readOnly; + + private boolean localTransactionMode; + + private boolean ignoreJtaTransactions; + + public AtomikosNonXADataSourceBean() { + + } + /** + * Sets the URL to use for getting connections. Required. + * + * @param url + */ + + public void setUrl ( String url ) + { + this.url = url; + } + + /** + * Gets the URL to connect. + */ + + public String getUrl() + { + return url; + } + + + /** + * Marks this datasource as being used for read-only work. Optional. + * + * Setting this to true will avoid warnings/errors on 2-phase commit. ReadOnly mode + * is intended to avoid XA configuration of databases where no updates are + * being done. + * + * @param readOnly Defaults to false. + */ + + public void setReadOnly ( boolean readOnly ) + { + this.readOnly = readOnly; + } + + /** + * @return Whether or not this datasource is marked as readOnly. + */ + + public boolean getReadOnly() + { + return readOnly; + } + + /** + * @return The password. + */ + + public String getPassword () + { + return password; + } + + /** + * Sets the password to use. + * + * @param string + */ + + public void setPassword ( String string ) + { + password = string; + } + + /** + * Set the user name to get connections with. + * + * @param string + */ + + public void setUser ( String string ) + { + user = string; + } + + /** + * @return The URL to connect with. + */ + + public String getUser () + { + return user; + } + + /** + * + * @return The DriverManager class name. + */ + + public String getDriverClassName () + { + return driverClassName; + } + + /** + * Sets the driver class name to be used by the DriverManager. Required. + * + * @param string + */ + public void setDriverClassName ( String string ) + { + driverClassName = string; + } + + + protected void doClose() + { + //nothing to do + } + + protected ConnectionFactory doInit() throws Exception + { + AtomikosNonXAConnectionFactory ret = null; + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo( + this + ": initializing with [" + + " uniqueResourceName=" + getUniqueResourceName() + "," + + " maxPoolSize=" + getMaxPoolSize() + "," + + " minPoolSize=" + getMinPoolSize() + "," + + " borrowConnectionTimeout=" + getBorrowConnectionTimeout() + "," + + " maxIdleTime=" + getMaxIdleTime() + "," + + " maintenanceInterval=" + getMaintenanceInterval() + "," + + " testQuery=" + getTestQuery() + "," + + " driverClassName=" + getDriverClassName() + "," + + " user=" + getUser() + "," + + " url=" + getUrl() + + " loginTimeout=" + getLoginTimeout() + "," + + " localTransactionMode=" + getLocalTransactionMode() + "," + + " ignoreJtaTransactions=" + getIgnoreJtaTransactions() + + "]" + ); + + + ret = new AtomikosNonXAConnectionFactory ( this , url , driverClassName , user , password , getLoginTimeout() , readOnly, ignoreJtaTransactions ) ; + ret.init(); + return ret; + } + + public Connection getConnection() throws SQLException + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": getConnection()..." ); + + init(); + + //let pool take care of reusing an existing handle + Connection connection = super.getConnection(); + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": returning " + connection ); + return connection; + } + + /** + * Should JTA transactions be ignored? Optional - most use cases should not change the default setting. + * If set, then this instance will act like a regular, non-JTA JDBC datasource. + * + * @param value Defaults to false. + */ + public void setLocalTransactionMode(boolean value) { + this.localTransactionMode = value; + } + + /** + * Tests if JTA transactions are ignored or not. + */ + public boolean getLocalTransactionMode() { + return localTransactionMode; + } + + @Override + protected boolean isAssignableFromWrappedVendorClass(Class iface) { + //we don't really care + return false; + } + + @Override + protected Object unwrapVendorInstance() { + throw new UnsupportedOperationException(); + } + + + + + private static class AtomikosNonXAConnectionFactory implements ConnectionFactory + { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosNonXAConnectionFactory.class); + + private final String url; + private final String driverClassName; + private final String user; + private final String password; + private final ConnectionPoolProperties props; + private final int loginTimeout; + private final boolean readOnly; + + + private Driver driver; + private Properties connectionProperties = new Properties(); + + private AtomikosNonXAConnectionFactory ( ConnectionPoolProperties props , + String url , String driverClassName , String user , + String password , int loginTimeout , boolean readOnly, boolean ignoreJtaTransactions ) + { + this.props = props; + this.user = user; + this.password = password; + this.url = url; + this.driverClassName = driverClassName; + this.loginTimeout = loginTimeout; + this.readOnly = readOnly; + } + + private void init() throws SQLException + { + try { + Class driverClass = ClassLoadingHelper.loadClass ( driverClassName ); + driver = driverClass.newInstance(); + if(user!=null){ + connectionProperties.put("user", user); + } + if(password!=null){ + connectionProperties.put("password",password); + } + } catch ( InstantiationException e ) { + AtomikosSQLException.throwAtomikosSQLException ( "Could not instantiate driver class: " + + driverClassName ); + } catch ( IllegalAccessException e ) { + AtomikosSQLException.throwAtomikosSQLException ( e.getMessage () ); + } catch ( ClassNotFoundException e ) { + AtomikosSQLException.throwAtomikosSQLException ( "Driver class not found: '" + + driverClassName + "' - please make sure the spelling is correct." ); + } catch (ClassCastException cce){ + String msg = "Driver class '" + driverClassName + "' does not seem to be a valid JDBC driver - please check the spelling and verify your JDBC vendor's documentation"; + AtomikosSQLException.throwAtomikosSQLException ( msg ); + } + DriverManager.setLoginTimeout ( loginTimeout ); + } + + private Connection getConnection() throws SQLException + { + + Connection ret = null; + //case : 61748 Usage of drivermanager is not possible, as it does not respect the ContextClassLoader + //ret = DriverManager.getConnection ( url , user, password ); + ret= driver.connect(url, connectionProperties); + return ret; + } + + public XPooledConnection createPooledConnection() throws CreateConnectionException { + Connection c; + try { + c = getConnection(); + } catch (SQLException e) { + LOGGER.logWarning ( "NonXAConnectionFactory: failed to create connection: " , e ); + throw new CreateConnectionException ( "Could not create JDBC connection" , e ); + } + return new AtomikosNonXAPooledConnection ( c , props , readOnly); + } + + } + + /** + * Should JTA transactions be ignored completely or not? + * Optional, defaults to false. + * @param ignoreJtaTransactions + */ + public void setIgnoreJtaTransactions(boolean ignoreJtaTransactions) { + this.ignoreJtaTransactions = ignoreJtaTransactions; + } + + @Override + public boolean getIgnoreJtaTransactions() { + return this.ignoreJtaTransactions; + } + + + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AbstractDataSourceBean.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AbstractDataSourceBean.java new file mode 100644 index 000000000..2f7996c2d --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AbstractDataSourceBean.java @@ -0,0 +1,439 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.sql.DataSource; + +import com.atomikos.datasource.pool.ConnectionFactory; +import com.atomikos.datasource.pool.ConnectionPool; +import com.atomikos.datasource.pool.ConnectionPoolException; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.ConnectionPoolWithConcurrentValidation; +import com.atomikos.datasource.pool.ConnectionPoolWithSynchronizedValidation; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.datasource.pool.PoolExhaustedException; +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.IntraVmObjectFactory; +import com.atomikos.util.IntraVmObjectRegistry; + + /** + * + * + * Abstract data source bean with generic functionality. + * + * + */ + +@SuppressWarnings("serial") +public abstract class AbstractDataSourceBean +implements DataSource, ConnectionPoolProperties, Referenceable, Serializable, OrderedLifecycleComponent +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AbstractDataSourceBean.class); + + private int minPoolSize = DEFAULT_POOL_SIZE; + private int maxPoolSize = DEFAULT_POOL_SIZE; + private int borrowConnectionTimeout = DEFAULT_BORROW_CONNECTION_TIMEOUT; + private int maxIdleTime = DEFAULT_MAX_IDLE_TIME; + private String testQuery; + private int maintenanceInterval = DEFAULT_MAINTENANCE_INTERVAL; + private int loginTimeout; + private transient ConnectionPool connectionPool; + private transient PrintWriter logWriter; + private String resourceName; + + private int defaultIsolationLevel = DEFAULT_ISOLATION_LEVEL_UNSET; + private int maxLifetime = DEFAULT_MAX_LIFETIME; + + private boolean enableConcurrentConnectionValidation = true; + + protected void throwAtomikosSQLException ( String msg ) throws AtomikosSQLException + { + throwAtomikosSQLException ( msg , null ); + } + + private void throwAtomikosSQLException ( String msg , Throwable cause ) throws AtomikosSQLException + { + AtomikosSQLException.throwAtomikosSQLException ( msg , cause ); + } + + /** + * Gets the minimum size of the pool. + */ + public int getMinPoolSize() { + return minPoolSize; + } + + /** + * Sets the minimum pool size. The amount of pooled connections won't go + * below that value. The pool will open this amount of connections during + * initialization. Optional, defaults to 1. + * + * @param minPoolSize + */ + public void setMinPoolSize(int minPoolSize) { + this.minPoolSize = minPoolSize; + } + + /** + * Get the maximum pool size. + */ + public int getMaxPoolSize() { + return maxPoolSize; + } + + /** + * Sets the maximum pool size. The amount of pooled connections won't go + * above this value. Optional, defaults to 1. + * + * @param maxPoolSize + */ + public void setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + } + + /** + * Sets both the minimal and maximal pool size. + * Required if the maxPoolSize is not set. Overrides any minPoolSize + * or maxPoolSize settings you might have configured before! + */ + public void setPoolSize(int poolSize) { + this.minPoolSize = poolSize; + this.maxPoolSize = poolSize; + } + + /** + * Get the maximum amount of time in seconds the pool will block + * waiting for a connection to become available in the pool when it + * is empty. + */ + public int getBorrowConnectionTimeout() { + return borrowConnectionTimeout; + } + + /** + * Sets the maximum amount of time in seconds the pool will block + * waiting for a connection to become available in the pool when it + * is empty. Optional. + * + * @param borrowConnectionTimeout The time in seconds. Zero or negative means no waiting at all. + * Defaults to 30 seconds. + */ + public void setBorrowConnectionTimeout(int borrowConnectionTimeout) { + this.borrowConnectionTimeout = borrowConnectionTimeout; + } + + + /** + * Sets the maintenance interval for the pool maintenance thread. + * Optional. + * + * @param maintenanceInterval The interval in seconds. If not set or not positive then the pool's default (60 secs) will be used. + */ + public void setMaintenanceInterval(int maintenanceInterval) { + this.maintenanceInterval = maintenanceInterval; + } + + /** + * Gets the maintenance interval as set. + */ + public int getMaintenanceInterval() { + return this.maintenanceInterval; + } + + /** + * Gets the maximum amount of time in seconds a connection can stay in the pool + * before being eligible for being closed during pool shrinking. + */ + public int getMaxIdleTime() { + return maxIdleTime; + } + + /** + * Sets the maximum amount of seconds that unused excess connections should stay in the pool. Optional. + * + * Note: excess connections are connections that are created above the minPoolSize limit. + * + * @param maxIdleTime The preferred idle time for unused excess connections. Note that this value is + * only an indication; the pool will check regularly as indicated by the maintenanceInteval property. + * The default is 60 seconds. + */ + public void setMaxIdleTime(int maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + + /** + * Sets the maximum amount of seconds that a connection is kept in the pool before + * it is destroyed automatically. Optional, defaults to 0 (no limit). + * @param maxLifetime + */ + public void setMaxLifetime(int maxLifetime) { + this.maxLifetime = maxLifetime; + } + + /** + * Gets the maximum lifetime in seconds. + * + */ + public int getMaxLifetime() { + return maxLifetime; + } + + /** + * Gets the SQL query used to test a connection before returning it. + */ + public String getTestQuery() { + return testQuery; + } + + /** + * Sets the SQL query or statement used to validate a connection before returning it. Optional. + * + * @param testQuery - The SQL query or statement to validate the connection with. Note that + * although you can specify updates here, these will NOT be part of any JTA transaction! + */ + public void setTestQuery(String testQuery) { + this.testQuery = testQuery; + } + + + /** + * Sets whether or not to use concurrent connection validation. + * Optional, defaults to true. + * + * @param value + */ + public void setConcurrentConnectionValidation(boolean value) { + this.enableConcurrentConnectionValidation = value; + } + + public boolean getConcurrentConnectionValidation() { + return enableConcurrentConnectionValidation; + } + + public int poolAvailableSize() { + return connectionPool.availableSize(); + } + + public int poolTotalSize() { + return connectionPool.totalSize(); + } + + public PrintWriter getLogWriter() throws SQLException { + return logWriter; + } + + public int getLoginTimeout() throws SQLException { + return loginTimeout; + } + + public void setLogWriter(PrintWriter out) throws SQLException { + this.logWriter = out; + } + + public void setLoginTimeout(int seconds) throws SQLException { + this.loginTimeout = seconds; + } + + public synchronized void init() throws AtomikosSQLException + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo ( this + ": init..." ); + if (connectionPool != null) + return; + if ( maxPoolSize < 1 ) + throwAtomikosSQLException ( "Property 'maxPoolSize' must be greater than 0, was: " + maxPoolSize ); + if ( minPoolSize < 0 || minPoolSize > maxPoolSize ) + throwAtomikosSQLException("Property 'minPoolSize' must be at least 0 and at most maxPoolSize, was: " + minPoolSize); + if ( getUniqueResourceName() == null ) + throwAtomikosSQLException("Property 'uniqueResourceName' cannot be null"); + if ( getMinPoolSize() == DEFAULT_POOL_SIZE ) { + LOGGER.logWarning ( this + ": poolSize equals default - this may cause performance problems!" ); + } + + try { + ConnectionFactory cf = doInit(); + if (enableConcurrentConnectionValidation) { + connectionPool = new ConnectionPoolWithConcurrentValidation(cf, this); + } else { + if ( getTestQuery() != null ) + LOGGER.logWarning ( this + ": testQuery set - pool may be slower / you might want to consider setting maxLifetime instead..." ); + connectionPool = new ConnectionPoolWithSynchronizedValidation(cf, this); + } + //initialize JNDI infrastructure for lookup + getReference(); + + + + } catch ( AtomikosSQLException e ) { + //these are logged at creation time -> just rethrow + throw e; + } catch ( Exception ex) { + String msg = "Cannot initialize AtomikosDataSourceBean"; + AtomikosSQLException.throwAtomikosSQLException ( msg , ex ); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": init done." ); + } + + public void close() + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo ( this + ": close..." ); + if (connectionPool != null) { + connectionPool.destroy(); + } + connectionPool = null; + doClose(); + try { + IntraVmObjectRegistry.removeResource ( getUniqueResourceName() ); + } catch ( NameNotFoundException e ) { + //ignore but log + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": Error removing from JNDI" , e ); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": close done." ); + } + + protected abstract ConnectionFactory doInit() throws Exception; + + protected abstract void doClose(); + + /* DataSource impl */ + + public Connection getConnection() throws SQLException + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": getConnection()..." ); + Connection connection = null; + + init(); + + try { + connection = connectionPool.borrowConnection(); + + } catch (CreateConnectionException ex) { + throwAtomikosSQLException("Failed to create a connection", ex); + } catch (PoolExhaustedException e) { + throwAtomikosSQLException ("Connection pool exhausted - try increasing 'maxPoolSize' and/or 'borrowConnectionTimeout' on the DataSourceBean."); + } catch (ConnectionPoolException e) { + throwAtomikosSQLException("Error borrowing connection", e ); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": returning " + connection ); + return connection; + } + + public Connection getConnection(String username, String password) throws SQLException + { + LOGGER.logWarning ( this + ": getConnection ( user , password ) ignores authentication - returning default connection" ); + return getConnection(); + } + + /** + * Get the resource name. + */ + public String getUniqueResourceName() { + return resourceName; + } + + /** + * Sets the resource name. Required. + * + * @param resourceName An arbitrary user-specified value that identifies + * this datasource. It must be unique for recovery purposes. + */ + public void setUniqueResourceName(String resourceName) { + this.resourceName = resourceName; + } + + /** + * Tests whether local transactions are allowed - defaults to false + * for JDBC. This property is used by the pooling mechanism. + * + * If false, then attempting to use a JDBC connection without a JTA transaction + * for the calling thread will fail. + */ + public boolean getLocalTransactionMode() { + return false; + } + + public Reference getReference() throws NamingException + { + return IntraVmObjectFactory.createReference ( this , getUniqueResourceName() ); + } + + /** + * Sets the default isolation level of connections returned by this datasource. + * Optional, defaults to the vendor-specific JDBC or DBMS settings. + * + * @param defaultIsolationLevel The default isolation level. + * Negative values are ignored and result in vendor-specific JDBC driver or DBMS internal defaults. + */ + public void setDefaultIsolationLevel(int defaultIsolationLevel) { + this.defaultIsolationLevel = defaultIsolationLevel; + } + + /** + * Gets the default isolation level for connections created by this datasource. + * + * @return The default isolation level, or -1 if no specific value was set. + * + */ + public int getDefaultIsolationLevel() { + return defaultIsolationLevel; + } + + public boolean isWrapperFor(Class iface) { + return isAssignableFromThisClass(iface) || isAssignableFromWrappedVendorClass(iface); + } + + protected abstract boolean isAssignableFromWrappedVendorClass(Class iface); + + private boolean isAssignableFromThisClass(Class iface) { + return iface.isAssignableFrom(getClass()); + } + + public T unwrap(Class iface) throws SQLException { + if (isAssignableFromThisClass(iface)) { + return (T) this; + } else if (isAssignableFromWrappedVendorClass(iface)) { + return (T) unwrapVendorInstance(); + } + throw new SQLException("Not a wrapper for class: " + iface); + } + + protected abstract Object unwrapVendorInstance(); + + /** + * JDK 1.7 requirement. + * + * @throws SQLFeatureNotSupportedException + */ + public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * Refreshes all available connections in the pool. + */ + public void refreshPool() { + if (connectionPool != null) connectionPool.refresh(); + } + + @Override + public String toString() { + return getUniqueResourceName(); + } +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AbstractJdbcConnectionProxy.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AbstractJdbcConnectionProxy.java new file mode 100644 index 000000000..e5fe41a2b --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AbstractJdbcConnectionProxy.java @@ -0,0 +1,265 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.DynamicProxySupport; +import com.atomikos.util.Proxied; + +public abstract class AbstractJdbcConnectionProxy extends DynamicProxySupport { + + private static final Logger LOGGER = LoggerFactory.createLogger(AbstractJdbcConnectionProxy.class); + + private List statements = new ArrayList(); + + public AbstractJdbcConnectionProxy(Connection delegate) { + super(delegate); + } + + protected synchronized void addStatement(Statement s) { + statements.add(s); + } + + protected synchronized void removeStatement(Statement s) { + statements.remove(s); + } + + protected abstract void updateTransactionContext() throws SQLException; + + protected abstract boolean isEnlistedInGlobalTransaction(); + + @Proxied + public Statement createStatement() throws SQLException { + updateTransactionContext(); + Statement s = delegate.createStatement(); + return createProxyStatement(s); + } + + private S createProxyStatement(S s) { + AtomikosJdbcStatementProxy ajsp = new AtomikosJdbcStatementProxy<>(this, s); + S proxy = ajsp.createDynamicProxy(); + addStatement(s); + return proxy; + } + + @Proxied + public PreparedStatement prepareStatement(String sql) throws SQLException { + updateTransactionContext(); + PreparedStatement s = this.delegate.prepareStatement(sql); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public CallableStatement prepareCall(String sql) throws SQLException { + updateTransactionContext(); + CallableStatement s = this.delegate.prepareCall(sql); + addStatement(s); + return createProxyStatement(s); + } + + + @Proxied + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + updateTransactionContext(); + Statement s = this.delegate.createStatement(resultSetType, resultSetConcurrency); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + updateTransactionContext(); + PreparedStatement s = this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + updateTransactionContext(); + CallableStatement s = this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + updateTransactionContext(); + Statement s = this.delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + addStatement(s); + return createProxyStatement(s); + } + + + @Proxied + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + updateTransactionContext(); + PreparedStatement s = this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + updateTransactionContext(); + CallableStatement s = this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + updateTransactionContext(); + PreparedStatement s = this.delegate.prepareStatement(sql, autoGeneratedKeys); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + updateTransactionContext(); + PreparedStatement s = this.delegate.prepareStatement(sql, columnIndexes); + addStatement(s); + return createProxyStatement(s); + } + + @Proxied + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + updateTransactionContext(); + PreparedStatement s = this.delegate.prepareStatement(sql, columnNames); + return createProxyStatement(s); + } + + @Proxied + public void setAutoCommit(boolean autoCommit) throws SQLException { + if (isEnlistedInGlobalTransaction()) { + if (autoCommit) { + AtomikosSQLException.throwAtomikosSQLException( + "Cannot call 'setAutoCommit(true)' while a global transaction is running"); + } + } else { + delegate.setAutoCommit(autoCommit); + } + } + + @Proxied + public boolean getAutoCommit() throws SQLException { + if (isEnlistedInGlobalTransaction()) { + return false; + } + return delegate.getAutoCommit(); + } + + @Proxied + public void commit() throws SQLException { + if (isEnlistedInGlobalTransaction()) { + AtomikosSQLException + .throwAtomikosSQLException("Cannot call method 'commit' while a global transaction is running"); + } + delegate.commit(); + } + + @Proxied + public void rollback() throws SQLException { + if (isEnlistedInGlobalTransaction()) { + AtomikosSQLException.throwAtomikosSQLException( + "Cannot call method '" + "rollback" + "' while a global transaction is running"); + } + delegate.rollback(); + + } + + @Proxied + public Savepoint setSavepoint() throws SQLException { + if (isEnlistedInGlobalTransaction()) { + AtomikosSQLException.throwAtomikosSQLException( + "Cannot call method '" + "setSavepoint()" + "' while a global transaction is running"); + } + return delegate.setSavepoint(); + } + + @Proxied + public Savepoint setSavepoint(String name) throws SQLException { + if (isEnlistedInGlobalTransaction()) { + AtomikosSQLException.throwAtomikosSQLException( + "Cannot call method '" + "setSavepoint(name)" + "' while a global transaction is running"); + } + return delegate.setSavepoint(name); + } + + @Proxied + public void rollback(Savepoint savepoint) throws SQLException { + if (isEnlistedInGlobalTransaction()) { + AtomikosSQLException.throwAtomikosSQLException( + "Cannot call method '" + "rollback(Savepoint)" + "' while a global transaction is running"); + } + delegate.rollback(savepoint); + + } + + @Proxied + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + if (isEnlistedInGlobalTransaction()) { + AtomikosSQLException.throwAtomikosSQLException( + "Cannot call method '" + "releaseSavepoint(savepoint)" + "' while a global transaction is running"); + } + delegate.releaseSavepoint(savepoint); + + } + + @Proxied + public boolean isClosed() throws SQLException { + return closed; + } + + + @Override + protected void throwInvocationAfterClose(String methodName) throws AtomikosSQLException { + String msg = "Connection was already closed - calling " + methodName + " is no longer allowed!"; + AtomikosSQLException.throwAtomikosSQLException(msg); + + } + + protected synchronized void forceCloseAllPendingStatements(boolean warn) { + Iterator it = statements.iterator(); + while (it.hasNext()) { + Statement s = it.next(); + try { + String msg = "Forcing close of pending statement: " + s; + if (warn) { + LOGGER.logWarning(msg); + } else { + LOGGER.logTrace(msg); + } + s.close(); + } catch (Exception e) { + // ignore but log + LOGGER.logWarning("Error closing pending statement: ", e); + } + // cf case 31275: also remove statement from list! + it.remove(); + } + } + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosJdbcConnectionProxy.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosJdbcConnectionProxy.java new file mode 100644 index 000000000..bcb0b12fa --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosJdbcConnectionProxy.java @@ -0,0 +1,194 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.Connection; +import java.sql.SQLException; + +import com.atomikos.datasource.xa.session.InvalidSessionHandleStateException; +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; +import com.atomikos.util.Proxied; + +public class AtomikosJdbcConnectionProxy extends AbstractJdbcConnectionProxy { + + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJdbcConnectionProxy.class); + + private final SessionHandleState sessionHandleState; + private final boolean localTransactionMode; + + public AtomikosJdbcConnectionProxy(Connection delegate, SessionHandleState sessionHandleState, boolean localTransactionMode) { + super(delegate); + this.sessionHandleState = sessionHandleState; + sessionHandleState.notifySessionBorrowed(); + this.localTransactionMode = localTransactionMode; + } + + private CompositeTransactionManager getCompositeTransactionManager() { + CompositeTransactionManager ret = Configuration.getCompositeTransactionManager(); + if (ret == null) { + LOGGER.logWarning(this + ": WARNING: transaction manager not running?"); + } + return ret; + } + + @Override + protected boolean isEnlistedInGlobalTransaction() { + CompositeTransactionManager compositeTransactionManager = getCompositeTransactionManager(); + if (compositeTransactionManager == null) { + return false; // TM is not running, we can only be in local TX mode + } + CompositeTransaction ct = compositeTransactionManager.getCompositeTransaction(); + return sessionHandleState.isActiveInTransaction(ct); + } + + @Override + protected void updateTransactionContext() throws SQLException { + try { + enlist(); + } catch (Exception e) { + // fix for bug 25678 + sessionHandleState.notifySessionErrorOccurred(); + LOGGER.logWarning("Error enlisting in transaction - connection might be broken? Please check the logs for more information...", e); + throw e; + } + } + + /** + * Enlist if necessary + * + * @return True if a JTA transaction was found, false otherwise. + * + * @throws AtomikosSQLException + */ + private boolean enlist() throws AtomikosSQLException { + boolean ret = false; + try { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": notifyBeforeUse " + sessionHandleState); + } + CompositeTransaction ct = null; + CompositeTransactionManager ctm = getCompositeTransactionManager(); + if (ctm != null) { + ct = ctm.getCompositeTransaction(); + // first notify the session handle - see case 27857 + sessionHandleState.notifyBeforeUse(ct); + if (ct != null && TransactionManagerImp.isJtaTransaction(ct)) { + ret = true; + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": detected transaction " + ct); + } + TxState state = ct.getState(); + if (state == TxState.ACTIVE) { + ct.registerSynchronization(new JdbcRequeueSynchronization(this, ct)); + } else if (state == TxState.MARKED_ABORT){ + AtomikosSQLException.throwAtomikosSQLException("The transaction has been set to rollback-only"); + } else { + AtomikosSQLException.throwAtomikosSQLException("The transaction has timed out - try increasing the timeout if needed"); + } + } else { + if (!localTransactionMode) { + AtomikosSQLException.throwAtomikosSQLException("A JTA transaction is required but none was found - please start one first (or set localTransactionMode=true to allow JDBC transactions)"); + } + } + } + } catch (InvalidSessionHandleStateException ex) { + AtomikosSQLException.throwAtomikosSQLException(ex.getMessage(), ex); + } + return ret; + } + + @Proxied + public void close() throws SQLException { + forceCloseAllPendingStatements(false); + markClosed(); + sessionHandleState.notifySessionClosed(); + } + + private class JdbcRequeueSynchronization implements Synchronization { + + private CompositeTransaction compositeTransaction; + private AtomikosJdbcConnectionProxy proxy; + private boolean afterCompletionDone; + + public JdbcRequeueSynchronization(AtomikosJdbcConnectionProxy proxy, + CompositeTransaction compositeTransaction) { + this.compositeTransaction = compositeTransaction; + this.proxy = proxy; + this.afterCompletionDone = false; + } + + public void afterCompletion(TxState state) { + + if (afterCompletionDone) { + return; + } + + if (state == TxState.ABORTING) { + // see bug 29708: close all pending statements to avoid reuse + // outside timed-out tx scope + forceCloseAllPendingStatements(true); + } + + if (state == TxState.TERMINATED || state.isHeuristic()) { + // connection is reusable! + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(proxy + ": detected termination of transaction " + compositeTransaction); + } + sessionHandleState.notifyTransactionTerminated(compositeTransaction); + afterCompletionDone = true; + forceCloseAllPendingStatements(false); // see case 73007 and 84252 + } + + } + + public void beforeCompletion() { + } + + @Override + public boolean equals(Object other) { + // override equals: synchronizations for the same tx are equal + // to avoid receiving double notifications on termination! + boolean ret = false; + if (other instanceof JdbcRequeueSynchronization) { + JdbcRequeueSynchronization o = (JdbcRequeueSynchronization) other; + ret = this.compositeTransaction.isSameTransaction(o.compositeTransaction); + } + return ret; + } + + public int hashCode() { + return compositeTransaction.hashCode(); + } + } + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + sessionHandleState.notifySessionErrorOccurred(); + throw e; + } + + @Override + public String toString() { + return "atomikosJdbcConnectionProxy (state = " + sessionHandleState + ") for vendor instance " + delegate; + } + + @Override + protected Class getRequiredInterfaceType() { + return Connection.class; + } + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosJdbcStatementProxy.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosJdbcStatementProxy.java new file mode 100644 index 000000000..d51a9a0f4 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosJdbcStatementProxy.java @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import com.atomikos.util.DynamicProxySupport; +import com.atomikos.util.Proxied; + +public class AtomikosJdbcStatementProxy extends DynamicProxySupport { + + private final AbstractJdbcConnectionProxy connection; + + public AtomikosJdbcStatementProxy(AbstractJdbcConnectionProxy connectionProxy, StmtInterface delegate) { + super(delegate); + this.connection = connectionProxy; + } + + + @Override + protected void throwInvocationAfterClose(String method) throws Exception { + String msg = "Statement was already closed - calling " + method + " is no longer allowed!"; + AtomikosSQLException.throwAtomikosSQLException ( msg ); + } + + + @Proxied + public void close() throws SQLException { + try { + this.delegate.close(); + } finally { + // safe to remove: statement will not be reused + this.connection.removeStatement(this.delegate); + } + + + } + + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + throw e; + } + + @Override + public String toString() { + return "atomikosJdbcStatementProxy for vendor instance " + delegate; + } + + @Override + protected Class getRequiredInterfaceType() { + if (delegate instanceof CallableStatement) { + return (Class) CallableStatement.class; + } else if (delegate instanceof PreparedStatement) { + return (Class) PreparedStatement.class; + } + return (Class) Statement.class; + } + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosNonXAParticipant.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosNonXAParticipant.java new file mode 100644 index 000000000..88ea08b59 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosNonXAParticipant.java @@ -0,0 +1,175 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + + +import java.util.Map; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * + * + * + * A participant for non-XA interactions. Instances are NOT recoverable in the + * sense that commit/rollback can fail after prepare. This is an implicit + * limitation of non-XA transactions. + * + * + * + * + */ + +class AtomikosNonXAParticipant implements Participant +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosNonXAParticipant.class); + + private boolean readOnly; + + private String name; + + private NonXaConnectionProxy connection; + + AtomikosNonXAParticipant ( NonXaConnectionProxy connection , String name ) + { + this.connection = connection; + this.name = name; + } + + /** + * @see Participant + */ + + public void setCascadeList ( Map allParticipants ) + throws SysException + { + // ignore + + } + + /** + * @see com.atomikos.icatch.Participant#setGlobalSiblingCount(int) + */ + public void setGlobalSiblingCount ( int count ) + { + // ignore + + } + + /* + * + * + * @see com.atomikos.icatch.Participant#prepare() + */ + public int prepare () throws RollbackException, HeurHazardException, + HeurMixedException, SysException + { + if (!readOnly) { + //see https://github.com/atomikos/transactions-essentials/issues/83 + String msg = this.toString(); + LOGGER.logWarning(msg); + throw new RollbackException(msg); + } + // DON'T return readOnly: we need the terminated event to reuse the connection in the pool! + int ret = Participant.READ_ONLY + 1; + return ret; + } + + /** + * @see com.atomikos.icatch.Participant#commit(boolean) + */ + + public void commit ( boolean onePhase ) + throws HeurRollbackException, HeurHazardException, + HeurMixedException, RollbackException, SysException + { + try { + connection.transactionTerminated ( true ); + } catch ( Exception e ) { + LOGGER.logError ( "Error in non-XA commit", e ); + //see case 30752: don't throw HAZARD because retries are probably useless + //and the connection won't be reused by the pool but destroyed instead + throw new HeurMixedException(); + } + } + + + /** + * @see com.atomikos.icatch.Participant#rollback() + */ + public void rollback () throws HeurCommitException, + HeurMixedException, HeurHazardException, SysException + { + + try { + connection.transactionTerminated ( false ); + } catch ( Exception e ) { + LOGGER.logError ( "Error in non-XA rollback", e ); + //see case 30752: don't throw HAZARD because retries are probably useless + //and the connection won't be reused by the pool but destroyed instead + throw new HeurMixedException(); + } + + } + + /** + * @see com.atomikos.icatch.Participant#forget() + */ + public void forget () + { + // do nothing special, since DB doesn't know about 2PC or heuristics + + } + + + public String getURI () + { + return null; + } + + public void setReadOnly ( boolean readOnly ) + { + this.readOnly = readOnly; + } + + + @Override + public String toString() { + return com.atomikos.jdbc.AtomikosNonXADataSourceBean.class.getName() + " '" + name + + "' [NB: this resource does not support two-phase commit unless configured as readOnly]"; + } + + @Override + public String getResourceName() { + return name; + } + + @Override + public boolean equals(Object o) { + boolean ret = false; + if (o instanceof AtomikosNonXAParticipant) { + AtomikosNonXAParticipant other = (AtomikosNonXAParticipant) o; + ret = connection == other.connection; + } + return ret; + } + + @Override + public int hashCode() { + return connection.hashCode(); + } +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosNonXAPooledConnection.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosNonXAPooledConnection.java new file mode 100644 index 000000000..36bff7034 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosNonXAPooledConnection.java @@ -0,0 +1,237 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import com.atomikos.datasource.pool.AbstractXPooledConnection; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.DynamicProxySupport; + + /** + * + * + * An implementation of XPooledConnection for non-xa drivers. + * + * + */ + +public class AtomikosNonXAPooledConnection extends AbstractXPooledConnection +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosNonXAPooledConnection.class); + + private static boolean isJdbc4Compatible = true; //optimistically assume we are compliant + + private Connection connection; + + private boolean erroneous; + + private boolean readOnly; + + private ConnectionPoolProperties props; + + public AtomikosNonXAPooledConnection ( Connection wrapped , ConnectionPoolProperties props , boolean readOnly ) + { + super ( props ); + this.connection = wrapped; + this.erroneous = false; + this.readOnly = readOnly; + this.props = props; + } + + void setErroneous() + { + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ( this + ": setErroneous" ); + } + this.erroneous = true; + } + + public void doDestroy() + { + try { + if ( connection != null ) { + connection.close(); + } + } catch ( SQLException e ) { + //ignore, just log + LOGGER.logWarning ( this + ": Error closing JDBC connection: " , e ); + } + + } + + protected Connection doCreateConnectionProxy() throws CreateConnectionException + { + Connection ret = null; + if (canBeRecycledForCallingThread()) { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": reusing existing proxy for thread..."); + } + ret = getCurrentConnectionProxy(); + + } else { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": creating connection proxy..."); + } + JdbcConnectionProxyHelper.setIsolationLevel(connection, getDefaultIsolationLevel()); + DynamicProxySupport proxy; + if (props.getIgnoreJtaTransactions()) { + proxy = new JtaUnawareThreadLocalConnection(this); + } else { + if (props.getLocalTransactionMode()) { + if (isInJtaTransaction()) { + proxy = new JtaAwareThreadLocalConnection(this, props.getUniqueResourceName()); + } else { + proxy = new JtaUnawareThreadLocalConnection(this); + } + } else { + proxy = new JtaAwareThreadLocalConnection(this, props.getUniqueResourceName()); + } + } + ret = proxy.createDynamicProxy(); + } + Object impl = Proxy.getInvocationHandler(ret); + if (impl instanceof JtaAwareThreadLocalConnection) { + JtaAwareThreadLocalConnection previous = (JtaAwareThreadLocalConnection) impl; + previous.incUseCount(); // cf case 176500 + } + return ret; + } + + + + private boolean isInJtaTransaction() { + CompositeTransaction ct = null; + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + if (ctm != null) { + ct = ctm.getCompositeTransaction(); + if (ct != null && TransactionManagerImp.isJtaTransaction(ct)) { + return true; + } + } + return false; + } + + Connection getConnection() + { + return connection; + } + + protected void testUnderlyingConnection() throws CreateConnectionException { + String testQuery = getTestQuery(); + if ( isErroneous() ) { + throw new CreateConnectionException ( this + ": connection is erroneous" ); + } + if ( maxLifetimeExceeded()) { + throw new CreateConnectionException ( this + ": connection too old - will be replaced" ); + } + + int queryTimeout = Math.max(0, getBorrowConnectionTimeout()); + if (testQuery != null) { + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ( this + ": testing connection with query [" + testQuery + "]" ); + } + Statement stmt = null; + try { + stmt = connection.createStatement(); + stmt.setQueryTimeout(queryTimeout); + //use execute instead of executeQuery - cf case 58830 + stmt.execute(testQuery); + stmt.close(); + } catch ( Exception e) { + //catch any Exception - cf case 22198 + throw new CreateConnectionException ( "Error executing testQuery" , e ); + } + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ( this + ": connection tested OK" ); + } + } else if (isJdbc4Compatible) { + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ( this + ": testing connection with connection.isValid()" ); + } + try { + if (!connection.isValid(queryTimeout)) { + throw new CreateConnectionException ("Connection no longer valid"); + } + } catch (CreateConnectionException e) { + throw e; + } catch (Throwable e) { + // happens if driver was compiled with old JDBC API version + LOGGER.logWarning("JDBC connection validation not supported by DBMS driver - please set a testQuery if validation is desired...", e); + isJdbc4Compatible = false; + } + } + } + + public boolean isAvailable() { + boolean ret = true; + + Object handle = getCurrentConnectionProxy(); + if ( handle != null ) { + NonXaConnectionProxy previous = (NonXaConnectionProxy) Proxy.getInvocationHandler(handle); + ret = previous.isAvailableForReuseByPool(); + } + + return ret; + + } + + public boolean isErroneous() { + return erroneous; + } + + //overridden for package-use here + protected void fireOnXPooledConnectionTerminated() + { + super.fireOnXPooledConnectionTerminated(); + updateLastTimeReleased(); + } + + public String toString() + { + return "AtomikosNonXAPooledConnection"; + } + + public boolean getReadOnly() { + return readOnly; + } + + public boolean canBeRecycledForCallingThread() + { + boolean ret = false; + Object handle = getCurrentConnectionProxy(); + if ( handle != null && !props.getIgnoreJtaTransactions()) { + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager (); + CompositeTransaction ct = null; + if ( ctm != null ) { + ct = ctm.getCompositeTransaction (); + } + if ( ct != null && TransactionManagerImp.isJtaTransaction(ct) ) { + Object previous = Proxy.getInvocationHandler(handle); + if (previous instanceof JtaAwareThreadLocalConnection) { + // instance check needed: see case 179070 + // don't recycle pooled connections that were created in localTransactionMode before the JTA transaction was there + JtaAwareThreadLocalConnection previousConnection = (JtaAwareThreadLocalConnection) previous; + ret = previousConnection.isInTransaction(ct); + } + } + } + return ret; + } +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosSQLException.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosSQLException.java new file mode 100644 index 000000000..1a31de3ab --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosSQLException.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.SQLException; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public class AtomikosSQLException extends SQLException { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosSQLException.class); + + /** + * Logs and throws an AtomikosSQLException. + * + * @param message The message to use. + * @param cause The causing error. + * @throws AtomikosSQLException + */ + public static void throwAtomikosSQLException ( String message , Throwable cause ) throws AtomikosSQLException + { + LOGGER.logWarning ( message , cause ); + throw new AtomikosSQLException ( message , cause ); + } + + /** + * Logs and throws an AtomikosSQLException. + * + * @param message The message to use. + * @throws AtomikosSQLException + */ + public static void throwAtomikosSQLException ( String message ) throws AtomikosSQLException + { + throwAtomikosSQLException ( message , null ); + } + + private AtomikosSQLException(String message, Throwable cause) { + super(message); + + if (cause instanceof SQLException) { + setNextException((SQLException) cause); + } + initCause(cause); + } + + private AtomikosSQLException(String message) { + super(message); + } + + private AtomikosSQLException(Throwable cause) { + if (cause instanceof SQLException) { + setNextException((SQLException) cause); + } + initCause(cause); + } + + + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosXAPooledConnection.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosXAPooledConnection.java new file mode 100644 index 000000000..9ccf475f5 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/AtomikosXAPooledConnection.java @@ -0,0 +1,169 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import javax.sql.XAConnection; + +import com.atomikos.datasource.pool.AbstractXPooledConnection; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.datasource.xa.jdbc.JdbcTransactionalResource; +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.datasource.xa.session.SessionHandleStateChangeListener; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public class AtomikosXAPooledConnection extends AbstractXPooledConnection +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosXAPooledConnection.class); + + private static boolean isJdbc4Compatible = true; //optimistically assume we are compliant + + private SessionHandleState sessionHandleState; + private XAConnection xaConnection; + private Connection connection; + private final boolean localTransactionMode; + + + public AtomikosXAPooledConnection ( XAConnection xaConnection, + JdbcTransactionalResource jdbcTransactionalResource, + ConnectionPoolProperties props ) + throws SQLException + { + super ( props ); + this.xaConnection = xaConnection; + this.connection = xaConnection.getConnection(); + this.sessionHandleState = new SessionHandleState ( jdbcTransactionalResource, xaConnection.getXAResource()); + sessionHandleState.registerSessionHandleStateChangeListener(new SessionHandleStateChangeListener() { + public void onTerminated() { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace( "SessionHandleState terminated, firing XPooledConnectionTerminated event for " + AtomikosXAPooledConnection.this); + updateLastTimeReleased(); + fireOnXPooledConnectionTerminated(); + } + }); + this.localTransactionMode = props.getLocalTransactionMode(); + + } + + + + public synchronized void doDestroy() + { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e ) { + //ignore but log + LOGGER.logWarning ( this + ": error closing Connection: " , e ); + } + } + if (xaConnection != null) { + try { + xaConnection.close(); + } catch (SQLException e ) { + //ignore but log + LOGGER.logWarning ( this + ": error closing XAConnection: " , e ); + } + } + + connection = null; + xaConnection = null; + } + + + + protected Connection doCreateConnectionProxy() throws CreateConnectionException + { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": creating connection proxy..." ); + JdbcConnectionProxyHelper.setIsolationLevel ( connection , getDefaultIsolationLevel() ); + AtomikosJdbcConnectionProxy proxy = new AtomikosJdbcConnectionProxy(connection, sessionHandleState, localTransactionMode); + return proxy.createDynamicProxy(); + } + + protected void testUnderlyingConnection() throws CreateConnectionException { + if ( isErroneous() ) throw new CreateConnectionException ( this + ": connection is erroneous" ); + if ( maxLifetimeExceeded()) throw new CreateConnectionException ( this + ": connection too old - will be replaced" ); + + int queryTimeout = Math.max(0, getBorrowConnectionTimeout()); + String testQuery = getTestQuery(); + + if (testQuery != null) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": testing connection with query [" + testQuery + "]" ); + try { + PreparedStatement stmt = connection.prepareStatement(testQuery); + stmt.setQueryTimeout(queryTimeout); + //use execute instead of executeQuery - cf case 58830 + stmt.execute(); + stmt.close(); + } catch ( Exception e) { + //catch any Exception - cf case 22198 + throw new CreateConnectionException ( "Error executing testQuery" , e ); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": connection tested OK" ); + } else if (isJdbc4Compatible) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": testing connection with connection.isValid()" ); + try { + if (!connection.isValid(queryTimeout)) { + throw new CreateConnectionException ("Connection no longer valid"); + } + } catch (CreateConnectionException e) { + throw e; + } catch (Throwable e) { + // happens if driver was compiled with old JDBC API version + LOGGER.logWarning("JDBC connection validation not supported by DBMS driver - please set a testQuery if validation is desired...", e); + isJdbc4Compatible = false; + } + } + } + + + + + + public boolean isAvailable() + { + boolean available = false; + available = sessionHandleState.isTerminated() && (xaConnection != null); + return available; + } + + public boolean isErroneous() + { + return sessionHandleState.isErroneous(); + } + + public String toString() { + return "atomikosXAPooledConnection with " + sessionHandleState; + } + + public boolean canBeRecycledForCallingThread () + { + boolean ret = false; + + CompositeTransactionManager tm = Configuration.getCompositeTransactionManager(); + if ( tm != null ) { //null for non-JTA use where recycling is pointless anyway + CompositeTransaction current = tm.getCompositeTransaction(); + if ((current != null) && (TransactionManagerImp.isJtaTransaction(current))) { + ret = sessionHandleState.isInactiveInTransaction(current); + } + } + + return ret; + } + + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JdbcConnectionProxyHelper.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JdbcConnectionProxyHelper.java new file mode 100644 index 000000000..53f069da0 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JdbcConnectionProxyHelper.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.Connection; +import java.sql.SQLException; + +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public class JdbcConnectionProxyHelper { + private static final Logger LOGGER = LoggerFactory.createLogger(JdbcConnectionProxyHelper.class); + + + public static void setIsolationLevel ( Connection connection , int defaultIsolationLevel ) + throws CreateConnectionException + { + + if (defaultIsolationLevel < 0) + return; + + try { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "setting isolation level to " + defaultIsolationLevel); + connection.setTransactionIsolation ( defaultIsolationLevel ); + } + catch (SQLException ex) { + LOGGER.logWarning ( "cannot set isolation level to " + defaultIsolationLevel, ex); + throw new CreateConnectionException ( "The configured default isolation level " + defaultIsolationLevel + + " seems unsupported by the driver - please check your JDBC driver documentation?" , ex ); + } + } + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JdbcNonXAConnectionHandleState.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JdbcNonXAConnectionHandleState.java new file mode 100644 index 000000000..1308492b8 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JdbcNonXAConnectionHandleState.java @@ -0,0 +1,208 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.util.HashSet; +import java.util.Stack; + +import com.atomikos.icatch.CompositeTransaction; + +/** + * State management for Non-XA JDBC connections borrowed from the pool. + */ + +class JdbcNonXAConnectionHandleState { + + + private CompositeTransaction currentTransaction; + private boolean readOnly; + private HashSet history; + + public JdbcNonXAConnectionHandleState() { + this(false); + } + + public JdbcNonXAConnectionHandleState(boolean readOnly) { + this.readOnly = readOnly; + this.history = new HashSet(); + } + + private void registerTransaction(CompositeTransaction ct) { + currentTransaction = ct; + history.add(ct); + } + + private boolean registeredBefore(CompositeTransaction ct) { + return ct != null && history.contains(ct); + } + + /** + * Updates the state to register the fact that we are going to use this connection + * as part of the given transaction. If a new transaction is detected, then a + * suitable exception will be throw for the caller to register participants. + * + * @param ct The transaction detected for the calling thread (assumed to be not null). + * Acceptable values are: + *
    + *
  1. A new transaction (where currently none is registered)
  2. + *
  3. The same transaction as currently registered
  4. + *
  5. A subtransaction of the current transaction
  6. + *
+ * + * All other cases throw InvalidTransactionContextException + * + * + * @throws TransactionContextException Catch-all exception + * @throws ParticipantRegistrationRequiredException If the caller should register a participant at the level of the local root transaction. + * @throws ReadOnlyParticipantRegistrationRequiredException If the caller should register a readOnly participant at the level of the local root transaction. + * @throws SubTxAwareParticipantRegistrationRequiredException If the caller should register a SubTxAwareParticipant for rollback to savepoint. + * @throws InvalidTransactionContextException If the supplied transaction cannot be accepted for data integrity reasons. + */ + public void notifyBeforeUse(CompositeTransaction ct) throws TransactionContextException { + + if (ct.isSameTransaction(currentTransaction)) { + if (registeredBefore(ct)) { + return; //do nothing: we're already doing work for this transaction + } else { + CompositeTransaction localRoot = findLocalRoot(ct); + if (localRoot != ct) { //subtx but not registered before + registerTransaction(ct); + throw new SubTxAwareParticipantRegistrationRequiredException(); + } + + } + } else if (ct.isDescendantOf(currentTransaction)) { + currentTransaction = ct; + registerTransaction(ct); + throw new SubTxAwareParticipantRegistrationRequiredException(); + } else if (currentTransaction == null){ + currentTransaction = ct; + registerTransaction(ct); + if (readOnly) { + throw new ReadOnlyParticipantRegistrationRequiredException(); + } else { + throw new ParticipantRegistrationRequiredException(); + } + } else { + throw new InvalidTransactionContextException("Connection accessed by transaction " + ct.getTid() + + "is already in use by another transaction: " + currentTransaction.getTid()); + } + + } + + /** + * Updates the state to register the termination of the current transaction. + * If the current transaction is a subtransaction, then the parent transaction will become the new + * current transaction as far as this state object is concerned. + */ + + public void subTransactionTerminated() { + Stack parentTransactions = currentTransaction.getLineage(); + currentTransaction = null; + if (parentTransactions != null && !parentTransactions.isEmpty()) { + CompositeTransaction parent = parentTransactions.peek(); + currentTransaction = parent; + } + } + + public boolean isEnlistedInGlobalTransaction() { + return currentTransaction != null; + } + + public boolean isEnlistedInGlobalTransaction(CompositeTransaction ct) { + boolean ret = false; + // See case 29060 and 28683: + // COPY attribute to avoid race conditions + // with NPE results + CompositeTransaction tx = currentTransaction; + if (tx != null && ct != null) { + ret = tx.isSameTransaction(ct); + } + return ret; + } + + public void reset() { + this.currentTransaction = null; + this.history.clear(); + } + + public CompositeTransaction findLocalRoot(CompositeTransaction ct) { + CompositeTransaction ret = ct; + Stack parents = ct.getLineage(); + if (parents != null) { + Stack parentsClone = (Stack) parents.clone(); + while (!parentsClone.isEmpty()) { + CompositeTransaction parent = parentsClone.pop(); + if (parent.isLocal()) { + ret = parent; + } + } + } + return ret; + } + + + static class TransactionContextException extends Exception { + + public TransactionContextException() {} + + public TransactionContextException(String msg) { + super(msg); + } + + private static final long serialVersionUID = 1L; + + } + + /** + * Exception indicating that the caller should register an AtomikosNonXAParticipant instance + * to take part in the commit phase. + */ + static class ParticipantRegistrationRequiredException extends TransactionContextException { + + private static final long serialVersionUID = 1L; + + } + + /** + * Exception indicating that the caller should register a read-only participant so the transaction + * termination will be detected, but without taking part in two-phase commit. + */ + static class ReadOnlyParticipantRegistrationRequiredException extends TransactionContextException { + + private static final long serialVersionUID = 1L; + + } + + /** + * Exception indicating that the connection is now in use for a subtransaction, meaning that + * the caller should register a SubTxAwareParticipant for subtransaction rollback to a savepoint, + * as well as an instance of AtomikosNonXAParticipant with the local root for two-phase commit. + */ + static class SubTxAwareParticipantRegistrationRequiredException extends TransactionContextException { + + private static final long serialVersionUID = 1L; + + } + + /** + * Exception indicating that the given transaction context is invalid for reuse of the connection. + * This usually means that the connection is currently in use by a different transaction. + */ + static class InvalidTransactionContextException extends TransactionContextException { + + public InvalidTransactionContextException(String msg) { + super(msg); + } + + private static final long serialVersionUID = 1L; + + } +} + diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JtaAwareThreadLocalConnection.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JtaAwareThreadLocalConnection.java new file mode 100644 index 000000000..3f4b1ba70 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JtaAwareThreadLocalConnection.java @@ -0,0 +1,357 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.util.Stack; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.ParticipantRegistrationRequiredException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.ReadOnlyParticipantRegistrationRequiredException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.SubTxAwareParticipantRegistrationRequiredException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.TransactionContextException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.Proxied; + +public class JtaAwareThreadLocalConnection extends AbstractJdbcConnectionProxy implements NonXaConnectionProxy { + + private static final Logger LOGGER = LoggerFactory.createLogger(JtaAwareThreadLocalConnection.class); + + private int useCount = 0; + + private boolean stale; + + private final AtomikosNonXAPooledConnection pooledConnection; + + private boolean originalAutoCommitState; + + private final String resourceName; + + private final JdbcNonXAConnectionHandleState state; + + public JtaAwareThreadLocalConnection(AtomikosNonXAPooledConnection pooledConnection, String resourceName) { + super(pooledConnection.getConnection()); + this.pooledConnection = pooledConnection; + this.resourceName =resourceName; + this.state = new JdbcNonXAConnectionHandleState(pooledConnection.getReadOnly()); + } + + + @Override + protected void updateTransactionContext() throws SQLException { + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + if (ctm == null ) { + return; + } + + CompositeTransaction ct = ctm.getCompositeTransaction(); + if (ct != null && TransactionManagerImp.isJtaTransaction(ct)) { + + try { + state.notifyBeforeUse(ct); + } catch (ParticipantRegistrationRequiredException e) { + registerAsParticipantFor(ct); + } catch (ReadOnlyParticipantRegistrationRequiredException e) { + registerAsReadOnlyParticipantFor(ct); + } catch (SubTxAwareParticipantRegistrationRequiredException e) { + registerAsSubTxAwareParticipantFor(ct); + } catch (TransactionContextException e) { + AtomikosSQLException.throwAtomikosSQLException(e.getMessage(), e); + } + + } else { + AtomikosSQLException.throwAtomikosSQLException("A JTA transaction is required but none was found - please start one first (or set localTransactionMode=true to allow JDBC transactions)"); + } + } + + public void transactionTerminated(boolean commit) throws SQLException { + + // delegate commit or rollback to the underlying connection + try { + if (commit) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": committing on connection..."); + } + this.delegate.commit(); + + } else { + // see case 84252 + forceCloseAllPendingStatements(false); + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": transaction aborting - " + + "pessimistically closing all pending statements to avoid autoCommit after timeout"); + LOGGER.logDebug(this + ": rolling back on connection..."); + } + this.delegate.rollback(); + + } + } catch (SQLException e) { + // make sure that reuse in pool is not possible + pooledConnection.setErroneous(); + String msg = "Error in commit on vendor connection"; + if (!commit) { + msg = "Error in rollback on vendor connection"; + } + AtomikosSQLException.throwAtomikosSQLException(msg, e); + } finally { + // reset attributes for next tx + resetForNextTransaction(); + // put connection in pool if no longer used + // note: if erroneous then the pool will destroy the connection (cf + // case 30752) + // which seems desirable to avoid pool exhaustion + markForReuseIfPossible(); + // see case 30752: resetting autoCommit should not be done here: + // connection may have been reused already! + } + + } + + private void resetForNextTransaction() { + state.reset(); + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": resetting autoCommit to " + originalAutoCommitState); + } + // see case 24567 + this.delegate.setAutoCommit(originalAutoCommitState); + } catch (Exception ex) { + LOGGER.logError(this + ": failed to reset original autoCommit state: " + ex.getMessage(), ex); + } + + } + + + /** + * Checks if the connection is being used on behalf of the given + * transaction. + * + * @param ct + * @return + */ + + boolean isInTransaction(CompositeTransaction ct) { + return state.isEnlistedInGlobalTransaction(ct); + } + + @Proxied + public void close() throws SQLException { + decUseCount(); + //markClosed(); cf case 179080: don't mark as closed to allow reuse in same transaction + } + + public void incUseCount() { + useCount++; + } + + private void decUseCount() { + useCount--; + markForReuseIfPossible(); + } + + boolean isStale() { + return stale; + } + + private void markForReuseIfPossible() { + + if (isAvailableForReuseByPool()) { + LOGGER.logTrace(this + ": detected reusability"); + setStale(); + forceCloseAllPendingStatements(false); + pooledConnection.fireOnXPooledConnectionTerminated(); + } else { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": not reusable yet"); + } + } + + } + + private void setStale() { + this.stale = true; + } + + public boolean isAvailableForReuseByPool() { + return useCount <= 0 && !isEnlistedInGlobalTransaction(); + } + + /** + * Checks if the connection is being used on behalf the a transaction. + * + * @return + */ + @Override + protected boolean isEnlistedInGlobalTransaction() { + return state.isEnlistedInGlobalTransaction(); + } + + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + pooledConnection.setErroneous(); + throw e; + } + + @Override + public String toString() { + return "jtaAwareThreadLocalConnection (isAvailable = " + isAvailableForReuseByPool() + ") for vendor instance " + delegate; + } + + private void registerAsParticipantFor(CompositeTransaction ct) throws SQLException { + originalAutoCommitState = this.delegate.getAutoCommit(); + this.delegate.setAutoCommit(false); + AtomikosNonXAParticipant participant = new AtomikosNonXAParticipant(this, resourceName); + CompositeTransaction localRoot = findLocalRoot(ct); + localRoot.addParticipant(participant); + if (localRoot != ct) { + //first registration happens for a subtransaction => also deal with subtx rollback + registerAsSubTxAwareParticipantFor(ct); + } + } + + private void registerAsReadOnlyParticipantFor(CompositeTransaction ct) throws SQLException { + originalAutoCommitState = this.delegate.getAutoCommit(); + this.delegate.setAutoCommit(false); + CompositeTransaction localRoot = findLocalRoot(ct); + ReadOnlyParticipant participant = new ReadOnlyParticipant(this); + localRoot.addSubTxAwareParticipant(participant); + } + + private void registerAsSubTxAwareParticipantFor(CompositeTransaction ct) throws SQLException { + boolean supportsSavepoints = false; + try { + supportsSavepoints = delegate.getMetaData().supportsSavepoints(); + } catch (Error e) { + LOGGER.logDebug("Failed to determine savepoint support in JDBC driver - see stacktrace for details", e); + //ignore, continue without subtx support + } + if (!supportsSavepoints) { + state.subTransactionTerminated(); + // no SubTxRollbackParticipant will be added so terminate here already to keep the transaction state clean + // note: next updateTransactionContext call will try and fail again but that is ok + AtomikosSQLException.throwAtomikosSQLException("You're trying to use nested transactions but the underlying JDBC driver does not support savepoints - which is required for this to work..."); + } + SubTxParticipant participant = new SubTxParticipant(ct.getTid(), this); + ct.addSubTxAwareParticipant(participant); + } + + private CompositeTransaction findLocalRoot(CompositeTransaction ct) { + CompositeTransaction ret = ct; + Stack parents = ct.getLineage(); + if (parents != null) { + Stack parentsClone = (Stack) parents.clone(); + while (!parentsClone.isEmpty()) { + CompositeTransaction parent = parentsClone.pop(); + if (parent.isLocal()) { + ret = parent; + } + } + } + return ret; + } + + private static class SubTxParticipant implements SubTxAwareParticipant { + private Savepoint savepoint; + private JtaAwareThreadLocalConnection owner; + + SubTxParticipant(String tid, JtaAwareThreadLocalConnection owner) throws SQLException { + this.owner = owner; + this.savepoint = owner.delegate.setSavepoint(tid); + } + + @Override + public void committed(CompositeTransaction transaction) { + owner.state.subTransactionTerminated(); + } + + @Override + public void rolledback(CompositeTransaction transaction) { + owner.state.subTransactionTerminated(); + try { + owner.delegate.rollback(savepoint); + } catch (SQLException e) { + LOGGER.logWarning("Failed to rollback to savepoint", e); + owner.pooledConnection.setErroneous(); + } + } + + @Override + public boolean equals(Object o) { + boolean ret = false; + if (o instanceof SubTxParticipant) { + SubTxParticipant other = (SubTxParticipant) o; + ret = owner == other.owner; + } + return ret; + } + + @Override + public int hashCode() { + return owner.hashCode(); + } + } + + private static class ReadOnlyParticipant implements SubTxAwareParticipant { + + JtaAwareThreadLocalConnection owner; + + ReadOnlyParticipant(JtaAwareThreadLocalConnection owner) { + this.owner = owner; + } + + @Override + public void rolledback(CompositeTransaction transaction) { + try { + owner.transactionTerminated(false); + } catch (SQLException e) { + LOGGER.logWarning("Unexpected error during rollback", e); + } + } + + @Override + public void committed(CompositeTransaction transaction) { + try { + owner.transactionTerminated(true); + } catch (SQLException e) { + LOGGER.logWarning("Unexpected error during commit", e); + } + + } + + @Override + public boolean equals(Object o) { + boolean ret = false; + if (o instanceof ReadOnlyParticipant) { + ReadOnlyParticipant other = (ReadOnlyParticipant) o; + ret = owner == other.owner; + } + return ret; + } + + @Override + public int hashCode() { + return owner.hashCode(); + } + + } + + @Override + protected Class getRequiredInterfaceType() { + return Connection.class; + } + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JtaUnawareThreadLocalConnection.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JtaUnawareThreadLocalConnection.java new file mode 100644 index 000000000..2d972e3e6 --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/JtaUnawareThreadLocalConnection.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.Connection; +import java.sql.SQLException; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.Proxied; + +public class JtaUnawareThreadLocalConnection extends AbstractJdbcConnectionProxy implements NonXaConnectionProxy { + + private static final Logger LOGGER = LoggerFactory.createLogger(JtaUnawareThreadLocalConnection.class); + + private final AtomikosNonXAPooledConnection pooledConnection; + private boolean dirty; + + + public JtaUnawareThreadLocalConnection(AtomikosNonXAPooledConnection pooledConnection) { + super(pooledConnection.getConnection()); + this.pooledConnection = pooledConnection; + } + + @Override + protected void updateTransactionContext() throws SQLException { + if (!getAutoCommit()) { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this+ ": marking as dirty..."); + } + dirty = true; + } + } + + @Override + protected boolean isEnlistedInGlobalTransaction() { + return false; + } + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + pooledConnection.setErroneous(); + throw e; + } + + @Override + public String toString() { + return "jtaUnawareThreadLocalConnection (isAvailable = " + isAvailableForReuseByPool() + ") for vendor instance " + delegate; + } + + @Override + public void transactionTerminated(boolean committed) throws SQLException { + //should never happen??? + AtomikosSQLException.throwAtomikosSQLException(this + ": transaction termination detected - which is incompatible with this type of connection???"); + } + + @Override + public boolean isAvailableForReuseByPool() { + return closed; + } + + @Proxied + @Override + public void commit() throws SQLException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": commit on vendor connection..."); + } + delegate.commit(); + dirty = false; + } + + @Proxied + @Override + public void rollback() throws SQLException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": rollback on vendor connection..."); + } + delegate.rollback(); + dirty = false; + + } + + + @Proxied + public void close() throws SQLException { + if (dirty && !delegate.getAutoCommit()) { + try { + rollback(); //case 175344: ensures clean connection state for reuse + } catch (Throwable t) { + LOGGER.logWarning(this + ": unexpected error trying to rollback on vendor connection - marking connection as erroneous so it will be replaced by the pool...", t); + pooledConnection.setErroneous(); //avoid reuse at all cost + } + } + markClosed(); + pooledConnection.fireOnXPooledConnectionTerminated(); + } + + @Override + protected Class getRequiredInterfaceType() { + return Connection.class; + } +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/NonXaConnectionProxy.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/NonXaConnectionProxy.java new file mode 100644 index 000000000..0f16df21f --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/internal/NonXaConnectionProxy.java @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.SQLException; + +interface NonXaConnectionProxy +{ + + void transactionTerminated ( boolean committed ) throws SQLException; + + boolean isAvailableForReuseByPool(); + +} diff --git a/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/nonxa/AtomikosNonXADataSourceBean.java b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/nonxa/AtomikosNonXADataSourceBean.java new file mode 100644 index 000000000..dfb86265a --- /dev/null +++ b/public/transactions-jdbc/src/main/java/com/atomikos/jdbc/nonxa/AtomikosNonXADataSourceBean.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.nonxa; + +/** + * + * @deprecated This class is preserved for backward compatibility of existing configurations + * and Spring Boot. It will be removed in some future release. + * + * @see com.atomikos.jdbc.AtomikosNonXADataSourceBean com.atomikos.jdbc.AtomikosNonXADataSourceBean should be used instead. + * + */ + +@Deprecated +public class AtomikosNonXADataSourceBean extends com.atomikos.jdbc.AtomikosNonXADataSourceBean { + + private static final long serialVersionUID = 1L; + +} diff --git a/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/internal/AtomikosNonXAParticipantTestJUnit.java b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/internal/AtomikosNonXAParticipantTestJUnit.java new file mode 100644 index 000000000..d05bb64a9 --- /dev/null +++ b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/internal/AtomikosNonXAParticipantTestJUnit.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import java.sql.SQLException; + +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; + +import junit.framework.TestCase; + +public class AtomikosNonXAParticipantTestJUnit extends TestCase +{ + AtomikosNonXAParticipant p; + TestJtaAwareNonXaConnection c; + + protected void setUp() throws Exception + { + super.setUp(); + c = new TestJtaAwareNonXaConnection(); + p = new AtomikosNonXAParticipant ( c , getName() ); + } + + public void testPrepareNeverReturnsReadOnlyForReadOnlyInstance() throws Exception { + p.setReadOnly ( true ); + int result = p.prepare(); + assertFalse ( Participant.READ_ONLY == result ); + assertNull ( c.isCommitted() ); + } + + public void testPrepareThrowsForNonReadOnlyInstance() throws Exception { + p.setReadOnly ( false ); + try { + p.prepare(); + fail("Prepare should fail if not readOnly"); + } catch (RollbackException ok) {} + assertNull ( c.isCommitted() ); + } + + + public void testEquals() { + assertEquals(p,p); + AtomikosNonXAParticipant p2 = new AtomikosNonXAParticipant ( c , getName() ); + assertEquals(p,p2); + } + + public void testHashCode() { + AtomikosNonXAParticipant p2 = new AtomikosNonXAParticipant ( c , getName() ); + assertEquals(p.hashCode(),p2.hashCode()); + } + + + private static class TestJtaAwareNonXaConnection implements NonXaConnectionProxy { + + private Boolean committed = null; + + public void transactionTerminated ( boolean committed ) + throws SQLException { + this.committed = new Boolean ( committed ); + } + + Boolean isCommitted () { + return committed; + } + + @Override + public boolean isAvailableForReuseByPool() { + return false; + } + + } + +} diff --git a/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/internal/JdbcNonXAConnectionHandleStateTestJUnit.java b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/internal/JdbcNonXAConnectionHandleStateTestJUnit.java new file mode 100644 index 000000000..223f5e675 --- /dev/null +++ b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/internal/JdbcNonXAConnectionHandleStateTestJUnit.java @@ -0,0 +1,160 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.internal; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Stack; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.InvalidTransactionContextException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.ParticipantRegistrationRequiredException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.ReadOnlyParticipantRegistrationRequiredException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.SubTxAwareParticipantRegistrationRequiredException; +import com.atomikos.jdbc.internal.JdbcNonXAConnectionHandleState.TransactionContextException; + +public class JdbcNonXAConnectionHandleStateTestJUnit { + + @Mock + private CompositeTransaction ct1, ct11, ct12; //nested transaction hierarchy + + @Mock + private CompositeTransaction ct2; //independent transaction + + private JdbcNonXAConnectionHandleState state; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Mockito.when(ct1.isSameTransaction(ct1)).thenReturn(true); + Mockito.when(ct11.isSameTransaction(ct11)).thenReturn(true); + Mockito.when(ct12.isSameTransaction(ct12)).thenReturn(true); + initNestedTransactionHierarchy(ct1, ct11, ct12); + Mockito.when(ct2.isSameTransaction(ct2)).thenReturn(true); + state = new JdbcNonXAConnectionHandleState(); + } + + private void initSubTransactionHierarchy(CompositeTransaction parent, CompositeTransaction subTx) { + Mockito.when(parent.isAncestorOf(subTx)).thenReturn(true); + Mockito.when(parent.isRelatedTransaction(subTx)).thenReturn(true); + Mockito.when(subTx.isDescendantOf(parent)).thenReturn(true); + Mockito.when(subTx.isRelatedTransaction(parent)).thenReturn(true); + Stack lineage = new Stack(); + lineage.push(parent); + Mockito.when(subTx.getLineage()).thenReturn(lineage); + } + + private void initNestedTransactionHierarchy(CompositeTransaction parent, CompositeTransaction subTx1, CompositeTransaction subTx2) { + initSubTransactionHierarchy(parent, subTx1); + initSubTransactionHierarchy(parent, subTx2); + Mockito.when(subTx1.isRelatedTransaction(subTx2)).thenReturn(true); + Mockito.when(subTx2.isRelatedTransaction(subTx1)).thenReturn(true); + } + + @Test(expected=ParticipantRegistrationRequiredException.class) + public void testNotifyWithNoExistingTransactionContextThrows() throws TransactionContextException { + state.notifyBeforeUse(ct1); + } + + @Test + public void testEnlistedReturnsTrueAfterNotify() throws TransactionContextException { + try { + state.notifyBeforeUse(ct1); + } catch (ParticipantRegistrationRequiredException ok) { + } + assertTrue(state.isEnlistedInGlobalTransaction()); + assertTrue(state.isEnlistedInGlobalTransaction(ct1)); + } + + @Test + public void testSecondNotifyWithSameTransactionContextDoesNotThrow() throws TransactionContextException { + try { + state.notifyBeforeUse(ct1); + } catch (ParticipantRegistrationRequiredException ok) { + } + state.notifyBeforeUse(ct1); + } + + @Test(expected=ReadOnlyParticipantRegistrationRequiredException.class) + public void testNotifyReadOnlyWithNoExistingTransactionContextThrows() throws TransactionContextException { + state = new JdbcNonXAConnectionHandleState(true); + state.notifyBeforeUse(ct1); + } + + @Test(expected=SubTxAwareParticipantRegistrationRequiredException.class) + public void testNotifyWithSubTransactionContextThrows() throws TransactionContextException { + try { + state.notifyBeforeUse(ct1); + } catch (ParticipantRegistrationRequiredException ok) { + } + state.notifyBeforeUse(ct12); + } + + @Test(expected=InvalidTransactionContextException.class) + public void testNotifyWithDifferentTransactionContextThrows() throws TransactionContextException { + try { + state.notifyBeforeUse(ct11); + } catch (ParticipantRegistrationRequiredException ok) { + } + state.notifyBeforeUse(ct12); + } + + @Test + public void testTransactionTerminatedClearsTransactionContext() throws TransactionContextException { + try { + state.notifyBeforeUse(ct1); + } catch (ParticipantRegistrationRequiredException ok) { + } + state.subTransactionTerminated(); + assertFalse(state.isEnlistedInGlobalTransaction()); + assertFalse(state.isEnlistedInGlobalTransaction(ct1)); + } + + + + @Test + public void testSubTransactionTerminatedRestoresParentTransactionContext() throws TransactionContextException { + try { + state.notifyBeforeUse(ct1); + } catch (ParticipantRegistrationRequiredException ok) { + } + try { + state.notifyBeforeUse(ct12); + } catch (SubTxAwareParticipantRegistrationRequiredException ok) { + } + state.subTransactionTerminated(); + assertTrue(state.isEnlistedInGlobalTransaction(ct1)); + } + + @Test + public void testSubTransactionsDoingSQLFirst() throws TransactionContextException { + try { + state.notifyBeforeUse(ct11); + } catch (ParticipantRegistrationRequiredException ok) { + } + assertTrue(state.isEnlistedInGlobalTransaction(ct11)); + state.subTransactionTerminated(); + assertTrue(state.isEnlistedInGlobalTransaction(ct1)); + try { + state.notifyBeforeUse(ct12); + } catch (SubTxAwareParticipantRegistrationRequiredException ok) { + } + assertTrue(state.isEnlistedInGlobalTransaction(ct12)); + state.subTransactionTerminated(); + assertTrue(state.isEnlistedInGlobalTransaction(ct1)); + } + +} diff --git a/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/nonxa/AtomikosNonXADataSourceBeanTestJUnit.java b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/nonxa/AtomikosNonXADataSourceBeanTestJUnit.java new file mode 100644 index 000000000..537277042 --- /dev/null +++ b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/nonxa/AtomikosNonXADataSourceBeanTestJUnit.java @@ -0,0 +1,192 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.nonxa; + +import java.sql.SQLException; + +import com.atomikos.jdbc.AtomikosNonXADataSourceBean; +import com.atomikos.jdbc.internal.AtomikosSQLException; + +import junit.framework.TestCase; + +public class AtomikosNonXADataSourceBeanTestJUnit extends TestCase +{ + private AtomikosNonXADataSourceBean ds; + + protected void setUp() { + ds = new AtomikosNonXADataSourceBean(); + } + + protected void tearDown() { + if ( ds != null ) ds.close(); + } + + public void testDriverClassName() + { + assertNull ( ds.getDriverClassName() ); + String name = "driver"; + ds.setDriverClassName ( name ); + assertEquals ( name , ds.getDriverClassName() ); + } + + public void testPassword() + { + assertNull ( ds.getPassword() ); + String pw = "secret"; + ds.setPassword( pw ); + assertEquals ( pw , ds.getPassword() ); + } + + public void testUrl() + { + assertNull ( ds.getUrl() ); + String url = "url"; + ds.setUrl(url); + assertEquals ( url , ds.getUrl() ); + } + + public void testUser() + { + assertNull ( ds.getUser() ); + String usr = "user"; + ds.setUser ( usr ); + assertEquals ( usr , ds.getUser() ); + + } + + public void testBorrowConnectionTimeout() + { + assertEquals ( 30 , ds.getBorrowConnectionTimeout() ); + ds.setBorrowConnectionTimeout( 14 ); + assertEquals ( 14 , ds.getBorrowConnectionTimeout() ); + } + + public void testLoginTimeout () throws SQLException + { + assertEquals ( 0 , ds.getLoginTimeout() ); + ds.setLoginTimeout( 5 ); + assertEquals ( 5 , ds.getLoginTimeout() ); + } + + public void testMaintenanceInterval() + { + assertEquals ( 60 , ds.getMaintenanceInterval() ); + ds.setMaintenanceInterval( 13 ); + assertEquals ( 13 , ds.getMaintenanceInterval() ); + } + + public void testMaxIdleTime() + { + assertEquals ( 60 , ds.getMaxIdleTime() ); + ds.setMaxIdleTime( 11 ); + assertEquals ( 11 , ds.getMaxIdleTime() ); + } + + public void testMaxPoolSize() + { + assertEquals ( 1 , ds.getMaxPoolSize() ); + ds.setMaxPoolSize( 3 ); + assertEquals ( 3 , ds.getMaxPoolSize() ); + } + + public void testMinPoolSize() + { + assertEquals ( 1 , ds.getMinPoolSize() ); + ds.setMinPoolSize( 4 ); + assertEquals ( 4 , ds.getMinPoolSize() ); + } + + public void testPoolSize() + { + assertEquals ( 1 , ds.getMinPoolSize() ); + assertEquals ( 1 , ds.getMaxPoolSize() ); + ds.setPoolSize ( 3 ); + assertEquals ( 3 , ds.getMinPoolSize() ); + assertEquals ( 3 , ds.getMaxPoolSize() ); + + } + + + public void testTestQuery() + { + assertNull ( ds.getTestQuery() ); + String query = "haha"; + ds.setTestQuery ( query ); + assertEquals ( query , ds.getTestQuery() ); + } + + public void testUniqueResourceName() + { + assertNull ( ds.getUniqueResourceName() ); + String name = "resource"; + ds.setUniqueResourceName( name ); + assertEquals ( name , ds.getUniqueResourceName() ); + } + + public void testInitWithDriverClassNotFoundThrowsMeaningfulException () throws SQLException + { + ds.setUniqueResourceName( "test" ); + ds.setDriverClassName ( "com.example.NonExistingClass" ); + try { + ds.getConnection(); + fail ( "getConnection works without existing driver class" ); + } catch ( AtomikosSQLException ok ) { + ok.printStackTrace(); + String expectedMsg = "Driver class not found: 'com.example.NonExistingClass' - please make sure the spelling is correct."; + assertEquals ( expectedMsg , ok.getMessage() ); + } + } + + public void testInitWithInvalidDriverClassThrowsMeaningfulException () throws SQLException + { + ds.setUniqueResourceName( "test" ); + ds.setDriverClassName ( "java.lang.String" ); + try { + ds.getConnection(); + fail ( "getConnection works with invalid driver class" ); + } catch ( AtomikosSQLException ok ) { + ok.printStackTrace(); + String expectedMsg = "Driver class 'java.lang.String' does not seem to be a valid JDBC driver - please check the spelling and verify your JDBC vendor's documentation"; + assertEquals ( expectedMsg , ok.getMessage() ); + } + } + + public void testReadOnly() throws Exception + { + assertFalse ( ds.getReadOnly() ); + ds.setReadOnly ( true ); + assertTrue ( ds.getReadOnly() ); + ds.setReadOnly ( false ); + assertFalse ( ds.getReadOnly() ); + } + + public void testDefaultIsolationLevel() throws Exception + { + assertEquals ( -1 , ds.getDefaultIsolationLevel() ); + ds.setDefaultIsolationLevel( 0 ); + assertEquals ( 0 , ds.getDefaultIsolationLevel() ); + } + + public void testLocalTransactionMode() throws Exception { + assertEquals ( false , ds.getLocalTransactionMode() ); + ds.setLocalTransactionMode(true); + assertEquals ( true , ds.getLocalTransactionMode() ); + assertEquals ( false , ds.getIgnoreJtaTransactions() ); + } + + public void testIgnoreJtaTransactions() throws Exception { + assertEquals ( false , ds.getIgnoreJtaTransactions() ); + ds.setIgnoreJtaTransactions(true); + assertEquals ( true , ds.getIgnoreJtaTransactions() ); + assertEquals ( false , ds.getLocalTransactionMode() ); + } + + + +} diff --git a/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/nonxa/JtaUnawareThreadLocalConnectionTestJUnit.java b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/nonxa/JtaUnawareThreadLocalConnectionTestJUnit.java new file mode 100644 index 000000000..97a10461e --- /dev/null +++ b/public/transactions-jdbc/src/test/java/com/atomikos/jdbc/nonxa/JtaUnawareThreadLocalConnectionTestJUnit.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jdbc.nonxa; + +import java.sql.Connection; +import java.sql.Statement; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.XPooledConnectionEventListener; +import com.atomikos.jdbc.internal.AtomikosNonXAPooledConnection; + +public class JtaUnawareThreadLocalConnectionTestJUnit { + + + @Mock + private Connection mockedVendorConnection; + @Mock + private Statement mockedVendorStatement; + @Mock + private ConnectionPoolProperties mockedCPP; + + @Mock + private XPooledConnectionEventListener mockedListener; + + + private AtomikosNonXAPooledConnection pc; + private Connection proxyConnection; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Mockito.when(mockedVendorConnection.isValid(Mockito.anyInt())).thenReturn(true); + Mockito.when(mockedVendorConnection.createStatement()).thenReturn(mockedVendorStatement); + mockedCPP = Mockito.mock(ConnectionPoolProperties.class); + Mockito.when(mockedCPP.getLocalTransactionMode()).thenReturn(true); + pc = new AtomikosNonXAPooledConnection(mockedVendorConnection, mockedCPP, false); + pc.registerXPooledConnectionEventListener(mockedListener); + proxyConnection = pc.createConnectionProxy(); + } + + @Test + public void testRollbackOnCloseOfDirtyConnection() throws Exception { + Mockito.when(mockedVendorConnection.getAutoCommit()).thenReturn(false); + proxyConnection.createStatement(); + proxyConnection.close(); + Mockito.verify(mockedVendorConnection, Mockito.times(1)).rollback(); + } + + @Test + public void testNoExtraRollbackOnCloseOfConnectionAfterRollback() throws Exception { + Mockito.when(mockedVendorConnection.getAutoCommit()).thenReturn(false); + proxyConnection.createStatement(); + proxyConnection.rollback(); + Mockito.verify(mockedVendorConnection, Mockito.times(1)).rollback(); + proxyConnection.close(); + Mockito.verify(mockedVendorConnection, Mockito.times(1)).rollback(); + } + + @Test + public void testNoExtraRollbackOnCloseOfConnectionAfterCommit() throws Exception { + Mockito.when(mockedVendorConnection.getAutoCommit()).thenReturn(false); + proxyConnection.createStatement(); + proxyConnection.commit(); + proxyConnection.close(); + Mockito.verify(mockedVendorConnection, Mockito.times(0)).rollback(); + } + + @Test + public void testNoExtraRollbackOnCloseWithAutoCommit() throws Exception { + Mockito.when(mockedVendorConnection.getAutoCommit()).thenReturn(true); + proxyConnection.createStatement(); + proxyConnection.close(); + Mockito.verify(mockedVendorConnection, Mockito.times(0)).rollback(); + } + + @Test + public void testCloseNotifiesPool() throws Exception { // test for case 189263 + proxyConnection.close(); + Mockito.verify(mockedListener).onXPooledConnectionTerminated(Mockito.any()); + } + +} diff --git a/public/transactions-jms/pom.xml b/public/transactions-jms/pom.xml new file mode 100644 index 000000000..3bb1c6cec --- /dev/null +++ b/public/transactions-jms/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-jms + Transactions JMS + + false + + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + provided + + + jakarta.jms + jakarta.jms-api + 2.0.3 + provided + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + org.slf4j + slf4j-simple + 1.4.3 + test + + + diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/AtomikosConnectionFactoryBean.java b/public/transactions-jms/src/main/java/com/atomikos/jms/AtomikosConnectionFactoryBean.java new file mode 100644 index 000000000..ed54a0d43 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/AtomikosConnectionFactoryBean.java @@ -0,0 +1,717 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms; + +import java.io.Serializable; +import java.util.Properties; + +import javax.jms.Connection; +import javax.jms.JMSContext; +import javax.jms.JMSException; +import javax.jms.XAConnection; +import javax.jms.XAConnectionFactory; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; + +import com.atomikos.beans.PropertyUtils; +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.datasource.pool.ConnectionFactory; +import com.atomikos.datasource.pool.ConnectionPool; +import com.atomikos.datasource.pool.ConnectionPoolException; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.ConnectionPoolWithConcurrentValidation; +import com.atomikos.datasource.pool.ConnectionPoolWithSynchronizedValidation; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.datasource.pool.PoolExhaustedException; +import com.atomikos.datasource.pool.XPooledConnection; +import com.atomikos.datasource.xa.jms.JmsTransactionalResource; +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.jms.internal.AtomikosJMSException; +import com.atomikos.jms.internal.AtomikosPooledJmsConnection; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.ClassLoadingHelper; +import com.atomikos.util.IntraVmObjectFactory; +import com.atomikos.util.IntraVmObjectRegistry; + + /** + * This class represents the Atomikos JMS 2.0 connection factory for JTA-enabled JMS. + * Use an instance of this class to make JMS participate in JTA transactions without + * having to issue the low-level XA calls yourself. The following use cases are supported, + * depending on the value you set for the sessionCreationMode property: + * + *
+ *
SessionCreationMode.JMS_2_0 (the default as of release 6.0)
+ *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
existing JTA transaction for threadlocalTransactionModeresulting session
trueignoredXA session
falsefalseXA session
falsetruenon-XA session according to sessionTransacted/acknowledgeMode parameters
+ *
+ *
SessionCreationMode.PRE_6_0 (optional, for backward compatibility)
+ *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
localTransactionModesessionTransactedFlagresulting session
falseignoredXA session
trueignorednon-XA session according to sessionTransacted/acknowledgeMode parameters
+ *
+ *
SessionCreationMode.PRE_3_9 (optional, for backward compatibility and equivalent to ignoreSessionTransactedFlag = false)
+ *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
localTransactionModesessionTransactedFlagresulting session
falsetrueXA session
otherothernon-XA session according to sessionTransacted/acknowledgeMode parameters
+ *
+ *
+ * + * IMPORTANT: for XA sessions, commit/rollback coincide with the JTA transaction outcome and this is the safest way of working. + * For non-XA sessions, commit/rollback are to be managed by your code - at the risk of message loss or duplicate messages. + * For details see http://www.atomikos.com/Publications/ReliableJmsWithTransactions. + */ + +public class AtomikosConnectionFactoryBean +implements javax.jms.ConnectionFactory, ConnectionPoolProperties, +Referenceable, Serializable, OrderedLifecycleComponent { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosConnectionFactoryBean.class); + + private static final long serialVersionUID = 1L; + + private String uniqueResourceName; + private int maxPoolSize = DEFAULT_POOL_SIZE; + private int minPoolSize = DEFAULT_POOL_SIZE; + private String xaConnectionFactoryClassName; + private int borrowConnectionTimeout = DEFAULT_BORROW_CONNECTION_TIMEOUT; + private Properties xaProperties = new Properties(); + private transient ConnectionPool connectionPool; + private transient XAConnectionFactory xaConnectionFactory; + private int maintenanceInterval = DEFAULT_MAINTENANCE_INTERVAL; + private int maxIdleTime = DEFAULT_MAX_IDLE_TIME; + private boolean localTransactionMode; + private int maxLifetime = DEFAULT_MAX_LIFETIME; + + private boolean enableConcurrentConnectionValidation = true; + private int sessionCreationMode = SessionCreationMode.JMS_2_0; + + public AtomikosConnectionFactoryBean() { + } + + private void throwAtomikosJMSException ( String msg ) throws AtomikosJMSException + { + throwAtomikosJMSException ( msg , null ); + } + + private void throwAtomikosJMSException ( String msg , Throwable cause ) + throws AtomikosJMSException + { + AtomikosJMSException.throwAtomikosJMSException(msg, cause); + } + + /** + * Gets the max size of the pool. + * @return int The max size of the pool. + */ + public int getMaxPoolSize() + { + return maxPoolSize; + } + + /** + * Sets the max size that the pool can reach. Optional, defaults to 1. + * @param maxPoolSize + */ + public void setMaxPoolSize(int maxPoolSize) + { + this.maxPoolSize = maxPoolSize; + } + + /** + * Gets the min size of the pool. + * + * @return The min size. + * + */ + public int getMinPoolSize() + { + return minPoolSize; + } + + /** + * Sets the min size of the pool. Optional, defaults to 1. + * + * @param minPoolSize + */ + public void setMinPoolSize(int minPoolSize) + { + this.minPoolSize = minPoolSize; + } + + /** + * Sets both the min and max size of the pool. Optional. + * + * Overrides any minPoolSize or maxPoolSize that you might + * have set before! + * + * @param minAndMaxSize + */ + + public void setPoolSize ( int minAndMaxSize ) + { + setMinPoolSize ( minAndMaxSize ); + setMaxPoolSize ( minAndMaxSize ); + } + + /** + * Gets the unique name for this resource. + * + * @return The name. + */ + public String getUniqueResourceName() + { + return uniqueResourceName; + } + + /** + * Sets the unique resource name for this resource, needed for recovery of transactions after restart. Required. + * + * @param resourceName + */ + + public void setUniqueResourceName(String resourceName) + { + this.uniqueResourceName = resourceName; + } + + /** + * Gets the name of the vendor-specific XAConnectionFactory class implementation. + * + * @return The name of the vendor class. + */ + public String getXaConnectionFactoryClassName() + { + return xaConnectionFactoryClassName; + } + + /** + * Sets the fully qualified name of a vendor-specific implementation of XAConnectionFatory. + * Required, unless you call setXaConnectionFactory. + * + * @param xaConnectionFactoryClassName + * + * @see javax.jms.XAConnectionFactory + */ + public void setXaConnectionFactoryClassName(String xaConnectionFactoryClassName) + { + this.xaConnectionFactoryClassName = xaConnectionFactoryClassName; + } + + /** + * Gets the vendor-specific XA properties to set. + * + * @return The properties as key,value pairs. + */ + public Properties getXaProperties() + { + return xaProperties; + } + + /** + * Sets the vendor-specific XA properties. + * Required, unless you call setXaConnectionFactory. + * + * @param xaProperties The properties, to be set (during initialization) on the + * specified XAConnectionFactory implementation. + */ + + public void setXaProperties(Properties xaProperties) + { + this.xaProperties = xaProperties; + } + + /** + * Gets the configured XAConnectionFactory. + * @return The factory, or null if not yet configured. + */ + public XAConnectionFactory getXaConnectionFactory() + { + return xaConnectionFactory; + } + + /** + * Sets the XAConnectionFactory directly, instead of calling setXaConnectionFactoryClassName and setXaProperties. + * + * @param xaConnectionFactory + */ + public void setXaConnectionFactory(XAConnectionFactory xaConnectionFactory) + { + this.xaConnectionFactory = xaConnectionFactory; + } + + /** + * Sets the maximum amount of seconds that a connection is kept in the pool before + * it is destroyed automatically. Optional, defaults to 0 (no limit). + * @param maxLifetime + */ + public void setMaxLifetime(int maxLifetime) { + this.maxLifetime = maxLifetime; + } + + /** + * Gets the maximum lifetime in seconds. + * + */ + public int getMaxLifetime() { + return maxLifetime; + } + + + /** + * Initializes the instance. It is highly recommended that this method be + * called early after VM startup, to ensure that recovery can start as soon as possible. + * + * @throws JMSException + */ + public synchronized void init() throws JMSException + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo ( this + ": init..." ); + if (connectionPool != null) + return; + + if (maxPoolSize < 1) + throwAtomikosJMSException("Property 'maxPoolSize' of class AtomikosConnectionFactoryBean must be greater than 0, was: " + maxPoolSize); + if (minPoolSize < 0 || minPoolSize > maxPoolSize) + throwAtomikosJMSException("Property 'minPoolSize' of class AtomikosConnectionFactoryBean must be at least 0 and at most maxPoolSize, was: " + minPoolSize); + if (getUniqueResourceName() == null) + throwAtomikosJMSException("Property 'uniqueResourceName' of class AtomikosConnectionFactoryBean cannot be null."); + + try { + ConnectionFactory cf = doInit(); + if (enableConcurrentConnectionValidation) { + connectionPool = new ConnectionPoolWithConcurrentValidation(cf, this); + } else { + connectionPool = new ConnectionPoolWithSynchronizedValidation(cf, this); + } + getReference(); + + } catch ( AtomikosJMSException e ) { + //don't log: AtomikosJMSException is logged on creation by the factory methods + throw e; + } + catch ( Exception ex) { + //don't log: AtomikosJMSException is logged on creation by the factory methods + throwAtomikosJMSException("Cannot initialize AtomikosConnectionFactoryBean", ex); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": init done." ); + + } + + private com.atomikos.datasource.pool.ConnectionFactory doInit() throws Exception + { + if (xaConnectionFactory == null) { + if (xaConnectionFactoryClassName == null) + throwAtomikosJMSException("Property 'xaConnectionFactoryClassName' of class AtomikosConnectionFactoryBean cannot be null."); + if (xaProperties == null) + throwAtomikosJMSException("Property 'xaProperties' of class AtomikosConnectionFactoryBean cannot be null."); + } + + + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo( + this + ": initializing with [" + + " xaConnectionFactory=" + xaConnectionFactory + "," + + " xaConnectionFactoryClassName=" + xaConnectionFactoryClassName + "," + + " uniqueResourceName=" + getUniqueResourceName() + "," + + " maxPoolSize=" + getMaxPoolSize() + "," + + " minPoolSize=" + getMinPoolSize() + "," + + " borrowConnectionTimeout=" + getBorrowConnectionTimeout() + "," + + " maxIdleTime=" + getMaxIdleTime() + "," + + " maintenanceInterval=" + getMaintenanceInterval() + "," + + " xaProperties=" + PropertyUtils.toString(xaProperties) + "," + + " localTransactionMode=" + localTransactionMode + "," + + " maxLifetime=" + maxLifetime + "," + + " enableConcurrentConnectionValidation=" + enableConcurrentConnectionValidation + "," + + " sessionCreationMode=" + sessionCreationMode + + "]" + ); + + if (xaConnectionFactory == null) { + try { + Class xaClass = ClassLoadingHelper.loadClass ( xaConnectionFactoryClassName ); + xaConnectionFactory = xaClass.newInstance(); + } catch ( ClassNotFoundException notFound ) { + AtomikosJMSException.throwAtomikosJMSException ( "The class '" + xaConnectionFactoryClassName + + "' specified by property 'xaConnectionFactoryClassName' of class AtomikosConnectionFactoryBean could not be found in the classpath. " + + "Please make sure the spelling in your setup is correct, and that the required jar(s) are in the classpath." , notFound ); + } catch (ClassCastException cce) { + AtomikosJMSException.throwAtomikosJMSException ( "The class '" + xaConnectionFactoryClassName + + "' specified by property 'xaConnectionFactoryClassName' of class AtomikosConnectionFactoryBean does not implement the required interface javax.jms.XAConnectionFactory. " + + "Please make sure the spelling in your setup is correct, and check your JMS driver vendor's documentation." ); + } + + PropertyUtils.setProperties(xaConnectionFactory, xaProperties ); + } + + JmsTransactionalResource tr = new JmsTransactionalResource(getUniqueResourceName() , xaConnectionFactory); + ConnectionFactory cf = new AtomikosJmsXAConnectionFactory(xaConnectionFactory, tr, this); + Configuration.addResource ( tr ); + return cf; + } + + + /** + * Gets the timeout for borrowing connections from the pool. + * + * @return int The timeout in seconds, during which connection requests should wait + * when no connection is available. + */ + public int getBorrowConnectionTimeout() + { + return borrowConnectionTimeout; + } + + /** + * Gets the pool maintenance interval. + * @return int The interval in seconds. + */ + public int getMaintenanceInterval() + { + return maintenanceInterval; + } + + /** + * Gets the max idle time for connections in the pool. + * + * @return int The max time in seconds. + */ + public int getMaxIdleTime() + { + return maxIdleTime; + } + + /** + * Gets a test query, currently defaults to null (not applicable to JMS). + */ + public String getTestQuery() + { + //not supported for now - maybe later? + return null; + } + + /** + * Sets the max timeout that connection requests should + * wait when no connection is available in the pool. Optional, defaults to 30 seconds. + * @param timeout The timeout in seconds. + */ + public void setBorrowConnectionTimeout ( int timeout ) + { + this.borrowConnectionTimeout = timeout; + } + + /** + * Sets the pool maintenance interval, i.e. the time (in seconds) between + * consecutive runs of the maintenance thread. Optional, defaults to 60 seconds. + * @param interval + */ + + public void setMaintenanceInterval ( int interval ) + { + this.maintenanceInterval = interval; + } + + /** + * Sets the max idle time after which connections are cleaned up + * from the pool. Optional, defaults to 60 seconds. + * + * @param time + */ + + public void setMaxIdleTime ( int time ) + { + this.maxIdleTime = time; + } + + + /** + * Gets the local transaction mode. + * + * @return boolean If true, then transactions are not done in XA mode but in local mode. + */ + public boolean getLocalTransactionMode() { + return localTransactionMode; + } + + /** + * Sets whether or not local transactions are desired. With local transactions, + * no XA enlist will be done - rather, the application should perform session-level + * JMS commit or rollback, or use explicit acknowledgement modes. + * Note that this feature also requires support from + * your JMS vendor. Optional, defaults to false. + * + * @param mode + */ + public void setLocalTransactionMode ( boolean mode ) { + this.localTransactionMode = mode; + } + + /* JMS does not support isolation levels */ + public int getDefaultIsolationLevel() { + return DEFAULT_ISOLATION_LEVEL_UNSET; + } + + + + /** + * Closes the instance. This method should be called when you are done using the factory. + */ + public synchronized void close() + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logInfo ( this + ": close..." ); + if ( connectionPool != null ) { + connectionPool.destroy(); + connectionPool = null; + } + + try { + IntraVmObjectRegistry.removeResource ( getUniqueResourceName() ); + } catch ( NameNotFoundException e ) { + //ignore but log + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": error removing from JNDI" , e ); + } + + RecoverableResource res = Configuration.getResource ( getUniqueResourceName() ); + if ( res != null ) { + Configuration.removeResource ( getUniqueResourceName() ); + //fix for case 26005: close resource! + res.close(); + } + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": close done." ); + } + + @Override + public String toString() + { + return getUniqueResourceName(); + } + + + /** + * @see javax.jms.ConnectionFactory + */ + public javax.jms.Connection createConnection() throws JMSException + { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": createConnection()..." ); + Connection ret = null; + try { + init(); + ret = (Connection) connectionPool.borrowConnection(); + } catch (CreateConnectionException ex) { + throwAtomikosJMSException("Failed to create a connection", ex); + } catch (PoolExhaustedException e) { + throwAtomikosJMSException ( "Connection pool exhausted - try increasing 'maxPoolSize' and/or 'borrowConnectionTimeout' on the AtomikosConnectionFactoryBean." , e ); + } catch (ConnectionPoolException e) { + throwAtomikosJMSException ( "Error borrowing connection", e ); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": createConnection() returning " + ret ); + return ret; + } + + /** + * @see javax.jms.ConnectionFactory + */ + + public javax.jms.Connection createConnection ( String user, String password ) throws JMSException + { + LOGGER.logWarning ( this + ": createConnection ( user , password ) ignores authentication - returning default connection" ); + return createConnection(); + } + + + /** + * @see javax.naming.Referenceable + */ + public Reference getReference() throws NamingException + { + Reference ret = null; + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": getReference()..." ); + ret = IntraVmObjectFactory.createReference ( this , getUniqueResourceName() ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": getReference() returning " + ret ); + return ret; + } + + public int poolAvailableSize() { + return connectionPool.availableSize(); + } + + public int poolTotalSize() { + return connectionPool.totalSize(); + } + + public void refreshPool() { + if (connectionPool != null) connectionPool.refresh(); + } + + public boolean getIgnoreSessionTransactedFlag() { + return sessionCreationMode != SessionCreationMode.PRE_3_9; + } + + /** + * Sets whether or not to ignore the sessionTransacted flag + * when creating JMS Sessions. If false then you will only get + * XA-capable JMS Session objects if the sessionTransacted flag + * is set upon session creation. Set this to false to get the + * pre-3.9 behavior of Atomikos. + * + * This setting is ignored for localTransactionMode: + * in localTransactionMode you never get XA-capable Session objects. + * + * @deprecated Kept for pre-3.9 configurations only. Otherwise, use the sessionCreationMode instead. + * + * @param value + */ + + @Deprecated + public void setIgnoreSessionTransactedFlag(boolean value) { + if (!value) { + sessionCreationMode = SessionCreationMode.PRE_3_9; + } else { + sessionCreationMode = SessionCreationMode.JMS_2_0; + } + } + + /** + * Sets whether or not to use concurrent connection validation. + * Optional, defaults to true. + * + * @param value + */ + public void setConcurrentConnectionValidation(boolean value) { + this.enableConcurrentConnectionValidation = value; + } + + public boolean getConcurrentConnectionValidation() { + return enableConcurrentConnectionValidation; + } + + + private static class AtomikosJmsXAConnectionFactory implements ConnectionFactory + { + + private final XAConnectionFactory xaConnectionFactory; + private final JmsTransactionalResource jmsTransactionalResource; + private final AtomikosConnectionFactoryBean atomikosConnectionFactory; + + + private AtomikosJmsXAConnectionFactory(XAConnectionFactory xaConnectionFactory, + JmsTransactionalResource jmsTransactionalResource, + AtomikosConnectionFactoryBean atomikosConnectionFactory) + { + this.xaConnectionFactory = xaConnectionFactory; + this.jmsTransactionalResource = jmsTransactionalResource; + this.atomikosConnectionFactory = atomikosConnectionFactory; + } + + public XPooledConnection createPooledConnection() throws CreateConnectionException + { + XAConnection xac; + try { + xac = xaConnectionFactory.createXAConnection(); + return new AtomikosPooledJmsConnection(atomikosConnectionFactory.getSessionCreationMode(), xac, jmsTransactionalResource, atomikosConnectionFactory); + } catch (JMSException ex) { + throw new CreateConnectionException("error creating JMS connection", ex); + } + } + + } + + + @Override + public JMSContext createContext() { + throw new UnsupportedOperationException(); + } + + @Override + public JMSContext createContext(String userName, String password) { + throw new UnsupportedOperationException(); + } + + @Override + public JMSContext createContext(String userName, String password, int sessionMode) { + throw new UnsupportedOperationException(); + } + + @Override + public JMSContext createContext(int sessionMode) { + throw new UnsupportedOperationException(); + } + + /** + * + * @return The SessionCreationMode + */ + public int getSessionCreationMode() { + return sessionCreationMode; + } + + /** + * + * Sets the creation mode for sessions. Optional, defaults to {@link SessionCreationMode#JMS_2_0}. + * + * @param mode + */ + public void setSessionCreationMode(int mode) { + SessionCreationMode.assertValidityOf(mode); + this.sessionCreationMode = mode; + } +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/SessionCreationMode.java b/public/transactions-jms/src/main/java/com/atomikos/jms/SessionCreationMode.java new file mode 100644 index 000000000..4550ba235 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/SessionCreationMode.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms; + + /** + * Constant values to indicate / tune the behavior in terms of JMS session creation + * (i.e., under what conditions to create XA sessions vs local sessions), + * so you can control backwards compatibility of your configuration. + * + * For historical reasons there have been several different interpretations + * of the JMS session creation semantics, mainly due to unclear JMS specifications. + */ + +public final class SessionCreationMode { + + static final void assertValidityOf(int value) { + if (value < 0 || value > 2) { + throw new IllegalArgumentException("The specified value should be between 0 and 2"); + } + } + + /** + * JMS session creation like in Atomikos releases prior to 3.9. + */ + + public static int PRE_3_9 = 0; + + /** + * JMS session creation like in Atomikos releases [3.9-6.0). + */ + + public static int PRE_6_0 = 1; + + /** + * As of Atomikos release 6.0, this is the default: + * JMS session creation along the clarified JMS 2.0 specification guidelines, + * see https://jakarta.ee/specifications/messaging/2.0/apidocs/ for details. + */ + + public static int JMS_2_0 = 2; +} \ No newline at end of file diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/AbstractJmsSenderTemplate.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/AbstractJmsSenderTemplate.java new file mode 100644 index 000000000..9cf2d728c --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/AbstractJmsSenderTemplate.java @@ -0,0 +1,486 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import java.io.Serializable; +import java.util.Map; +import java.util.Map.Entry; + +import javax.jms.BytesMessage; +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MapMessage; +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; +import javax.transaction.Status; +import javax.transaction.SystemException; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jms.AtomikosConnectionFactoryBean; +import com.atomikos.jms.internal.AtomikosJMSException; +import com.atomikos.jms.internal.AtomikosTransactionRequiredJMSException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * Common functionality for the sender templates. + * + */ + +public abstract class AbstractJmsSenderTemplate implements JmsSenderTemplate +{ + private static final Logger LOGGER = LoggerFactory.createLogger(AbstractJmsSenderTemplate.class); + + private AtomikosConnectionFactoryBean connectionFactoryBean; + private String user; + private String password; + private Destination destination; + private String destinationName; + private Destination replyToDestination; + private String replyToDestinationName; + private int deliveryMode; + private int priority; + private long timeToLive; + private boolean inited; + + protected AbstractJmsSenderTemplate() + { + // set default values according to Sun's JMS javadocs + setTimeToLive ( 0 ); + setDeliveryMode ( javax.jms.DeliveryMode.PERSISTENT ); + setPriority ( 4 ); + } + + protected abstract Session getOrRefreshSession ( Connection c ) throws JMSException; + + protected abstract Connection getOrReuseConnection() throws JMSException; + + protected abstract void afterUseWithoutErrors ( Connection c , Session s ) throws JMSException; + + + protected void destroy ( Connection c , Session s) + throws JMSException { + + try { + if ( s != null ) s.close(); + } catch ( JMSException warn ) { + LOGGER.logWarning ( this + ": error closing session" , warn); + } + + try { + if ( c != null ) c.close(); + } catch ( JMSException warn ) { + LOGGER.logWarning ( this + ": error closing connection" , warn); + } + + } + + protected synchronized Connection refreshConnection() throws JMSException { + Connection connection = null; + if ( getDestinationName() == null ) + throw new JMSException ( "Please call setDestination or setDestinationName first!" ); + + if ( user != null ) { + connection = connectionFactoryBean.createConnection ( + user, password ); + + } else { + connection = connectionFactoryBean.createConnection (); + } + connection.start (); + return connection; + } + + + /** + * Initializes the session for sending. + * Call this method first. + */ + + public void init() throws JMSException + { + if ( ! inited ) { + if ( connectionFactoryBean == null ) throw new IllegalStateException ( "Property 'atomikosConnectionFactoryBean' must be set first!" ); + if ( getDestinationName() == null ) { + throw new IllegalStateException ( "Property 'destination' or 'destinationName' must be set first!" ); + } + StringBuffer msg = new StringBuffer(); + msg.append ( this + ":configured with [" ); + msg.append ( "user=" ).append ( getUser() ).append ( ", " ); + msg.append ( "password=" ).append ( password ).append ( ", " ); + msg.append ( "deliveryMode=" ).append ( getDeliveryMode() ).append ( ", " ); + msg.append ( "timeToLive=" ).append ( getTimeToLive() ).append ( ", " ); + msg.append ( "priority=" ).append ( getPriority() ).append ( ", " ); + msg.append ( "destination=" ).append( getDestinationName() ).append ( ", " ); + msg.append ( "replyToDestination=" ).append ( getReplyToDestinationName() ); + msg.append ( "]" ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( msg.toString() ); + inited = true; + } + } + + private void retrieveDestinationIfNecessary() throws JMSException + { + if ( getDestination() == null ) { + String dName = getDestinationName(); + RetrieveDestinationCallback cb = new RetrieveDestinationCallback ( dName ); + executeCallbackInternal ( cb ); + setDestination ( cb.getDestination() ); + } + + } + + private void retrieveReplyToDestinationIfNecessary() throws JMSException + { + if ( getReplyToDestination() == null ) { + String dName = getReplyToDestinationName(); + if ( dName != null ) { + RetrieveDestinationCallback cb = new RetrieveDestinationCallback ( dName ); + executeCallbackInternal ( cb ); + setReplyToDestination ( cb.getDestination() ); + } + + } + + } + + /** + * Sets the connection factory to use. Required. + * @param connectionFactory + */ + public void setAtomikosConnectionFactoryBean(AtomikosConnectionFactoryBean connectionFactory) { + this.connectionFactoryBean = connectionFactory; + } + + public AtomikosConnectionFactoryBean getAtomikosConnectionFactoryBean() { + return connectionFactoryBean; + } + + public Destination getDestination() { + return destination; + } + + + /** + * Sets the (provider-specific) destination name in order + * to lookup the destination (rather than providing one directly). + * + * Required, unless you set the destination directly. + * + * @param destinationName + */ + + public void setDestinationName ( String destinationName ) + { + this.destinationName = destinationName; + } + + /** + * Sets the destination to send to. Required, unless + * you set the destinationName instead. + * + * @param destination + */ + public void setDestination(Destination destination) { + this.destination = destination; + } + + private String getName(Destination d, String destinationName ) { + String ret = destinationName; + if ( ret == null ) { + if ( d instanceof Queue ) { + Queue q = ( Queue ) d; + try { + ret = q.getQueueName(); + } catch ( JMSException e ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": error retrieving queue name" , e ); + } + } else if ( d instanceof Topic ) { + Topic t = ( Topic ) d; + try { + ret = t.getTopicName(); + } catch ( JMSException e ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": error retrieving topic name" , e ); + } + } + } + return ret; + } + + protected String getDestinationName() { + return getName ( getDestination() , destinationName ); + } + + protected String getReplyToDestinationName() { + return getName ( getReplyToDestination() , replyToDestinationName ); + } + + /** + * @return The user to connect with, or null if no explicit authentication + * is to be used. + */ + public String getUser() { + return user; + } + + /** + * If this session is used for sending request/reply messages, then this + * property indicates the destination where the replies are to be sent (optional). The + * session uses this to set the JMSReplyTo header accordingly. This property + * can be omitted if no reply is needed. + * + *

+ * The replyToDestination should be in the same JMS vendor domain as the send + * queue. To cross domains, configure a bridge for both the request and the + * reply channels. + */ + public void setReplyToDestination(Destination destination) + { + this.replyToDestination = destination; + } + + /** + * Sets the provider-specific replyToDestinationName. Optional. + * + * @param replyToDestinationName + */ + + public void setReplyToDestinationName ( String replyToDestinationName ) + { + this.replyToDestinationName = replyToDestinationName; + + } + + /** + * Gets the replyToDestination. + * + * @return + */ + public Destination getReplyToDestination() { + return replyToDestination; + } + + /** + * Set the password for explicit authentication (optional). + * This is only required if + * the user has also been set. + * + * @param password + * The password. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Set the user to use for explicit authentication (optional). If no explicit + * authentication is required then this method should not be called. + * + * @param user + */ + public void setUser(String user) { + this.user = user; + } + + protected void executeCallbackInternal ( + JmsSenderTemplateCallback callback ) throws JMSException { + + init(); + Session session = null; + Connection conn = null; + try { + conn = getOrReuseConnection(); + session = getOrRefreshSession ( conn ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Calling callback..." ); + callback.doInJmsSession ( session ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Callback done!" ); + afterUseWithoutErrors ( conn , session ); + + } catch ( AtomikosTransactionRequiredJMSException notx ) { + destroy ( conn , session ); + String msg = "The JMS session you are using requires a JTA transaction context for the calling thread and none was found." + "\n" + + "Please correct your code to start a JTA transaction before sending any message."; + LOGGER.logWarning ( msg ); + AtomikosTransactionRequiredJMSException.throwAtomikosTransactionRequiredJMSException ( msg ); + + } catch ( JMSException e ) { + e.printStackTrace(); + destroy ( conn , session ); + String msg = this + ": error in sending JMS message"; + AtomikosJMSException.throwAtomikosJMSException( msg , e ); + } + } + + /* (non-Javadoc) + * @see com.atomikos.jms.extra.JmsSenderTemplate#executeCallback(com.atomikos.jms.extra.JmsSenderTemplateCallback) + */ + + public void executeCallback(JmsSenderTemplateCallback callback) throws JMSException { + + init(); + + + retrieveDestinationIfNecessary(); + retrieveReplyToDestinationIfNecessary(); + + UserTransactionManager tm = new UserTransactionManager (); + try { + if ( tm.getStatus () != Status.STATUS_ACTIVE ) + throw new JMSException ( "This method requires an active transaction!" ); + } catch ( SystemException e ) { + LOGGER.logError ( this +": error in getting transaction status", e ); + throw new RuntimeException ( e.getMessage () ); + } + + executeCallbackInternal ( callback ); + + } + + + + /** + * @return The deliverymode for messages sent in this session. + */ + public int getDeliveryMode() { + return deliveryMode; + } + + /** + * @return The priority for messages sent in this session. + */ + public int getPriority() { + return priority; + } + + /** + * @return The timeToLive for messages sent in this session. + */ + public long getTimeToLive() { + return timeToLive; + } + + /** + * + * Set the deliverymode for messages sent in this session (optional). Defaults to + * persistent. + * + * @param + */ + public void setDeliveryMode(int i) { + deliveryMode = i; + } + + /** + * Set the priority for messages sent in this session (optional). Defaults to 4. + * + * @param + */ + public void setPriority(int i) { + priority = i; + } + + /** + * Set the time to live for messages sent in this session (optional). Defaults to 0. + * + * @param + */ + public void setTimeToLive(long l) { + timeToLive = l; + } + + /* (non-Javadoc) + * @see com.atomikos.jms.extra.JmsSenderTemplate#sendTextMessage(java.lang.String) + */ + @Override + public void sendTextMessage(String content) throws JMSException { + retrieveDestinationIfNecessary(); + retrieveReplyToDestinationIfNecessary(); + MessageCallback cb = new MessageCallback ( getDestination() , getReplyToDestination() , getDeliveryMode() , getPriority() , getTimeToLive() ) { + @Override + Message createMessage(Session session) throws JMSException { + return session.createTextMessage(content); + } + }; + executeCallback ( cb ); + } + + /* (non-Javadoc) + * @see com.atomikos.jms.extra.JmsSenderTemplate#sendMapMessage(java.util.Map) + */ + @Override + public void sendMapMessage(Map content) throws JMSException { + retrieveDestinationIfNecessary(); + retrieveReplyToDestinationIfNecessary(); + MessageCallback cb = new MessageCallback ( getDestination() , getReplyToDestination() , getDeliveryMode() , getPriority() , getTimeToLive() ) { + @Override + Message createMessage(Session session) throws JMSException { + MapMessage message = session.createMapMessage(); + for (Entry element : content.entrySet()) { + message.setObject(element.getKey(), element.getValue()); + } + return message; + } + }; + executeCallback ( cb ); + } + + /* (non-Javadoc) + * @see com.atomikos.jms.extra.JmsSenderTemplate#sendObjectMessage(java.io.Serializable) + */ + @Override + public void sendObjectMessage(Serializable content) throws JMSException { + retrieveDestinationIfNecessary(); + retrieveReplyToDestinationIfNecessary(); + MessageCallback cb = new MessageCallback ( getDestination() , getReplyToDestination() , getDeliveryMode() , getPriority() , getTimeToLive() ) { + @Override + Message createMessage(Session session) throws JMSException { + return session.createObjectMessage(content); + } + }; + + executeCallback ( cb ); + } + + /* (non-Javadoc) + * @see com.atomikos.jms.extra.JmsSenderTemplate#sendBytesMessage(byte[]) + */ + @Override + public void sendBytesMessage(byte[] content) throws JMSException { + retrieveDestinationIfNecessary(); + retrieveReplyToDestinationIfNecessary(); + MessageCallback cb = new MessageCallback ( getDestination() , getReplyToDestination() , getDeliveryMode() , getPriority() , getTimeToLive() ) { + @Override + Message createMessage(Session session) throws JMSException { + BytesMessage msg = session.createBytesMessage(); + msg.writeBytes ( content ); + + return msg; + } + }; + executeCallback ( cb ); + } + + /** + * Closes all resources. + */ + public void close() { + try { + Connection c = getOrReuseConnection(); + Session s = getOrRefreshSession(c); + destroy(c, s); + } catch (JMSException e) { + LOGGER.logWarning ( this + ": error closing" , e ); + } + connectionFactoryBean.close(); + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/ConcurrentJmsSenderTemplate.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/ConcurrentJmsSenderTemplate.java new file mode 100644 index 000000000..fc581a11d --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/ConcurrentJmsSenderTemplate.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Session; + + +/** + * This is a long-lived JMS sender session, representing a + * self-refreshing JMS session that can be used to send JMS messages in a + * transacted way (a JTA transaction context is required). + * + * The client code does not have to worry about refreshing or + * closing JMS objects explicitly: this is all handled in this class. All the + * client needs to do is indicate when it wants to start or stop using the + * session. + *

+ * This class produces instances for concurrent use by different threads: + * threaded applications can share one instance. + *

+ * Important: if you change any properties AFTER sending on the session, then + * you will need to explicitly stop and restart the session to have the changes + * take effect! + * + * + */ + + +public class ConcurrentJmsSenderTemplate extends AbstractJmsSenderTemplate +{ + + + public ConcurrentJmsSenderTemplate() + { + super(); + } + + + + protected Session getOrRefreshSession ( Connection c ) throws JMSException + { + + Session ret = null; + ret = c.createSession ( true , 0 ); + return ret; + } + + + + public String toString() + { + return "AbstractJmsSenderTemplate"; + } + + + protected Connection getOrReuseConnection() throws JMSException { + return refreshConnection(); + } + + + + @Override +protected void afterUseWithoutErrors(Connection c, Session s) + throws JMSException { + // close anyway: pooling will do the reuse + destroy(c,s); + + } + + + + + + + + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/DestinationHelper.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/DestinationHelper.java new file mode 100644 index 000000000..59e448fec --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/DestinationHelper.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Session; + +import com.atomikos.jms.internal.AtomikosJMSException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * Helper class for common destination logic. + * + * + */ + +class DestinationHelper +{ + private static final Logger LOGGER = LoggerFactory.createLogger(DestinationHelper.class); + + /** + * Finds a destination with a given provider-specific name. + * + * @param destinationName + * @param session + * @return The destination + * @throws JMSException If not found or if any other JMS error occurs. + */ + public static Destination findDestination ( String destinationName , Session session ) + throws JMSException { + Destination destination = null; + + try { + destination = session.createQueue ( destinationName ); + } catch ( Exception failed ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Failed to find queue with name: " + destinationName , failed ); + } + if ( destination == null ) { + try { + destination = session.createTopic ( destinationName ); + } catch ( Exception failed ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Failed to find topic with name: " + destinationName , failed ); + } + } + if ( destination == null ) { + AtomikosJMSException.throwAtomikosJMSException ( "The specified destination could not be found: " + destinationName ); + } + return destination; + } +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/JmsSenderTemplate.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/JmsSenderTemplate.java new file mode 100644 index 000000000..766eb7414 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/JmsSenderTemplate.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import java.io.Serializable; +import java.util.Map; + +import javax.jms.JMSException; + +/** + * Common interface for JMS send functionality so client can benefit from dependency injection. + */ +public interface JmsSenderTemplate { + + /** + * Executes an application-level call-back within the managed session. + * + * @param callback + * @throws JMSException + */ + void executeCallback(JmsSenderTemplateCallback callback) throws JMSException; + + /** + * Sends a TextMessage. + * + * @param content The text as a string. + * @throws JMSException + */ + void sendTextMessage(String content) throws JMSException; + + /** + * Sends a MapMessage. + * + * @param content The Map to get the content from. + * + * @throws JMSException + */ + + void sendMapMessage(Map content) throws JMSException; + + /** + * Sends an ObjectMessage. + * + * @param content The serializable object content. + * @throws JMSException + */ + void sendObjectMessage(Serializable content) + throws JMSException; + + /** + * Sends a ByteMessage. + * + * @param content The content as a byte array. + * @throws JMSException + */ + void sendBytesMessage(byte[] content) throws JMSException; + +} \ No newline at end of file diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/JmsSenderTemplateCallback.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/JmsSenderTemplateCallback.java new file mode 100644 index 000000000..0ecc98b2b --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/JmsSenderTemplateCallback.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.JMSException; +import javax.jms.Session; + + /** + * This is a call-back interface for doing more advanced + * and non-standard processing with the JmsSenderTemplate classes. + * + * Application code can implement this interface to get full access + * to the underlying (and managed!) JMS Session object. + * + */ + +@FunctionalInterface +public interface JmsSenderTemplateCallback +{ + + /** + * Performs some application-specific processing on the + * underlying JMS session. + * + * @param session The JMS session, as managed by the JmsSenderTemplate classes. + * @throws JMSException On errors. + */ + + public void doInJmsSession ( Session session ) throws JMSException; +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageCallback.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageCallback.java new file mode 100644 index 000000000..8dcb48115 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageCallback.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import javax.jms.Session; + +abstract class MessageCallback implements JmsSenderTemplateCallback +{ + + private final Destination replyToDestination; + private final Destination destination; + private final int priority; + private final long ttl; + private final int deliveryMode; + + protected MessageCallback ( Destination destination , Destination replyToDestination , int deliveryMode , int priority , long ttl ) + { + this.destination = destination; + this.replyToDestination = replyToDestination; + this.priority = priority; + this.ttl = ttl; + this.deliveryMode = deliveryMode; + } + + protected void sendMessage ( Message m , Session s ) throws JMSException + { + if ( replyToDestination != null ) + m.setJMSReplyTo ( replyToDestination ); + MessageProducer mp = s.createProducer( destination ); + mp.send ( m , deliveryMode, priority, ttl ); + mp.close(); + } + @Override + public void doInJmsSession(Session session) throws JMSException { + sendMessage(createMessage(session), session); + } + + + abstract Message createMessage(Session session) throws JMSException; + + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageConsumerSession.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageConsumerSession.java new file mode 100644 index 000000000..893109248 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageConsumerSession.java @@ -0,0 +1,684 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import java.util.HashMap; +import java.util.Map; + +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.ExceptionListener; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jms.AtomikosConnectionFactoryBean; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * + * + * Common message-driven session functionality. + * + */ + +class MessageConsumerSession +{ + private static final Logger LOGGER = LoggerFactory.createLogger(MessageConsumerSession.class); + + private AtomikosConnectionFactoryBean factory; + private Destination destination; + private String destinationName; + private MessageConsumerSessionProperties properties; + private boolean notifyListenerOnClose; + private String messageSelector; + private boolean daemonThreads; + private transient MessageListener listener; + protected transient ReceiverThread current; + private UserTransactionManager tm; + private boolean active; + private ExceptionListener exceptionListener; + + //for durable subscribers only + private boolean noLocal; + private String subscriberName; + private String clientID; + + private Map messageCounterMap = new HashMap<>(); + + protected MessageConsumerSession ( MessageConsumerSessionProperties properties ) + { + this.properties = properties; + tm = new UserTransactionManager(); + noLocal = false; + subscriberName = null; + } + + protected String getSubscriberName() + { + return subscriberName; + } + + protected void setSubscriberName ( String name ) + { + this.subscriberName = name; + } + + protected void setNoLocal ( boolean value ) + { + this.noLocal = value; + } + + protected boolean getNoLocal() + { + return noLocal; + } + + protected void setAtomikosConnectionFactoryBean ( AtomikosConnectionFactoryBean bean ) + { + this.factory = bean; + } + + protected AtomikosConnectionFactoryBean getAtomikosConnectionFactoryBean() + { + return factory; + } + + /** + * Sets whether threads should be daemon threads or not. + * Default is false. + * @param value If true then threads will be daemon threads. + */ + public void setDaemonThreads ( boolean value ) + { + this.daemonThreads = value; + } + + /** + * Tests whether threads are daemon threads. + * @return True if threads are deamons. + */ + public boolean getDaemonThreads() + { + return daemonThreads; + } + + /** + * Get the message selector (if any) + * + * @return The selector, or null if none. + */ + public String getMessageSelector() + { + return messageSelector; + } + + /** + * Set the message selector to use. + * + * @param selector + */ + public void setMessageSelector(String selector) + { + this.messageSelector = selector; + } + + /** + * Gets the destination. + * + * @return Null if none was set. + */ + public Destination getDestination() + { + return destination; + } + + /** + * Sets the destination to listen on. + * @param destination + */ + public void setDestination ( Destination destination ) + { + this.destination = destination; + } + + + + /** + * Get the transaction timeout in seconds. + * + * @return + */ + public int getTransactionTimeout() { + return properties.getTransactionTimeout(); + } + + /** + * Set the message listener for this session. Only one message listener per + * session is allowed. After this method is called, the listener will + * receive incoming messages in its onMessage method, in a JTA transaction. + * By default, the receiver will commit the transaction unless the onMessage + * method throws a runtime exception (in which case rollback will happen). + * + * If no more messages are desired, then this method should be called a + * second time with a null argument. + * + * @param listener + */ + public void setMessageListener(MessageListener listener) { + this.listener = listener; + } + + /** + * Get the message listener of this session, if any. + * + * @return + */ + public MessageListener getMessageListener() { + return listener; + } + + /** + * Start listening for messages. + * + */ + public void startListening() throws JMSException, SystemException { + + if ( active ) throw new IllegalStateException ( "MessageConsumerSession: startListening() called a second time without stopListening() in between" ); + + if ( getDestinationName() == null ) + throw new JMSException ( "Please set property 'destination' or 'destinationName' first" ); + if ( factory == null ) + throw new JMSException ( + "Please set the ConnectionFactory first" ); + + + tm.setStartupTransactionService ( true ); + tm.init(); + //disable startup to avoid threads re-start the core + //during shutdown!!! (see ISSUE 10084) + tm.setStartupTransactionService ( false ); + active = true; + startNewThread(); + + StringBuffer msg = new StringBuffer(); + msg.append ( "MessageConsumerSession configured with [" ); + msg.append ( "transactionTimeout=" ).append ( getTransactionTimeout() ).append ( ", " ); + msg.append ( "destination=" ).append( getDestinationName() ).append ( ", " ); + msg.append ( "notifyListenerOnClose= " ).append( getNotifyListenerOnClose() ).append( ", " ); + msg.append ( "messageSelector=" ).append( getMessageSelector() ).append( ", " ); + msg.append ( "daemonThreads=" ).append ( getDaemonThreads() ).append ( ", " ); + msg.append ( "messageListener=" ).append ( getMessageListener() ).append ( ", " ); + msg.append ( "exceptionListener=" ).append ( getExceptionListener() ).append ( ", " ); + msg.append ( "connectionFactory=" ).append ( getAtomikosConnectionFactoryBean() ); + msg.append ( "]" ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( msg.toString() ); + + } + + /** + * Gets the destination name, either as set directly or + * as set by the destinationName property. + * + * @return The destination's provider-specific name, or null if none set. + */ + public String getDestinationName() + { + String ret = destinationName; + if ( ret == null ) { + if ( destination instanceof Queue ) { + Queue q = ( Queue ) destination; + try { + ret = q.getQueueName(); + } catch (JMSException e) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Error retrieving queue name" , e ); + } + } else if ( destination instanceof Topic ) { + Topic t = ( Topic ) destination; + try { + ret = t.getTopicName(); + } catch (JMSException e) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Error retrieving topic name" , e ); + } + } + } + return ret; + } + + /** + * Sets the provider-specific destination name. Required, unless + * setDestination is called instead. + * + * @param destinationName + */ + public void setDestinationName ( String destinationName ) + { + this.destinationName = destinationName; + } + + protected void startNewThread() { + if ( active ) { + current = new ReceiverThread (); + //FIXED 10082 + current.setDaemon ( daemonThreads ); + current.start (); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: started new thread: " + current ); + } + //if not active: ignore + } + + private synchronized void notifyExceptionListener ( JMSException e ) + { + if ( exceptionListener != null ) exceptionListener.onException ( e ); + } + + /** + * Stop listening for messages. If notifyListenerOnClose is set then + * calling this method will indirectly lead to the invocation of the + * listener's onMessage method with a null argument (and without a + * transaction). This allows receivers to detect shutdown. + * + */ + public void stopListening() { + + if ( current != null ) { + ReceiverThread t = current; + // cf issue 62452: next, FIRST set current to null + // to allow listener thread to exit + // needed because the subsequent JMS cleanup will wait + // for the listener thread to finish!!! + current = null; + t.closeJmsResources ( true ); + } + tm.close(); + active = false; + } + + /** + * + * Check wether the session is configured to notify the listener upon close. + * + * @return boolean If true then the listener will receive a null message + * when the session is closed. + * + */ + public boolean getNotifyListenerOnClose() { + return notifyListenerOnClose; + } + + /** + * Set whether the listener should be notified on close. + * + * @param b + */ + public void setNotifyListenerOnClose(boolean b) { + notifyListenerOnClose = b; + } + + class ReceiverThread extends Thread + { + private Connection connection; + private Session session; + + private ReceiverThread () + { + } + + private synchronized MessageConsumer refreshJmsResources () throws JMSException + { + MessageConsumer ret = null; + + connection = factory.createConnection (); + + if (clientID != null ) { + //see http://activemq.apache.org/virtual-destinations.html + String connectionClientID = connection.getClientID(); + if ( connectionClientID == null ) connection.setClientID(clientID); + else LOGGER.logWarning ( "Reusing connection with preset clientID: " + connectionClientID ); + } + + connection.start (); + + session = connection.createSession ( true, 0 ); + if ( getDestination() == null ) { + Destination d = DestinationHelper.findDestination ( getDestinationName() , session ); + setDestination ( d ); + } + + String subscriberName = getSubscriberName(); + if ( subscriberName == null ) { + // cf case 33305: only use the noLocal flag if the destination is a topic + if ( destination instanceof Topic ) { + // topic -> use noLocal + ret = session.createConsumer ( destination, getMessageSelector () , getNoLocal() ); + } + else { + // queue -> noLocal flag not defined in JMS 1.1! + ret = session.createConsumer ( destination , getMessageSelector() ); + } + } + else { + // subscriberName is not null -> topic -> use noLocal flag too + ret = session.createDurableSubscriber( ( Topic ) destination , subscriberName , getMessageSelector() , getNoLocal() ); + } + + return ret; + } + + private synchronized void closeJmsResources ( boolean threadWillStop ) + { + try { + if ( session != null ) { + + if ( threadWillStop ) { + + try { + LOGGER.logInfo ( "MessageConsumerSession: unsubscribing " + subscriberName + "..."); + if ( Thread.currentThread() != this ) { + + //see case 62452 and 80464: wait for listener thread to exit so the subscriber is no longer in use + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: waiting for listener thread to finish..." ); + this.join(); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: waiting done." ); + + } + + if (subscriberName != null && properties.getUnsubscribeOnClose()) { + LOGGER.logInfo ( "MessageConsumerSession: unsubscribing " + subscriberName + "..."); + session.unsubscribe ( subscriberName ); + } + + } catch ( JMSException e ) { + + if ( LOGGER.isDebugEnabled() ) { + LOGGER.logDebug ("MessageConsumerSession: Error closing on JMS session", e ); + LOGGER.logDebug ( "MessageConsumerSession: linked exception is " , e.getLinkedException() ); + } + } + } + + try { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: closing JMS session..." ); + session.close (); + session = null; + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: JMS session closed." ); + } catch ( JMSException e ) { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: Error closing JMS session", e ); + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: linked exception is " , e.getLinkedException() ); + } + } + if ( connection != null ) + try { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: closing JMS connection..." ); + connection.close (); + connection = null; + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: JMS connection closed." ); + } catch ( JMSException e ) { + LOGGER.logWarning ( "MessageConsumerSession: Error closing JMS connection", e ); + LOGGER.logWarning ( "MessageConsumerSession: linked exception is " , e.getLinkedException() ); + } + } catch ( Throwable e ) { + LOGGER.logWarning ( "MessageConsumerSession: Unexpected error during close: " , e ); + //DON'T rethrow + } + } + + public void run () + { + MessageConsumer receiver = null; + + try { + // FIRST set transaction timeout, to trigger + // TM startup if needed; otherwise the logging + // to Configuration will not work! + tm.setTransactionTimeout ( getTransactionTimeout() ); + } catch ( SystemException e ) { + LOGGER.logError ( "MessageConsumerSession: Error in JMS thread while setting transaction timeout", e ); + } + + LOGGER.logDebug ( "MessageConsumerSession: Starting JMS listener thread." ); + + while ( Thread.currentThread () == current ) { + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: JMS listener thread iterating..." ); + boolean refresh = false; + boolean commit = true; + try { + Message msg = null; + + while ( receiver == null ) { + try { + receiver = refreshJmsResources (); + } catch ( JMSException connectionGone ) { + LOGGER.logWarning ( "Error refreshing JMS connection" , connectionGone ); + closeJmsResources(false); + // wait a while to avoid OutOfMemoryError with MQSeries + // cf case 73406 + Thread.sleep ( getTransactionTimeout() * 1000 / 4 ); + } + } + + + tm.setTransactionTimeout ( getTransactionTimeout() ); + + if ( tm.getTransaction () != null ) { + LOGGER.logFatal ( "MessageConsumerSession: detected pending transaction: " + tm.getTransaction () ); + // this is fatal and should not happen due to cleanup in previous iteration + throw new IllegalStateException ( "Can't reuse listener thread with pending transaction!" ); + } + + tm.begin (); + msg = receiveNextMessage(receiver); + + try { + + if ( msg != null && listener != null && Thread.currentThread () == current ) { + processMessage(msg); + } else { + commit = false; + } + } catch ( Exception e ) { + if ( LOGGER.isDebugEnabled() ) { + LOGGER.logDebug ("MessageConsumerSession: Error during JMS processing of message " + + msg.toString () + " - rolling back.", e ); + } + + // This happens if the listener generated the error. + // In that case, don't refresh the connection but rather + // only rollback. There is no reason to assume that the + // connection is corrupted here. + commit = false; + } + + } catch ( JMSException e ) { + LOGGER.logWarning ( "MessageConsumerSession: Error in JMS thread", e ); + Exception linkedException = e.getLinkedException(); + if ( linkedException != null ) { + LOGGER.logWarning ( "Linked JMS exception is: " , linkedException ); + } + // refresh connection to avoid corruption of thread state. + refresh = true; + commit = false; + notifyExceptionListener ( e ); + + } catch ( Throwable e ) { + LOGGER.logError ("MessageConsumerSession: Error in JMS thread", e ); + // Happens if there is an error not generated by the listener; + // refresh connection to avoid corruption of thread state. + refresh = true; + commit = false; + JMSException listenerError = new JMSException ( "Unexpected error - please see application log for more info" ); + notifyExceptionListener ( listenerError ); + + } finally { + + // Make sure no tx exists for thread, or we can't reuse + // the thread for later transactions! + try { + if ( commit ) + tm.commit (); + else { + tm.rollback (); + } + } catch ( RollbackException e ) { + // thread still OK + LOGGER.logWarning ( "MessageConsumerSession: Error in ending transaction", e ); + } catch ( HeuristicMixedException e ) { + // thread still OK + LOGGER.logWarning ( "MessageConsumerSession: Error in ending transaction", e ); + } catch ( HeuristicRollbackException e ) { + // thread still OK + LOGGER.logWarning ( "MessageConsumerSession: Error in ending transaction", e ); + } catch ( Throwable e ) { + LOGGER.logWarning ("MessageConsumerSession: Error ending thread tx association", e ); + // In this case, we suspend the tx so that it is no longer associated with this thread. + // This allows thread reuse for later messages. + // If suspend fails, then we can only start a new thread. + try { + LOGGER.logTrace ( "MessageConsumerSession: Suspending any active transaction..." ); + tm.suspend (); + } catch ( SystemException err ) { + LOGGER.logError ( "MessageConsumerSession: Error suspending transaction", err ); + // start new thread to replace this one, because we can't risk a pending transaction + try { + LOGGER.logTrace ( "MessageConsumerSession: Starting new thread..." ); + startNewThread(); + } catch ( Throwable fatal ) { + // happens if queue or factory no longer set + // in this case, we can't do anything else - + // just let the current thread exit and log + LOGGER.logFatal ( "MessageConsumerSession: Error starting new thread - stopping listener", e ); + // set current to null to make this thread exit, + // since reuse is impossible due to risk of pending transaction! + stopListening (); + } + } + + } + + if ( refresh && Thread.currentThread () == current) { + // close resources here and let the actual refresh be done by the next iteration + try { + receiver.close(); + } catch ( Throwable e ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: Error closing receiver" , e ); + } + receiver = null; + closeJmsResources ( false ); + } + } + + } + + // thread is going to die, close the receiver here. + if(receiver != null) { + try { + receiver.close(); + } catch ( Throwable e ) { + LOGGER.logTrace ( "MessageConsumerSession: Error closing receiver" , e ); + } + receiver = null; + } + + LOGGER.logDebug ( "MessageConsumerSession: JMS listener thread exiting." ); + if ( listener != null && current == null && notifyListenerOnClose ) { + // if this session stops listening (no more threads active) then + // notify the listener of shutdown by calling with null argument + listener.onMessage ( null ); + } + + } + + + + + } + + private void cleanRedeliveryLimit(Message msg) throws JMSException { + messageCounterMap.remove(msg.getJMSMessageID()); + } + + private void checkRedeliveryLimit(Message msg) throws JMSException { + if (msg.getJMSRedelivered()) { + String key = msg.getJMSMessageID(); + Long redeliveryCount = messageCounterMap.get(key); + if (redeliveryCount == null) { + redeliveryCount = 1L; + } else { + redeliveryCount++; + if (redeliveryCount > 5) { + LOGGER.logWarning("Possible poison message detected - check https://www.atomikos.com/Documentation/PoisonMessage: " + msg.toString()); + } + } + messageCounterMap.put(key, redeliveryCount); + } + } + + /** + * Gets the exception listener (if any). + * @return Null if no ExceptionListener was set. + */ + public ExceptionListener getExceptionListener() + { + return exceptionListener; + } + + /** + * Sets the exception listener. The listener will be + * notified of connection-level JMS errors. + * IMPORTANT: exception listeners will NOT be + * notified of any errors thrown by the MessageListener. + * Instead, the ExceptionListener mechanism is meant + * for system-level connectivity errors towards and from + * the underlying message system. + * + * @param exceptionListener + */ + public void setExceptionListener ( ExceptionListener exceptionListener ) + { + this.exceptionListener = exceptionListener; + } + + public void setClientID(String clientID) { + this.clientID = clientID; + } + + /** + * Gets the receive timeout in seconds. + * + * @return + */ + public int getReceiveTimeout() { + return properties.getReceiveTimeout(); + } + + protected Message receiveNextMessage(MessageConsumer receiver) throws JMSException { + // wait for at most half of the tx timeout + // cf case 83599: use separate timeout for receive to speedup shutdown + return receiver.receive ( getReceiveTimeout() * 1000 ); + } + + protected void processMessage(Message msg) throws JMSException { + LOGGER.logDebug ( "MessageConsumerSession: Consuming message: " + msg.toString () ); + checkRedeliveryLimit(msg); + listener.onMessage ( msg ); + LOGGER.logTrace ( "MessageConsumerSession: Consumed message: " + msg.toString () ); + cleanRedeliveryLimit(msg); + } +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageConsumerSessionProperties.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageConsumerSessionProperties.java new file mode 100644 index 000000000..188042fbd --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageConsumerSessionProperties.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + + + /** + * Configuration properties for the consumer session. + * + * An interface like this allows for the detection of hot-changes in settings. + * + */ + +interface MessageConsumerSessionProperties +{ + + public int getTransactionTimeout(); + + public boolean getUnsubscribeOnClose(); + + public int getReceiveTimeout(); + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageDrivenContainer.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageDrivenContainer.java new file mode 100644 index 000000000..c611ca8fb --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/MessageDrivenContainer.java @@ -0,0 +1,485 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jms.Destination; +import javax.jms.ExceptionListener; +import javax.jms.JMSException; +import javax.jms.MessageListener; + +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.jms.AtomikosConnectionFactoryBean; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * + * A message-driven container for asynchronously receiving JMS messages + * from a topic or queue, within a managed JTA transaction context. + * + * Upon start, an instance of this class will create a number of + * concurrent sessions that listen for incoming messages on the same destination. + * MessageListener instances should be thread-safe if the pool size is larger + * than one. Note: in general, after start() any changed properties are only + * effective on the next start() event. + * + *

+ * IMPORTANT: the transactional behaviour guarantees redelivery after failures. + * As a side-effect, this can lead to so-called poison messages: messages + * whose processing repeatedly fails due to some recurring error (for instance, a primary + * key violation in the database, a NullPointerException, ...). Poison messages are problematic + * because they can prevent other messages from being processed, and block the system. + * + * To avoid poison messages, make sure that your MessageListener implementation + * only throws a RuntimeException when the problem is transient. In that + * case, the system will perform rollback and the message will be redelivered + * facing a clean system state. All non-transient errors (i.e., those that happen + * each time a message is delivered) indicate problems at the application level + * and should be dealt with by writing better application code. + */ + +public class MessageDrivenContainer +implements MessageConsumerSessionProperties, OrderedLifecycleComponent +{ + private static final Logger LOGGER = LoggerFactory.createLogger(MessageDrivenContainer.class); + + private static final int DEFAULT_TIMEOUT = 30; + + + private AtomikosConnectionFactoryBean connectionFactoryBean; + private MessageListener messageListener; + private Destination destination; + private String destinationName; + private int transactionTimeout; + private int poolSize; + private List sessions; + private boolean daemonThreads; + private boolean notifyListenerOnClose; + private String messageSelector; + private ExceptionListener exceptionListener; + private String subscriberName; + private boolean noLocal; + private boolean unsubscribeOnClose; + private String clientID; + + private int receiveTimeout; + + public MessageDrivenContainer() + { + sessions = new ArrayList (); + notifyListenerOnClose = false; + setPoolSize ( 1 ); + setTransactionTimeout ( DEFAULT_TIMEOUT ); + } + + protected MessageConsumerSession createSession() + { + return new MessageConsumerSession ( this ); + } + + /** + * Sets the clientID for durable subscriptions. Optional. + * + * @param clientID + */ + public void setClientID ( String clientID ) { + this.clientID = clientID; + } + + + + /** + * Sets the connection factory to use. Required. + * @param bean + */ + public void setAtomikosConnectionFactoryBean ( AtomikosConnectionFactoryBean bean ) + { + this.connectionFactoryBean = bean; + } + + public AtomikosConnectionFactoryBean getAtomikosConnectionFactoryBean() + { + return connectionFactoryBean; + } + + /** + * Gets the destination. + * + * @return The destination, or null if not set. + */ + public Destination getDestination() + { + return destination; + } + + /** + * Sets the JMS destination to listen on (required unless the destinationName is set instead). + * + * @param dest + */ + public void setDestination ( Destination dest ) + { + this.destination = dest; + } + + /** + * Gets the destination name. + * + * @return The name, or null if not set. + */ + public String getDestinationName() + { + return destinationName; + } + + /** + * Sets the JMS provider-specific destination name + * (required unless the destination is set directly). + * + * @param destinationName + */ + public void setDestinationName ( String destinationName ) + { + this.destinationName = destinationName; + } + + + /** + * Sets whether threads should be daemon threads or not (optional). + * Default is false. + * @param value If true then threads will be daemon threads. + */ + public void setDaemonThreads ( boolean value ) + { + this.daemonThreads = value; + } + + /** + * Tests whether threads are daemon threads. + * @return True if threads are deamons. + */ + public boolean getDaemonThreads() + { + return daemonThreads; + } + + /** + * + * Get the message listener if any. + * + * @return + */ + public MessageListener getMessageListener() + { + return messageListener; + } + + /** + * Get the transaction timeout. + * + * @return + */ + public int getTransactionTimeout() + { + return transactionTimeout; + } + + + /** + * Set the message listener to use (required). + * The same instance will be used for each + * session in the pool, meaning that instances need to be thread-safe. Only + * one listener is allowed at a time. Call this method with a null argument + * to unset the listener. + * + * @param listener + */ + public void setMessageListener ( MessageListener listener ) + { + + messageListener = listener; + Iterator it = sessions.iterator (); + while ( it.hasNext () ) { + MessageConsumerSession s = (MessageConsumerSession) it.next (); + s.setMessageListener ( listener ); + } + } + + + /** + * Set the transaction timeout in seconds (optional). + * + * @param seconds + */ + public void setTransactionTimeout ( int seconds ) + { + transactionTimeout = seconds; + } + + + /** + * Get the message selector (if any) + * + * @return The selector, or null if none. + */ + public String getMessageSelector() + { + return this.messageSelector; + } + + /** + * Set the message selector to use (optional). + * + * @param selector + */ + public void setMessageSelector ( String selector ) + { + this.messageSelector = selector; + } + + /** + * Get the size of the pool. + * + * @return + */ + public int getPoolSize() + { + return poolSize; + } + + /** + * Sets the size of the session pool (optional). + * Default is 1. + * + * @param size + */ + public void setPoolSize ( int size ) + { + poolSize = size; + } + + /** + * Gets the exception listener (if any). + * @return Null if no ExceptionListener was set. + */ + public ExceptionListener getExceptionListener() + { + return exceptionListener; + } + + /** + * Sets the exception listener (optional). The listener will be + * notified of connection-level JMS errors. + * + * @param exceptionListener + */ + public void setExceptionListener ( ExceptionListener exceptionListener ) + { + this.exceptionListener = exceptionListener; + } + + /** + * Test if this instance will receive sends from the same connection. + * + * @return + */ + public boolean isNoLocal() { + return noLocal; + } + + /** + * Sets whether or not this topic should receive sends from the + * same connection (optional). + * + * @param noLocal + */ + + public void setNoLocal(boolean noLocal) { + this.noLocal = noLocal; + } + + /** + * Gets the subscriber name (for durable subscribers). + * @return The name, or null if not set (no durable subscriber). + */ + + public String getSubscriberName() { + return subscriberName; + } + + /** + * Sets the name to use for durable subscriptions (optional). + *
+ * Note: this name will be appended with a suffix to ensure uniqueness + * among instances in the pool. Otherwise, the JMS back-end would see + * multiple instances subscribing with the same name - an error. + * + * @param subscriberName + */ + + public void setSubscriberName(String subscriberName) { + this.subscriberName = subscriberName; + } + + protected boolean getNoLocal() { + + return isNoLocal(); + } + + /** + * Start listening for messages. + * + * @throws JMSException + */ + public void start() throws JMSException + { + if ( destination == null && destinationName == null ) + throw new JMSException ( + "MessageDrivenContainer: destination not specified" ); + if ( connectionFactoryBean == null ) + throw new JMSException ( + "MessageDrivenContainer: factory not set" ); + if ( messageListener == null ) + throw new JMSException ( + "MessageDrivenContainer: messageListener not set" ); + for ( int i = 0; i < poolSize; i++ ) { + MessageConsumerSession s = createSession(); + s.setMessageListener ( messageListener ); + s.setDestination ( destination ); + s.setDestinationName ( destinationName ); + s.setAtomikosConnectionFactoryBean ( connectionFactoryBean ); + s.setDaemonThreads ( daemonThreads ); + s.setNotifyListenerOnClose ( notifyListenerOnClose ); + s.setMessageSelector ( getMessageSelector () ); + s.setExceptionListener ( exceptionListener ); + s.setNoLocal( noLocal ); + s.setSubscriberName( subscriberName ); + //set subscriber name with suffix to ensure unique names + if ( getSubscriberName() != null ) s.setSubscriberName ( getSubscriberName() + "-" + i ); + s.setNoLocal ( getNoLocal() ); + s.setClientID(clientID); + try { + s.startListening (); + } catch ( Exception e ) { + LOGGER.logFatal ( "Error starting pool", e ); + } + sessions.add ( s ); + } + + // set listener again to trigger listening + setMessageListener ( messageListener ); + } + + /** + * Stop listening for messages. If notifyListenerOnClose is set then + * calling this method will notify the listener by calling its onMessage + * method with a null argument (and also without transaction context). + * + * This method will wait for all active receive operations to unblock, which may take + * up to receiveTimeout seconds per active thread. + */ + public void stop() + { + Iterator it = sessions.iterator (); + while ( it.hasNext () ) { + MessageConsumerSession s = (MessageConsumerSession) it.next (); + s.stopListening (); + } + } + + /** + * Getter to check whether the listener is notified on close. + * + * @return + */ + public boolean getNotifyListenerOnClose() + { + return notifyListenerOnClose; + } + + /** + * Set whether the listener should be notified of close events on the pool + * (optional). Default is false. + * + * @param b + * If true, then the listener will receive a null message if the + * pool is closed. + */ + public void setNotifyListenerOnClose ( boolean b ) + { + notifyListenerOnClose = b; + Iterator it = sessions.iterator (); + while ( it.hasNext () ) { + MessageConsumerSession s = it.next (); + s.setNotifyListenerOnClose ( b ); + } + } + + /** + * Sets whether unsubscribe should be done at closing time (optional). Default is false. + * + * @param b If true, then unsubscribe will be done at closing time. This only applies to + * durable subscribers (i.e., cases where subscriberName is set). + */ + public void setUnsubscribeOnClose ( boolean b ) + { + this.unsubscribeOnClose = b; + } + + /** + * Getter to test if unsubscribe should be called on close. + */ + + public boolean getUnsubscribeOnClose() + { + return unsubscribeOnClose; + } + + + /** + * Gets the receive timeout in seconds. + * + * @return + */ + public int getReceiveTimeout() { + int ret = receiveTimeout; + if ( ret <=0 ) ret = getTransactionTimeout()/2; + return ret; + } + + + /** + * Sets the receive timeout in seconds, + * i.e. the number of seconds to wait for incoming messages in the message listener thread's event loop. + * + * This property is optional and defaults to half the transactionTimeout, but typically this should be lower + * because the time required to shutdown (stop) this container will be bound by this value multiplied by + * the number of threads (as indicated by poolSize). + * + * @param seconds + */ + public void setReceiveTimeout(int seconds) { + this.receiveTimeout = seconds; + } + + @Override + public void close() throws Exception { + stop(); + } + + @Override + public void init() throws Exception { + start(); + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/RetrieveDestinationCallback.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/RetrieveDestinationCallback.java new file mode 100644 index 000000000..b0b41a435 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/RetrieveDestinationCallback.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Session; + +import com.atomikos.jms.internal.AtomikosJMSException; + +class RetrieveDestinationCallback implements JmsSenderTemplateCallback +{ + + private String destinationName; + private Destination destination; + + RetrieveDestinationCallback ( String destinationName ) { + this.destinationName = destinationName; + } + + public void doInJmsSession ( Session session ) throws JMSException + { + if ( destinationName == null ) + AtomikosJMSException.throwAtomikosJMSException ( + "Property 'destinationName' was not set" ); + + destination = DestinationHelper.findDestination ( destinationName , session ); + } + + Destination getDestination() + { + return destination; + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/extra/SingleThreadedJmsSenderTemplate.java b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/SingleThreadedJmsSenderTemplate.java new file mode 100644 index 000000000..17856e12c --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/extra/SingleThreadedJmsSenderTemplate.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Session; + +/** + * This is a long-lived JMS sender session, representing a + * self-refreshing JMS session that can be used to send JMS messages in a + * transacted way (a JTA transaction context is required). + * + * The client code does not have to worry about refreshing or + * closing JMS objects explicitly: this is all handled in this class. All the + * client needs to do is indicate when it wants to start or stop using the + * session. + *

+ * Note that instances are not meant for concurrent use by different threads: + * threaded applications should use the {@link ConcurrentJmsSenderTemplate} instead. + *

+ * Important: if you change any properties AFTER sending on the session, then + * you will need to explicitly stop and restart the session to have the changes + * take effect! + * + * + */ + +public class SingleThreadedJmsSenderTemplate extends AbstractJmsSenderTemplate +{ + + + private Session session; + private Connection connection; + + public SingleThreadedJmsSenderTemplate() + { + super(); + } + + protected Session getOrRefreshSession ( Connection c ) throws JMSException + { + //just reuse the prepared session + return session; + } + + + + public String toString() + { + return "SingleThreadedJmsSenderTemplate"; + } + + protected void afterUseWithoutErrors ( Session session ) throws JMSException { + //do nothing here: reuse session next time + } + + + + protected void afterUseWithoutErrors ( Connection c, Session s ) + throws JMSException { + //reuse connection and session next time + } + + protected Connection getOrReuseConnection() throws JMSException + { + if ( connection == null ) { + connection = refreshConnection(); + session = connection.createSession ( true , 0 ); + } + return connection; + } + + @Override + public void destroy ( Connection c , Session s ) throws JMSException { + super.destroy(c,s); + this.connection = null; + this.session = null; + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AbstractJmsSessionProxy.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AbstractJmsSessionProxy.java new file mode 100644 index 000000000..dc15d289c --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AbstractJmsSessionProxy.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import javax.jms.Session; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.util.DynamicProxySupport; + +public abstract class AbstractJmsSessionProxy extends DynamicProxySupport { + + public AbstractJmsSessionProxy(Session delegate) { + super(delegate); + } + + protected abstract boolean isAvailable(); + + protected abstract boolean isErroneous(); + + protected abstract boolean isInTransaction(CompositeTransaction ct); + + protected boolean isInactiveTransaction(CompositeTransaction ct) { + // default to false: be pessimistic and disallow reuse if not sure + return false; + } + + public abstract void recycle(); +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJMSException.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJMSException.java new file mode 100644 index 000000000..2b49ddf8d --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJMSException.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import javax.jms.JMSException; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * An extension of the standard JMSException with custom logic for error + * reporting. + */ + +public class AtomikosJMSException extends JMSException { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJMSException.class); + + private static final long serialVersionUID = 1L; + + /** + * Logs and throws and AtomikosJMSException. + * + * @throws AtomikosJMSException + */ + public static void throwAtomikosJMSException(String msg, Throwable cause) throws AtomikosJMSException { + LOGGER.logWarning(msg, cause); + throw new AtomikosJMSException(msg, cause); + } + + /** + * Logs and throws an AtomikosJMSException. + * + */ + + public static void throwAtomikosJMSException(String msg) throws AtomikosJMSException { + throwAtomikosJMSException(msg, null); + } + + public AtomikosJMSException(String reason) { + super(reason); + } + + public AtomikosJMSException(String reason, Throwable t) { + super(reason); + initCause(t); + if (t instanceof Exception) { + setLinkedException((Exception) t); + } + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsConnectionProxy.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsConnectionProxy.java new file mode 100644 index 000000000..07f35967c --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsConnectionProxy.java @@ -0,0 +1,421 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Session; +import javax.jms.XAConnection; +import javax.jms.XASession; + +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.datasource.xa.session.SessionHandleStateChangeListener; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.jms.SessionCreationMode; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.DynamicProxySupport; +import com.atomikos.util.Proxied; + +public class AtomikosJmsConnectionProxy extends DynamicProxySupport + implements SessionHandleStateChangeListener { + + private static Logger LOGGER = LoggerFactory.createLogger(AtomikosJmsConnectionProxy.class); + + private List sessions; + private XATransactionalResource jmsTransactionalResource; + private int sessionCreationMode; + private ConnectionPoolProperties props; + private SessionHandleStateChangeListener owner; + private boolean erroneous; + + public AtomikosJmsConnectionProxy(XAConnection delegate, int sessionCreationMode, + XATransactionalResource jmsTransactionalResource, SessionHandleStateChangeListener owner, + ConnectionPoolProperties props) { + super(delegate); + this.sessions = new ArrayList(); + this.jmsTransactionalResource = jmsTransactionalResource; + this.closed = false; + this.owner = owner; + this.props = props; + this.sessionCreationMode = sessionCreationMode; + + } + + @Override + protected void throwInvocationAfterClose(String methodName) throws Exception { + String msg = "Connection is closed already - calling method " + methodName + " no longer allowed."; + LOGGER.logWarning(this + ": " + msg); + throw new javax.jms.IllegalStateException(msg); + + } + + @Override + public void onTerminated() { + //a session has terminated -> remove it from the list of sessions to enable GC + List sessionsToCheck = cloneSessionsToAvoidDeadlock(); + List sessionsToRemove = new ArrayList(); + //call member sessions outside sync to avoid deadlock + Iterator it = sessionsToCheck.iterator(); + while ( it.hasNext() ) { + Session handle = it.next(); + AbstractJmsSessionProxy session = (AbstractJmsSessionProxy) Proxy.getInvocationHandler(handle); + if ( session.isAvailable() ) { + sessionsToRemove.add(handle); + } + } + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug ( this + ": removing " + sessionsToRemove.size() + " session(s)..."); + } + synchronized (sessions) { + for (Session s : sessionsToRemove) { + Iterator pendingIt = sessions.iterator(); + while (pendingIt.hasNext()) { + Session pending = pendingIt.next(); + if (pending == s) { // cf case 139936: don't rely on equals for dynamic proxy! + pendingIt.remove(); + } + } + + } + } + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug ( this + ": keeping " + sessions.size() + " sessions..."); + } + } + + @Proxied + public Session createSession() throws JMSException { + return createSession(Session.AUTO_ACKNOWLEDGE); + } + + @Proxied + public Session createSession(int sessionMode) throws JMSException { + if (sessionMode == Session.SESSION_TRANSACTED) { + return createSession(true, 0); + } else { + return createSession(false, sessionMode); + } + } + + @Proxied + public Session createSession(boolean transacted, int acknowledgeMode) throws JMSException { + + Session session = null; + synchronized (sessions) { //cf case 123502: deadlock + if (closed) { + //cf case 123502: make it safe to clone sessions on close... + throw new JMSException("Connection was closed already - creating new sessions is no longer allowed."); + } + if (createXaSession(transacted)) { + session = recycleSession(); + if (session == null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": creating XA-capable session..."); + } + forceConnectionIntoXaMode(delegate); + XASession wrapped = delegate.createXASession(); + session = AtomikosJmsXaSessionProxy.newInstance(wrapped, jmsTransactionalResource, owner, this); + addSession(session); + } + } else { + if (jtaTransactionExistsForThread()) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": creating NON-XA session - the resulting JMS work will NOT be part of the JTA transaction!"); + } + } + + Session wrapped = delegate.createSession(transacted, acknowledgeMode); + session = (Session) AtomikosJmsNonXaSessionProxy.newInstance(wrapped, owner, this); + addSession(session); + } + } + return session; + + } + + private boolean jtaTransactionExistsForThread() { + CompositeTransaction ct = null; + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + if (ctm != null) { + ct = ctm.getCompositeTransaction(); + } + return ct != null && TransactionManagerImp.isJtaTransaction(ct); + } + + private void addSession(Session session) { + // fix for case 62041: synchronized! + synchronized (sessions) { + sessions.add(session); + } + } + + private static void forceConnectionIntoXaMode(Connection c) { + // ORACLE AQ WORKAROUND: + // force connection into global tx mode + // cf ISSUE 10095 + Session s = null; + try { + s = c.createSession(true, Session.AUTO_ACKNOWLEDGE); + s.rollback(); + } catch (Exception e) { + // ignore: workaround code + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("JMS: driver complains while enforcing XA mode - ignore if no later errors:", e); + } + } finally { + if (s != null) { + try { + s.close(); + } catch (JMSException e) { + // ignore: workaround code + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("JMS: driver complains while enforcing XA mode - ignore if no later errors:", e); + } + } + } + } + } + + private boolean createXaSession(boolean sessionTransactedFlag) throws JMSException { + if (sessionCreationMode == SessionCreationMode.JMS_2_0) { + if (jtaTransactionExistsForThread()) { + return true; // cf case 185528 + } else { + return !props.getLocalTransactionMode(); + } + } else if (sessionCreationMode == SessionCreationMode.PRE_6_0) { + return !props.getLocalTransactionMode(); + } else if (sessionCreationMode == SessionCreationMode.PRE_3_9) { + return sessionTransactedFlag && !props.getLocalTransactionMode(); + } else { + AtomikosJMSException.throwAtomikosJMSException("Unhandled value of SessionCreationMode: " + sessionCreationMode); + } + return true; // we should never get here - just keep compiler happy + } + + private synchronized Session recycleSession() { + CompositeTransactionManager tm = Configuration.getCompositeTransactionManager(); + if (tm == null) + return null; + + CompositeTransaction current = tm.getCompositeTransaction(); + if (current != null && TransactionManagerImp.isJtaTransaction(current)) { + synchronized (sessions) { + for (int i = 0; i < sessions.size(); i++) { + Session session = sessions.get(i); + AbstractJmsSessionProxy proxy = (AbstractJmsSessionProxy) Proxy.getInvocationHandler(session); + + // recycle if either inactive in this tx, OR if active + // (since a new session will be created anyway, and + // concurrent sessions are allowed on the same underlying + // connection! + if (proxy.isInactiveTransaction(current) || proxy.isInTransaction(current)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": recycling session " + proxy); + } + proxy.recycle(); + return session; + } + } + } // synchronized (sessions) + } + return null; + } + + public boolean isErroneous() { + boolean ret = erroneous; + if (!ret) { + List sessionsToCheck = cloneSessionsToAvoidDeadlock(); + Iterator it = sessionsToCheck.iterator(); + while (it.hasNext() && !ret) { + Object handle = it.next(); + AbstractJmsSessionProxy session = (AbstractJmsSessionProxy) Proxy.getInvocationHandler(handle); + if (session.isErroneous()) { + ret = true; + } + } + } + return ret; + } + + public boolean isInTransaction(CompositeTransaction ct) { + boolean ret = false; + synchronized (sessions) { + Iterator it = sessions.iterator(); + while (it.hasNext() && !ret) { + Session handle = it.next(); + AbstractJmsSessionProxy session = (AbstractJmsSessionProxy) Proxy.getInvocationHandler(handle); + if (session.isInTransaction(ct)) + ret = true; + } + } + return ret; + } + + boolean isInactiveInTransaction(CompositeTransaction ct) { + if (!closed) { // cf case 174179 + return false; + } + boolean ret = false; + synchronized (sessions) { + Iterator it = sessions.iterator(); + while (it.hasNext() && !ret) { + Session handle = it.next(); + AbstractJmsSessionProxy session = (AbstractJmsSessionProxy) Proxy.getInvocationHandler(handle); + if (session.isInactiveTransaction(ct)) { + ret = true; + } + } + } + return ret; + } + + public boolean isAvailable() { + boolean ret = false; + List sessionsToCheck = new ArrayList(); + synchronized (sessions) { + if (closed) { + ret = true; //tentatively, depends on sessionsToCheck! + sessionsToCheck = cloneSessionsToAvoidDeadlock(); //cf case 123502: deadlock + } + } + Iterator it = sessionsToCheck.iterator(); //empty if not closed + while (it.hasNext() && ret) { + Object handle = it.next(); + AbstractJmsSessionProxy session = (AbstractJmsSessionProxy) Proxy.getInvocationHandler(handle); + if (!session.isAvailable()) { + ret = false; //wait for tx to terminate + } + } + return ret; + } + + // should only be called after ALL sessions are done, i.e. when the + // connection can be pooled again + synchronized void destroy() { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": closing connection and all " + sessions.size() + " session(s)"); + } + + // close all sessions to make sure the session close notifications are done! + synchronized (sessions) { + for (int i = 0; i < sessions.size(); i++) { + Session session = (Session) sessions.get(i); + try { + session.close(); + } catch (JMSException ex) { + LOGGER.logWarning(this + ": error closing session " + session, ex); + } + } + } + + sessions.clear(); + } + + @Proxied + public void close() { + if ( LOGGER.isDebugEnabled() ) { + LOGGER.logDebug ( this + ": closing " + sessions.size() + " session(s)"); + } + if (!closed) { // cf case 136113 + List sessionsToClose; + //close all sessions to make sure the session close notifications are done! + synchronized (sessions) { + markClosed(); //no new sessions will be added => safe to clone session list + sessionsToClose = cloneSessionsToAvoidDeadlock(); //cf case 123502: call close outside of sync to avoid deadlocks + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": closing " + sessionsToClose.size() + " session(s)"); + } + for (int i = 0; i < sessionsToClose.size(); i++) { + Session session = (Session) sessionsToClose.get(i); + try { + session.close(); + } catch (JMSException ex) { + LOGGER.logWarning(this + ": error closing session " + session, ex); + } + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": is available ? " + isAvailable()); + } + if (isAvailable()) { + owner.onTerminated(); + } + } + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ( this + ": closed." ); + } + //leave destroy to the owning pooled connection - that one knows when any and all 2PCs are done + } + + public static Connection newInstance(int sessionCreationMode, XAConnection xaConnection, + XATransactionalResource jmsTransactionalResource, + SessionHandleStateChangeListener sessionHandleStateChangeListener, ConnectionPoolProperties props) { + AtomikosJmsConnectionProxy proxy = new AtomikosJmsConnectionProxy(xaConnection, sessionCreationMode, + jmsTransactionalResource, sessionHandleStateChangeListener, props); + return proxy.createDynamicProxy(); + } + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + erroneous = true; + throw e; + } + + @Override + public String toString() { + return "atomikosJmsConnectionProxy (isAvailable = " + isAvailable() + ") for vendor instance " + delegate; + } + + private List cloneSessionsToAvoidDeadlock() { //cf case 123502: deadlock + List sessionsToReturn = null; + synchronized (sessions) { + sessionsToReturn = new ArrayList(sessions); + } + return sessionsToReturn; + } + + //should only be called after ALL sessions are done, i.e. when the connection can be pooled again + void closeAllPendingSessions() { + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ( this + ": closing connection and all " + sessions.size() + " session(s)" ); + } + + //close all sessions to make sure the session close notifications are done! + List sessionsToClose = cloneSessionsToAvoidDeadlock(); + for (Session session : sessionsToClose) { + try { + session.close (); + } catch (JMSException ex) { + LOGGER.logWarning ( this + ": error closing session " + session, ex ); + } + } + synchronized (sessions) { //cf BugzID: 126795 + sessions.removeAll(sessionsToClose); + } + + + } + + @Override + protected Class getRequiredInterfaceType() { + return XAConnection.class; + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsMessageConsumerWrapper.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsMessageConsumerWrapper.java new file mode 100644 index 000000000..fdf0f1efe --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsMessageConsumerWrapper.java @@ -0,0 +1,152 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; + +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +class AtomikosJmsMessageConsumerWrapper extends ConsumerProducerSupport implements MessageConsumer { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJmsMessageConsumerWrapper.class); + + private MessageConsumer delegate; + + public AtomikosJmsMessageConsumerWrapper(MessageConsumer delegate, SessionHandleState state) { + super(state); + this.delegate = delegate; + } + + protected MessageConsumer getDelegate() { + return delegate; + } + + public Message receive() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": receive()..."); + } + Message ret = null; + try { + enlist(); + ret = delegate.receive(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": receive returning " + ret); + } + return ret; + } + + public Message receive(long timeout) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": receive ( " + timeout + ")..."); + } + + Message ret = null; + try { + enlist(); + ret = delegate.receive(timeout); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": receive returning " + ret); + } + return ret; + } + + public Message receiveNoWait() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": receiveNoWait()..."); + } + + Message ret = null; + try { + enlist(); + ret = delegate.receiveNoWait(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": receiveNoWait returning " + ret); + } + return ret; + } + + public void close() throws JMSException { + // note: delist is done at session level! + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": close..."); + } + try { + delegate.close(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": close done."); + } + } + + public MessageListener getMessageListener() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getMessageListener()..."); + } + MessageListener ret = null; + try { + ret = delegate.getMessageListener(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getMessageListener() returning " + ret); + } + return ret; + } + + public String getMessageSelector() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getMessageSelector()..."); + } + String ret = null; + try { + ret = delegate.getMessageSelector(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getMessageSelector() returning " + ret); + } + return ret; + } + + public void setMessageListener(MessageListener listener) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setMessageListener ( " + listener + " )..."); + } + try { + delegate.setMessageListener(listener); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setMessageListener done."); + } + } + + public String toString() { + return "atomikosJmsMessageConsumerWrapper for " + delegate; + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsMessageProducerWrapper.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsMessageProducerWrapper.java new file mode 100644 index 000000000..c47b78f61 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsMessageProducerWrapper.java @@ -0,0 +1,282 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import javax.jms.CompletionListener; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; + +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +class AtomikosJmsMessageProducerWrapper extends ConsumerProducerSupport implements MessageProducer { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJmsMessageProducerWrapper.class); + + private MessageProducer delegate; + + AtomikosJmsMessageProducerWrapper(MessageProducer delegate, SessionHandleState state) { + super(state); + this.delegate = delegate; + } + + public void send(Message msg) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( message )..."); + } + enlist(); + delegate.send(msg); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + public void close() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": close..."); + } + delegate.close(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": close done."); + } + } + + public int getDeliveryMode() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getDeliveryMode()..."); + } + int ret = delegate.getDeliveryMode(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getDeliveryMode() returning " + ret); + } + return ret; + } + + public Destination getDestination() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getDestination()..."); + } + Destination ret = delegate.getDestination(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getDestination() returning " + ret); + } + return ret; + } + + public boolean getDisableMessageID() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getDisableMessageID()..."); + } + boolean ret = delegate.getDisableMessageID(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getDisableMessageID() returning " + ret); + } + return ret; + } + + public boolean getDisableMessageTimestamp() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getDisableMessageTimestamp()..."); + } + boolean ret = delegate.getDisableMessageTimestamp(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getDisableMessageTimestamp() returning " + ret); + } + return ret; + } + + public int getPriority() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getPriority()..."); + } + int ret = delegate.getPriority(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getPriority() returning " + ret); + } + return ret; + } + + public long getTimeToLive() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getTimeToLive()..."); + } + long ret = delegate.getTimeToLive(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getTimeToLive() returning " + ret); + } + return ret; + } + + public void send(Destination dest, Message msg) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( destination , message )..."); + } + enlist(); + delegate.send(dest, msg); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + public void send(Message msg, int deliveryMode, int priority, long timeToLive) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( message , deliveryMode , priority , timeToLive )..."); + } + enlist(); + delegate.send(msg, deliveryMode, priority, timeToLive); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + public void send(Destination dest, Message msg, int deliveryMode, int priority, long timeToLive) + throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( destination , message , deliveryMode , priority , timeToLive )..."); + } + enlist(); + delegate.send(dest, msg, deliveryMode, priority, timeToLive); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + public void setDeliveryMode(int mode) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setDeliveryMode ( " + mode + " )..."); + } + delegate.setDeliveryMode(mode); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setDeliveryMode done."); + } + } + + public void setDisableMessageID(boolean mode) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setDisableMessageID ( " + mode + " )..."); + } + delegate.setDisableMessageID(mode); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setDisableMessageID done."); + } + } + + public void setDisableMessageTimestamp(boolean mode) throws JMSException { + + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setDisableMessageTimestamp ( " + mode + " )..."); + } + delegate.setDisableMessageTimestamp(mode); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setDisableMessageTimestamp done."); + } + } + + public void setPriority(int pty) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setPriority ( " + pty + " )..."); + } + delegate.setPriority(pty); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setPriority done."); + } + } + + public void setTimeToLive(long ttl) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setTimeToLive ( " + ttl + " )..."); + } + delegate.setTimeToLive(ttl); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setTimeToLive done."); + } + } + + public String toString() { + return "atomikosJmsMessageProducerWrapper for " + delegate; + } + + @Override + public void setDeliveryDelay(long deliveryDelay) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": setDeliveryDelay ( " + deliveryDelay + " )..."); + } + delegate.setDeliveryDelay(deliveryDelay); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": setDeliveryDelay done."); + } + } + + @Override + public long getDeliveryDelay() throws JMSException { + long ret = 0; + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getDeliveryDelay()..."); + } + ret = delegate.getDeliveryDelay(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getDeliveryDelay() returning " + ret); + } + return ret; + } + + @Override + public void send(Message message, CompletionListener completionListener) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( message , completionListener )..." ); + } + enlist(); + delegate.send(message, completionListener); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + @Override + public void send(Message message, int deliveryMode, int priority, long timeToLive, + CompletionListener completionListener) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( message , deliveryMode , priority , timeToLive , completionListener)..."); + } + enlist(); + delegate.send(message, deliveryMode, priority, timeToLive, completionListener); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + @Override + public void send(Destination destination, Message message, CompletionListener completionListener) + throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( destination , message , completionListener "); + } + enlist(); + delegate.send(destination, message, completionListener); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + + @Override + public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, + CompletionListener completionListener) throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": send ( destination , message , deliveryMode , priority , timeToLive , completionListener )..."); + } + enlist(); + delegate.send(destination, message, deliveryMode, priority, timeToLive, completionListener); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": send done."); + } + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsNonXaSessionProxy.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsNonXaSessionProxy.java new file mode 100644 index 000000000..ac663e48c --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsNonXaSessionProxy.java @@ -0,0 +1,143 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import java.lang.reflect.Method; + +import javax.jms.JMSException; +import javax.jms.Session; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import com.atomikos.datasource.xa.session.SessionHandleStateChangeListener; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.Proxied; + +public class AtomikosJmsNonXaSessionProxy extends AbstractJmsSessionProxy { + + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJmsNonXaSessionProxy.class); + + private boolean errorsOccurred = false; + private final SessionHandleStateChangeListener owner; + private final SessionHandleStateChangeListener connectionProxy; + + public AtomikosJmsNonXaSessionProxy(Session delegate, SessionHandleStateChangeListener owner, + SessionHandleStateChangeListener connectionProxy) { + super(delegate); + this.owner = owner; + this.connectionProxy = connectionProxy; + } + + @Override + protected void throwInvocationAfterClose(String methodName) throws Exception { + String msg = "session was closed already - calling " + methodName + " is no longer allowed."; + LOGGER.logWarning(this + ": " + msg); + throw new javax.jms.IllegalStateException(msg); + } + + @Proxied + public void close() { + destroy(); + markClosed(); + } + + @Override + public synchronized Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // MUST be synchronized: see case 126795 + checkForTransactionContextAndLogWarningIfSo(); + return super.invoke(proxy, method, args); + } + + private void checkForTransactionContextAndLogWarningIfSo() { + TransactionManager tm = TransactionManagerImp.getTransactionManager(); + if (tm != null) { + Transaction tx = null; + try { + tx = tm.getTransaction(); + } catch (SystemException e) { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": failed to get transaction.", e); + } + // ignore + } + if (tx != null) { + String msg = this + + ": WARNING - detected JTA transaction context while using non-transactional session." + "\n" + + "Beware that any JMS operations you perform are NOT part of the JTA transaction." + "\n" + + "To enable JTA, make sure to do all of the following:" + "\n" + + "1. Make sure that the AtomikosConnectionFactoryBean is configured with localTransactionMode=false, and" + + "\n" + "2. Make sure to call create JMS sessions with the transacted flag set to true."; + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(msg); + } + } + } + } + + protected void destroy() { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": destroying session..."); + } + if (!closed) { + closed = true; + delegate.close(); + owner.onTerminated(); + connectionProxy.onTerminated(); + } + } catch (JMSException e) { + LOGGER.logWarning(this + ": could not close JMS session", e); + } + + } + + protected boolean isAvailable() { + return closed; + } + + protected boolean isErroneous() { + return errorsOccurred; + } + + protected boolean isInTransaction(CompositeTransaction ct) { + return false; + } + + public String toString() { + return "atomikosJmsNonXaSessionProxy (isAvailable = "+ closed + ") for vendor instance " + delegate; + } + + public static Session newInstance(Session wrapped, SessionHandleStateChangeListener owner, + AtomikosJmsConnectionProxy atomikosJmsConnectionProxy) { + AtomikosJmsNonXaSessionProxy proxy = new AtomikosJmsNonXaSessionProxy(wrapped, owner, + atomikosJmsConnectionProxy); + return proxy.createDynamicProxy(); + } + + @Override + public void recycle() { + LOGGER.logWarning(this + ": unexpected call of recycle() - this is probably a bug?"); + } + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + errorsOccurred = true; + throw e; + } + + @Override + protected Class getRequiredInterfaceType() { + return Session.class; + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsTopicSubscriberWrapper.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsTopicSubscriberWrapper.java new file mode 100644 index 000000000..ff5080c43 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsTopicSubscriberWrapper.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import javax.jms.JMSException; +import javax.jms.Topic; +import javax.jms.TopicSubscriber; + +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +class AtomikosJmsTopicSubscriberWrapper extends AtomikosJmsMessageConsumerWrapper implements TopicSubscriber { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJmsTopicSubscriberWrapper.class); + + public AtomikosJmsTopicSubscriberWrapper(TopicSubscriber delegate, SessionHandleState state) { + super(delegate, state); + + } + + private TopicSubscriber getDelegateTopicSubscriber() { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getDelegateTopicSubscriber()..."); + } + TopicSubscriber ret = (TopicSubscriber) getDelegate(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getDelegateTopicSubscriber() returning " + ret); + } + return ret; + } + + public boolean getNoLocal() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getNoLocal()..."); + } + boolean ret = false; + try { + ret = getDelegateTopicSubscriber().getNoLocal(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getNoLocal() returning " + ret); + } + return ret; + } + + public Topic getTopic() throws JMSException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": getTopic()..."); + } + Topic ret = null; + try { + ret = getDelegateTopicSubscriber().getTopic(); + } catch (Exception e) { + handleException(e); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": getTopic() returning " + ret); + } + return ret; + } + + public String toString() { + return "atomikosJmsTopicSubscriberWrapper for " + getDelegate(); + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsXaSessionProxy.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsXaSessionProxy.java new file mode 100644 index 000000000..81c0583f2 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosJmsXaSessionProxy.java @@ -0,0 +1,282 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import java.lang.reflect.Method; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicSubscriber; +import javax.jms.TransactionInProgressException; +import javax.jms.XASession; + +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.datasource.xa.session.SessionHandleStateChangeListener; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.Proxied; + +public class AtomikosJmsXaSessionProxy extends AbstractJmsSessionProxy implements SessionHandleStateChangeListener { + + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosJmsXaSessionProxy.class); + private final SessionHandleState state; + + public AtomikosJmsXaSessionProxy(XASession delegate, XATransactionalResource jmsTransactionalResource, + SessionHandleStateChangeListener pooledConnection, SessionHandleStateChangeListener connectionProxy) { + super(delegate); + this.state = new SessionHandleState(jmsTransactionalResource, delegate.getXAResource()); + state.registerSessionHandleStateChangeListener(pooledConnection); + state.registerSessionHandleStateChangeListener(connectionProxy); + state.registerSessionHandleStateChangeListener(this); + // for JMS, session borrowed corresponds to creation of the session + state.notifySessionBorrowed(); + + } + + @Override + protected void throwInvocationAfterClose(String methodName) throws Exception { + String msg = "Session was closed already - calling " + methodName + " is no longer allowed."; + LOGGER.logWarning(this + ": " + msg); + throw new javax.jms.IllegalStateException(msg); + } + + @Override + public synchronized Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // MUST be synchronized: see case 126795 + return super.invoke(proxy, method, args); + } + + @Proxied + public void commit() throws JMSException { + String msg = "Calling commit/rollback is not allowed on a managed session!"; + // When using the Spring PlatformTransactionManager, there is always a + // call to commit on the Session in a synchronization's afterCompletion. + // The PlatformTransactionManager uses that mechanism for non-JTA TX + // commit which happens because + // DefaultMessageListenerContainer.sessionTransacted + // must be set to true. Spring catches TransactionInProgressException in + // case of JTA TX management. + // This is fine except that we used to log this message at warning level + // when this happens which is annoying as it repeats once per TX -> + // lowered it to info. + // + // See: + // org.springframework.jms.connection.ConnectionFactoryUtils$JmsResourceSynchronization.afterCommit() + // and org.springframework.jms.connection.JmsResourceHolder.commitAll() + // (as of Spring 2.0.8) + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": " + msg); + } + + throw new javax.jms.TransactionInProgressException(msg); + + } + + @Proxied + public void rollback() throws JMSException { + String msg = "Calling commit/rollback is not allowed on a managed session!"; + // When using the Spring PlatformTransactionManager, there is always a + // call to commit on the Session in a synchronization's afterCompletion. + // The PlatformTransactionManager uses that mechanism for non-JTA TX + // commit which happens because + // DefaultMessageListenerContainer.sessionTransacted + // must be set to true. Spring catches TransactionInProgressException in + // case of JTA TX management. + // This is fine except that we used to log this message at warning level + // when this happens which is annoying as it repeats once per TX -> + // lowered it to info. + // + // See: + // org.springframework.jms.connection.ConnectionFactoryUtils$JmsResourceSynchronization.afterCommit() + // and org.springframework.jms.connection.JmsResourceHolder.commitAll() + // (as of Spring 2.0.8) + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": " + msg); + } + + throw new javax.jms.TransactionInProgressException(msg); + + } + + @Proxied + public void close() throws JMSException { + state.notifySessionClosed(); + if (state.isTerminated()) { + // only destroy if there is no pending 2PC - otherwise this is done + // in the registered synchronization + destroy(true); + } else { + // close this handle but keep vendor session open for 2PC + // see case 71079 + destroy(false); + } + markClosed(); + } + + protected void destroy(boolean closeXaSession) { + if (closeXaSession) { + // see case 71079: don't close vendor session if transaction is not done yet + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": closing underlying vendor session"); + } + try { + delegate.close(); + } catch (JMSException e) { + LOGGER.logWarning(this + ": could not close underlying vendor session", e); + } + } + closed = true; + } + + @Proxied + public MessageProducer createProducer(Destination destination) throws JMSException { + MessageProducer vendorProducer = delegate.createProducer(destination); + return new AtomikosJmsMessageProducerWrapper(vendorProducer, state); + } + + @Proxied + public MessageConsumer createConsumer(Destination destination) throws JMSException { + MessageConsumer vendorConsumer = delegate.createConsumer(destination); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public MessageConsumer createConsumer(Destination destination, String messageSelector) throws JMSException { + MessageConsumer vendorConsumer = delegate.createConsumer(destination, messageSelector); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean NoLocal) + throws JMSException { + MessageConsumer vendorConsumer = delegate.createConsumer(destination, messageSelector, NoLocal); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public TopicSubscriber createDurableSubscriber(Topic topic, String name) throws JMSException { + TopicSubscriber vendorSubscriber = delegate.createDurableSubscriber(topic, name); + return new AtomikosJmsTopicSubscriberWrapper(vendorSubscriber, state); + } + + @Proxied + public TopicSubscriber createDurableSubscriber(Topic topic, String name, String messageSelector, boolean noLocal) + throws JMSException { + TopicSubscriber vendorSubscriber = delegate.createDurableSubscriber(topic, name, messageSelector, noLocal); + return new AtomikosJmsTopicSubscriberWrapper(vendorSubscriber, state); + } + + @Proxied + public MessageConsumer createDurableConsumer(Topic topic, String name) + throws JMSException { + MessageConsumer vendorConsumer = delegate.createDurableConsumer(topic, name); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + + } + + @Proxied + public MessageConsumer createDurableConsumer(Topic topic, String name, String messageSelector, boolean noLocal) + throws JMSException + { + MessageConsumer vendorConsumer = delegate.createDurableConsumer(topic, name, messageSelector, noLocal); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public MessageConsumer createSharedConsumer(Topic topic, String sharedSubscriptionName) + throws JMSException { + MessageConsumer vendorConsumer = delegate.createSharedConsumer(topic, sharedSubscriptionName); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public MessageConsumer createSharedConsumer(Topic topic, String sharedSubscriptionName, java.lang.String messageSelector) + throws JMSException { + MessageConsumer vendorConsumer = delegate.createSharedConsumer(topic, sharedSubscriptionName, messageSelector); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public MessageConsumer createSharedDurableConsumer(Topic topic, String name) + throws JMSException { + MessageConsumer vendorConsumer = delegate.createSharedDurableConsumer(topic, name); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Proxied + public MessageConsumer createSharedDurableConsumer(Topic topic, String name, String messageSelector) + throws JMSException { + MessageConsumer vendorConsumer = delegate.createSharedDurableConsumer(topic, name, messageSelector); + return new AtomikosJmsMessageConsumerWrapper(vendorConsumer, state); + } + + @Override + protected boolean isAvailable() { + return state.isTerminated(); + } + + @Override + protected boolean isErroneous() { + return state.isErroneous(); + } + + @Override + protected boolean isInTransaction(CompositeTransaction ct) { + return state.isActiveInTransaction(ct); + } + + @Override + protected boolean isInactiveTransaction(CompositeTransaction ct) { + return state.isInactiveInTransaction(ct); + } + + public void onTerminated() { + destroy(true); + } + + public static Session newInstance(XASession wrapped, XATransactionalResource jmsTransactionalResource, + SessionHandleStateChangeListener pooledConnection, SessionHandleStateChangeListener connectionProxy) { + AtomikosJmsXaSessionProxy proxy = new AtomikosJmsXaSessionProxy(wrapped, jmsTransactionalResource, + pooledConnection, connectionProxy); + return proxy.createDynamicProxy(); + } + + @Override + public void recycle() { + synchronized (this) { + this.closed = false; + state.notifySessionBorrowed(); + } + } + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + if (!(e instanceof TransactionInProgressException)) { + state.notifySessionErrorOccurred(); + } + throw e; + } + + @Override + public String toString() { + return "atomikosJmsXaSessionProxy (state = " + state + ") for vendor instance " + delegate; + } + + @Override + protected Class getRequiredInterfaceType() { + return Session.class; + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosPooledJmsConnection.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosPooledJmsConnection.java new file mode 100644 index 000000000..ca27a272e --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosPooledJmsConnection.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import java.lang.reflect.Proxy; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.XAConnection; + +import com.atomikos.datasource.pool.AbstractXPooledConnection; +import com.atomikos.datasource.pool.ConnectionPoolProperties; +import com.atomikos.datasource.pool.CreateConnectionException; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.datasource.xa.session.SessionHandleStateChangeListener; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.jms.SessionCreationMode; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public class AtomikosPooledJmsConnection extends AbstractXPooledConnection + implements SessionHandleStateChangeListener { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosPooledJmsConnection.class); + + private XAConnection xaConnection; + private XATransactionalResource jmsTransactionalResource; + private Connection currentProxy; + private ConnectionPoolProperties props; + private boolean erroneous; + + private int sessionCreationMode; + + public AtomikosPooledJmsConnection(int sessionCreationMode, XAConnection xac, + XATransactionalResource jmsTransactionalResource, ConnectionPoolProperties props) { + super(props); + this.jmsTransactionalResource = jmsTransactionalResource; + this.xaConnection = xac; + this.props = props; + this.erroneous = false; + this.sessionCreationMode = sessionCreationMode; + } + + protected Connection doCreateConnectionProxy() throws CreateConnectionException { + currentProxy = AtomikosJmsConnectionProxy.newInstance(sessionCreationMode, xaConnection, + jmsTransactionalResource, this, props); + return currentProxy; + } + + protected void testUnderlyingConnection() throws CreateConnectionException { + if (isErroneous()) { + throw new CreateConnectionException(this + ": connection is erroneous"); + } + if (maxLifetimeExceeded()) { + throw new CreateConnectionException(this + ": connection too old - will be replaced"); + } + } + + public void doDestroy() { + if (xaConnection != null) { + try { + xaConnection.close(); + } catch (JMSException ex) { + // ignore but log + LOGGER.logWarning(this + ": error closing XAConnection: ", ex); + } + } + xaConnection = null; + } + + public synchronized boolean isAvailable() { + boolean ret = true; + if (currentProxy != null) { + AtomikosJmsConnectionProxy proxy = (AtomikosJmsConnectionProxy) Proxy.getInvocationHandler(currentProxy); + ret = proxy.isAvailable(); + } + return ret; + } + + public synchronized boolean isErroneous() { + boolean ret = erroneous; + if (currentProxy != null) { + AtomikosJmsConnectionProxy proxy = (AtomikosJmsConnectionProxy) Proxy.getInvocationHandler(currentProxy); + ret = ret || proxy.isErroneous(); + } + return ret; + } + + public synchronized boolean isInTransaction(CompositeTransaction ct) { + boolean ret = false; + if (currentProxy != null) { + AtomikosJmsConnectionProxy proxy = (AtomikosJmsConnectionProxy) Proxy.getInvocationHandler(currentProxy); + ret = proxy.isInTransaction(ct); + } + return ret; + } + + public void onTerminated() { + boolean fireTerminatedEvent = false; + AtomikosJmsConnectionProxy proxy = null; + synchronized ( this ) { + //a session has terminated -> check reusability of all remaining + fireTerminatedEvent = isAvailable(); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": a session has terminated, is connection now available ? " + fireTerminatedEvent ); + if ( currentProxy != null ) { + proxy = (AtomikosJmsConnectionProxy) Proxy.getInvocationHandler(currentProxy); + if ( proxy.isErroneous() ) erroneous = true; + } + } + + if ( fireTerminatedEvent ) { + if (proxy != null) proxy.closeAllPendingSessions(); + //callbacks done outside synch to avoid deadlock in case 27614 + fireOnXPooledConnectionTerminated(); + } + + + } + + public boolean canBeRecycledForCallingThread() { + boolean ret = false; + if (currentProxy != null) { + CompositeTransactionManager tm = Configuration.getCompositeTransactionManager(); + + CompositeTransaction current = tm.getCompositeTransaction(); + if (current != null && TransactionManagerImp.isJtaTransaction(current)) { + AtomikosJmsConnectionProxy proxy = (AtomikosJmsConnectionProxy) Proxy.getInvocationHandler(currentProxy); + // recycle only if inactive in this tx - i.e., if proxy was closed! + ret = proxy.isInactiveInTransaction(current); + } + } + + return ret; + } + + public String toString() { + return "atomikosPooledJmsConnection for resource " + jmsTransactionalResource.getName(); + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosTransactionRequiredJMSException.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosTransactionRequiredJMSException.java new file mode 100644 index 000000000..a68ea7072 --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/AtomikosTransactionRequiredJMSException.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public class AtomikosTransactionRequiredJMSException extends AtomikosJMSException { + private static final long serialVersionUID = 1L; + + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosTransactionRequiredJMSException.class); + + public static void throwAtomikosTransactionRequiredJMSException(String reason) + throws AtomikosTransactionRequiredJMSException { + LOGGER.logWarning(reason); + throw new AtomikosTransactionRequiredJMSException(reason); + } + + AtomikosTransactionRequiredJMSException(String reason) { + super(reason); + } + +} diff --git a/public/transactions-jms/src/main/java/com/atomikos/jms/internal/ConsumerProducerSupport.java b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/ConsumerProducerSupport.java new file mode 100644 index 000000000..25696f60f --- /dev/null +++ b/public/transactions-jms/src/main/java/com/atomikos/jms/internal/ConsumerProducerSupport.java @@ -0,0 +1,136 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.internal; + +import javax.jms.JMSException; + +import com.atomikos.datasource.xa.session.InvalidSessionHandleStateException; +import com.atomikos.datasource.xa.session.SessionHandleState; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * Support for common logic in producer and consumer. + * + */ + +abstract class ConsumerProducerSupport { + private static final Logger LOGGER = LoggerFactory.createLogger(ConsumerProducerSupport.class); + + private SessionHandleState state; + + protected ConsumerProducerSupport(SessionHandleState state) { + this.state = state; + } + + protected void handleException(Exception e) throws AtomikosJMSException { + state.notifySessionErrorOccurred(); + AtomikosJMSException.throwAtomikosJMSException("Error in proxy", e); + } + + private CompositeTransactionManager getCompositeTransactionManager() { + CompositeTransactionManager ret = null; + ret = Configuration.getCompositeTransactionManager(); + return ret; + } + + protected void enlist() throws JMSException { + CompositeTransaction ct = null; + CompositeTransactionManager ctm = getCompositeTransactionManager(); + boolean enlist = false; + + if (ctm != null) { + ct = ctm.getCompositeTransaction(); + if (ct != null && TransactionManagerImp.isJtaTransaction(ct)) { + enlist = true; + } + } + + if (enlist) { + registerSynchronization(ct); + try { + state.notifyBeforeUse(ct); + } catch (InvalidSessionHandleStateException ex) { + String msg = "error during enlist: " + ex.getMessage(); + LOGGER.logWarning(this + ": " + msg); + AtomikosJMSException.throwAtomikosJMSException(msg, ex); + } + } else { + String msg = "The JMS session you are using requires a JTA transaction context for the calling thread and none was found." + "\n" + + "Please correct your code to do one of the following: " + "\n" + + "1. start a JTA transaction if you want your JMS operations to be subject to JTA commit/rollback, or" + "\n" + + "2. create a non-transacted session and do session acknowledgment yourself, or" + "\n" + + "3. set localTransactionMode to true so connection-level commit/rollback are enabled."; + LOGGER.logWarning(this + ": " + msg); + AtomikosTransactionRequiredJMSException.throwAtomikosTransactionRequiredJMSException(msg); + } + + } + + private void registerSynchronization(CompositeTransaction ct) throws AtomikosJMSException { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": detected transaction " + ct); + } + ct.registerSynchronization(new JmsRequeueSynchronization(ct)); + } + + private class JmsRequeueSynchronization implements Synchronization { + + private CompositeTransaction compositeTransaction; + private boolean afterCompletionDone; + + public JmsRequeueSynchronization(CompositeTransaction compositeTransaction) { + this.compositeTransaction = compositeTransaction; + this.afterCompletionDone = false; + } + + public void afterCompletion(TxState txState) { + if (afterCompletionDone) + return; + + if (txState.isHeuristic() || txState == TxState.TERMINATED) { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("JmsRequeueSynchronization: detected termination of transaction " + compositeTransaction); + } + state.notifyTransactionTerminated(compositeTransaction); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("JmsRequeueSynchronization: is in terminated state ? " + state.isTerminated()); + } + afterCompletionDone = true; + } + + } + + public void beforeCompletion() { + + } + + // override equals: synchronizations for the same tx are equal + // to avoid receiving double notifications on termination! + public boolean equals(Object other) { + boolean ret = false; + if (other instanceof JmsRequeueSynchronization) { + JmsRequeueSynchronization o = (JmsRequeueSynchronization) other; + ret = this.compositeTransaction.isSameTransaction(o.compositeTransaction); + } + return ret; + } + + public int hashCode() { + return compositeTransaction.hashCode(); + } + } + +} diff --git a/public/transactions-jms/src/test/java/com/atomikos/jms/AtomikosConnectionFactoryBeanTestJUnit.java b/public/transactions-jms/src/test/java/com/atomikos/jms/AtomikosConnectionFactoryBeanTestJUnit.java new file mode 100644 index 000000000..1fff1c792 --- /dev/null +++ b/public/transactions-jms/src/test/java/com/atomikos/jms/AtomikosConnectionFactoryBeanTestJUnit.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms; + +import java.util.Properties; + +import javax.naming.Reference; + +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.util.IntraVmObjectFactory; + +import junit.framework.TestCase; + +public class AtomikosConnectionFactoryBeanTestJUnit extends TestCase +{ + + private AtomikosConnectionFactoryBean bean; + + protected void setUp() throws Exception + { + super.setUp(); + bean = new AtomikosConnectionFactoryBean(); + } + + public void testMinPoolSize() + { + assertEquals ( 1 , bean.getMinPoolSize() ); + bean.setMinPoolSize ( 2 ); + assertEquals ( 2 , bean.getMinPoolSize() ); + } + + public void testMaxPoolSize() + { + assertEquals ( 1 , bean.getMaxPoolSize() ); + bean.setMaxPoolSize ( 3 ); + assertEquals ( 3 , bean.getMaxPoolSize() ); + } + + public void testPoolSize() + { + assertEquals ( 1 , bean.getMaxPoolSize() ); + assertEquals ( 1 , bean.getMinPoolSize() ); + bean.setPoolSize ( 4 ); + assertEquals ( 4 , bean.getMaxPoolSize() ); + assertEquals ( 4 , bean.getMinPoolSize() ); + + } + + public void testXaConnectionFactoryClassName() + { + assertEquals ( null , bean.getXaConnectionFactoryClassName() ); + String name = "blabla"; + bean.setXaConnectionFactoryClassName ( name ); + assertEquals ( name , bean.getXaConnectionFactoryClassName() ); + } + + public void testXaProperties() + { + assertTrue ( bean.getXaProperties().isEmpty() ); + Properties p = new Properties(); + String pname = "property"; + String pvalue = "value"; + p.setProperty ( pname , pvalue ); + bean.setXaProperties ( p ); + assertEquals ( p , bean.getXaProperties() ); + assertEquals ( pvalue , bean.getXaProperties().getProperty(pname)); + assertEquals ( p.size() , bean.getXaProperties().size() ); + } + + public void testBorrowConnectionTimeout() + { + assertEquals ( 30 , bean.getBorrowConnectionTimeout() ); + int timeout = 45; + bean.setBorrowConnectionTimeout ( timeout ); + assertEquals ( 45 , bean.getBorrowConnectionTimeout() ); + } + + public void testMaintenanceInterval() + { + assertEquals ( 60 , bean.getMaintenanceInterval() ); + int interval = 90; + bean.setMaintenanceInterval ( interval ); + assertEquals ( interval , bean.getMaintenanceInterval() ); + } + + public void testMaxIdleTime() + { + assertEquals ( 60 , bean.getMaxIdleTime() ); + int time = 99; + bean.setMaxIdleTime ( time ); + assertEquals ( time , bean.getMaxIdleTime() ); + } + + public void testReferenceable() throws Exception + { + bean.setUniqueResourceName ( "testReferenceable" ); + Reference ref = bean.getReference(); + assertNotNull ( ref ); + IntraVmObjectFactory f = new IntraVmObjectFactory(); + Object res = null; + res = f.getObjectInstance ( ref , null , null , null ); + assertNotNull ( res ); + assertSame ( bean , res ); + } + + public void testUniqueResourceName() + { + assertNull ( bean.getUniqueResourceName() ); + String name = "testname"; + bean.setUniqueResourceName ( name ); + assertEquals ( name , bean.getUniqueResourceName() ); + } + + public void testLocalTransactionMode() + { + assertFalse ( bean.getLocalTransactionMode() ); + bean.setLocalTransactionMode ( true ); + assertTrue ( bean.getLocalTransactionMode() ); + bean.setLocalTransactionMode ( false ); + assertFalse ( bean.getLocalTransactionMode() ); + } + + public void testIgnoreSessionTransactedFlag() { + assertTrue(bean.getIgnoreSessionTransactedFlag()); + bean.setIgnoreSessionTransactedFlag(false); + assertFalse(bean.getIgnoreSessionTransactedFlag()); + bean.setIgnoreSessionTransactedFlag(true); + assertTrue(bean.getIgnoreSessionTransactedFlag()); + + } + + public void testImplementsOrderedLifecycleComponent() { + assertTrue(bean instanceof OrderedLifecycleComponent); + } + + public void testSessionCreationModeDefaultsToJMS_2_0() { + assertEquals(SessionCreationMode.JMS_2_0, bean.getSessionCreationMode()); + } + + public void testSetSessionCreationMode() { + bean.setSessionCreationMode(SessionCreationMode.PRE_6_0); + assertEquals(SessionCreationMode.PRE_6_0, bean.getSessionCreationMode()); + } + + public void testIgnoreSessionFlagFalseMeansSessionCreationModePre3_9() { + bean.setIgnoreSessionTransactedFlag(false); + assertEquals(SessionCreationMode.PRE_3_9, bean.getSessionCreationMode()); + } + + public void testSetConnectionCreationModeAsInt() { + bean.setSessionCreationMode(0); + } +} diff --git a/public/transactions-jms/src/test/java/com/atomikos/jms/AtomikosJMSExceptionTestJUnit.java b/public/transactions-jms/src/test/java/com/atomikos/jms/AtomikosJMSExceptionTestJUnit.java new file mode 100644 index 000000000..24cad14ac --- /dev/null +++ b/public/transactions-jms/src/test/java/com/atomikos/jms/AtomikosJMSExceptionTestJUnit.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms; + +import javax.jms.JMSException; + +import com.atomikos.jms.internal.AtomikosJMSException; + +import junit.framework.TestCase; + +public class AtomikosJMSExceptionTestJUnit extends TestCase { + + + public void testLinkedException() { + JMSException linked = new JMSException ( "test" ); + AtomikosJMSException e = new AtomikosJMSException ( getName() ); + e.setLinkedException ( linked ); + assertSame ( linked , e.getLinkedException() ); + assertNull ( e.getCause() ); + assertEquals ( getName() , e.getMessage() ); + } + + public void testCause() { + Exception cause = new Exception ( getName() ); + AtomikosJMSException e = new AtomikosJMSException ( getName() , cause ); + assertSame ( cause , e.getCause() ); + assertSame ( cause , e.getLinkedException() ); + assertEquals ( getName() , e.getMessage() ); + } + +} diff --git a/public/transactions-jms/src/test/java/com/atomikos/jms/TestQueue.java b/public/transactions-jms/src/test/java/com/atomikos/jms/TestQueue.java new file mode 100644 index 000000000..0dbbb5675 --- /dev/null +++ b/public/transactions-jms/src/test/java/com/atomikos/jms/TestQueue.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms; + +import javax.jms.JMSException; +import javax.jms.Queue; + +/** + * + * + * + * + * + * + */ +public class TestQueue implements Queue +{ + + private String name = "TESTQUEUE"; + + public String getQueueName() throws JMSException + { + return name; + } + + public String toString() + { + return name; + } + +} diff --git a/public/transactions-jms/src/test/java/com/atomikos/jms/extra/ConcurrentJmsSenderTemplateTestJUnit.java b/public/transactions-jms/src/test/java/com/atomikos/jms/extra/ConcurrentJmsSenderTemplateTestJUnit.java new file mode 100644 index 000000000..3dc09176c --- /dev/null +++ b/public/transactions-jms/src/test/java/com/atomikos/jms/extra/ConcurrentJmsSenderTemplateTestJUnit.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.DeliveryMode; +import javax.jms.Destination; + +import com.atomikos.jms.AtomikosConnectionFactoryBean; +import com.atomikos.jms.TestQueue; + +import junit.framework.TestCase; + +public class ConcurrentJmsSenderTemplateTestJUnit extends TestCase { + + private ConcurrentJmsSenderTemplate template; + + protected void setUp() throws Exception { + super.setUp(); + template = new ConcurrentJmsSenderTemplate(); + } + + public void testUser() + { + assertNull ( template.getUser() ); + String user = "user"; + template.setUser ( user ); + assertEquals ( user , template.getUser() ); + } + + public void testDestination() + { + assertNull ( template.getDestination() ); + Destination destination = new TestQueue(); + template.setDestination(destination); + assertEquals ( destination, template.getDestination() ); + } + + public void testDestinationName() + { + assertNull ( template.getDestinationName() ); + String name = "name"; + template.setDestinationName ( name ); + assertEquals ( name , template.getDestinationName() ); + } + + public void testReplyToDestinationName() + { + assertNull ( template.getReplyToDestinationName() ); + String name = "name"; + template.setReplyToDestinationName ( name ); + assertEquals ( name , template.getReplyToDestinationName() ); + } + + public void testReplyToDestination() + { + assertNull ( template.getReplyToDestination() ); + Destination destination = new TestQueue(); + template.setReplyToDestination(destination); + assertEquals ( destination, template.getReplyToDestination() ); + } + + public void testDeliveryMode() + { + assertEquals ( DeliveryMode.PERSISTENT , template.getDeliveryMode() ); + template.setDeliveryMode ( DeliveryMode.NON_PERSISTENT ); + assertEquals ( DeliveryMode.NON_PERSISTENT , template.getDeliveryMode() ); + } + + public void testPriority() + { + assertEquals ( 4 , template.getPriority() ); + template.setPriority(5); + assertEquals ( 5 , template.getPriority() ); + } + + public void testTimeToLive() + { + assertEquals ( 0 , template.getTimeToLive() ); + template.setTimeToLive(3); + assertEquals ( 3 , template.getTimeToLive() ); + } + + public void testInitWithoutDestinationThrowsMeaningfulException() throws Exception + { + template.setAtomikosConnectionFactoryBean(new AtomikosConnectionFactoryBean() ); + try { + template.init(); + } catch ( IllegalStateException ok ) { + assertEquals ( "Property 'destination' or 'destinationName' must be set first!" , ok.getMessage() ); + } + } + + public void testInitWithoutConnectionFactoryThrowsMeaningfulException() throws Exception + { + template.setDestination ( new TestQueue() ); + try { + template.init(); + } catch ( IllegalStateException ok ) { + assertEquals ( "Property 'atomikosConnectionFactoryBean' must be set first!" , ok.getMessage() ); + } + } + +} diff --git a/public/transactions-jms/src/test/java/com/atomikos/jms/extra/SingleThreadedJmsSenderTemplateTestJUnit.java b/public/transactions-jms/src/test/java/com/atomikos/jms/extra/SingleThreadedJmsSenderTemplateTestJUnit.java new file mode 100644 index 000000000..50ae4061f --- /dev/null +++ b/public/transactions-jms/src/test/java/com/atomikos/jms/extra/SingleThreadedJmsSenderTemplateTestJUnit.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jms.extra; + +import javax.jms.DeliveryMode; +import javax.jms.Destination; + +import com.atomikos.jms.AtomikosConnectionFactoryBean; +import com.atomikos.jms.TestQueue; + +import junit.framework.TestCase; + +public class SingleThreadedJmsSenderTemplateTestJUnit extends TestCase { + + private SingleThreadedJmsSenderTemplate template; + + protected void setUp() throws Exception { + super.setUp(); + template = new SingleThreadedJmsSenderTemplate(); + } + + public void testUser() + { + assertNull ( template.getUser() ); + String user = "user"; + template.setUser ( user ); + assertEquals ( user , template.getUser() ); + } + + public void testDestination() + { + assertNull ( template.getDestination() ); + Destination destination = new TestQueue(); + template.setDestination(destination); + assertEquals ( destination, template.getDestination() ); + } + + public void testReplyToDestination() + { + assertNull ( template.getReplyToDestination() ); + Destination destination = new TestQueue(); + template.setReplyToDestination(destination); + assertEquals ( destination, template.getReplyToDestination() ); + } + + public void testDeliveryMode() + { + assertEquals ( DeliveryMode.PERSISTENT , template.getDeliveryMode() ); + template.setDeliveryMode ( DeliveryMode.NON_PERSISTENT ); + assertEquals ( DeliveryMode.NON_PERSISTENT , template.getDeliveryMode() ); + } + + public void testPriority() + { + assertEquals ( 4 , template.getPriority() ); + template.setPriority(5); + assertEquals ( 5 , template.getPriority() ); + } + + public void testTimeToLive() + { + assertEquals ( 0 , template.getTimeToLive() ); + template.setTimeToLive(3); + assertEquals ( 3 , template.getTimeToLive() ); + } + + public void testInitWithoutDestinationThrowsMeaningfulException() throws Exception + { + template.setAtomikosConnectionFactoryBean(new AtomikosConnectionFactoryBean() ); + try { + template.init(); + } catch ( IllegalStateException ok ) { + assertEquals ( "Property 'destination' or 'destinationName' must be set first!" , ok.getMessage() ); + } + } + + public void testInitWithoutConnectionFactoryThrowsMeaningfulException() throws Exception + { + template.setDestination ( new TestQueue() ); + try { + template.init(); + } catch ( IllegalStateException ok ) { + assertEquals ( "Property 'atomikosConnectionFactoryBean' must be set first!" , ok.getMessage() ); + } + } + +} diff --git a/public/transactions-jndi-provider/pom.xml b/public/transactions-jndi-provider/pom.xml new file mode 100644 index 000000000..fa4ee652a --- /dev/null +++ b/public/transactions-jndi-provider/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-jndi-provider + + false + + + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + provided + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + test + + + jakarta.jms + jakarta.jms-api + 2.0.3 + provided + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + test + + + + diff --git a/public/transactions-jndi-provider/src/main/java/com/atomikos/jndi/AtomikosContext.java b/public/transactions-jndi-provider/src/main/java/com/atomikos/jndi/AtomikosContext.java new file mode 100644 index 000000000..b5f8a786d --- /dev/null +++ b/public/transactions-jndi-provider/src/main/java/com/atomikos/jndi/AtomikosContext.java @@ -0,0 +1,206 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jndi; + +import java.util.Hashtable; + +import javax.naming.Binding; +import javax.naming.CompositeName; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NameNotFoundException; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.OperationNotSupportedException; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.IntraVmObjectRegistry; + +public class AtomikosContext implements Context { + + private static final Logger log = LoggerFactory.createLogger(AtomikosContext.class); + private static final String USER_TRANSACTION_NAME = "java:comp/UserTransaction"; + + private NameParser nameParser = new CompositeNameParser(); + private UserTransactionManager userTransactionManager; + + @Override + public void close() throws NamingException { + + } + + @Override + public Object lookup(Name name) throws NamingException { + return lookup(name.toString()); + } + + @Override + public Object lookup(String s) throws NamingException { + Object ret; + if (log.isTraceEnabled()) { + log.logTrace("looking up '" + s + "'"); + } + + if (USER_TRANSACTION_NAME.equals(s)) { + if (userTransactionManager == null) { + userTransactionManager = new UserTransactionManager(); + } + ret = userTransactionManager; + } else { + ret = IntraVmObjectRegistry.getResource(s); + } + if (ret == null) { + throw new NameNotFoundException("unable to find a bound object with name '" + s + "'"); + } + return ret; + } + + @Override + public void bind(Name name, Object o) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void bind(String s, Object o) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void rebind(Name name, Object o) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void rebind(String s, Object o) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void unbind(Name name) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void unbind(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void rename(Name name, Name name1) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void rename(String s, String s1) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public NamingEnumeration list(Name name) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public NamingEnumeration list(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public NamingEnumeration listBindings(Name name) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public NamingEnumeration listBindings(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void destroySubcontext(Name name) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public void destroySubcontext(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Context createSubcontext(Name name) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Context createSubcontext(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Object lookupLink(Name name) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Object lookupLink(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public NameParser getNameParser(Name name) throws NamingException { + return nameParser; + } + + @Override + public NameParser getNameParser(String s) throws NamingException { + return nameParser; + } + + @Override + public Name composeName(Name name, Name name1) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public String composeName(String s, String s1) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Object addToEnvironment(String s, Object o) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Object removeFromEnvironment(String s) throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public Hashtable getEnvironment() throws NamingException { + throw new OperationNotSupportedException(); + } + + @Override + public String getNameInNamespace() throws NamingException { + throw new OperationNotSupportedException(); + } + + private final static class CompositeNameParser implements NameParser { + + @Override + public Name parse(final String name) throws NamingException { + return new CompositeName(name); + } + } + +} diff --git a/public/transactions-jndi-provider/src/main/java/com/atomikos/jndi/AtomikosContextFactory.java b/public/transactions-jndi-provider/src/main/java/com/atomikos/jndi/AtomikosContextFactory.java new file mode 100644 index 000000000..141745b5f --- /dev/null +++ b/public/transactions-jndi-provider/src/main/java/com/atomikos/jndi/AtomikosContextFactory.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jndi; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; + +public class AtomikosContextFactory implements InitialContextFactory { + + @Override + public Context getInitialContext(Hashtable arg0) + throws NamingException { + return new AtomikosContext(); + } + +} diff --git a/public/transactions-jndi-provider/src/test/java/com/atomikos/jndi/JndiTestJUnit.java b/public/transactions-jndi-provider/src/test/java/com/atomikos/jndi/JndiTestJUnit.java new file mode 100644 index 000000000..8829f3c14 --- /dev/null +++ b/public/transactions-jndi-provider/src/test/java/com/atomikos/jndi/JndiTestJUnit.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.jndi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Hashtable; + +import javax.naming.CompositeName; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.sql.XADataSource; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jdbc.AtomikosDataSourceBean; + +public class JndiTestJUnit { + + private Context ctx ; + @Before + public void before() throws NamingException { + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, AtomikosContextFactory.class.getName()); + ctx = new InitialContext(env); + } + + @Test + public void testNameParser() throws Exception { + AtomikosContext atomikosCtd = new AtomikosContext(); + Name name = atomikosCtd.getNameParser("").parse("java:comp/UserTransaction"); + assertEquals("java:comp/UserTransaction", name.toString()); + assertSame(UserTransactionManager.class, atomikosCtd.lookup(name).getClass()); + + name = atomikosCtd.getNameParser(new CompositeName()).parse("java:comp/UserTransaction"); + assertEquals("java:comp/UserTransaction", name.toString()); + assertSame(UserTransactionManager.class, atomikosCtd.lookup(name).getClass()); + } + + @Test + public void testDefaultUserTransactionAndResources() throws Exception { + AtomikosDataSourceBean adsb = new AtomikosDataSourceBean(); + adsb.setMaxPoolSize(1); + adsb.setXaDataSource(Mockito.mock(XADataSource.class)); + adsb.setUniqueResourceName("jdbc/pds"); + adsb.init(); + assertTrue(adsb == ctx.lookup("jdbc/pds")); + } + + @After + public void after() throws NamingException { + ctx.close(); + } + + @Test + public void userTransactionIsBound() throws NamingException { + Object lookup = ctx.lookup("java:comp/UserTransaction"); + System.err.println(lookup); + Object lookup2 = ctx.lookup("java:comp/UserTransaction"); + System.err.println(lookup2); + assertTrue( lookup instanceof UserTransactionManager); + } + + + +} \ No newline at end of file diff --git a/public/transactions-jta/pom.xml b/public/transactions-jta/pom.xml new file mode 100644 index 000000000..85f154abe --- /dev/null +++ b/public/transactions-jta/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-jta + Transactions JTA + + false + + + + com.atomikos + transactions-api + 6.0.1-SNAPSHOT + + + com.atomikos + transactions + 6.0.1-SNAPSHOT + runtime + + + com.atomikos + atomikos-util + 6.0.1-SNAPSHOT + + + jakarta.jms + jakarta.jms-api + 2.0.3 + provided + + + jakarta.transaction + jakarta.transaction-api + 1.3.3 + provided + + + diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/AbstractXPooledConnection.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/AbstractXPooledConnection.java new file mode 100644 index 000000000..9c98b6f4c --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/AbstractXPooledConnection.java @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + + /** + * + * + * Abstract superclass with generic support for XPooledConnection. + * + * + */ + +public abstract class AbstractXPooledConnection implements XPooledConnection { + private static final Logger LOGGER = LoggerFactory.createLogger(AbstractXPooledConnection.class); + + private long lastTimeAcquired = System.currentTimeMillis(); + private long lastTimeReleased = System.currentTimeMillis(); + private List> poolEventListeners = new ArrayList>(); + private ConnectionType currentProxy = null; + private ConnectionPoolProperties props; + private long creationTime = System.currentTimeMillis(); + private final AtomicBoolean isConcurrentlyBeingAcquired = new AtomicBoolean(false); + private final long maxLifetimeInMillis; + + protected AbstractXPooledConnection ( ConnectionPoolProperties props ) + { + this.props = props; + this.maxLifetimeInMillis = props.getMaxLifetime()*1000; + + } + + public long getLastTimeAcquired() { + return lastTimeAcquired; + } + + public long getLastTimeReleased() { + return lastTimeReleased; + } + + public synchronized ConnectionType createConnectionProxy() throws CreateConnectionException + { + updateLastTimeAcquired(); + testUnderlyingConnection(); + currentProxy = doCreateConnectionProxy(); + isConcurrentlyBeingAcquired.set(false); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": returning proxy " + currentProxy ); + return currentProxy; + } + + + + public void registerXPooledConnectionEventListener(XPooledConnectionEventListener listener) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": registering listener " + listener ); + poolEventListeners.add(listener); + } + + public void unregisterXPooledConnectionEventListener(XPooledConnectionEventListener listener) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": unregistering listener " + listener ); + poolEventListeners.remove(listener); + } + + protected void fireOnXPooledConnectionTerminated() { + for (int i=0; i listener = poolEventListeners.get(i); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": notifying listener: " + listener ); + listener.onXPooledConnectionTerminated(this); + } + updateLastTimeReleased(); + } + + protected String getTestQuery() + { + return props.getTestQuery(); + } + + protected void updateLastTimeReleased() { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": updating last time released" ); + lastTimeReleased = System.currentTimeMillis(); + } + + private void updateLastTimeAcquired() { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": updating last time acquired" ); + lastTimeAcquired = System.currentTimeMillis(); + + } + + protected ConnectionType getCurrentConnectionProxy() { + return currentProxy; + } + + public boolean canBeRecycledForCallingThread () + { + //default is false + return false; + } + + protected int getDefaultIsolationLevel() + { + return props.getDefaultIsolationLevel(); + } + + protected int getBorrowConnectionTimeout() { + return props.getBorrowConnectionTimeout(); + } + + public long getCreationTime() { + return creationTime; + } + + public boolean markAsBeingAcquiredIfAvailable() { + synchronized (isConcurrentlyBeingAcquired) { + if (isConcurrentlyBeingAcquired.get()) { + return false; + } + isConcurrentlyBeingAcquired.set(isAvailable()); + return isConcurrentlyBeingAcquired.get(); + } + } + + protected abstract ConnectionType doCreateConnectionProxy() throws CreateConnectionException; + + protected abstract void testUnderlyingConnection() throws CreateConnectionException; + + protected boolean maxLifetimeExceeded() { + boolean ret = false; + if (maxLifetimeInMillis>0) { + ret = getCreationTime() + maxLifetimeInMillis < System.currentTimeMillis(); + } + return ret; + } + + public synchronized void destroy() { + if (!isAvailable()) { + return; // cf case 172524 + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": destroying..." ); + doDestroy(); + } + + protected abstract void doDestroy(); +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionFactory.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionFactory.java new file mode 100644 index 000000000..d29f14cfb --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionFactory.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + + +public interface ConnectionFactory +{ + + /** + * Opens a new physical connection to the underlying resource and wraps it in a + * pooling-capable {@link XPooledConnection}. + * + * @return the {@link XPooledConnection} wrapping the physical connection. + * + * @throws CreateConnectionException If no connection could be created. + * + */ + XPooledConnection createPooledConnection() throws CreateConnectionException; + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPool.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPool.java new file mode 100644 index 000000000..2847e4a1b --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPool.java @@ -0,0 +1,360 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.thread.InterruptedExceptionHelper; +import com.atomikos.thread.TaskManager; +import com.atomikos.timing.AlarmTimer; +import com.atomikos.timing.AlarmTimerListener; +import com.atomikos.timing.PooledAlarmTimer; + + +public abstract class ConnectionPool implements XPooledConnectionEventListener +{ + private static Logger LOGGER = LoggerFactory.createLogger(ConnectionPool.class); + + private final static int DEFAULT_MAINTENANCE_INTERVAL = 60; + + protected List> connections = new ArrayList>(); + private ConnectionFactory connectionFactory; + private ConnectionPoolProperties properties; + private boolean destroyed; + private PooledAlarmTimer maintenanceTimer; + private String name; + private ExecutorService dynamicallyGrowPoolExecutor = Executors.newFixedThreadPool(1); + + + public ConnectionPool ( ConnectionFactory connectionFactory , ConnectionPoolProperties properties ) throws ConnectionPoolException + { + this.connectionFactory = connectionFactory; + this.properties = properties; + this.destroyed = false; + this.name = properties.getUniqueResourceName(); + init(); + } + + private void assertNotDestroyed() throws ConnectionPoolException + { + if (destroyed) throw new ConnectionPoolException ( "Pool was already destroyed - you can no longer use it" ); + } + + private void init() throws ConnectionPoolException + { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": initializing..." ); + addConnectionsIfMinPoolSizeNotReached(); + launchMaintenanceTimer(); + } + + private void launchMaintenanceTimer() { + int maintenanceInterval = properties.getMaintenanceInterval(); + if ( maintenanceInterval <= 0 ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": using default maintenance interval..." ); + maintenanceInterval = DEFAULT_MAINTENANCE_INTERVAL; + } + maintenanceTimer = new PooledAlarmTimer ( maintenanceInterval * 1000 ); + maintenanceTimer.addAlarmTimerListener(new AlarmTimerListener() { + public void alarm(AlarmTimer timer) { + removeConnectionsThatExceededMaxLifetime(); + addConnectionsIfMinPoolSizeNotReached(); + removeIdleConnectionsIfMinPoolSizeExceeded(); + } + }); + TaskManager.SINGLETON.executeTask ( maintenanceTimer ); + } + + private synchronized void addConnectionsIfMinPoolSizeNotReached() { + int connectionsToAdd = properties.getMinPoolSize() - totalSize(); + for ( int i = 0 ; i < connectionsToAdd ; i++ ) { + try { + XPooledConnection xpc = createPooledConnection(); + connections.add ( xpc ); + xpc.registerXPooledConnectionEventListener ( this ); + } catch ( Exception dbDown ) { + //see case 26380 + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": could not establish initial connection" , dbDown ); + } + } + } + + private XPooledConnection createPooledConnection() + throws CreateConnectionException { + XPooledConnection xpc = connectionFactory.createPooledConnection(); + return xpc; + } + + protected abstract ConnectionType recycleConnectionIfPossible() throws Exception; + + /** + * Borrows a connection from the pool. + * @return The connection + * @throws CreateConnectionException If the pool attempted to grow but failed. + * @throws PoolExhaustedException If the pool could not grow because it is exhausted. + * @throws ConnectionPoolException Other errors. + */ + public ConnectionType borrowConnection() throws CreateConnectionException, PoolExhaustedException, + ConnectionPoolException { + assertNotDestroyed(); + ConnectionType ret = null; + ret = findExistingOpenConnectionForCallingThread(); + if (ret == null) { + ret = findOrWaitForAnAvailableConnection(); + } + return ret; + } + + private ConnectionType findOrWaitForAnAvailableConnection() throws ConnectionPoolException { + ConnectionType ret = null; + long remainingTime = properties.getBorrowConnectionTimeout() * 1000L; + do { + long before = System.currentTimeMillis(); + ret = retrieveFirstAvailableConnectionAndGrowPoolIfNecessary(remainingTime); + remainingTime -= calculateDelta(before); + if ( ret == null ) { + remainingTime = waitForAtLeastOneAvailableConnection(remainingTime); + assertNotDestroyed(); + } + } while ( ret == null ); + return ret; + } + + private long calculateDelta(long before) { + long now = System.currentTimeMillis(); + return (now - before); + } + + private ConnectionType retrieveFirstAvailableConnectionAndGrowPoolIfNecessary(long remainingTime) throws CreateConnectionException { + + ConnectionType ret = retrieveFirstAvailableConnection(); + if ( ret == null && canGrow() ) { + growPool(remainingTime); + ret = retrieveFirstAvailableConnection(); + } + return ret; + } + + private ConnectionType findExistingOpenConnectionForCallingThread() { + ConnectionType recycledConnection = null ; + try { + recycledConnection = recycleConnectionIfPossible(); + } catch (Exception e) { + //ignore but log + LOGGER.logDebug ( this + ": error while trying to recycle" , e ); + } + return recycledConnection; + } + + protected void logCurrentPoolSize() { + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace( this + ": current size: " + availableSize() + "/" + totalSize()); + } + } + + private boolean canGrow() { + return totalSize() < properties.getMaxPoolSize(); + } + + protected abstract ConnectionType retrieveFirstAvailableConnection(); + + private synchronized void growPool(long remainingTime) throws CreateConnectionException { + if (canGrow()) { // cf case 181871 + Future> futureXPC = + dynamicallyGrowPoolExecutor.submit(() -> createPooledConnection()); //cf case 192016: in separate thread to allow control of timeout + XPooledConnection ret = null; + try { + ret = futureXPC.get(remainingTime, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + InterruptedExceptionHelper.handleInterruptedException(e); + } catch (TimeoutException e) { + String msg = this + ": timed out waiting for new pooled connection..."; + LOGGER.logDebug( msg , e); + } catch (Exception e) { + String msg = this + ": failed to grow pool due to unexpected exception..."; + LOGGER.logWarning( msg , e); + } + if (ret != null) { + connections.add(ret); + ret.registerXPooledConnectionEventListener(this); + } else { + // try to liberate executor's thread for reuse + futureXPC.cancel(true); + // WORST CASE: the dedicated thread could be blocked forever IF AND ONLY IF: + // 1. the network IO blocks forever for a new (!) connection, AND + // 2. the driver does not support interrupts (so cancel did not work) + // => in that case the pool remains at current size (>=minPoolSize) + // NB: in that case, minPoolSize is done by the maintenance thread, + // not the (blocked) worker thread + } + } + logCurrentPoolSize(); + } + + private synchronized void removeIdleConnectionsIfMinPoolSizeExceeded() { + if (connections == null || properties.getMaxIdleTime() <= 0 ) + return; + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": trying to shrink pool" ); + List> connectionsToRemove = new ArrayList>(); + int maxConnectionsToRemove = totalSize() - properties.getMinPoolSize(); + if ( maxConnectionsToRemove > 0 ) { + for ( int i=0 ; i < connections.size() ; i++ ) { + XPooledConnection xpc = connections.get(i); + long lastRelease = xpc.getLastTimeReleased(); + long maxIdle = properties.getMaxIdleTime(); + long now = System.currentTimeMillis(); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": connection idle for " + (now - lastRelease) + "ms"); + if ( xpc.isAvailable() && ( (now - lastRelease) >= (maxIdle * 1000L) ) && ( connectionsToRemove.size() < maxConnectionsToRemove ) ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": connection idle for more than " + maxIdle + "s, closing it: " + xpc); + destroyPooledConnection(xpc); + connectionsToRemove.add(xpc); + } + } + } + connections.removeAll(connectionsToRemove); + logCurrentPoolSize(); + } + + protected void destroyPooledConnection(XPooledConnection xpc) { + xpc.destroy(); + } + + private synchronized void removeConnectionsThatExceededMaxLifetime() + { + long maxLifetime = properties.getMaxLifetime() * 1000L; + if ( connections == null || maxLifetime <= 0 ) return; + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": closing connections that exceeded maxLifetime" ); + + Iterator> it = connections.iterator(); + long now = System.currentTimeMillis(); + while ( it.hasNext() ) { + XPooledConnection xpc = it.next(); + long creationTime = xpc.getCreationTime(); + if ( xpc.isAvailable() ) { + if ((now - creationTime) >= (maxLifetime)) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": connection in use for more than maxLifetime, destroying it: " + xpc ); + destroyPooledConnection(xpc); + it.remove(); + } + } + } + logCurrentPoolSize(); + } + + public synchronized void destroy() + { + + if ( ! destroyed ) { + LOGGER.logInfo ( this + ": destroying pool..." ); + for ( int i=0 ; i < connections.size() ; i++ ) { + XPooledConnection xpc = connections.get(i); + if ( !xpc.isAvailable() ) { + LOGGER.logWarning ( this + ": connection is still in use on pool destroy: " + xpc + + " - please check your shutdown sequence to avoid heuristic termination " + + "of ongoing transactions!" ); + } + destroyPooledConnection(xpc); + } + connections = null; + destroyed = true; + maintenanceTimer.stopTimer(); + dynamicallyGrowPoolExecutor.shutdownNow(); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": pool destroyed." ); + } + } + + public synchronized void refresh() { + List> connectionsToRemove = new ArrayList>(); + for (XPooledConnection conn : connections) { + if (conn.isAvailable()) { + connectionsToRemove.add(conn); + destroyPooledConnection(conn); + } + } + connections.removeAll(connectionsToRemove); + addConnectionsIfMinPoolSizeNotReached(); + } + + /** + * Wait until the connection pool contains an available connection or a timeout happens. + * Returns immediately if the pool already contains a connection in state available. + * @throws CreateConnectionException if a timeout happened while waiting for a connection + */ + private synchronized long waitForAtLeastOneAvailableConnection(long waitTime) throws PoolExhaustedException + { + while (availableSize() == 0) { + if ( waitTime <= 0 ) throw new PoolExhaustedException ( "ConnectionPool: pool is empty - increase either maxPoolSize or borrowConnectionTimeout" ); + long before = System.currentTimeMillis(); + try { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": about to wait for connection during " + waitTime + "ms..."); + this.wait (waitTime); + + } catch (InterruptedException ex) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( ex ); + // ignore + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": interrupted during wait" , ex ); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": done waiting." ); + waitTime -= calculateDelta(before); + } + return waitTime; + } + + /** + * The amount of pooled connections in state available. + * @return the amount of pooled connections in state available. + */ + public synchronized int availableSize() + { + int ret = 0; + + if ( !destroyed ) { + int count = 0; + for ( int i=0 ; i < connections.size() ; i++ ) { + XPooledConnection xpc = connections.get(i); + if (xpc.isAvailable()) count++; + } + ret = count; + } + return ret; + } + + /** + * The total amount of pooled connections in any state. + * @return the total amount of pooled connections in any state + */ + public synchronized int totalSize() + { + if ( destroyed ) return 0; + + return connections.size(); + } + + public synchronized void onXPooledConnectionTerminated(XPooledConnection connection) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace( this + ": connection " + connection + " became available, notifying potentially waiting threads"); + this.notify(); + + } + + public String toString() { + return "atomikos connection pool '" + name + "'"; + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolException.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolException.java new file mode 100644 index 000000000..3dbcdf32e --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolException.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + + /** + * Common superclass for all exceptions thrown by the + * pooling mechanism. + * + */ + +public class ConnectionPoolException extends Exception +{ + + private static final long serialVersionUID = 1L; + + ConnectionPoolException ( String reason , Exception cause ) + { + super ( reason , cause ); + } + ConnectionPoolException ( String reason ) + { + super ( reason ); + } + public ConnectionPoolException() { + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolProperties.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolProperties.java new file mode 100644 index 000000000..717d5d0df --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolProperties.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + + /** + * This interface describes connection + * pool properties. + * + * + */ + +public interface ConnectionPoolProperties +{ + int DEFAULT_ISOLATION_LEVEL_UNSET = -1; + int DEFAULT_POOL_SIZE = 1; + int DEFAULT_BORROW_CONNECTION_TIMEOUT = 30; + int DEFAULT_MAX_IDLE_TIME = 60; + int DEFAULT_MAINTENANCE_INTERVAL = 60; + int DEFAULT_MAX_LIFETIME = 0; + /** + * Gets the unique resource name. + * + * @return + */ + public String getUniqueResourceName(); + + + /** + * Gets the maximum pool size. + * @return + */ + int getMaxPoolSize(); + + /** + * Gets the minimum pool size. + * @return + */ + int getMinPoolSize(); + + /** + * Gets the borrow connection timeout. + * @return + */ + int getBorrowConnectionTimeout(); + + /** + * Gets the max time in seconds a connection can stay idle before being closed. + * @return + */ + int getMaxIdleTime(); + + /** + * Gets the max time in seconds that a connection may be kept alive. + * @return + */ + int getMaxLifetime(); + + /** + * Gets the maintenance interval of the pool's maintenance thread. + * @return + */ + int getMaintenanceInterval(); + + + /** + * Gets the query used to test connections. + * @return + */ + String getTestQuery(); + + /** + * Tests whether local transactions are to be allowed or not. + * More precisely: can connections be used without a JTA transaction context? + * @return + */ + boolean getLocalTransactionMode(); + + /** + * Tests whether jta transactions are ignored or not. + * If true, then commit or rollback will happen outside JTA. + * @return + */ + public default boolean getIgnoreJtaTransactions() { + return false; + } + /** + * Gets the default isolation level preference. + * + * @return The level, or -1 if not set. + */ + int getDefaultIsolationLevel(); + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolWithConcurrentValidation.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolWithConcurrentValidation.java new file mode 100644 index 000000000..1888629e7 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolWithConcurrentValidation.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + +import java.util.Iterator; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + +public class ConnectionPoolWithConcurrentValidation extends ConnectionPool +{ + private static final Logger LOGGER = LoggerFactory.createLogger(ConnectionPoolWithConcurrentValidation.class); + + public ConnectionPoolWithConcurrentValidation ( ConnectionFactory connectionFactory , ConnectionPoolProperties properties ) throws ConnectionPoolException + { + super(connectionFactory, properties); + } + + @Override + protected ConnectionType recycleConnectionIfPossible() throws Exception { + ConnectionType ret = null; + XPooledConnection xpc = findFirstRecyclablePooledConnectionForCallingThread(); + if (xpc != null) { + ret = concurrentlyTryToRecycle(xpc); + } + return ret; + } + + @Override + protected ConnectionType retrieveFirstAvailableConnection() { + ConnectionType ret = null; + XPooledConnection xpc = claimFirstAvailablePooledConnection(); + if (xpc != null) { + ret = concurrentlyTryToUse(xpc); + } + return ret; + } + + private ConnectionType concurrentlyTryToRecycle(XPooledConnection xpc) throws Exception { + ConnectionType ret = null; + synchronized(xpc) { // just to be sure, although concurrent threads should not happen + if (xpc.canBeRecycledForCallingThread()) { + ret = xpc.createConnectionProxy(); + } + } + return ret; + } + + private ConnectionType concurrentlyTryToUse(XPooledConnection xpc) { + ConnectionType ret = null; + try { + ret = xpc.createConnectionProxy(); + // here, connection is no longer available for other threads + } catch ( CreateConnectionException ex ) { + String msg = this + ": error creating proxy of connection " + xpc; + LOGGER.logDebug( msg , ex); + removePooledConnection(xpc); + } finally { + logCurrentPoolSize(); + } + return ret; + } + + private synchronized XPooledConnection claimFirstAvailablePooledConnection() { + XPooledConnection ret = null; + Iterator> it = connections.iterator(); + while ( it.hasNext() && ret == null ) { + XPooledConnection xpc = it.next(); + if (xpc.markAsBeingAcquiredIfAvailable()) { + ret = xpc; + } + } + return ret; + } + + private synchronized XPooledConnection findFirstRecyclablePooledConnectionForCallingThread() { + XPooledConnection ret = null; + Iterator> it = connections.iterator(); + while ( it.hasNext() && ret == null ) { + XPooledConnection xpc = it.next(); + if (xpc.canBeRecycledForCallingThread()) { + ret = xpc; + } + } + return ret; + } + + private synchronized void removePooledConnection(XPooledConnection xpc) { + connections.remove(xpc); + destroyPooledConnection(xpc); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolWithSynchronizedValidation.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolWithSynchronizedValidation.java new file mode 100644 index 000000000..185b8ed76 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/ConnectionPoolWithSynchronizedValidation.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + +import java.util.Iterator; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + +/** + * Copyright (C) 2000-2017 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +public class ConnectionPoolWithSynchronizedValidation extends ConnectionPool { + + private static final Logger LOGGER = LoggerFactory.createLogger(ConnectionPoolWithSynchronizedValidation.class); + + public ConnectionPoolWithSynchronizedValidation( + ConnectionFactory connectionFactory, + ConnectionPoolProperties properties) throws ConnectionPoolException { + super(connectionFactory, properties); + } + + @Override + public synchronized ConnectionType borrowConnection() throws CreateConnectionException , PoolExhaustedException, ConnectionPoolException + { + return super.borrowConnection(); + } + + protected ConnectionType recycleConnectionIfPossible() throws Exception + { + ConnectionType ret = null; + for (int i = 0; i < totalSize(); i++) { + XPooledConnection xpc = connections.get(i); + + if (xpc.canBeRecycledForCallingThread()) { + ret = xpc.createConnectionProxy(); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace( this + ": recycling connection from pool..." ); + return ret; + } + } + return ret; + } + + protected ConnectionType retrieveFirstAvailableConnection() { + ConnectionType ret = null; + Iterator> it = connections.iterator(); + while ( it.hasNext() && ret == null ) { + XPooledConnection xpc = it.next(); + if (xpc.isAvailable()) { + try { + ret = xpc.createConnectionProxy(); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace( this + ": got connection from pool"); + } catch ( CreateConnectionException ex ) { + String msg = this + ": error creating proxy of connection " + xpc; + LOGGER.logDebug( msg , ex); + it.remove(); + destroyPooledConnection(xpc); + } finally { + logCurrentPoolSize(); + } + } + } + return ret; + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/CreateConnectionException.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/CreateConnectionException.java new file mode 100644 index 000000000..4851ef3b2 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/CreateConnectionException.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + + /** + * An exception to signal errors during the creation of connections. + * + * + */ + +public class CreateConnectionException extends ConnectionPoolException +{ + + private static final long serialVersionUID = 1858243647893576738L; + public CreateConnectionException ( String reason , Exception cause ) + { + super ( reason , cause ); + } + public CreateConnectionException ( String reason ) + { + super ( reason ); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/PoolExhaustedException.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/PoolExhaustedException.java new file mode 100644 index 000000000..3ec41ca92 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/PoolExhaustedException.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + + /** + * Exception signaling pool exhaustion. + * + */ +public class PoolExhaustedException extends ConnectionPoolException { + + + private static final long serialVersionUID = 7266245068986719051L; + + PoolExhaustedException ( String reason ) { + super ( reason ); + } + + public PoolExhaustedException() { + super(); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/XPooledConnection.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/XPooledConnection.java new file mode 100644 index 000000000..18c25e899 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/XPooledConnection.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + +/** + * A pooling-capable object wrapping a physical connection to an underlying resource. + */ +public interface XPooledConnection +{ + + /** + * Is the connection available to be taken out of the pool ? + * @return + */ + boolean isAvailable(); + + + /** + * Can the connection be recycled (if not available) for the calling thread? + * @return + */ + public boolean canBeRecycledForCallingThread(); + + /** + * Destroy the pooled connection by closing the underlying physical connection. + */ + void destroy(); + + /** + * Get the last time the connection was acquired. + * @return + */ + long getLastTimeAcquired(); + + /** + * Get the last time the connection was released, i.e. the last time when + * it has become available. + * @return + */ + long getLastTimeReleased(); + + /** + * Create a disposable connection object that acts a controller for + * the pooled connection. What exactly a connection object is depends + * on the implementation. Calling this method will also clear any + * flags set by markAsBeingAcquiredIfAvailable. + * + * @return + * @throws CreateConnectionException + */ + ConnectionType createConnectionProxy() throws CreateConnectionException; + + /** + * Is the pooled connection broken ? + * @return + */ + boolean isErroneous(); + + /** + * Get the moment when the connection was created. + * @return + */ + long getCreationTime(); + + void registerXPooledConnectionEventListener(XPooledConnectionEventListener listener); + + void unregisterXPooledConnectionEventListener(XPooledConnectionEventListener listener); + + /** + * Attempt to claim this connection for use. + * + * @return True if the connection is available, and could be claimed for the caller of this method. + * In that case, it is up to the caller to call createConnectionProxy at a later time (which will in turn trigger + * a round-trip to the resource if there is any testQuery set) or the pool will be leaking. + */ + boolean markAsBeingAcquiredIfAvailable(); + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/XPooledConnectionEventListener.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/XPooledConnectionEventListener.java new file mode 100644 index 000000000..23969b13d --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/pool/XPooledConnectionEventListener.java @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.pool; + + +public interface XPooledConnectionEventListener +{ + + /** + * fired when a connection changed its state to terminated + * @param connection + */ + void onXPooledConnectionTerminated(XPooledConnection connection); + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/AbstractXidFactory.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/AbstractXidFactory.java new file mode 100644 index 000000000..96db4f10c --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/AbstractXidFactory.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * + * + * + * An abstract superclass for all XidFactory implementations. This class + * provides the functionality to create really unique XIDs. + * + * + * + * + */ +abstract class AbstractXidFactory implements XidFactory +{ + + private static final int MAX_LENGTH_OF_COUNTER = String.valueOf(Long.MAX_VALUE).length(); + + static AtomicLong counter = new AtomicLong(0); + + public AbstractXidFactory () + { + super (); + + } + + /** + * @see com.atomikos.datasource.xa.XidFactory + */ + + public XID createXid ( String tid , String branchIdentifier, String uniqueResourceName ) + { + + if ( branchIdentifier.getBytes().length + MAX_LENGTH_OF_COUNTER > XID.MAXBQUALSIZE ) { + // see case 73086 + throw new IllegalArgumentException ( "Value too long: " + branchIdentifier ); + } + + // first increment counter to make sure it is + // different from the last call that was done + // by the SAME tid (works because calls within + // one TID are serial) + return new XID (tid, branchIdentifier + counter.incrementAndGet(), uniqueResourceName); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/DefaultXidFactory.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/DefaultXidFactory.java new file mode 100644 index 000000000..abd318b1d --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/DefaultXidFactory.java @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + + + +/** + * A default Xid factory. + */ + +class DefaultXidFactory extends AbstractXidFactory implements + java.io.Serializable +{ + + + private static final long serialVersionUID = -8667085263366123575L; + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/RecoveryScan.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/RecoveryScan.java new file mode 100644 index 000000000..3a6be734b --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/RecoveryScan.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +public class RecoveryScan { + + public static interface XidSelector { + boolean selects(XID xid); + } + + public static List recoverXids(XAResource xaResource, XidSelector selector) throws XAException { + List ret = new ArrayList(); + + boolean done = false; + int flags = XAResource.TMSTARTRSCAN; + Xid[] xidsFromLastScan = null; + List allRecoveredXidsSoFar = new ArrayList(); + do { + xidsFromLastScan = xaResource.recover(flags); + flags = XAResource.TMNOFLAGS; + done = (xidsFromLastScan == null || xidsFromLastScan.length == 0); + if (!done) { + + // TEMPTATIVELY SET done TO TRUE + // TO TOLERATE ORACLE 8.1.7 INFINITE + // LOOP (ALWAYS RETURNS SAME RECOVER + // SET). IF A NEW SET OF XIDS IS RETURNED + // THEN done WILL BE RESET TO FALSE + + done = true; + for ( int i = 0; i < xidsFromLastScan.length; i++ ) { + XID xid = new XID (xidsFromLastScan[i]); + // our own XID implements equals and hashCode properly + if (!allRecoveredXidsSoFar.contains(xid)) { + // a new xid is returned -> we can not be in a recovery loop -> go on + allRecoveredXidsSoFar.add(xid); + done = false; + if (selector.selects(xid)) { + ret.add(xid); + } + } + } + } + } while (!done); + + return ret; + } +} + diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/ResourceTransactionSuspender.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/ResourceTransactionSuspender.java new file mode 100644 index 000000000..5d896538b --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/ResourceTransactionSuspender.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +class ResourceTransactionSuspender implements SubTxAwareParticipant { + + private static Logger LOGGER = LoggerFactory.createLogger(ResourceTransactionSuspender.class); + + private XAResourceTransaction branch; + + ResourceTransactionSuspender(XAResourceTransaction branch) { + this.branch = branch; + } + + @Override + public void committed(CompositeTransaction transaction) { + try { + branch.suspend(); + } catch (Exception e) { + LOGGER.logDebug("Unexpected exception while trying to suspend the branch: ", e); + //ignore: just a courtesy + } + } + + @Override + public void rolledback(CompositeTransaction transaction) { + try { + branch.suspend(); + } catch (Exception e) { + LOGGER.logDebug("Unexpected exception while trying to suspend the branch: ", e); + //ignore: just a courtesy + } + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/SiblingMapper.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/SiblingMapper.java new file mode 100644 index 000000000..9e50f0602 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/SiblingMapper.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.atomikos.datasource.ResourceException; +import com.atomikos.datasource.ResourceTransaction; +import com.atomikos.icatch.CompositeTransaction; + +/** + * A SiblingMapper encapsulates the policies for creating or reusing XA branches. + * + * Assumption: there is one instance per root transaction, per resource. + */ + +class SiblingMapper +{ + + private Map> siblingsOfSameRoot; + private XATransactionalResource res; + + private String root ; + + SiblingMapper ( XATransactionalResource res , String root ) + { + this.siblingsOfSameRoot = new HashMap>(); + this.res = res; + this.root = root; + } + + private XAResourceTransaction findSiblingBranchToJoin(CompositeTransaction ct) { + XAResourceTransaction ret = null; + if (ct.isSerial()) { + Iterator > allSiblingLists = this.siblingsOfSameRoot.values().iterator(); + while (ret == null && allSiblingLists.hasNext()) { + List siblings = allSiblingLists.next(); + ret = findJoinableBranchInList(siblings); + } + } + return ret; + } + + private XAResourceTransaction findJoinableBranchInList(List siblings) { + XAResourceTransaction ret = null; + Iterator it = siblings.iterator(); + while (ret == null && it.hasNext()) { + XAResourceTransaction candidate = it.next(); + if (candidate.supportsTmJoin()) ret = candidate; + } + return ret; + } + + protected synchronized ResourceTransaction findOrCreateBranchForTransaction(CompositeTransaction ct) + throws ResourceException, IllegalStateException + { + XAResourceTransaction ret = findOrCreateBranchWithResourceException(ct); + ct.addParticipant(ret); + return ret; + } + + private XAResourceTransaction findOrCreateBranchWithResourceException(CompositeTransaction ct) { + XAResourceTransaction ret = null; + try { + ret = findOrCreateBranch(ct); + } catch (Exception e) { + throw new ResourceException ( "Failed to get branch", e ); + } + return ret; + } + + private XAResourceTransaction findOrCreateBranch(CompositeTransaction ct) { + XAResourceTransaction ret; + ret = findPreviousBranchToJoin(ct); + if (ret == null) { + ret = findSiblingBranchToJoin(ct); + if (ret == null) { + ret = createNewBranch(ct); + } + } + ct.addSubTxAwareParticipant(new ResourceTransactionSuspender(ret)); //cf case 175941 + return ret; + } + + private XAResourceTransaction createNewBranch(CompositeTransaction ct) { + XAResourceTransaction ret; + ret = new XAResourceTransaction(this.res, ct, this.root); + rememberBranch(ct, ret); + return ret; + } + + private XAResourceTransaction findPreviousBranchToJoin(CompositeTransaction ct) { + List candidates = findSiblingsForTransaction(ct); + return findJoinableBranchInList(candidates); + } + + private List findSiblingsForTransaction(CompositeTransaction ct) { + List ret = this.siblingsOfSameRoot.get(ct); + if (ret == null) { + ret = new ArrayList(); + } + return Collections.unmodifiableList(ret); + } + + private void rememberBranch(CompositeTransaction ct, XAResourceTransaction branch) { + List list = this.siblingsOfSameRoot.get(ct); + if (list == null) { + list = new ArrayList(); + this.siblingsOfSameRoot.put(ct,list); + } + list.add(branch); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XAExceptionHelper.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XAExceptionHelper.java new file mode 100644 index 000000000..019eb1725 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XAExceptionHelper.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import javax.transaction.xa.XAException; + +public class XAExceptionHelper { + + public static String convertErrorCodeToVerboseMessage(int errorCode) { + String msg = "unkown"; + switch (errorCode) { + case XAException.XAER_RMFAIL: + msg = "the XA resource has become unavailable"; + break; + case XAException.XA_RBROLLBACK: + msg = "the XA resource has rolled back for an unspecified reason"; + break; + case XAException.XA_RBCOMMFAIL: + msg = "the XA resource rolled back due to a communication failure"; + break; + case XAException.XA_RBDEADLOCK: + msg = "the XA resource has rolled back because of a deadlock"; + break; + case XAException.XA_RBINTEGRITY: + msg = "the XA resource has rolled back due to a constraint violation"; + break; + case XAException.XA_RBOTHER: + msg = "the XA resource has rolled back for an unknown reason"; + break; + case XAException.XA_RBPROTO: + msg = "the XA resource has rolled back because it did not expect this command in the current context"; + break; + case XAException.XA_RBTIMEOUT: + msg = "the XA resource has rolled back because the transaction took too long"; + break; + case XAException.XA_RBTRANSIENT: + msg = "the XA resource has rolled back for a temporary reason - the transaction can be retried later"; + break; + case XAException.XA_NOMIGRATE: + msg = "XA resume attempted in a different place from where suspend happened"; + break; + case XAException.XA_HEURHAZ: + msg = "the XA resource may have heuristically completed the transaction"; + break; + case XAException.XA_HEURCOM: + msg = "the XA resource has heuristically committed"; + break; + case XAException.XA_HEURRB: + msg = "the XA resource has heuristically rolled back"; + break; + case XAException.XA_HEURMIX: + msg = "the XA resource has heuristically committed some parts and rolled back other parts"; + break; + case XAException.XA_RETRY: + msg = "the XA command had no effect and may be retried"; + break; + case XAException.XA_RDONLY: + msg = "the XA resource had no updates to perform for this transaction"; + break; + case XAException.XAER_RMERR: + msg = "the XA resource detected an internal error"; + break; + case XAException.XAER_NOTA: + msg = "the supplied XID is invalid for this XA resource"; + break; + case XAException.XAER_INVAL: + msg = "invalid arguments were given for the XA operation"; + break; + case XAException.XAER_PROTO: + msg = "the XA resource did not expect this command in the current context"; + break; + case XAException.XAER_DUPID: + msg = "the supplied XID already exists in this XA resource"; + break; + case XAException.XAER_OUTSIDE: + msg = "the XA resource is currently involved in a local (non-XA) transaction"; + break; + default: + msg = "unknown"; + } + return msg; + } + + public static String formatLogMessage(String msg, XAException e, String impact) { + StringBuilder ret = new StringBuilder(); + ret.append(msg). + append(": "). + append(convertErrorCodeToVerboseMessage(e.errorCode)). + append(" - "). + append(impact); + return ret.toString(); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XAResourceTransaction.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XAResourceTransaction.java new file mode 100644 index 000000000..f852184bb --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XAResourceTransaction.java @@ -0,0 +1,701 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.util.Map; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import com.atomikos.datasource.ResourceException; +import com.atomikos.datasource.ResourceTransaction; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; +import com.atomikos.util.Assert; + +/** + * + * + * An implementation of ResourceTransaction for XA transactions. + */ + +public class XAResourceTransaction implements ResourceTransaction, Participant { + private static final Logger LOGGER = LoggerFactory.createLogger(XAResourceTransaction.class); + + static final long serialVersionUID = -8227293322090019196L; + + private static String interpretErrorCode(String resourceName, + String opCode, Xid xid, int errorCode) { + + String msg = XAExceptionHelper.convertErrorCodeToVerboseMessage(errorCode); + return "XA resource '" + resourceName + "': " + opCode + " for XID '" + + xid + "' raised " + errorCode + ": " + msg; + } + + + private String tid, root; + private boolean isXaSuspended; + private TxState state; + private String resourcename; + private transient XID xid; + + private transient String toString; + + private void setXid(XID xid) { + this.xid = xid; + this.toString = "XAResourceTransaction: " + xid; + } + + private transient final XATransactionalResource resource; + private transient XAResource xaresource; + private transient boolean knownInResource; + private transient int timeout; + + + XAResourceTransaction(XATransactionalResource resource, + CompositeTransaction transaction, String root) { + Assert.notNull("resource cannot be null", resource); + this.resource=resource; + this.timeout = (int) transaction.getTimeout() / 1000; + + this.tid = transaction.getCompositeCoordinator().getCoordinatorId(); // cf case 162083 + this.root = root; + this.resourcename = resource.getName(); + setXid(this.resource.createXid(this.tid)); + setState(TxState.ACTIVE); + this.isXaSuspended = false; + this.knownInResource = false; + } + + + + void setState(TxState state) { + if (state.isHeuristic()) { + LOGGER.logWarning("Heuristic termination of " + toString() + " with state " + state); + } + this.state = state; + } + + + + + + protected void testOrRefreshXAResourceFor2PC() throws XAException { + try { + // fix for case 31209: refresh entire XAConnection on heur hazard + if (state == TxState.HEUR_HAZARD) + forceRefreshXAConnection(); + else if (xaresource != null) { // null if connection failure + assertConnectionIsStillAlive(); + } + } catch (XAException xa) { + // timed out? + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this.resourcename + ": XAResource needs refresh?", xa); + } + } + if (xaresource == null) { + xaresource = resource.getXAResource(); + } + } + + private void assertConnectionIsStillAlive() throws XAException { + this.xaresource.isSameRM(this.xaresource); + } + + private void forceRefreshXAConnection() throws XAException { + if (LOGGER.isTraceEnabled()) + LOGGER.logTrace(this.resourcename + + ": forcing refresh of XAConnection..."); + + try { + this.xaresource = this.resource.refreshXAConnection(); + } catch (ResourceException re) { + LOGGER.logWarning(this.resourcename + + ": could not refresh XAConnection", re); + } + } + + /** + * Needed for garbage collection of res tx instances: if no new siblings can + * arrive, this method removes any pointers to res txs in the resource + */ + + private void terminateInResource() { + if (this.resource != null) + this.resource.removeSiblingMap(this.root); + } + + /** + * @see ResourceTransaction. + */ + public String getTid() { + return this.tid; + } + + /** + * @see ResourceTransaction. + */ + + @Override + public synchronized void suspend() throws ResourceException { + + // BugzID: 20545 + // State may be IN_DOUBT or TERMINATED when a connection is closed AFTER + // commit! + // In that case, don't call END again, and also don't generate any + // error! + // This is required for some hibernate connection release strategies. + if (this.state.equals(TxState.ACTIVE)) { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.end ( " + xid + + " , XAResource.TMSUCCESS ) on resource " + + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + this.xaresource.end(this.xid, XAResource.TMSUCCESS); + + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "end", + this.xid, xaerr.errorCode); + if (LOGGER.isTraceEnabled()) + LOGGER.logTrace(msg, xaerr); + // don't throw: fix for case 102827 + } + setState(TxState.LOCALLY_DONE); + } + } + + boolean supportsTmJoin() { + return !(isActive()); + } + + /** + * @see ResourceTransaction. + */ + + @Override + public synchronized void resume() throws ResourceException { + int flag = 0; + String logFlag = ""; + if (this.state.equals(TxState.LOCALLY_DONE)) {// reused instance + flag = XAResource.TMJOIN; + logFlag = "XAResource.TMJOIN"; + } else if (!this.knownInResource) {// new instance + flag = XAResource.TMNOFLAGS; + logFlag = "XAResource.TMNOFLAGS"; + } else + throw new IllegalStateException("Wrong state for resume: " + + this.state); + + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.start ( " + xid + + " , " + logFlag + " ) on resource " + + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + this.xaresource.start(this.xid, flag); + + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "resume", + this.xid, xaerr.errorCode); + LOGGER.logWarning(msg, xaerr); + throw new ResourceException(msg, xaerr); + } + setState(TxState.ACTIVE); + this.knownInResource = true; + } + + /** + * @see Participant + */ + + @Override + public void setCascadeList(Map allParticipants) + throws SysException { + // nothing to do: local participant + } + + public Object getState() { + return this.state; + } + + private boolean beforePrepare() { + return TxState.ACTIVE.equals(this.state) + || TxState.LOCALLY_DONE.equals(this.state); + } + + /** + * @see Participant + */ + + @Override + public void setGlobalSiblingCount(int count) { + // nothing to be done here + } + + /** + * @see Participant + */ + + @Override + public synchronized void forget() { + terminateInResource(); + try { + if (this.xaresource != null) { // null if recovery failed + this.xaresource.forget(this.xid); + } + } catch (Exception err) { + LOGGER.logTrace("Error forgetting xid: " + this.xid, err); + // we don't care here + } + setState(TxState.TERMINATED); + } + + /** + * @see Participant + */ + + @Override + public synchronized int prepare() throws RollbackException, + HeurHazardException, HeurMixedException, SysException { + int ret = 0; + terminateInResource(); + + if (TxState.ACTIVE == this.state) { + // tolerate non-delisting apps/servers + suspend(); + } + + // duplicate prepares can happen for siblings in serial subtxs!!! + // in that case, the second prepare just returns READONLY + if (this.state == TxState.IN_DOUBT) + return Participant.READ_ONLY; + else if (!(this.state == TxState.LOCALLY_DONE)) + throw new SysException("Wrong state for prepare: " + this.state); + try { + // refresh xaresource for MQSeries: seems to close XAResource after + // suspend??? + testOrRefreshXAResourceFor2PC(); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("About to call prepare on XAResource instance: " + + this.xaresource); + } + ret = this.xaresource.prepare(this.xid); + + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "prepare", + this.xid, xaerr.errorCode); + if (XAException.XA_RBBASE <= xaerr.errorCode + && xaerr.errorCode <= XAException.XA_RBEND) { + LOGGER.logWarning(msg, xaerr); // see case 84253 + throw new RollbackException(msg, xaerr); + } else { + LOGGER.logError(msg, xaerr); + throw new SysException(msg, xaerr); + } + } + setState(TxState.IN_DOUBT); + if (ret == XAResource.XA_RDONLY) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.prepare ( " + xid + + " ) returning XAResource.XA_RDONLY " + "on resource " + + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + return Participant.READ_ONLY; + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.prepare ( " + xid + + " ) returning OK " + "on resource " + + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + return Participant.READ_ONLY + 1; + } + } + + /** + * @see Participant. + */ + + @Override + public synchronized void rollback() + throws HeurCommitException, HeurMixedException, + HeurHazardException, SysException { + terminateInResource(); + + if (rollbackShouldDoNothing()) { + return; + } + if (this.state.equals(TxState.TERMINATED)) { + return; + } + + if (this.state.equals(TxState.HEUR_MIXED)) { + throw new HeurMixedException(); + } + if (this.state.equals(TxState.HEUR_COMMITTED)) { + throw new HeurCommitException(); + } + + try { + if (this.state.equals(TxState.ACTIVE)) { // first suspend xid + suspend(); + } + + // refresh xaresource for MQSeries: seems to close XAResource after + // suspend??? + testOrRefreshXAResourceFor2PC(); + if (this.xaresource == null) { + throw new HeurHazardException("XAResourceTransaction " + + getXid() + ": no XAResource to rollback?"); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.rollback ( " + xid + + " ) " + "on resource " + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + this.xaresource.rollback(this.xid); + + } catch (ResourceException resErr) { + // failure of suspend + throw new SysException("Error in rollback: " + resErr.getMessage(), + resErr); + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "rollback", + this.xid, xaerr.errorCode); + if (XAException.XA_RBBASE <= xaerr.errorCode + && xaerr.errorCode <= XAException.XA_RBEND) { + if (LOGGER.isTraceEnabled()) + LOGGER.logTrace(msg); + } else { + LOGGER.logWarning(msg, xaerr); + switch (xaerr.errorCode) { + case XAException.XA_HEURHAZ: + setState(TxState.HEUR_HAZARD); + throw new HeurHazardException(); + case XAException.XA_HEURMIX: + setState(TxState.HEUR_MIXED); + throw new HeurMixedException(); + case XAException.XA_HEURCOM: + setState(TxState.HEUR_COMMITTED); + throw new HeurCommitException(); + case XAException.XA_HEURRB: + forget(); + break; + case XAException.XAER_NOTA: + // see case 21552 + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("XAResource.rollback: invalid Xid - already rolled back in resource?"); + } + setState(TxState.TERMINATED); + // ignore error - corresponds to semantics of rollback! + break; + default: + // fix for bug 31209 + setState(TxState.HEUR_HAZARD); + throw new SysException(msg, xaerr); + } + } + } + setState(TxState.TERMINATED); + } + + private boolean rollbackShouldDoNothing() { + return !this.knownInResource && beforePrepare(); + } + + /** + * @see Participant + */ + + @Override + public synchronized void commit(boolean onePhase) + throws HeurRollbackException, HeurHazardException, + HeurMixedException, RollbackException, SysException { + + terminateInResource(); + + if (this.state.equals(TxState.TERMINATED)) { + return; + } + if (this.state.equals(TxState.HEUR_MIXED)) { + throw new HeurMixedException(); + } + if (this.state.equals(TxState.HEUR_ABORTED)) { + throw new HeurRollbackException(); + } + if (!(this.state.isOneOf(TxState.LOCALLY_DONE, TxState.IN_DOUBT, TxState.HEUR_HAZARD))) { + throw new SysException("Wrong state for commit: " + this.state); + } + + if (onePhase) { + if (xaresource == null) { + throw new com.atomikos.icatch.RollbackException(toString() + ": no XAResource to commit"); + } + try { + if (TxState.ACTIVE.equals(state)) { + // tolerate non-delisting apps/servers + suspend(); + } + } catch (ResourceException re) { + // happens if already rolled back or something else; + // in any case the transaction can be trusted to act + // as if rollback already happened + throw new com.atomikos.icatch.RollbackException(re.getMessage()); + } + } + + + try { + // refresh xaresource for MQSeries: seems to close XAResource after suspend??? + if (!onePhase) { // cf case 167209 + testOrRefreshXAResourceFor2PC(); + if (xaresource == null) { + String msg = toString + ": no XAResource to commit - recovery will handle this in the background..."; + LOGGER.logWarning(msg); + throw new HeurHazardException(msg); + } + } + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.commit ( " + xid + + " , " + onePhase + " ) on resource " + this.resourcename + + " represented by XAResource instance " + this.xaresource); + } + this.xaresource.commit(this.xid, onePhase); + + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "commit", + this.xid, xaerr.errorCode); + LOGGER.logWarning(msg, xaerr); + + if (XAException.XA_RBBASE <= xaerr.errorCode + && xaerr.errorCode <= XAException.XA_RBEND) { + + if (!onePhase) + throw new SysException(msg, xaerr); + else + throw new com.atomikos.icatch.RollbackException( + "Already rolled back in resource.", xaerr); + } else { + switch (xaerr.errorCode) { + case XAException.XA_HEURHAZ: + setState(TxState.HEUR_HAZARD); + throw new HeurHazardException(); + case XAException.XA_HEURMIX: + setState(TxState.HEUR_MIXED); + throw new HeurMixedException(); + case XAException.XA_HEURCOM: + forget(); + break; + case XAException.XA_HEURRB: + setState(TxState.HEUR_ABORTED); + throw new HeurRollbackException(); + case XAException.XAER_NOTA: + if (!onePhase) { + // see case 21552 + LOGGER.logWarning("XAResource.commit: invalid Xid - transaction already committed in resource?"); + setState(TxState.TERMINATED); + break; + } + default: + // fix for bug 31209 + setState(TxState.HEUR_HAZARD); + throw new SysException(msg, xaerr); + } + } + } + setState(TxState.TERMINATED); + } + + /** + * Absolutely necessary for coordinator to work correctly + */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + + // NOTE: basing equals on the xid means that if two + // different instances for the same xid exist then these + // will be considered the same, and a second will + // NOT be added to the coordinator's participant list. + // However, this is not a problem, since the first added instance + // will do all commit work (they are equivalent). + // Note that this can ONLY happen for two invocations of the + // SAME local composite transaction to the SAME resource. + // Because INSIDE ONE local transaction there is no + // internal parallellism, having two invocations to the same + // resource execute with the same xid is not a problem. + + if (!(o instanceof XAResourceTransaction)) + return false; + + XAResourceTransaction other = (XAResourceTransaction) o; + return this.xid.equals(other.xid); + } + + /** + * Absolutely necessary for coordinator to work correctly + */ + + @Override + public int hashCode() { + return xid.hashCode(); + } + + @Override + public String toString() { + + return this.toString; + } + + /** + * Get the Xid. Needed by jta mappings. + * + * @return Xid The Xid of this restx. + */ + + public Xid getXid() { + return this.xid; + } + + /** + * Set the XAResource attribute. + * + * @param xaresource + * The new XAResource to use. This new XAResource represents the + * new connection to the XA database. Necessary because on reuse, + * the old xaresource may be in use by another thread, for + * another transaction. + */ + + public void setXAResource(XAResource xaresource) { + if (LOGGER.isTraceEnabled()) + LOGGER.logTrace(this + ": about to switch to XAResource " + + xaresource); + this.xaresource = xaresource; + try { + this.xaresource.setTransactionTimeout(this.timeout); + } catch (XAException e) { + String msg = interpretErrorCode(this.resourcename, + "setTransactionTimeout", this.xid, e.errorCode); + LOGGER.logWarning(msg, e); + // we don't care + } + if (LOGGER.isTraceEnabled()) + LOGGER.logTrace("XAResourceTransaction " + getXid() + + ": switched to XAResource " + xaresource); + } + + /** + * Perform an XA suspend. + */ + + public void xaSuspend() throws XAException { + // cf case 61305: make XA suspend idempotent so appserver suspends do + // not interfere with our suspends (triggered by transaction suspend) + if (!this.isXaSuspended) { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.suspend ( " + + xid + + " , XAResource.TMSUSPEND ) on resource " + + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + this.xaresource.end(this.xid, XAResource.TMSUSPEND); + + this.isXaSuspended = true; + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "suspend", + this.xid, xaerr.errorCode); + LOGGER.logWarning(msg, xaerr); + throw xaerr; + } + } + } + + /** + * Perform an xa resume + */ + + public void xaResume() throws XAException { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("XAResource.start ( " + xid + + " , XAResource.TMRESUME ) on resource " + + this.resourcename + + " represented by XAResource instance " + + this.xaresource); + } + this.xaresource.start(this.xid, XAResource.TMRESUME); + + this.isXaSuspended = false; + } catch (XAException xaerr) { + String msg = interpretErrorCode(this.resourcename, "resume", + this.xid, xaerr.errorCode); + LOGGER.logWarning(msg, xaerr); + throw xaerr; + } + + } + + /** + * Test if the resource has been ended with TMSUSPEND. + * + * @return boolean True if so. + */ + public boolean isXaSuspended() { + return this.isXaSuspended; + } + + /** + * Test if the restx is active (in use). + * + * @return boolean True if so. + */ + public boolean isActive() { + return this.state.equals(TxState.ACTIVE); + } + + /** + * @see com.atomikos.icatch.Participant#getURI() + */ + @Override + public String getURI() { + return xid.getBranchQualifierAsString(); + } + + public String getResourceName() { + return this.resourcename; + } + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XATransactionalResource.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XATransactionalResource.java new file mode 100644 index 000000000..41ffd0747 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XATransactionalResource.java @@ -0,0 +1,413 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.util.Collection; +import java.util.Hashtable; +import java.util.Stack; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.datasource.ResourceException; +import com.atomikos.datasource.ResourceTransaction; +import com.atomikos.datasource.TransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.RecoveryService; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.xa.XARecoveryManager; + + +/** + * + * + * An abstract XA implementation of a transactional resource. + * + * For a particular XA data source, it is necessary to implement the + * refreshXAConnection method, because in general there is no standard way of + * getting XAResource instances. Therefore, this class is agnostic about it. + * + * It is assumed that there is at most one instance per (root transaction, + * server) combination. Otherwise, siblings can not be mapped to the same + * ResourceTransaction! This instance is responsible for mapping siblings to + * ResourceTransaction instances. + */ + +public abstract class XATransactionalResource implements TransactionalResource +{ + private static final Logger LOGGER = LoggerFactory.createLogger(XATransactionalResource.class); + + protected XAResource xares_; + private String uniqueResourceName; + private Hashtable rootTransactionToSiblingMapperMap; + private XidFactory xidFact; + private boolean closed; + private String branchIdentifier; + + private static final String MAX_LONG_STR = String.valueOf(Long.MAX_VALUE); + private static final int MAX_LONG_LEN = MAX_LONG_STR.getBytes().length; + /** + * Construct a new instance with a default XidFactory. + * + */ + + public XATransactionalResource ( String uniqueResourceName ) + { + + this.uniqueResourceName = uniqueResourceName; + this.rootTransactionToSiblingMapperMap = new Hashtable(); + // name should be less than 64 for xid compatibility + + //branch id is server name + long value! + + if ( uniqueResourceName.getBytes ().length > 64- MAX_LONG_LEN ) + throw new RuntimeException ( + "Max length of resource name exceeded: should be less than " + ( 64 - MAX_LONG_LEN ) ); + this.xidFact = new DefaultXidFactory (); + this.closed = false; + } + + /** + * Construct a new instance with a custom XidFactory. + * + * + */ + + public XATransactionalResource ( String uniqueResourceName , XidFactory factory ) + { + this ( uniqueResourceName ); + this.xidFact = factory; + } + + /** + * Utility method to establish and refresh the XAResource. An XAResource is + * actually a connection to a back-end resource, and this connection needs + * to stay open for the transactional resource instance. The resource uses + * the XAResource regularly, but sometimes the back-end server can close the + * connection after a time-out. At intialization time and also after such a + * time-out, this method is called to refresh the XAResource instance. This + * is typically done by (re-)establishing a connection to the server and + * keeping this connection open!. + * + * @return XAResource A XAResource instance that will be used to represent + * the server. + * @exception ResourceException + * On failure. + */ + + protected abstract XAResource refreshXAConnection () + throws ResourceException; + + /** + * Get the xidFactory for this instance. Needed by XAResourceTransaction to + * create new XID. + * + * @return XidFactory The XidFactory for the resource. + */ + + public XidFactory getXidFactory () + { + return this.xidFact; + } + + void removeSiblingMap ( String root ) + { + synchronized ( this.rootTransactionToSiblingMapperMap ) { + this.rootTransactionToSiblingMapperMap.remove ( root ); + } + + } + + private SiblingMapper getSiblingMap ( String root ) + { + synchronized ( this.rootTransactionToSiblingMapperMap ) { + if ( this.rootTransactionToSiblingMapperMap.containsKey ( root ) ) + return this.rootTransactionToSiblingMapperMap.get ( root ); + else { + SiblingMapper map = new SiblingMapper ( this , root ); + this.rootTransactionToSiblingMapperMap.put ( root, map ); + return map; + } + } + } + + /** + * Check if the XAResource needs to be refreshed. + * + * @return boolean True if the XAResource needs refresh. + */ + + protected boolean needsRefresh () + { + boolean ret = true; + + // check if connection has not timed out + try { + // we should be the same as ourselves! + // NOTE: xares_ is null if no connection could be gotten + // in that case we just return true + // otherwise, test the xaresource liveness + if ( this.xares_ != null ) { + this.xares_.isSameRM ( this.xares_ ); + ret = false; + } + } catch ( XAException xa ) { + // timed out? + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this.uniqueResourceName + + ": XAResource needs refresh?", xa ); + + } + return ret; + } + + + /** + * Test if the XAResource is used by this instance. + * + * @param xares + * The XAResource to test. + * @return boolean True iff this instance uses the same back-end resource, + * in as far as this can be determined by this instance. + */ + + public boolean usesXAResource ( XAResource xares ) + { + XAResource xaresource = getXAResource (); + if (xaresource == null) return false; + // if no connection could be gotten + + boolean ret = false; + + if ( !xares.getClass ().getName ().equals ( + xaresource.getClass ().getName () ) ) { + // if the implementation classes are different, + // the resources are not the same + // this check is needed to cope with + // vendor-specific errors in XAResource.isSameRM() + ret = false; + } else { + // in this case, the implementation class names are the same + // so delegate to xares instances + try { + if ( xares.isSameRM ( xaresource ) ) { + ret = true; + } else { + LOGGER.logTrace ( "XAResources claim to be different: " + + xares + " and " + xaresource ); + } + } catch ( XAException xe ) { + throw new SysException ( "Error in XAResource comparison: " + + xe.getMessage (), xe ); + } + } + return ret; + } + + /** + * Get the XAResource instance that this instance is using. + * + * @return XAResource The XAResource instance. + */ + + public synchronized XAResource getXAResource () + { + // null on first invocation + if ( needsRefresh () ) { + refreshXAResource(); + } + + return this.xares_; + } + + /** + * @see TransactionalResource + */ + @SuppressWarnings("unchecked") + @Override + public ResourceTransaction getResourceTransaction ( CompositeTransaction ct ) + throws ResourceException, IllegalStateException + { + if ( this.closed ) throw new IllegalStateException("XATransactionResource already closed"); + + if ( ct == null ) return null; // happens in create method of beans? + + Stack lineage = ct.getLineage (); + String root = null; + if (lineage == null || lineage.isEmpty ()) root = ct.getTid (); + else { + Stack tmp = (Stack) lineage.clone (); + while ( !tmp.isEmpty() ) { + CompositeTransaction next = (CompositeTransaction) tmp.pop(); + if (next.isRoot()) root = next.getTid (); + } + } + return getSiblingMap ( root ).findOrCreateBranchForTransaction ( ct ); + + } + + + /** + * @see TransactionalResource + */ + + @Override + public String getName () + { + return this.uniqueResourceName; + } + + /** + * The default close operation. Subclasses may need to override this method + * in order to process XA-specific close procedures such as closing + * connections. + * + */ + + @Override + public void close () throws ResourceException + { + this.closed = true; + } + + /** + * Test if the resource is closed. + * + * @return boolean True if closed. + * @throws ResourceException + */ + @Override + public boolean isClosed () throws ResourceException + { + return this.closed; + } + + /** + * @see RecoverableResource + */ + + @Override + public boolean isSameRM ( RecoverableResource res ) + throws ResourceException + { + if ( res == null || !(res instanceof XATransactionalResource) ) { + return false; + } + + XATransactionalResource xatxres = (XATransactionalResource) res; + if ( xatxres.uniqueResourceName == null || this.uniqueResourceName == null ) { + return false; + } + + if (xatxres.uniqueResourceName.equals ( this.uniqueResourceName )) { + return true; + } + try { + if (xares_ != null) { + XAResource other = xatxres.getXAResource(); + if (other != null) { + return xares_.isSameRM(other); //cf case 170618 + } + } + } catch (XAException e) { + String msg = "Failed to compare XAResources"; + XAExceptionHelper.formatLogMessage(msg, e, "pessimistically assuming they are different"); + } + return false; + } + + /** + * @see RecoverableResource + */ + + @Override + public void setRecoveryService ( RecoveryService recoveryService ) + throws ResourceException + { + + if ( recoveryService != null ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Installing recovery service on resource " + + getName () ); + this.branchIdentifier=recoveryService.getName(); + } + + } + + /** + * Set the XID factory, needed for online management tools. + * + * @param factory + */ + public void setXidFactory(XidFactory factory) { + this.xidFact = factory; + } + + /** + * Create an XID for the given tx. + * + * @param tid + * The tx id. + * @return Xid A globally unique Xid that can be recovered by any resource + * that connects to the same EIS. + */ + + protected XID createXid(String tid) { + if (this.branchIdentifier == null) throw new IllegalStateException("Not yet initialized"); + return getXidFactory().createXid (tid , this.branchIdentifier, this.uniqueResourceName); + } + + @Override + public boolean recover(long startOfRecoveryScan, Collection expiredCommittingCoordinators, Collection indoubtForeignCoordinatorsToKeep) { + boolean ret = false; + XARecoveryManager xaResourceRecoveryManager = XARecoveryManager.getInstance(); + if (xaResourceRecoveryManager != null) { //null for LogCloud recovery + if(getXAResource() != null) { //null if backend down + try { + ret = xaResourceRecoveryManager.recover(getXAResource(), startOfRecoveryScan, expiredCommittingCoordinators, indoubtForeignCoordinatorsToKeep, uniqueResourceName); + } catch (Exception e) { + LOGGER.logWarning(e.getMessage(),e); //cf case 164148 & 164147 + refreshXAResource(); //cf case 156968 + } + } + } + return ret; + } + + + + private void refreshXAResource() { + LOGGER.logTrace ( this.uniqueResourceName + ": refreshing XAResource..." ); + this.xares_ = refreshXAConnection (); + LOGGER.logInfo ( this.uniqueResourceName + ": refreshed XAResource" ); + } + + @Override + public boolean equals(Object o) { + boolean ret = false; + if (o instanceof RecoverableResource) { + RecoverableResource other = (RecoverableResource) o; + return other.isSameRM(this); + } + return ret; + } + + @Override + public int hashCode() { + return 0; // pessimistic - since xaresource can be null if backend down + } + + @Override + public boolean hasPendingParticipantsFromLastRecoveryScan() { + return XARecoveryManager.getInstance().hasPendingXids(uniqueResourceName); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XID.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XID.java new file mode 100644 index 000000000..504c71713 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XID.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.io.Serializable; + +import javax.transaction.xa.Xid; + +/** + * Our Xid class with correct equals and hashCode. + */ + +public class XID implements Serializable, Xid +{ + + private static final long serialVersionUID = 4796496938014754464L; + + private static final int DEFAULT_FORMAT = ('A' << 24) + ('T' << 16) + ('O' << 8) + 'M'; + + // same formatID for each transaction + // -1 for null Xid, 0 for OSI CCR and positive for proprietary format... + + private String cachedToStringForPerformance; + private final int formatId; + private final byte[] branchQualifier; + private final byte[] globalTransactionId; + private final String branchQualifierStr; + private final String globalTransactionIdStr; + private final String uniqueResourceName; + + /** + * Create a new instance with the resource name as branch. This is the main + * constructor for new instances. + * + * @param tid + * @param branchQualifier + * + */ + + public XID (String tid, String branchQualifier, String uniqueResourceName) + { + this.formatId = DEFAULT_FORMAT; + this.uniqueResourceName = uniqueResourceName; + this.globalTransactionIdStr=tid; + this.globalTransactionId = tid.toString ().getBytes (); + if ( this.globalTransactionId.length > Xid.MAXGTRIDSIZE ) + throw new RuntimeException ( "Max global tid length exceeded." ); + + this.branchQualifierStr = branchQualifier; + this.branchQualifier = branchQualifier.getBytes (); + if ( this.branchQualifier.length > Xid.MAXBQUALSIZE ) + throw new RuntimeException ( + "Max branch qualifier length exceeded." ); + + } + + /** + * Copy constructor needed during recovery: if the data source returns + * inappropriate instances (that do not implement equals and hashCode) then + * we will need this constructor. + * + */ + + public XID (Xid xid) + { + this.formatId = xid.getFormatId (); + this.globalTransactionId = xid.getGlobalTransactionId (); + this.branchQualifier = xid.getBranchQualifier (); + this.globalTransactionIdStr = new String(xid.getGlobalTransactionId ()); + this.branchQualifierStr= new String(xid.getBranchQualifier ()); + this.uniqueResourceName = null; + } + + @Override + public int getFormatId () + { + return this.formatId; + } + + @Override + public byte[] getBranchQualifier () + { + return this.branchQualifier; + } + + @Override + public byte[] getGlobalTransactionId () + { + return this.globalTransactionId; + } + + @Override + public boolean equals ( Object obj ) + { + if (this == obj) + return true; + if (obj instanceof XID) { + XID xid = (XID) obj; + return toString().equals(xid.toString()); + } + return false; + } + + @Override + public String toString () + { + if ( this.cachedToStringForPerformance == null ) { + String gtrid = byteArrayToHexString(getGlobalTransactionId()); + String bqual = byteArrayToHexString(getBranchQualifier()); + this.cachedToStringForPerformance = "XID: " + gtrid + ":" + bqual; + } + return this.cachedToStringForPerformance; + } + + + private static final byte[] HEX_CHAR_TABLE = { + (byte)'0', (byte)'1', (byte)'2', (byte)'3', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', + (byte)'8', (byte)'9', (byte)'A', (byte)'B', + (byte)'C', (byte)'D', (byte)'E', (byte)'F' + }; + + private static String byteArrayToHexString(byte[] raw) + { + byte[] hex = new byte[2 * raw.length]; + int index = 0; + + for (byte b : raw) { + int v = b & 0xFF; + hex[index++] = HEX_CHAR_TABLE[v >>> 4]; + hex[index++] = HEX_CHAR_TABLE[v & 0xF]; + } + return new String(hex); + } + + public String getBranchQualifierAsString() { + return this.branchQualifierStr; + } + + public String getGlobalTransactionIdAsString() { + return this.globalTransactionIdStr; + } + + @Override + public int hashCode () + { + return toString ().hashCode (); + } + + public String getUniqueResourceName() { + return uniqueResourceName; + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XidFactory.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XidFactory.java new file mode 100644 index 000000000..e1cd21e6b --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/XidFactory.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + + +/** + * + * + * A factory for creating new Xid instances. This allows different factories for + * different resources, which is needed because some resources need a custom Xid + * format. + */ + +public interface XidFactory +{ + /** + * Creates a new Xid instance for a given composite transaction id and + * branch identifier. + * + * @param tid + * @param branchIdentifier + * @param uniqueResourceName + */ + + public XID createXid (String tid , String branchIdentifier, String uniqueResourceName); +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/jdbc/JdbcTransactionalResource.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/jdbc/JdbcTransactionalResource.java new file mode 100644 index 000000000..8cb481de2 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/jdbc/JdbcTransactionalResource.java @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.jdbc; + +import java.sql.SQLException; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.ResourceException; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.util.Assert; + +/** + * + * + * A default XATransactionalResource implementation for JDBC. + */ + +public class JdbcTransactionalResource extends XATransactionalResource +{ + private XADataSource xaDataSource; + private XAConnection xaConnection; + private String user; // null if not set + private String password; // null if not set + /** + * Constructs a new instance with a given name and XADataSource. + * + * @param serverName + * The unique name. + * @param xads + * The data source. + */ + + public JdbcTransactionalResource ( String serverName , XADataSource xads ) + { + super(serverName); + Assert.notNull("XADataSource must not be null", xads); + this.xaDataSource = xads; + this.xaConnection = null; + } + + /** + * Get the user + * + * @return String The user, or empty string. + */ + private String getUser () + { + String ret = ""; + if ( this.user != null ) + ret = this.user; + + return ret; + } + + /** + * Get the passwd + * + * @return String the password, or empty string + */ + private String getPassword () + { + String ret = ""; + if ( this.password != null ) + ret = this.password; + return ret; + } + + /** + * Implements the functionality to get an XAResource handle. + * + * @return XAResource The XAResource instance. + */ + + @Override + protected synchronized XAResource refreshXAConnection () + throws ResourceException + { + XAResource res = null; + + if ( this.xaConnection != null ) { + try { + this.xaConnection.close (); + } catch ( Exception err ) { + // happens if connection has timed out + // which is probably normal, otherwise + // refresh would not be called in the first place + } + } + + try { + this.xaConnection = createXAConnection(); + if ( this.xaConnection != null ) + res = this.xaConnection.getXAResource (); + // null if db down during recovery + } catch ( SQLException sql ) { + throw new ResourceException ( "Error in getting XA resource",sql ); + } + + return res; + + } + + /** + * Optionally set the user name with which to get connections for recovery. + * + * If not set, then the right user name should be configured on the + * XADataSource directly. + * + * @param user + * The user name. + */ + + public void setUser ( String user ) + { + this.user = user; + } + + /** + * Optionally set the password with which to get connections for recovery. + * + * If not set, then the right password should be configured on the + * XADataSource directly. + * + * @param password + * The password. + */ + + public void setPassword ( String password ) + { + this.password = password; + } + + /** + * Overrides default close to include closing any open connections to the + * XADataSource. + */ + + @Override + public void close () throws ResourceException + { + super.close (); + try { + if ( this.xaConnection != null ) + this.xaConnection.close (); + } catch ( SQLException err ) { + // throw new ResourceException ( err.getMessage() ); + // exception REMOVED because it close clashes + // with shutdown hooks (in which the order of TS and + // DS shutdown is unpredictable) + } + } + + private XAConnection createXAConnection() + { + XAConnection conn = null; + try { + if ( "".equals ( getUser () ) ) + conn = this.xaDataSource.getXAConnection (); + else + conn = this.xaDataSource.getXAConnection ( getUser (), getPassword () ); + } catch ( SQLException noConnection ) { + // ignore and return null: happens if + // db is down at this time (during + // recovery for instance) + conn = null; + } + return conn; + } + + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/jms/JmsTransactionalResource.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/jms/JmsTransactionalResource.java new file mode 100644 index 000000000..c1ae8fb3f --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/jms/JmsTransactionalResource.java @@ -0,0 +1,104 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.jms; + +import javax.jms.JMSException; +import javax.jms.XAConnection; +import javax.jms.XAConnectionFactory; +import javax.jms.XASession; +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.ResourceException; +import com.atomikos.datasource.xa.XATransactionalResource; + +/** + * + * resource implementation for JMS queues. + * + * + */ + +public class JmsTransactionalResource extends XATransactionalResource +{ + + private XAConnectionFactory factory; + + private XAConnection conn; + + /** + * Create a new instance. + * + * @param name + * The unique resource name. + * @param factory + * The xa connection factory to use. + */ + + public JmsTransactionalResource ( String name , XAConnectionFactory factory ) + { + super ( name ); + this.factory = factory; + this.conn = null; + } + + /** + * Implements the functionality to get an XAResource handle. + * + * @return XAResource The XAResource instance. + */ + + @Override + protected synchronized XAResource refreshXAConnection () + throws ResourceException + { + XAResource res = null; + + if ( this.conn != null ) { + try { + this.conn.close (); + } catch ( Exception err ) { + // happens if connection has timed out + // which is probably normal, otherwise + // refresh would not be called in the first place + } + } + + try { + this.conn = this.factory.createXAConnection (); + XASession session = this.conn.createXASession (); + // note: session does not have to be kept in attribute, + // since JMS explicitly states that closing the connection + // also closes all sessions. + res = session.getXAResource (); + } catch ( JMSException jms ) { + throw new ResourceException ( "Error in getting XA resource", + jms ); + } + + return res; + + } + + /** + * Overrides default close to include closing any open connections to the + * JMS infrastructure. + */ + + @Override + public void close () throws ResourceException + { + super.close (); + try { + if ( this.conn != null ) this.conn.close (); + } catch ( JMSException err ) { + throw new ResourceException ( err.getMessage () ); + } + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchEndedStateHandler.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchEndedStateHandler.java new file mode 100644 index 000000000..ecbf3ad73 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchEndedStateHandler.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import com.atomikos.datasource.xa.XAResourceTransaction; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * + * + * State handler for when delisted in an XA branch. + * + */ + +class BranchEndedStateHandler +extends TransactionContextStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(BranchEndedStateHandler.class); + + private CompositeTransaction ct; + + BranchEndedStateHandler ( XATransactionalResource resource , XAResourceTransaction branch , CompositeTransaction ct ) + { + super ( resource , null ); + this.ct = ct; + branch.suspend(); + } + + TransactionContextStateHandler checkEnlistBeforeUse ( CompositeTransaction ct) + throws InvalidSessionHandleStateException + { + String msg = "Detected illegal attempt to use a closed XA session"; + LOGGER.logError ( msg ); + throw new InvalidSessionHandleStateException ( msg ); + } + + TransactionContextStateHandler sessionClosed() + { + //close can happen several times -> should be idempotent + return null; + } + + + TransactionContextStateHandler transactionTerminated ( CompositeTransaction tx ) + { + TransactionContextStateHandler ret = null; + if ( ct.isSameTransaction ( tx ) ) ret = new TerminatedStateHandler (); + return ret; + } + + boolean isInactiveInTransaction ( CompositeTransaction tx ) + { + return ct.isSameTransaction ( tx ); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchEnlistedStateHandler.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchEnlistedStateHandler.java new file mode 100644 index 000000000..07cc0a650 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchEnlistedStateHandler.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.XAResourceTransaction; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * + * + * State handler for when enlisted in an XA branch. + * + */ + +class BranchEnlistedStateHandler extends TransactionContextStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(BranchEnlistedStateHandler.class); + + private CompositeTransaction ct; + private XAResourceTransaction branch; + + BranchEnlistedStateHandler ( XATransactionalResource resource , CompositeTransaction ct , XAResource xaResource) + { + super ( resource , xaResource ); + this.ct = ct; + this.branch = ( XAResourceTransaction ) resource.getResourceTransaction ( ct ); + branch.setXAResource ( xaResource ); + branch.resume(); + } + + public BranchEnlistedStateHandler ( + XATransactionalResource resource, CompositeTransaction ct, + XAResource xaResource , XAResourceTransaction branch ) throws InvalidSessionHandleStateException + { + super ( resource , xaResource ); + this.ct = ct; + this.branch = branch; + branch.setXAResource ( xaResource ); + try { + branch.xaResume(); + } catch ( XAException e ) { + String msg = "Failed to resume branch: " + branch; + throw new InvalidSessionHandleStateException ( msg ); + } + } + + TransactionContextStateHandler checkEnlistBeforeUse ( CompositeTransaction currentTx) + throws InvalidSessionHandleStateException, UnexpectedTransactionContextException + { + + if ( currentTx == null || !currentTx.isSameTransaction ( ct ) ) { + //OOPS! we are being used a different tx context than the one expected... + + //TODO check: what if subtransaction? Possible solution: ignore if serial_jta mode, error otherwise. + + String msg = "The connection/session object is already enlisted in a (different) transaction."; + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( msg ); + throw new UnexpectedTransactionContextException(); + } + + //tx context is still the same -> no change in state required + return null; + } + + TransactionContextStateHandler sessionClosed() + { + return new BranchEndedStateHandler ( getXATransactionalResource() , branch , ct ); + } + + + TransactionContextStateHandler transactionTerminated ( CompositeTransaction tx ) + { + TransactionContextStateHandler ret = null; + if ( ct.isSameTransaction ( tx ) ) ret = new NotInBranchStateHandler ( getXATransactionalResource() , getXAResource() ); + return ret; + + } + + public TransactionContextStateHandler transactionSuspended() throws InvalidSessionHandleStateException + { + try { + branch.xaSuspend(); + } catch ( XAException e ) { + LOGGER.logWarning ( "Error in suspending transaction context for transaction: " + ct , e ); + String msg = "Failed to suspend branch: " + branch; + throw new InvalidSessionHandleStateException ( msg , e ); + } + return new BranchSuspendedStateHandler ( getXATransactionalResource() , branch , ct , getXAResource() ); + } + + boolean isInTransaction ( CompositeTransaction tx ) + { + return ct.isSameTransaction ( tx ); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchSuspendedStateHandler.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchSuspendedStateHandler.java new file mode 100644 index 000000000..aa6d15264 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/BranchSuspendedStateHandler.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.XAResourceTransaction; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +class BranchSuspendedStateHandler extends TransactionContextStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(BranchSuspendedStateHandler.class); + + private XAResourceTransaction branch; + private CompositeTransaction ct; + + BranchSuspendedStateHandler ( XATransactionalResource resource, XAResourceTransaction branch , CompositeTransaction ct , XAResource xaResource ) + { + super ( resource, xaResource ); + this.branch = branch; + this.ct = ct; + } + + TransactionContextStateHandler checkEnlistBeforeUse ( CompositeTransaction ct) + throws InvalidSessionHandleStateException, + UnexpectedTransactionContextException + { + String msg = "Detected illegal attempt to use a suspended XA session"; + LOGGER.logError ( msg ); + throw new InvalidSessionHandleStateException ( msg ); + } + + TransactionContextStateHandler sessionClosed() + { + return new BranchEndedStateHandler ( getXATransactionalResource() , branch , ct ); + } + + TransactionContextStateHandler transactionTerminated ( CompositeTransaction tx ) + { + TransactionContextStateHandler ret = null; + if ( ct.isSameTransaction ( tx ) ) ret = new NotInBranchStateHandler ( getXATransactionalResource() , getXAResource() ); + return ret; + } + + boolean isSuspendedInTransaction ( CompositeTransaction tx ) + { + boolean ret = false; + if ( tx != null && ct.isSameTransaction ( tx ) ) ret = true; + return ret; + } + + public TransactionContextStateHandler transactionResumed() throws InvalidSessionHandleStateException + { + return new BranchEnlistedStateHandler ( getXATransactionalResource() , ct , getXAResource() , branch ); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/InvalidSessionHandleStateException.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/InvalidSessionHandleStateException.java new file mode 100644 index 000000000..3a6ba329f --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/InvalidSessionHandleStateException.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + + + /** + * + * + * + * Exception signaling that the state has + * been corrupted. Occurrences should almost + * invariably cause the session handle to be + * discarded. + * + * + */ + +public class InvalidSessionHandleStateException +extends Exception +{ + + private static final long serialVersionUID = 2838873552114439968L; + + + InvalidSessionHandleStateException ( String msg ) + { + super ( msg ); + } + + + InvalidSessionHandleStateException ( String msg , Exception cause ) + { + super ( msg , cause ); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/NotInBranchStateHandler.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/NotInBranchStateHandler.java new file mode 100644 index 000000000..4febb7c91 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/NotInBranchStateHandler.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.recovery.TxState; + + /** + * + * + * State handler dealing with the situation where there is no + * current XA branch associated with the session. + */ + +class NotInBranchStateHandler extends TransactionContextStateHandler +{ + + NotInBranchStateHandler ( XATransactionalResource resource , XAResource xaResource ) + { + super ( resource , xaResource ); + } + + TransactionContextStateHandler checkEnlistBeforeUse ( CompositeTransaction ct ) throws InvalidSessionHandleStateException + { + TransactionContextStateHandler ret = null; + if ( ct != null && TransactionManagerImp.isJtaTransaction(ct) ) { + if ( TxState.MARKED_ABORT.equals ( ct.getState() ) ) { + //see case 27857 + throw new InvalidSessionHandleStateException ( + "Transaction is marked for rollback only or has timed out" + ); + } + + //JTA transaction found for calling thread -> enlist + //also see the state diagram documentation + ret = new BranchEnlistedStateHandler ( getXATransactionalResource() , ct , getXAResource() ); + + } + return ret; + } + + + TransactionContextStateHandler sessionClosed() + { + //see the state diagram documentation + return new TerminatedStateHandler(); + } + + TransactionContextStateHandler transactionTerminated ( CompositeTransaction ct ) + { + return null; + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/SessionHandleState.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/SessionHandleState.java new file mode 100644 index 000000000..33a479912 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/SessionHandleState.java @@ -0,0 +1,292 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * + * + * A reusable state tracker for XA session/connection handles. + * An instance of this class can be used for automatically tracking the + * enlistment and termination states of all branches + * that a connection handle is involved in. It does this by switching states + * behind the scenes, so the same instance can be used for several branches. + */ + +public class SessionHandleState +{ + private static final Logger LOGGER = LoggerFactory.createLogger(SessionHandleState.class); + + private TransactionContext currentContext; + private Set allContexts; + private XATransactionalResource resource; + private XAResource xaResource; + private boolean erroneous; + private boolean closed; + private List sessionHandleStateChangeListeners = new ArrayList(); + + + public SessionHandleState ( XATransactionalResource resource , XAResource xaResource ) + { + this.resource = resource; + this.xaResource = xaResource; + this.allContexts = new HashSet(); + this.erroneous = false; + this.closed = true; + } + + /** + * Checks if the session handle is terminated (i.e., can be discarded) and the + * underlying vendor xa connection/session can be reused or destroyed. + * + * @return True if the underlying vendor connection can be reused or destroyed. + * The session handle itself (i.e., the Atomikos proxy) should be discarded. + */ + + public synchronized boolean isTerminated() + { + boolean terminated = true; + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("isTerminated: checking " + allContexts.size() + " contexts..."); + } + Iterator it = allContexts.iterator(); + while ( it.hasNext() ) { + TransactionContext b = ( TransactionContext ) it.next(); + if ( b.isTerminated() ) { + it.remove(); + } + else terminated = false; + } + + if ( terminated ) currentContext = null; + + return terminated; + } + + /** + * Notification that the session was gotten from the pool. + * + * + */ + public synchronized void notifySessionBorrowed() + { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": notifySessionBorrowed" ); + currentContext = new TransactionContext ( resource , xaResource ); + allContexts.add ( currentContext ); + closed = false; + } + + /** + * Notification that the session handle has been closed by + * the application. + * + */ + + public void notifySessionClosed() + { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": entering notifySessionClosed" ); + boolean notifyOfClosedEvent = false; + + synchronized ( this ) { + boolean alreadyTerminated = isTerminated(); + Iterator it = allContexts.iterator(); + while ( it.hasNext() ) { + TransactionContext b = it.next(); + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace ("delegating session close to " + b ); // avoid "this" / toString - see case 201164 + } + b.sessionClosed(); + } + closed = true; + if ( isTerminated() && !alreadyTerminated ) notifyOfClosedEvent = true; + } + //do callbacks out of synch!!! + if ( notifyOfClosedEvent ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": all contexts terminated, firing TerminatedEvent" ); + fireTerminatedEvent(); + } + } + + /** + * Notification that the session handle is about to be used in the current + * transaction context (i.e. whatever transaction exists for the calling thread). + * This method MUST be called BEFORE any work is delegated to the underlying + * vendor connection. + * @param ct The current transaction, or null if none. + * + * @throws InvalidSessionHandleStateException + */ + + public synchronized void notifyBeforeUse ( CompositeTransaction ct ) throws InvalidSessionHandleStateException + { + if ( closed ) throw new InvalidSessionHandleStateException ( "The underlying XA session is closed" ); + + try { + //first check if a suspended context exists for the current tx; + //this happens if a transaction was suspended and now resumed + TransactionContext suspended = null; + if ( ct != null ) { + Iterator it = allContexts.iterator(); + while ( it.hasNext() && suspended == null ) { + TransactionContext b = ( TransactionContext ) it.next(); + if ( b.isSuspendedInTransaction ( ct ) ) { + suspended = b; + } + } + } + //check enlistment + if ( suspended != null ) { + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": resuming suspended XA context for transaction " + ct.getTid() ); + currentContext = suspended; + currentContext.transactionResumed(); + } + else { + //no suspended branch was found -> try to use the current branch + try { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": checking XA context for transaction " + ct ); + currentContext.checkEnlistBeforeUse ( ct ); + } + catch ( UnexpectedTransactionContextException txBoundaryPassed ) { + //we are being used in a different context than expected -> suspend! + if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( this + ": suspending existing XA context and creating a new one for transaction " + ct ); + currentContext.transactionSuspended(); + currentContext = new TransactionContext ( resource , xaResource ); + allContexts.add ( currentContext ); + //note: we keep all branches - if the new current branch is a Subtransaction + //then it will not terminate early and needs to stay around + try { + currentContext.checkEnlistBeforeUse ( ct ); + } catch ( UnexpectedTransactionContextException e ) { + String msg = "Unexpected error in session handle"; + LOGGER.logError ( msg , e ); + throw new InvalidSessionHandleStateException ( msg ); + } + } + } + } catch ( InvalidSessionHandleStateException e ) { + //avoid reuse in pool + notifySessionErrorOccurred(); + throw e; + } + + } + + /** + * Checks if the session has had any errors. + * This method can be used to decide whether or not to reuse the underlying + * vendor connection in the pool. + * + * @return True if sessionErrorOccurred has been called, false if not. + */ + + public boolean isErroneous() + { + return erroneous; + } + + /** + * Marks this session as erroneous. This has no other effect than that + * isErroneous returns true. + * + */ + + public void notifySessionErrorOccurred() + { + this.erroneous = true; + } + + /** + * Notifies the session that the transaction was terminated. + * + * @param ct + */ + + public void notifyTransactionTerminated ( CompositeTransaction ct ) + { + + boolean notifyOfTerminatedEvent = false; + synchronized ( this ) { + boolean alreadyTerminated = isTerminated(); + Iterator it = allContexts.iterator(); + while ( it.hasNext() ) { + TransactionContext b = it.next(); + b.transactionTerminated ( ct ); + } + if ( isTerminated() && !alreadyTerminated ) notifyOfTerminatedEvent = true; + } + + //check termination status CHANGES - only fire event once for safety! + if ( notifyOfTerminatedEvent ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace( this + ": all contexts terminated, firing TerminatedEvent for " + this); + fireTerminatedEvent(); + } + } + + + public void registerSessionHandleStateChangeListener(SessionHandleStateChangeListener listener) + { + sessionHandleStateChangeListeners.add(listener); + } + + public void unregisterSessionHandleStateChangeListener(SessionHandleStateChangeListener listener) + { + sessionHandleStateChangeListeners.remove(listener); + } + + private void fireTerminatedEvent() + { + for (int i=0; i + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + + +public interface SessionHandleStateChangeListener { + + /** + * fired when all contexts of the SessionHandleState changed to terminated state + */ + void onTerminated(); + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TerminatedStateHandler.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TerminatedStateHandler.java new file mode 100644 index 000000000..7dae752e0 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TerminatedStateHandler.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +class TerminatedStateHandler +extends TransactionContextStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(TerminatedStateHandler.class); + + TerminatedStateHandler() + { + super ( null , null ); + } + + TransactionContextStateHandler checkEnlistBeforeUse ( CompositeTransaction ct) throws InvalidSessionHandleStateException + { + String msg = "Detected illegal attempt to use a terminated XA session"; + LOGGER.logError ( msg ); + throw new InvalidSessionHandleStateException ( msg ); + } + + TransactionContextStateHandler sessionClosed() + { + return null; + } + + TransactionContextStateHandler transactionTerminated ( CompositeTransaction ct ) + { + return null; + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TransactionContext.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TransactionContext.java new file mode 100644 index 000000000..1d1de1ae8 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TransactionContext.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * This class represents a particular branch association of + * the session handle. There is one instance for each + * such association of a session handle. + */ +class TransactionContext { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionContext.class); + + private TransactionContextStateHandler state; // never null + + TransactionContext ( XATransactionalResource resource , XAResource xaResource ) { + // we're not transactional until we are actually used + setState ( new NotInBranchStateHandler ( resource , xaResource ) ); + } + + private synchronized void setState ( TransactionContextStateHandler state ) { + if ( state != null ) { // null if no change + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": changing to state " + state ); + this.state = state; + } + } + + /** + * Checks if the session handle state is terminated (i.e., if the session handle + * can be discarded and the underlying connection can be closed). + * @return True if terminated. + */ + synchronized boolean isTerminated() { + return state instanceof TerminatedStateHandler; + } + + /** + * Checks and enlists with the current transaction if appropriate. + * @param ct The current transaction. + * @param hmsg The heuristic message. + * @throws InvalidSessionHandleStateException If the current handle is being used in an unexpected (wrong) context. + * @throws UnexpectedTransactionContextException If the current transaction context is not what would be expected. + */ + synchronized void checkEnlistBeforeUse ( CompositeTransaction ct ) throws InvalidSessionHandleStateException, UnexpectedTransactionContextException { + TransactionContextStateHandler nextState = state.checkEnlistBeforeUse ( ct ); + setState ( nextState ); + } + + /** + * Notification that the session handle was closed. + * + */ + synchronized void sessionClosed() { + TransactionContextStateHandler nextState = state.sessionClosed(); + setState ( nextState ); + } + + /** + * Notification that the branch has been suspended. + * @throws InvalidSessionHandleStateException + * + */ + + synchronized void transactionSuspended() throws InvalidSessionHandleStateException { + TransactionContextStateHandler nextState = state.transactionSuspended(); + setState ( nextState ); + } + + /** + * Notification that the branch has been resumed. + * @throws InvalidSessionHandleStateException + * + */ + + synchronized void transactionResumed() throws InvalidSessionHandleStateException { + TransactionContextStateHandler nextState = state.transactionResumed(); + setState ( nextState ); + } + + /** + * Notification of transaction termination. + * @param ct The transaction. Irrelevant transactions should be ignored. + */ + + synchronized void transactionTerminated ( CompositeTransaction ct ) { + TransactionContextStateHandler nextState = state.transactionTerminated ( ct ); + setState ( nextState ); + } + + /** + * Checks if this branch is suspended in the given transaction. + * @param ct The transaction. + * @return True iff this branch is suspended for the ct. + */ + synchronized boolean isSuspendedInTransaction ( CompositeTransaction ct ) { + return state.isSuspendedInTransaction ( ct ); + } + + public String toString() { + return "a TransactionContext in state " + state; + } + + boolean isInTransaction ( CompositeTransaction tx ) { + return state.isInTransaction ( tx ); + } + + boolean isInactiveInTransaction(CompositeTransaction tx) { + return state.isInactiveInTransaction ( tx ); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TransactionContextStateHandler.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TransactionContextStateHandler.java new file mode 100644 index 000000000..c44f8c09e --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/TransactionContextStateHandler.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; + + /** + * + * + * The common logic for the branch state handlers. + * Methods that can lead to a state change return + * a state handler object, or null if no state change + * should occur. In general, the methods here + * correspond to all relevant events; it is up to + * each subclass to ignore those events that are + * irrelevant to it. + * + */ + +abstract class TransactionContextStateHandler +{ + + private XATransactionalResource resource; + private XAResource xaResource; + + TransactionContextStateHandler ( XATransactionalResource resource , XAResource xaResource ) + { + this.resource = resource; + this.xaResource = xaResource; + } + + XATransactionalResource getXATransactionalResource() + { + return resource; + } + + XAResource getXAResource() + { + return xaResource; + } + + /** + * Checks and performs an XA enlist if needed. + * @param ct The transaction to enlist with, null if none. + * + * @return The next state, or null if no change. + * + * @throws InvalidSessionHandleStateException If the state does not allow + * enlisting for the given transaction. + * + * @throws UnexpectedTransactionContextException If the transaction context is not + * what was expected by this state. + * + */ + + abstract TransactionContextStateHandler checkEnlistBeforeUse ( CompositeTransaction ct ) + throws InvalidSessionHandleStateException, UnexpectedTransactionContextException; + + /** + * Notification that the session has been + * closed by the application. + * + * @return The next state, or null if no change. + */ + + abstract TransactionContextStateHandler sessionClosed(); + + /** + * Notification that the transaction has been terminated. + * @param ct The transaction. Irrelevant transactions should be ignored. + * @return The next state, or null if no change. + */ + + abstract TransactionContextStateHandler transactionTerminated ( CompositeTransaction ct ); + + /** + * Checks if the branch is suspended for this tx. + * @param ct The transaction + * @return True iff suspended in this transaction. + */ + + boolean isSuspendedInTransaction ( CompositeTransaction ct ) + { + return false; + } + + /** + * Notification that the current branch is being suspended. + * @return The next state, or null if no change. + * @throws InvalidSessionHandleStateException + */ + + TransactionContextStateHandler transactionSuspended() throws InvalidSessionHandleStateException + { + throw new InvalidSessionHandleStateException ( "Could not suspend in state: " + this ); + } + + /** + * Notification that the current branch is being resumed. + * @return The next state, or null if no change. + * @throws InvalidSessionHandleStateException + */ + + TransactionContextStateHandler transactionResumed() throws InvalidSessionHandleStateException + { + throw new InvalidSessionHandleStateException ( "Could not resume in state: " + this ); + } + + /** + * Tests if the state is active in the given transaction. + * @param tx + * @return + */ + + boolean isInTransaction ( CompositeTransaction tx ) + { + return false; + } + + /** + * Tests if the state is inactive in the given transaction. + * @param tx + * @return + */ + boolean isInactiveInTransaction ( CompositeTransaction tx ) + { + + return false; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/UnexpectedTransactionContextException.java b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/UnexpectedTransactionContextException.java new file mode 100644 index 000000000..4e3859606 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/datasource/xa/session/UnexpectedTransactionContextException.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa.session; + + + /** + * + * + * + * An exception to signal that an unexpected + * transaction context exists for the current thread. + */ + +class UnexpectedTransactionContextException +extends Exception +{ + + private static final long serialVersionUID = -1180906964991835993L; + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/ExtendedSystemException.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/ExtendedSystemException.java new file mode 100644 index 000000000..a23e9d6f6 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/ExtendedSystemException.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import javax.transaction.SystemException; + +/** + * A better system exception, containing throwable cause. + */ + +public class ExtendedSystemException extends SystemException { + + private static final long serialVersionUID = 1475357523769839371L; + + public ExtendedSystemException(String msg, Throwable cause) { + super(msg); + initCause(cause); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/J2eeTransactionManager.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/J2eeTransactionManager.java new file mode 100644 index 000000000..57370d9a1 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/J2eeTransactionManager.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.io.Serializable; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.atomikos.util.SerializableObjectFactory; + +/** + * An implementation of TransactionManager that should be used by J2EE + * applications. Instances can be bound in JNDI if the application server allows + * this. + */ +public class J2eeTransactionManager +implements TransactionManager, Serializable, Referenceable, UserTransaction +{ + + private static final long serialVersionUID = 8584376600562353607L; + + private transient TransactionManagerImp tm; + + private void checkSetup() throws SystemException + { + tm = (TransactionManagerImp) TransactionManagerImp.getTransactionManager (); + if (tm == null) { + throw new RuntimeException("Transaction Service not running?"); + } + } + + /** + * @see javax.transaction.TransactionManager#begin() + */ + public void begin() throws NotSupportedException, SystemException + { + checkSetup(); + tm.begin(); + + } + + /** + * @see javax.transaction.TransactionManager#commit() + */ + public void commit() throws RollbackException, HeuristicMixedException, + HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException + { + checkSetup(); + tm.commit(); + + } + + /** + * @see javax.transaction.TransactionManager#getStatus() + */ + public int getStatus() throws SystemException + { + checkSetup(); + return tm.getStatus(); + } + + /** + * @see javax.transaction.TransactionManager#getTransaction() + */ + public Transaction getTransaction() throws SystemException + { + checkSetup(); + return tm.getTransaction(); + } + + /** + * @see javax.transaction.TransactionManager#resume(javax.transaction.Transaction) + */ + public void resume(Transaction tx) throws InvalidTransactionException, + IllegalStateException, SystemException + { + checkSetup(); + tm.resume(tx); + + } + + /** + * @see javax.transaction.TransactionManager#rollback() + */ + public void rollback() throws IllegalStateException, SecurityException, + SystemException + { + checkSetup(); + tm.rollback(); + + } + + /** + * @see javax.transaction.TransactionManager#setRollbackOnly() + */ + public void setRollbackOnly() throws IllegalStateException, + SystemException + { + checkSetup(); + tm.setRollbackOnly(); + + } + + /** + * @see javax.transaction.TransactionManager#setTransactionTimeout(int) + */ + public void setTransactionTimeout(int secs) throws SystemException + { + checkSetup(); + tm.setTransactionTimeout(secs); + + } + + /** + * @see javax.transaction.TransactionManager#suspend() + */ + public Transaction suspend() throws SystemException + { + checkSetup(); + return tm.suspend(); + } + + public Reference getReference() throws NamingException + { + return SerializableObjectFactory.createReference(this); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/J2eeUserTransaction.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/J2eeUserTransaction.java new file mode 100644 index 000000000..842855e26 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/J2eeUserTransaction.java @@ -0,0 +1,132 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.io.Serializable; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.transaction.NotSupportedException; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.atomikos.util.SerializableObjectFactory; + +/** + * + * + * A J2EE UserTransaction implementation. J2EE applications can use instances of + * this class to delimit transactions. Note: J2EE applications should NOT use + * the default UserTransactionImp in order to avoid that the transaction service + * is started twice in different locations. Instances can be bound in JNDI (if + * the application server allows this). + */ + +public class J2eeUserTransaction implements UserTransaction, Serializable, + Referenceable +{ + + private static final long serialVersionUID = -7656447860674832182L; + + private transient TransactionManager txmgr_; + + public J2eeUserTransaction() + { + } + + /** + * Referenceable mechanism requires later setup of txmgr_, otherwise binding + * into JNDI already requires that TM is running. + */ + + private void checkSetup() + { + txmgr_ = TransactionManagerImp.getTransactionManager (); + if (txmgr_ == null) throw new RuntimeException ( "Transaction Service Not Running?" ); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void begin() throws NotSupportedException, SystemException + { + checkSetup(); + txmgr_.begin(); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void commit() throws javax.transaction.RollbackException, + javax.transaction.HeuristicMixedException, + javax.transaction.HeuristicRollbackException, + javax.transaction.SystemException, java.lang.IllegalStateException, + java.lang.SecurityException + { + checkSetup(); + txmgr_.commit(); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void rollback() throws IllegalStateException, SystemException, + SecurityException + { + checkSetup(); + txmgr_.rollback(); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void setRollbackOnly() throws IllegalStateException, + SystemException + { + checkSetup (); + txmgr_.setRollbackOnly (); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public int getStatus() throws SystemException + { + checkSetup(); + return txmgr_.getStatus(); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void setTransactionTimeout(int seconds) throws SystemException + { + checkSetup(); + txmgr_.setTransactionTimeout(seconds); + } + + // + // + // IMPLEMENTATION OF REFERENCEABLE + // + // + + public Reference getReference() throws NamingException + { + return SerializableObjectFactory.createReference(this); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/JtaTransactionServicePlugin.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/JtaTransactionServicePlugin.java new file mode 100644 index 000000000..f911494c1 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/JtaTransactionServicePlugin.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import com.atomikos.icatch.TransactionServicePlugin; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.RecoveryLog; +import com.atomikos.recovery.xa.XARecoveryManager; + +public class JtaTransactionServicePlugin implements TransactionServicePlugin { + + private static Logger LOGGER = LoggerFactory.createLogger(JtaTransactionServicePlugin.class); + + + /** + * The name of the property that specifies the default timeout (in + * milliseconds) that is set for transactions when no timeout is specified. + * + * Expands to {@value}. + */ + public static final String DEFAULT_JTA_TIMEOUT_PROPERTY_NAME = "com.atomikos.icatch.default_jta_timeout"; + + /** + * The name of the property that indicates whether JTA transactions are to + * be in serial mode or not. + * + * Expands to {@value}. + */ + public static final String SERIAL_JTA_TRANSACTIONS_PROPERTY_NAME = "com.atomikos.icatch.serial_jta_transactions"; + + /** + * The name of the property indicating whether remote clients can start + * transactions on this service or not. + * + * Expands to {@value}. + */ + public static final String CLIENT_DEMARCATION_PROPERTY_NAME = "com.atomikos.icatch.client_demarcation"; + + + @Override + public void beforeInit() { + ConfigProperties configProperties = Configuration.getConfigProperties(); + long defaultTimeoutInMillis = configProperties.getAsLong(DEFAULT_JTA_TIMEOUT_PROPERTY_NAME); + int defaultJtaTimeout = 0; + defaultJtaTimeout = (int) defaultTimeoutInMillis/1000; + if ( defaultJtaTimeout <= 0 ) { + LOGGER.logWarning ( "WARNING: " + DEFAULT_JTA_TIMEOUT_PROPERTY_NAME + " should be more than 1000 milliseconds - resetting to 10000 milliseconds instead..." ); + defaultJtaTimeout = 10; + } + TransactionManagerImp.setDefaultTimeout(defaultJtaTimeout); + boolean defaultSerial = configProperties.getAsBoolean(SERIAL_JTA_TRANSACTIONS_PROPERTY_NAME); + TransactionManagerImp.setDefaultSerial(defaultSerial); + + } + + + @Override + public void afterShutdown() { + TransactionManagerImp.installTransactionManager ( null ); + XARecoveryManager.installXARecoveryManager(null); + } + + @Override + public void afterInit() { + TransactionManagerImp.installTransactionManager(Configuration.getCompositeTransactionManager()); + RecoveryLog recoveryLog = Configuration.getRecoveryLog(); + if (recoveryLog != null) { + XARecoveryManager.installXARecoveryManager(Configuration.getConfigProperties().getTmUniqueName()); + } + + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/ResumePreviousTransactionSubTxAwareParticipant.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/ResumePreviousTransactionSubTxAwareParticipant.java new file mode 100644 index 000000000..19a1d91e5 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/ResumePreviousTransactionSubTxAwareParticipant.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.util.Assert; + +/** + * A subtx aware participant that resumes a previous + * transaction upon termination of the (sub)transaction + * it is registered with. + * + */ +public class ResumePreviousTransactionSubTxAwareParticipant implements + SubTxAwareParticipant +{ + + private static final Logger LOGGER = LoggerFactory.createLogger(ResumePreviousTransactionSubTxAwareParticipant.class); + + private CompositeTransaction previous; + + public ResumePreviousTransactionSubTxAwareParticipant ( + CompositeTransaction previous ) + { + Assert.notNull( "Previous transaction is null?", previous ); + this.previous = previous; + } + + private void resume() + { + CompositeTransactionManager ctm = + Configuration.getCompositeTransactionManager(); + if ( ctm == null ) { + LOGGER.logWarning ( "ResumePreviousTransactionSubTxAwareParticipant: no transaction manager found?" ); + } else { + try { + ctm.resume ( previous ); + } + catch ( Exception error ) { + LOGGER.logWarning ( "ResumePreviousTransactionSubTxAwareParticipant: could not resume previous transaction" , error ); + } + } + + } + + public void committed ( CompositeTransaction tx ) + { + resume(); + } + + public void rolledback ( CompositeTransaction tx ) + { + resume(); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/Sync2Sync.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/Sync2Sync.java new file mode 100644 index 000000000..b83cebe26 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/Sync2Sync.java @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import javax.transaction.Status; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +class Sync2Sync implements com.atomikos.icatch.Synchronization +{ + + private static final Logger LOGGER = LoggerFactory.createLogger(Sync2Sync.class); + + private javax.transaction.Synchronization sync; + + private Boolean committed; //null for readonly + + Sync2Sync ( javax.transaction.Synchronization sync ) + { + this.sync = sync; + } + + @Override + public void beforeCompletion () + { + this.sync.beforeCompletion (); + resetForReuse(); + LOGGER.logDebug("beforeCompletion() called on Synchronization: " + this.sync.toString()); + } + + private void resetForReuse() { + this.committed = null; + } + + @Override + public void afterCompletion ( TxState state ) + { + if ( state == TxState.TERMINATED ) { + if ( this.committed == null ) { //readonly: unknown + this.sync.afterCompletion ( Status.STATUS_UNKNOWN ); + LOGGER.logDebug ( "afterCompletion ( STATUS_UNKNOWN ) called " + + " on Synchronization: " + this.sync.toString () ); + } else { + boolean commit = this.committed.booleanValue (); + if ( commit ) { + this.sync.afterCompletion ( Status.STATUS_COMMITTED ); + LOGGER.logDebug ( "afterCompletion ( STATUS_COMMITTED ) called " + + " on Synchronization: " + + this.sync.toString () ); + } else { + this.sync.afterCompletion ( Status.STATUS_ROLLEDBACK ); + LOGGER.logDebug ( "afterCompletion ( STATUS_ROLLEDBACK ) called " + + " on Synchronization: " + + this.sync.toString () ); + } + } + } else if ( state == TxState.COMMITTING ) this.committed = Boolean.TRUE; + else if ( state == TxState.ABORTING ) this.committed = Boolean.FALSE; + + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionImp.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionImp.java new file mode 100644 index 000000000..fc5a5ac13 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionImp.java @@ -0,0 +1,473 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.datasource.ResourceException; +import com.atomikos.datasource.TransactionalResource; +import com.atomikos.datasource.xa.XAResourceTransaction; +import com.atomikos.datasource.xa.XATransactionalResource; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * Implementation of the javax.transaction.Transaction interface. + */ + +class TransactionImp implements Transaction { + private static final Logger LOGGER = LoggerFactory + .createLogger(TransactionImp.class); + + private static void rethrowAsJtaRollbackException(String msg, Throwable cause) + throws javax.transaction.RollbackException { + javax.transaction.RollbackException ret = new javax.transaction.RollbackException( + msg); + ret.initCause(cause); + throw ret; + } + + private static void rethrowAsJtaHeuristicMixedException(String msg, Throwable cause) + throws javax.transaction.HeuristicMixedException { + javax.transaction.HeuristicMixedException ret = new javax.transaction.HeuristicMixedException( + msg); + ret.initCause(cause); + throw ret; + } + + private static void rethrowAsJtaHeuristicRollbackException(String msg, + Throwable cause) + throws javax.transaction.HeuristicRollbackException { + javax.transaction.HeuristicRollbackException ret = new javax.transaction.HeuristicRollbackException( + msg); + ret.initCause(cause); + throw ret; + } + + private CompositeTransaction compositeTransaction; + + private Map xaResourceToResourceTransactionMap_; + + private Map transactionSynchronizationRegistryMap; + + private List interposedSynchronizations; + + TransactionImp(CompositeTransaction ct) { + this.compositeTransaction = ct; + this.xaResourceToResourceTransactionMap_ = new HashMap(); + this.interposedSynchronizations = new ArrayList<>(); + this.transactionSynchronizationRegistryMap = new HashMap(); + } + + CompositeTransaction getCT() { + return this.compositeTransaction; + } + + private synchronized void addXAResourceTransaction( + XAResourceTransaction restx, XAResource xares) { + this.xaResourceToResourceTransactionMap_.put(new XAResourceKey(xares), + restx); + } + + private void assertActiveOrSuspended(XAResourceTransaction restx) { + if (!(restx.isActive() || restx.isXaSuspended())) { + LOGGER.logWarning("Unexpected resource transaction state for " + restx); + } + } + + private synchronized XAResourceTransaction findXAResourceTransaction( + XAResource xares) { + XAResourceTransaction ret = null; + ret = this.xaResourceToResourceTransactionMap_.get(new XAResourceKey( + xares)); + if (ret != null) { + assertActiveOrSuspended(ret); + } + return ret; + } + + private synchronized void removeXAResourceTransaction(XAResource xares) { + this.xaResourceToResourceTransactionMap_ + .remove(new XAResourceKey(xares)); + } + + /** + * @see javax.transaction.Transaction + */ + + @Override + public void registerSynchronization(javax.transaction.Synchronization s) + throws java.lang.IllegalStateException, + javax.transaction.SystemException { + try { + Sync2Sync adaptor = new Sync2Sync(s); + this.compositeTransaction.registerSynchronization(adaptor); + } catch (SysException se) { + String msg = "Unexpected error during registerSynchronization"; + LOGGER.logWarning(msg, se); + throw new ExtendedSystemException(msg, se); + } + + } + + /** + * @see javax.transaction.Transaction + */ + + @Override + public int getStatus() { + TxState state = this.compositeTransaction.getState(); + switch (state) { + case IN_DOUBT: + return Status.STATUS_PREPARED; + case PREPARING: + return Status.STATUS_PREPARING; + case ACTIVE: + return Status.STATUS_ACTIVE; + case MARKED_ABORT: + return Status.STATUS_MARKED_ROLLBACK; + case COMMITTING: + return Status.STATUS_COMMITTING; + case ABORTING: + return Status.STATUS_ROLLING_BACK; + case COMMITTED: + return Status.STATUS_COMMITTED; + case ABORTED: + return Status.STATUS_ROLLEDBACK; + default: + return Status.STATUS_UNKNOWN; + } + } + + /** + * @see javax.transaction.Transaction. + */ + + @Override + public void commit() throws javax.transaction.RollbackException, + javax.transaction.HeuristicMixedException, + javax.transaction.HeuristicRollbackException, + javax.transaction.SystemException, java.lang.SecurityException { + registerInterposedSynchronizations(); + try { + this.compositeTransaction.commit(); + } catch (HeurHazardException hh) { + rethrowAsJtaHeuristicMixedException(hh.getMessage(), hh); + } catch (HeurMixedException hm) { + rethrowAsJtaHeuristicMixedException(hm.getMessage(), hm); + } catch (SysException se) { + LOGGER.logError(se.getMessage(), se); + throw new ExtendedSystemException(se.getMessage(), se); + } catch (com.atomikos.icatch.RollbackException rb) { + // see case 29708: all statements have been closed + String msg = rb.getMessage(); + Throwable cause = rb.getCause(); + if (cause == null) + cause = rb; + rethrowAsJtaRollbackException(msg, cause); + } + } + + /** + * @see javax.transaction.Transaction. + */ + + @Override + public void rollback() throws IllegalStateException, SystemException { + registerInterposedSynchronizations(); + try { + this.compositeTransaction.rollback(); + } catch (SysException se) { + LOGGER.logError(se.getMessage(), se); + throw new ExtendedSystemException(se.getMessage(), se); + } + + } + + /** + * @see javax.transaction.Transaction. + */ + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + this.compositeTransaction.setRollbackOnly(); + } + + /** + * @see javax.transaction.Transaction. + */ + + @Override + public boolean enlistResource(XAResource xares) + throws javax.transaction.RollbackException, + javax.transaction.SystemException, IllegalStateException { + TransactionalResource res = null; + XAResourceTransaction restx = null; + int status = getStatus(); + switch (status) { + case Status.STATUS_MARKED_ROLLBACK: + case Status.STATUS_ROLLEDBACK: + case Status.STATUS_ROLLING_BACK: + String msg = "Transaction rollback - enlisting more resources is useless."; + LOGGER.logWarning(msg); + throw new javax.transaction.RollbackException(msg); + case Status.STATUS_COMMITTED: + case Status.STATUS_PREPARED: + case Status.STATUS_UNKNOWN: + msg = "Enlisting more resources is no longer permitted: transaction is in state " + + this.compositeTransaction.getState(); + LOGGER.logWarning(msg); + throw new IllegalStateException(msg); + } + + XAResourceTransaction suspendedXAResourceTransaction = findXAResourceTransaction(xares); + + if (suspendedXAResourceTransaction != null) { + + if (!suspendedXAResourceTransaction.isXaSuspended()) { + String msg = "The given XAResource instance is being enlisted a second time without delist in between?"; + LOGGER.logWarning(msg); + throw new IllegalStateException(msg); + } + + // note: for suspended XAResources, the lookup MUST SUCCEED + // since the TMRESUME must be called on the SAME XAResource + // INSTANCE, and lookup also works on the instance level + try { + suspendedXAResourceTransaction.setXAResource(xares); + suspendedXAResourceTransaction.xaResume(); + } catch (XAException xaerr) { + if (XAException.XA_RBBASE <= xaerr.errorCode + && xaerr.errorCode <= XAException.XA_RBEND) + rethrowAsJtaRollbackException( + "Transaction was already rolled back inside the back-end resource. Further enlists are useless.", + xaerr); + throw new ExtendedSystemException( + "Unexpected error during enlist", xaerr); + } + + } else { + + res = findRecoverableResourceForXaResource(xares); + + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("enlistResource ( " + xares + + " ) with transaction " + toString()); + } + + if (res == null) { + String msg = "There is no registered resource that can recover the given XAResource instance. " + + "\n" + + "Please register a corresponding resource first."; + LOGGER.logWarning(msg); + throw new javax.transaction.SystemException(msg); + } + + try { + restx = (XAResourceTransaction) res + .getResourceTransaction(this.compositeTransaction); + + // next, we MUST set the xa resource again, + // because ONLY the instance we got as argument + // is available for use now ! + // older instances (set in restx from previous sibling) + // have connections that may be in reuse already + // ->old xares not valid except for 2pc operations + + restx.setXAResource(xares); + restx.resume(); + } catch (ResourceException re) { + throw new ExtendedSystemException( + "Unexpected error during enlist", re); + } catch (RuntimeException e) { + throw e; + } + + addXAResourceTransaction(restx, xares); + } + + return true; + } + + private TransactionalResource findRecoverableResourceForXaResource( + XAResource xares) { + TransactionalResource ret = null; + XATransactionalResource xatxres; + + synchronized (Configuration.class) { + // synchronized to avoid case 61740 and 142795 + + Collection resources = Configuration.getResources(); + for (RecoverableResource rres : resources) { + if (rres instanceof XATransactionalResource) { + xatxres = (XATransactionalResource) rres; + if (xatxres.usesXAResource(xares)) + ret = xatxres; + } + } + + } + + return ret; + } + + /** + * @see javax.transaction.Transaction. + */ + + @Override + public boolean delistResource(XAResource xares, int flag) + throws java.lang.IllegalStateException, + javax.transaction.SystemException { + + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("delistResource ( " + xares + " ) with transaction " + + toString()); + } + + XAResourceTransaction active = findXAResourceTransaction(xares); + // NOTE: the lookup MUST have succeeded since the delist must be + // done by the same XAResource INSTANCE as the enlist before, + // and lookup also uses instance comparison. + if (active == null) { + String msg = "Illegal attempt to delist an XAResource instance that was not previously enlisted."; + LOGGER.logWarning(msg); + throw new IllegalStateException(msg); + } + + if (flag == XAResource.TMSUCCESS || flag == XAResource.TMFAIL) { + + try { + active.suspend(); + } catch (ResourceException re) { + throw new ExtendedSystemException( + "Error in delisting the given XAResource", re); + } + removeXAResourceTransaction(xares); + if (flag == XAResource.TMFAIL) + setRollbackOnly(); + + } else if (flag == XAResource.TMSUSPEND) { + try { + active.xaSuspend(); + } catch (XAException xaerr) { + throw new ExtendedSystemException( + "Error in delisting the given XAResource", xaerr); + } + + } else { + String msg = "Unknown delist flag: " + flag; + LOGGER.logWarning(msg); + throw new javax.transaction.SystemException(msg); + } + return true; + } + + /** + * Compares to another object. + * + * @param o + * The other object to compare to. + * + * @return boolean True iff the underlying tx is the same. + */ + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof TransactionImp)) + return false; + TransactionImp other = (TransactionImp) o; + return this.compositeTransaction.isSameTransaction(other.compositeTransaction); + } + + /** + * Computes a hash value for the object. + * + * @return int The hash value. + */ + + @Override + public int hashCode() { + return this.compositeTransaction.hashCode(); + } + + @Override + public String toString() { + return this.compositeTransaction.getTid().toString(); + } + + void suspendEnlistedXaResources() throws ExtendedSystemException { + // cf case 61305 + Iterator xaResourceTransactions = this.xaResourceToResourceTransactionMap_ + .values().iterator(); + while (xaResourceTransactions.hasNext()) { + XAResourceTransaction resTx = xaResourceTransactions.next(); + try { + resTx.xaSuspend(); + } catch (XAException e) { + throw new ExtendedSystemException( + "Error in suspending the given XAResource", e); + } + } + } + + void resumeEnlistedXaReources() throws ExtendedSystemException { + Iterator xaResourceTransactions = this.xaResourceToResourceTransactionMap_ + .values().iterator(); + while (xaResourceTransactions.hasNext()) { + XAResourceTransaction resTx = xaResourceTransactions.next(); + try { + resTx.xaResume(); + xaResourceTransactions.remove(); + } catch (XAException e) { + throw new ExtendedSystemException( + "Error in resuming the given XAResource", e); + } + } + } + + public void registerInterposedSynchronization(javax.transaction.Synchronization s) + throws java.lang.IllegalStateException, + javax.transaction.SystemException { + this.interposedSynchronizations.add(s); + } + + private void registerInterposedSynchronizations() throws IllegalStateException, SystemException { + for (javax.transaction.Synchronization s : interposedSynchronizations) { + registerSynchronization(s); + } + } + + public void putResource(Object key, Object value) { + transactionSynchronizationRegistryMap.put(key, value); + } + + public Object getResource(Object key) { + return transactionSynchronizationRegistryMap.get(key); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionManagerFactory.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionManagerFactory.java new file mode 100644 index 000000000..36e7b9da7 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionManagerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +/** + * + * + * + * + * + * An Object factory for the Transaction Manager. + */ + +public class TransactionManagerFactory implements ObjectFactory +{ + + /** + * + */ + + public TransactionManagerFactory () + { + super (); + + } + + /** + * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, + * javax.naming.Name, javax.naming.Context, java.util.Hashtable) + */ + + @SuppressWarnings("rawtypes") + public Object getObjectInstance ( Object obj , Name reg , Context arg2 , + Hashtable arg3 ) throws Exception + { + Object ret = null; + if ( obj == null || !(obj instanceof Reference) ) + return null; + + Reference ref = (Reference) obj; + if ( ref.getClassName ().equals ( + "com.atomikos.icatch.jta.TransactionManagerImp" ) ) + ret = TransactionManagerImp.getTransactionManager (); + else if ( ref.getClassName ().equals ( + "com.atomikos.icatch.jta.J2eeTransactionManager" ) ) + ret = new J2eeTransactionManager (); + else + ret = null; + + return ret; + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionManagerImp.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionManagerImp.java new file mode 100644 index 000000000..e5a94f2cf --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionManagerImp.java @@ -0,0 +1,490 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.util.HashMap; +import java.util.Map; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.naming.StringRefAddr; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * The main JTA transaction manager singleton. + */ + +public class TransactionManagerImp implements TransactionManager, + SubTxAwareParticipant, Referenceable, UserTransaction + +{ + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionManagerImp.class); + + + /** + * Transaction property name to indicate that the transaction is a + * JTA transaction. If this property is set to an arbitrary non-null + * value then a JTA transaction is assumed by this class. + * This property is used for detecting incompatible existing transactions: + * if a transaction exists without this property then the begin method + * will suspend it (and resume afterwards). + */ + public static final String JTA_PROPERTY_NAME = "com.atomikos.icatch.jta.transaction"; + + private static TransactionManagerImp singleton = null; + + private static int defaultTimeoutInSecondsForNewTransactions; + + private static boolean jtaTransactionsAreSerialByDefault = false; + + private ThreadLocal timeoutInSecondsForNewTransactions = new ThreadLocal() { + protected Integer initialValue() { + return defaultTimeoutInSecondsForNewTransactions; + }; + }; + + private Map jtaTransactionToCoreTransactionMap; + + private CompositeTransactionManager compositeTransactionManager; + + + + private static final void raiseNoTransaction() + { + StringBuffer msg = new StringBuffer(); + msg.append ( "This method needs a transaction for the calling thread and none exists.\n" ); + msg.append ( "Possible causes: either you didn't start a transaction,\n" ); + msg.append ( "it rolledback due to timeout, or it was committed already.\n" ); + msg.append ( "ACTIONS: You can try one of the following: \n" ); + msg.append ( "1. Make sure you started a transaction for the thread.\n" ); + msg.append ( "2. Make sure you didn't terminate it yet.\n" ); + msg.append ( "3. Increase the transaction timeout to avoid automatic rollback of long transactions;\n" ); + msg.append ( " check http://www.atomikos.com/Documentation/JtaProperties for how to do this." ); + LOGGER.logWarning ( msg.toString() ); + throw new IllegalStateException ( msg.toString() ); + } + + /** + * Sets the default serial mode for new txs. + * + * @param value + * If true, then new txs will be set to serial mode. + */ + + public static void setDefaultSerial ( boolean value ) + { + jtaTransactionsAreSerialByDefault = value; + } + + /** + * Gets the default mode for new txs. + * + * @return boolean If true, then new txs started through here will be in + * serial mode. + */ + + public static boolean getDefaultSerial () + { + return jtaTransactionsAreSerialByDefault; + } + + /** + * Set the default transaction timeout value. + * + * @param defaultTimeoutValueInSeconds + * the default transaction timeout value in seconds. + */ + public static void setDefaultTimeout ( int defaultTimeoutValueInSeconds ) + { + defaultTimeoutInSecondsForNewTransactions = defaultTimeoutValueInSeconds; + } + + /** + * Get the default timeout value. + * + * @return the default transaction timeout value in seconds. + */ + public static int getDefaultTimeout () + { + return defaultTimeoutInSecondsForNewTransactions; + } + + + /** + * Install a transaction manager. + * + * @param ctm + * The composite transaction manager to use. + * @param automaticResourceRegistration + * If true, then unknown XAResource instances should lead to the + * addition of a new temporary resource. If false, then unknown + * resources will lead to an exception. + * + */ + + public static synchronized void installTransactionManager ( + CompositeTransactionManager ctm) + { + if ( ctm != null ) { + singleton = new TransactionManagerImp(ctm); + } else { + singleton = null; + } + + } + + /** + * Gets the installed transaction manager, if any. + * + * @return TransactionManager The installed instance, null if none. + */ + + public static TransactionManager getTransactionManager () + { + return singleton; + } + + private TransactionManagerImp(CompositeTransactionManager ctm) + { + compositeTransactionManager = ctm; + jtaTransactionToCoreTransactionMap = new HashMap(); + } + + private void addToMap ( String tid , TransactionImp tx ) + { + synchronized ( jtaTransactionToCoreTransactionMap ) { + jtaTransactionToCoreTransactionMap.put ( tid , tx ); + } + } + + private void removeFromMap ( String tid ) + { + synchronized ( jtaTransactionToCoreTransactionMap ) { + jtaTransactionToCoreTransactionMap.remove ( tid ); + } + } + + /** + * @return TransactionImp The relevant instance, or null. + */ + TransactionImp getJtaTransactionWithId ( String tid ) + { + synchronized ( jtaTransactionToCoreTransactionMap ) { + return jtaTransactionToCoreTransactionMap.get ( tid ); + } + } + + private CompositeTransaction getCompositeTransaction() throws ExtendedSystemException + { + CompositeTransaction ct = null; + try { + ct = compositeTransactionManager.getCompositeTransaction (); + } catch ( SysException se ) { + String msg = "Error while retrieving the transaction for the calling thread"; + LOGGER.logError( msg , se); + throw new ExtendedSystemException ( msg , se ); + } + establishJtaTransactionContextIfNecessary(ct); + return ct; + } + + private void establishJtaTransactionContextIfNecessary( + CompositeTransaction ct) { + if ( isJtaTransaction(ct) ) { + TransactionImp jtaTransaction = getJtaTransactionWithId ( ct.getTid () ); + if ( jtaTransaction == null ) { + recreateCompositeTransactionAsJtaTransaction(ct); + } + } + } + + + + /** + * Gets any previous transaction with the given identifier. + * + * @return Transaction The instance, or null if not found. + */ + + public Transaction getTransaction ( String tid ) + { + return getJtaTransactionWithId ( tid ); + } + + /** + * Creates a new transaction and associate it with the current thread. If the + * current thread already has a transaction, then a local subtransaction + * will be created. + */ + + public void begin () throws NotSupportedException, SystemException + { + + begin ( getTransactionTimeout() ); + } + + /** + * Custom begin to guarantee a timeout value through an argument. + */ + + public void begin ( int timeout ) throws NotSupportedException, + SystemException + { + CompositeTransaction ct = null; + ResumePreviousTransactionSubTxAwareParticipant resumeParticipant = null; + + ct = compositeTransactionManager.getCompositeTransaction(); + if ( ct != null && ct.getProperty ( JTA_PROPERTY_NAME ) == null ) { + LOGGER.logWarning ( "JTA: temporarily suspending incompatible transaction: " + ct.getTid() + + " (will be resumed after JTA transaction ends)" ); + ct = compositeTransactionManager.suspend(); + resumeParticipant = new ResumePreviousTransactionSubTxAwareParticipant ( ct ); + } + + try { + ct = compositeTransactionManager.createCompositeTransaction ( ( ( long ) timeout ) * 1000 ); + if ( resumeParticipant != null ) ct.addSubTxAwareParticipant ( resumeParticipant ); + if(ct.isRoot() && timeout != defaultTimeoutInSecondsForNewTransactions) { + ct.addSubTxAwareParticipant(new SubTxAwareParticipant() { + + @Override + public void rolledback(CompositeTransaction transaction) { + timeoutInSecondsForNewTransactions.set(defaultTimeoutInSecondsForNewTransactions); + } + + @Override + public void committed(CompositeTransaction transaction) { + timeoutInSecondsForNewTransactions.set(defaultTimeoutInSecondsForNewTransactions); + } + }); + } + if ( ct.isRoot () && getDefaultSerial () ) + ct.setSerial (); + markAsJtaTransaction(ct); + + } catch ( SysException se ) { + String msg = "Error in begin()"; + LOGGER.logError( msg , se ); + throw new ExtendedSystemException ( msg , se ); + } + recreateCompositeTransactionAsJtaTransaction(ct); + } + + public static void markAsJtaTransaction(CompositeTransaction ct) { + ct.setProperty ( JTA_PROPERTY_NAME , "true" ); + } + + /** + * @see javax.transaction.TransactionManager + */ + + public Transaction getTransaction () throws SystemException + { + TransactionImp ret = null; + CompositeTransaction ct = getCompositeTransaction(); + if ( ct != null) ret = getJtaTransactionWithId ( ct.getTid () ); + return ret; + } + + private TransactionImp recreateCompositeTransactionAsJtaTransaction( + CompositeTransaction ct) { + TransactionImp ret = null; + if (ct.getState ().equals ( TxState.ACTIVE )) { // setRollbackOnly may have been called! + ret = new TransactionImp(ct); + addToMap ( ct.getTid (), ret ); + ct.addSubTxAwareParticipant ( this ); + } + return ret; + } + + public static boolean isJtaTransaction(CompositeTransaction ct) { + boolean ret = false; + if (ct !=null && ct.getProperty( JTA_PROPERTY_NAME ) != null) ret = true; + return ret; + } + + /** + * @see javax.transaction.TransactionManager + */ + + public void setTransactionTimeout ( int seconds ) throws SystemException + { + if ( seconds > 0 ) { + timeoutInSecondsForNewTransactions.set(seconds); + } else if ( seconds == 0 ) { + timeoutInSecondsForNewTransactions.set(defaultTimeoutInSecondsForNewTransactions); + } else { + String msg = "setTransactionTimeout: value must be >= 0"; + LOGGER.logWarning( msg ); + throw new SystemException ( msg ); + } + + } + + public int getTransactionTimeout () + { + return timeoutInSecondsForNewTransactions.get(); + } + + /** + * @see javax.transaction.TransactionManager + */ + + public Transaction suspend() throws SystemException + { + TransactionImp ret = (TransactionImp) getTransaction(); + if ( ret != null ) { + suspendUnderlyingCompositeTransaction(); + ret.suspendEnlistedXaResources(); // cf case 61305 + } + return ret; + } + + private void suspendUnderlyingCompositeTransaction() + throws ExtendedSystemException { + try { + compositeTransactionManager.suspend(); + } catch ( SysException se ) { + String msg = "Unexpected error while suspending the existing transaction for the current thread"; + LOGGER.logError( msg , se ); + throw new ExtendedSystemException ( msg , se ); + } + } + + /** + * @see javax.transaction.TransactionManager + */ + + public void resume ( Transaction tobj ) throws InvalidTransactionException, + IllegalStateException, SystemException + { + if ( tobj == null || !(tobj instanceof TransactionImp) ) { + String msg = "The specified transaction object is invalid for this configuration: " + tobj; + LOGGER.logWarning( msg ); + throw new InvalidTransactionException ( msg ); + } + + TransactionImp tximp = (TransactionImp) tobj; + try { + compositeTransactionManager.resume ( tximp.getCT () ); + } catch ( SysException se ) { + String msg = "Unexpected error while resuming the transaction in the calling thread"; + LOGGER.logError( msg , se ); + throw new ExtendedSystemException(msg , se ); + } + tximp.resumeEnlistedXaReources(); + + } + + /** + * @see javax.transaction.TransactionManager + */ + + public int getStatus() throws SystemException + { + int ret = Status.STATUS_NO_TRANSACTION; + Transaction tx = getTransaction(); + if ( tx == null ) { + ret = Status.STATUS_NO_TRANSACTION; + } else { + ret = tx.getStatus (); + } + return ret; + } + + /** + * @see javax.transaction.TransactionManager + */ + + public void commit () throws javax.transaction.RollbackException, + javax.transaction.HeuristicMixedException, + javax.transaction.HeuristicRollbackException, + javax.transaction.SystemException, java.lang.IllegalStateException, + java.lang.SecurityException + { + Transaction tx = getTransaction(); + if ( tx == null ) raiseNoTransaction(); + tx.commit(); + } + + /** + * @see javax.transaction.TransactionManager + */ + + public void rollback () throws IllegalStateException, SystemException, + SecurityException + { + Transaction tx = getTransaction(); + if ( tx == null ) raiseNoTransaction(); + tx.rollback(); + } + + /** + * @see javax.transaction.TransactionManager + */ + + public void setRollbackOnly () throws IllegalStateException, + SystemException + { + Transaction tx = getTransaction(); + if ( tx == null ) raiseNoTransaction(); + try { + tx.setRollbackOnly (); + } catch ( SecurityException se ) { + String msg = "Unexpected error during setRollbackOnly"; + LOGGER.logError( msg , se ); + throw new ExtendedSystemException ( msg, se ); + } + } + + /** + * @see com.atomikos.icatch.SubTxAwareParticipant + */ + + public void committed ( CompositeTransaction tx ) + { + removeFromMap ( tx.getTid () ); + } + + /** + * @see com.atomikos.icatch.SubTxAwareParticipant + */ + + public void rolledback ( CompositeTransaction tx ) + { + removeFromMap ( tx.getTid () ); + } + + /** + * @see javax.naming.Referenceable#getReference() + */ + + public Reference getReference () throws NamingException + { + return new Reference ( getClass ().getName (), new StringRefAddr ( + "name", "TransactionManager" ), TransactionManagerFactory.class + .getName (), null ); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionSynchronizationRegistryImp.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionSynchronizationRegistryImp.java new file mode 100644 index 000000000..cd3afc280 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/TransactionSynchronizationRegistryImp.java @@ -0,0 +1,147 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.io.Serializable; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; + +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.icatch.SysException; +import com.atomikos.util.SerializableObjectFactory; + +public class TransactionSynchronizationRegistryImp +implements javax.transaction.TransactionSynchronizationRegistry, +Serializable, Referenceable, OrderedLifecycleComponent { + + + private static final long serialVersionUID = 1L; + + private transient TransactionManager tm; + + private void assertTransactionManagerAvailable() { + tm = TransactionManagerImp.getTransactionManager(); + if (tm == null) { + throw new IllegalStateException("Transaction service not running"); + } + } + + @Override + public Object getTransactionKey() { + assertTransactionManagerAvailable(); + try { + return tm.getTransaction(); + } catch (SystemException e) { + throw new SysException(e); + } + } + + @Override + public void putResource(Object key, Object value) { + assertTransactionManagerAvailable(); + if (key == null) { + throw new NullPointerException(); + } + try { + TransactionImp tx = (TransactionImp) tm.getTransaction(); + if (tx == null) { + throwNoTransactionFound(); + } + tx.putResource(key, value); + } catch (SystemException e) { + throw new SysException(e); + } + } + + @Override + public Object getResource(Object key) { + assertTransactionManagerAvailable(); + if (key == null) { + throw new NullPointerException(); + } + try { + TransactionImp tx = (TransactionImp) tm.getTransaction(); + if (tx == null) { + throwNoTransactionFound(); + } + return tx.getResource(key); + } catch (SystemException e) { + throw new SysException(e); + } + } + + @Override + public void registerInterposedSynchronization(Synchronization sync) { + assertTransactionManagerAvailable(); + try { + TransactionImp tx = (TransactionImp) tm.getTransaction(); + if (tx == null) { + throwNoTransactionFound(); + } + tx.registerInterposedSynchronization(sync); + } catch (SystemException e) { + throw new SysException(e); + } + } + + private void throwNoTransactionFound() { + throw new IllegalStateException("No transaction found for calling thread"); + } + + @Override + public int getTransactionStatus() { + assertTransactionManagerAvailable(); + try { + return tm.getStatus(); + } catch (SystemException e) { + throw new SysException(e); + } + } + + @Override + public void setRollbackOnly() { + assertTransactionManagerAvailable(); + try { + tm.setRollbackOnly(); + } catch (SystemException e) { + throw new SysException(e); + } + } + + @Override + public boolean getRollbackOnly() { + assertTransactionManagerAvailable(); + if (getTransactionStatus() == Status.STATUS_NO_TRANSACTION) { + throwNoTransactionFound(); + } + return false; // hard to test, be optimistic and return false + } + + @Override + public Reference getReference() throws NamingException { + return SerializableObjectFactory.createReference(this); + } + + @Override + public void init() throws Exception { + //nothing to do: marker interface for this class + } + + @Override + public void close() throws Exception { + //nothing to do: marker interface for this class + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionFactory.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionFactory.java new file mode 100644 index 000000000..0e4e23e5a --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionFactory.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +/** + * + * + * The factory for JNDI lookup of UserTransactionImp objects. + */ + +public class UserTransactionFactory implements ObjectFactory +{ + + public UserTransactionFactory () + { + } + + /** + * @see javax.naming.spi.ObjectFactory + */ + @SuppressWarnings("rawtypes") + public Object getObjectInstance ( Object obj , Name name , Context nameCtx , + Hashtable environment ) throws Exception + { + Object ret = null; + if ( obj == null || !(obj instanceof Reference) ) + return null; + + Reference ref = (Reference) obj; + if ( ref.getClassName ().equals ( + "com.atomikos.icatch.jta.UserTransactionImp" ) ) + ret = new UserTransactionImp (); + else if ( ref.getClassName ().equals ( + "com.atomikos.icatch.jta.J2eeUserTransaction" ) ) + ret = new J2eeUserTransaction (); + else if ( ref.getClassName().equals ( + "javax.transaction.UserTransaction" ) ) + //ISSUE 10121: fix for Tomcat 5.5: class is always the JTA type + ret = new UserTransactionImp(); + else + ret = null; + + return ret; + + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionImp.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionImp.java new file mode 100644 index 000000000..f141b3fd7 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionImp.java @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.io.Serializable; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.transaction.NotSupportedException; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.config.UserTransactionServiceImp; +import com.atomikos.util.SerializableObjectFactory; + +/** + * Our UserTransaction implementation for J2SE transactions. This class is + * special in that it automatically starts up and recover the transaction + * service on first use. Note: don't use this class in J2EE applications in + * order to avoid starting different transaction engines in the same application + * server! J2EE applications should use J2eeUserTransaction instead. + */ + +public class UserTransactionImp implements UserTransaction, Serializable, + Referenceable +{ + private static final long serialVersionUID = -865418426269785202L; + + private transient TransactionManager txmgr_; + + /** + * No-argument constructor. + */ + + public UserTransactionImp () + { + } + + /** + * Referenceable mechanism requires later setup of txmgr_, otherwise binding + * into JNDI already requires that TM is running. + */ + + private void checkSetup () + { + synchronized ( TransactionManagerImp.class ) { + txmgr_ = TransactionManagerImp.getTransactionManager (); + if ( txmgr_ == null ) { + UserTransactionService uts = new UserTransactionServiceImp (); + uts.init(); + txmgr_ = TransactionManagerImp.getTransactionManager (); + } + } + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void begin () throws NotSupportedException, SystemException + { + checkSetup (); + txmgr_.begin (); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void commit () throws javax.transaction.RollbackException, + javax.transaction.HeuristicMixedException, + javax.transaction.HeuristicRollbackException, + javax.transaction.SystemException, java.lang.IllegalStateException, + java.lang.SecurityException + { + checkSetup (); + txmgr_.commit (); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void rollback () throws IllegalStateException, SystemException, + SecurityException + { + checkSetup (); + txmgr_.rollback (); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void setRollbackOnly () throws IllegalStateException, + SystemException + { + checkSetup (); + txmgr_.setRollbackOnly (); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public int getStatus () throws SystemException + { + checkSetup (); + return txmgr_.getStatus (); + } + + /** + * @see javax.transaction.UserTransaction + */ + + public void setTransactionTimeout ( int seconds ) throws SystemException + { + checkSetup (); + txmgr_.setTransactionTimeout ( seconds ); + } + + // + // + // IMPLEMENTATION OF REFERENCEABLE + // + // + + public Reference getReference () throws NamingException + { + return SerializableObjectFactory.createReference ( this ); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionManager.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionManager.java new file mode 100644 index 000000000..77c135f44 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/UserTransactionManager.java @@ -0,0 +1,254 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.io.Serializable; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.atomikos.icatch.OrderedLifecycleComponent; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.util.SerializableObjectFactory; + +/** + * A straightforward, zero-setup implementation of a transaction manager. + * Applications can use an instance of this class to get a handle to the + * transaction manager, and automatically startup or recover the transaction + * service on first use. + */ +public class UserTransactionManager implements TransactionManager, + Serializable, Referenceable, UserTransaction, OrderedLifecycleComponent +{ + private static final long serialVersionUID = -655789038710288096L; + + private transient TransactionManagerImp tm; + + private boolean forceShutdown; + + private boolean startupTransactionService; + + private boolean closed; + + private boolean coreStartedHere; + + private void checkSetup () throws SystemException + { + if (!closed) initializeTransactionManagerSingleton(); + } + + private void initializeTransactionManagerSingleton() throws SystemException { + tm = (TransactionManagerImp) TransactionManagerImp + .getTransactionManager (); + if ( tm == null ) { + if ( getStartupTransactionService() ) { + startupTransactionService(); + tm = (TransactionManagerImp) TransactionManagerImp + .getTransactionManager (); + } + else { + throw new SystemException ( "Transaction service not running" ); + } + } + } + + private void startupTransactionService() { + coreStartedHere = Configuration.init(); + } + + + private void shutdownTransactionService() { + if (coreStartedHere) { + Configuration.shutdown(forceShutdown); + coreStartedHere = false; + } + } + + public UserTransactionManager() + { + //startup by default, to have backward compatibility + this.startupTransactionService = true; + this.closed = false; + } + + /** + * Sets whether the transaction service should be + * started if not already running. Optional, defaults to true. + * + * @param startup + */ + public void setStartupTransactionService ( boolean startup ) + { + this.startupTransactionService = startup; + } + + /** + * Returns true if the transaction service will + * be started if not already running. + * @return + */ + public boolean getStartupTransactionService() + { + return this.startupTransactionService; + } + + /** + * Performs initialization if necessary. + * This will startup the TM (if not running) + * and perform recovery, unless getStartupTransactionService + * returns false. + * + * @throws SystemException + */ + + public void init() throws SystemException + { + closed = false; + checkSetup(); + } + + /** + * @see javax.transaction.TransactionManager#begin() + */ + public void begin () throws NotSupportedException, SystemException + { + if ( closed ) throw new SystemException ( "This UserTransactionManager instance was closed already. Call init() to reuse if desired." ); + checkSetup (); + tm.begin (); + + } + + public boolean getForceShutdown() + { + return forceShutdown; + } + + /** + * Sets the force shutdown mode to use during close. + * @param value + */ + public void setForceShutdown ( boolean value ) + { + this.forceShutdown = value; + } + + /** + * @see javax.transaction.TransactionManager#commit() + */ + public void commit () throws RollbackException, HeuristicMixedException, + HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException + { + if ( closed ) throw new SystemException ( "This UserTransactionManager instance was closed already - commit no longer allowed or possible." ); + checkSetup (); + tm.commit (); + + } + + /** + * @see javax.transaction.TransactionManager#getStatus() + */ + public int getStatus () throws SystemException + { + checkSetup (); + return tm.getStatus (); + } + + /** + * @see javax.transaction.TransactionManager#getTransaction() + */ + public Transaction getTransaction () throws SystemException + { + checkSetup (); + return tm.getTransaction (); + } + + /** + * @see javax.transaction.TransactionManager#resume(javax.transaction.Transaction) + */ + public void resume ( Transaction tx ) throws InvalidTransactionException, + IllegalStateException, SystemException + { + checkSetup (); + tm.resume ( tx ); + + } + + /** + * @see javax.transaction.TransactionManager#rollback() + */ + public void rollback () throws IllegalStateException, SecurityException, + SystemException + { + tm.rollback (); + + } + + /** + * @see javax.transaction.TransactionManager#setRollbackOnly() + */ + public void setRollbackOnly () throws IllegalStateException, + SystemException + { + tm.setRollbackOnly (); + + } + + /** + * @see javax.transaction.TransactionManager#setTransactionTimeout(int) + */ + public void setTransactionTimeout ( int secs ) throws SystemException + { + checkSetup (); + tm.setTransactionTimeout ( secs ); + + } + + /** + * @see javax.transaction.TransactionManager#suspend() + */ + public Transaction suspend () throws SystemException + { + checkSetup (); + return tm.suspend (); + } + + /** + * @see javax.naming.Referenceable#getReference() + */ + public Reference getReference () throws NamingException + { + return SerializableObjectFactory.createReference ( this ); + } + + /** + * Closes the transaction service, but only if it was + * implicitly started via this instance. + * In other words, if the transaction service was started + * in another way then this method will not do anything. + * + */ + public void close() + { + shutdownTransactionService(); + closed = true; + } + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/XAResourceKey.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/XAResourceKey.java new file mode 100644 index 000000000..3b4bce19a --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/XAResourceKey.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +/** + * Instances of this class can be used to lookup resource transactions in a + * hashtable that uses the XAResource instance for mapping. This is needed + * because otherwise the JTA wouldn't work with XAResource implementations that + * have overridden equals. + * + */ +class XAResourceKey +{ + + private XAResource xares; + + public XAResourceKey ( XAResource xares ) + { + super (); + this.xares = xares; + } + + public boolean equals ( Object o ) + { + boolean ret = false; + if ( o instanceof XAResourceKey ) { + XAResourceKey other = (XAResourceKey) o; + try { + ret = (other.xares == xares || other.xares.isSameRM ( xares )); + } catch ( XAException e ) { + // just return false + } + } + + return ret; + } + + public int hashCode () + { + return xares.getClass ().getName ().toString ().hashCode (); + } + + public String toString () + { + return xares.toString (); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/MandatoryTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/MandatoryTemplate.java new file mode 100644 index 000000000..b38012d99 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/MandatoryTemplate.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.util.concurrent.Callable; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +class MandatoryTemplate extends TransactionTemplate { + + public MandatoryTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + Transaction existingTransaction = utm.getTransaction(); + if (existingTransaction == null) { + throw new IllegalStateException("A transaction is required but none exists."); + } + return super.execute(work); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NestedTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NestedTemplate.java new file mode 100644 index 000000000..18c053aa1 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NestedTemplate.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.Callable; + +import javax.transaction.TransactionManager; + +class NestedTemplate extends TransactionTemplate { + + protected NestedTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + T ret = null; + try { + beginTransaction(); + ret = work.call(); + utm.commit(); + } catch (Exception e) { + utm.rollback(); + throw e; + } catch (Throwable e) { + utm.rollback(); + throw new UndeclaredThrowableException(e); + } + return ret; + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NeverTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NeverTemplate.java new file mode 100644 index 000000000..c9b83244c --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NeverTemplate.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.util.concurrent.Callable; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +class NeverTemplate extends TransactionTemplate { + + public NeverTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + Transaction existingTransaction = utm.getTransaction(); + if (existingTransaction != null) { + throw new IllegalStateException("A transaction exists but none is allowed."); + } + return work.call(); + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NotSupportedTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NotSupportedTemplate.java new file mode 100644 index 000000000..c25c60aa0 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/NotSupportedTemplate.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.util.concurrent.Callable; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +class NotSupportedTemplate extends TransactionTemplate { + + public NotSupportedTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + T ret = null; + Transaction tx = null; + try { + tx = utm.suspend(); + ret = work.call(); + } finally { + if (tx != null) { + utm.resume(tx); + } + } + return ret; + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/RequiredTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/RequiredTemplate.java new file mode 100644 index 000000000..ebe9eb6bf --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/RequiredTemplate.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.Callable; + +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; + +class RequiredTemplate extends TransactionTemplate { + + protected RequiredTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + T ret = null; + boolean transactionStartedHere = false; + try { + if (utm.getTransaction() == null) { + beginTransaction(); + transactionStartedHere = true; + } + ret = work.call(); + if (transactionStartedHere) { + utm.commit(); + } + } catch (Exception e) { + handleException(transactionStartedHere); + throw e; + } catch (Throwable e) { + handleException(transactionStartedHere); + throw new UndeclaredThrowableException(e); + } + return ret; + } + + private void handleException(boolean transactionStartedHere) throws SystemException { + if (transactionStartedHere) { + utm.rollback(); + } else { + utm.setRollbackOnly(); + } + } +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/RequiresNewTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/RequiresNewTemplate.java new file mode 100644 index 000000000..1d3a92483 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/RequiresNewTemplate.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.Callable; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +class RequiresNewTemplate extends TransactionTemplate { + + + public RequiresNewTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + T ret = null; + Transaction suspendedTransaction = null; + try { + suspendedTransaction = utm.suspend(); + beginTransaction(); + ret = work.call(); + utm.commit(); + } catch (Exception e) { + utm.rollback(); + throw e; + } catch (Throwable e) { + utm.rollback(); + throw new UndeclaredThrowableException(e); + } finally { + if (suspendedTransaction != null) { + utm.resume(suspendedTransaction); + } + } + return ret; + } + + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/SupportsTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/SupportsTemplate.java new file mode 100644 index 000000000..5096b3049 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/SupportsTemplate.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.util.concurrent.Callable; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +class SupportsTemplate extends TransactionTemplate { + + public SupportsTemplate(TransactionManager utm, int timeout) { + super(utm, timeout); + } + + public T execute(Callable work) throws Exception { + Transaction existingTransaction = utm.getTransaction(); + if (existingTransaction != null) { + return super.execute(work); + } else { + return work.call(); + } + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/TransactionTemplate.java b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/TransactionTemplate.java new file mode 100644 index 000000000..3f58cb108 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/icatch/jta/template/TransactionTemplate.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import java.util.concurrent.Callable; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import com.atomikos.icatch.jta.UserTransactionManager; + + /** + * Template (builder-style) logic for light-weight transaction demarcation. + * + * Example usage: + * + *

+  * {@code
+  *     TransactionTemplate template = new TransactionTemplate();
+  *     template.withTimeout(5).required().execute(() -> {
+  *         //your transactional code as a lambda expression
+  *     });
+  *  }
+  * 
+ * + * Instead of a lambda expression, you can also supply a Callable instance. + */ + +public class TransactionTemplate { + + private int timeout; //0 means: use default + protected TransactionManager utm; + + + public TransactionTemplate() { + this(new UserTransactionManager(), 0); + } + + TransactionTemplate(TransactionManager utm, int timeout) { + this.timeout = timeout; + this.utm = utm; + } + + public TransactionTemplate withTimeout(int timeout) { + this.timeout = timeout; + return this; + } + + /** + * + * @return An instance that executes work with REQUIRED semantics. + * + *
    + *
  • For new transactions: any exception will lead to rollback, no exception means commit.
  • + *
  • For existing transactions: any exception will mark the transaction as rollbackOnly.
  • + *
+ * + */ + public TransactionTemplate required() { + return new RequiredTemplate(utm, timeout); + } + + /** + * @return An instance that executes work in a new (nested) transaction and commits if there are no exceptions. + * + * Any exception will lead to rollback of the (nested) transaction. + * + */ + public TransactionTemplate nested() { + return new NestedTemplate(utm, timeout); + } + + protected Transaction beginTransaction() throws Exception { + utm.setTransactionTimeout(timeout); + utm.begin(); + return utm.getTransaction(); + } + + /** + * Defaults to required() strategy. + * + * @param work + * @return + * @throws Exception + */ + public T execute(Callable work) throws Exception { + return new RequiredTemplate(utm, timeout).execute(work); + } + + /** + * + * @return An instance that executes work with REQUIRES_NEW semantics. + * + * Any exception will lead to rollback, no exception means commit. + */ + + public TransactionTemplate requiresNew() { + return new RequiresNewTemplate(utm, timeout); + } + + /** + * + * @return An instance that executes work with MANDATORY semantics. + * + * Any exception will lead to rollbackOnly status of the existing transaction. + */ + public TransactionTemplate mandatory() { + return new MandatoryTemplate(utm, timeout); + } + + /** + * + * @return An instance that executes work with NEVER semantics. + */ + + public TransactionTemplate never() { + return new NeverTemplate(utm, timeout); + } + + /** + * + * @return An instance that executes work with SUPPORTS semantics. + */ + + public TransactionTemplate supports() { + return new SupportsTemplate(utm, timeout); + } + + /** + * + * @return An instance that executes work with NOT_SUPPORTED semantics. + */ + + public TransactionTemplate notSupported() { + return new NotSupportedTemplate(utm, timeout); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/InMemoryPreviousXidRepository.java b/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/InMemoryPreviousXidRepository.java new file mode 100644 index 000000000..fe75a3cb7 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/InMemoryPreviousXidRepository.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.xa; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.atomikos.datasource.xa.XID; + +public class InMemoryPreviousXidRepository implements PreviousXidRepository { + + private Map> cache = new HashMap<>(); + + @Override + public synchronized List findXidsExpiredAt(long startOfRecoveryScan) { + List xids = new ArrayList<>(); + for (Long expiration : cache.keySet()) { + if(expiration it = cache.keySet().iterator(); + while (it.hasNext()) { + Long expiration = it.next(); + if(expiration<=startOfRecoveryScan) { + it.remove(); + } + } + } + + @Override + public synchronized boolean isEmpty() { + return cache.isEmpty(); + } + + @Override + public synchronized void remember(XID xidToStoreForNextScan, long expiration) { + List xids = cache.get(expiration); + if (xids == null) { + xids = new ArrayList<>(); + } + xids.add(xidToStoreForNextScan); + cache.put(expiration, xids); + } + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/PreviousXidRepository.java b/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/PreviousXidRepository.java new file mode 100644 index 000000000..f5124b4e5 --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/PreviousXidRepository.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.xa; + +import java.util.List; + +import com.atomikos.datasource.xa.XID; + +public interface PreviousXidRepository { + + List findXidsExpiredAt(long startOfRecoveryScan); + + /** + * Remembers the given XID for later. + * @param xidToStoreForNextScan + * @param expiration + */ + void remember(XID xidToStoreForNextScan, long expiration); + + void forgetXidsExpiredAt(long startOfRecoveryScan); + + boolean isEmpty(); + +} diff --git a/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/XARecoveryManager.java b/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/XARecoveryManager.java new file mode 100644 index 000000000..43907bdaa --- /dev/null +++ b/public/transactions-jta/src/main/java/com/atomikos/recovery/xa/XARecoveryManager.java @@ -0,0 +1,283 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.xa; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +import com.atomikos.datasource.xa.RecoveryScan; +import com.atomikos.datasource.xa.RecoveryScan.XidSelector; +import com.atomikos.datasource.xa.XAExceptionHelper; +import com.atomikos.datasource.xa.XID; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.event.transaction.ParticipantHeuristicEvent; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.publish.EventPublisher; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; + +public class XARecoveryManager { +private static final Logger LOGGER = LoggerFactory.createLogger(XARecoveryManager.class); + + private static XARecoveryManager instance; + + private XidSelector xidSelector; + private String tmUniqueName; + private long maxTimeout; + + private Map previousXidRepositoryMap = new HashMap(); + + + public XARecoveryManager(final String tmUniqueName) { + this.tmUniqueName = tmUniqueName; + this.xidSelector=new XidSelector() { + @Override + public boolean selects(XID xid) { + boolean ret = false; + String branch = xid.getBranchQualifierAsString(); + + if ( branch.startsWith ( tmUniqueName ) ) { + ret = true; + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug(this + ": recovering XID: " + xid); + } + } else { + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug(this + ": XID " + xid + + " with branch " + branch + " is not under my responsibility"); + } + } + return ret; + } + + @Override + public String toString() { + return XARecoveryManager.this.toString(); + } + + + }; + maxTimeout = Configuration.getConfigProperties().getMaxTimeout(); + } + + + public static XARecoveryManager getInstance() { + return instance; + } + + + public static void installXARecoveryManager(String tmUniqueName) { + if (tmUniqueName == null) { + instance = null; + } else { + instance = new XARecoveryManager(tmUniqueName); + } + } + + + public boolean recover(XAResource xaResource, long startOfRecoveryScan, Collection expiredCommittingCoordinators, Collection indoubtForeignCoordinatorsToKeep, String uniqueResourceName) throws XAException, LogReadException { + List xidsToRecover = retrievePreparedXidsFromXaResource(xaResource); + PreviousXidRepository previousXidRepository = getPreviousXidRepository(uniqueResourceName); + boolean success = recoverXids(xidsToRecover, previousXidRepository, expiredCommittingCoordinators, indoubtForeignCoordinatorsToKeep, xaResource, startOfRecoveryScan); + previousXidRepository.forgetXidsExpiredAt(startOfRecoveryScan); // ignore if success or not: any pending XIDs have been added for the future + return success; + } + + + + + private PreviousXidRepository getPreviousXidRepository( + String uniqueResourceName) { + PreviousXidRepository ret = previousXidRepositoryMap.get(uniqueResourceName); + if (ret == null) { + ret = new InMemoryPreviousXidRepository(); + previousXidRepositoryMap.put(uniqueResourceName, ret); + } + return ret; + } + + + private boolean recoverXids(List xidsToRecover, PreviousXidRepository previousXidRepository, + Collection expiredCommittingCoordinators, Collection indoubtForeignCoordinatorsToKeep, XAResource xaResource, + long startOfRecoveryScan) { + boolean allExpiredCommitsDone = true; + long xidDetectionTime = System.currentTimeMillis(); + List expiredPreviousXids = previousXidRepository.findXidsExpiredAt(startOfRecoveryScan); + Collection expiredCommittingCoordinatorIds = PendingTransactionRecord.extractCoordinatorIds(expiredCommittingCoordinators, TxState.COMMITTING, TxState.IN_DOUBT); // in-doubt for subtxs with committing superior + Collection foreignIndoubtCoordinatorIds = PendingTransactionRecord.extractCoordinatorIds(indoubtForeignCoordinatorsToKeep, TxState.IN_DOUBT); // filter out what remote recovery has already resolved + for (XID xid : xidsToRecover) { + String coordinatorId = xid.getGlobalTransactionIdAsString(); + if (expiredCommittingCoordinatorIds.contains(coordinatorId)) { + boolean replayCommitOK = replayCommit(xid, xaResource); + if (!replayCommitOK) { + previousXidRepository.remember(xid, startOfRecoveryScan + 1); // cf case 185455: so hasPendingXids returns true + allExpiredCommitsDone = false; + } + } else if (expiredPreviousXids.contains(xid)) { + if (foreignIndoubtCoordinatorIds.contains(coordinatorId) || !presumedAbort(xid, xaResource)) { + //foreign indoubt, or presumed abort failed (probably hazard) => try again later next scan (ASAP) + previousXidRepository.remember(xid, startOfRecoveryScan + 1); + } + } else { + previousXidRepository.remember(xid, xidDetectionTime + maxTimeout); // unknown xid => wait for maxTimeout + } + } + return allExpiredCommitsDone; + } + + private boolean replayCommit(XID xid, XAResource xaResource) { + if (LOGGER.isDebugEnabled()) LOGGER.logDebug(this + ": replaying commit of xid: " + xid); + boolean forgetInLog = false; + try { + xaResource.commit(xid, false); + forgetInLog = true; + } catch (XAException e) { + if (alreadyHeuristicallyTerminatedByResource(e)) { + forgetInLog = handleHeuristicTerminationByResource(xid, xaResource, e, true);; + } else if (xidTerminatedInResourceByConcurrentCommit(e)) { + forgetInLog = true; + } else { + LOGGER.logWarning(XAExceptionHelper.formatLogMessage("Transient error while replaying commit", e, "will retry later")); + } + } + + return forgetInLog; + } + + private boolean alreadyHeuristicallyTerminatedByResource(XAException e) { + boolean ret = false; + switch (e.errorCode) { + case XAException.XA_HEURHAZ: + case XAException.XA_HEURCOM: + case XAException.XA_HEURMIX: + case XAException.XA_HEURRB: + ret = true; + } + return ret; + } + + private boolean xidTerminatedInResourceByConcurrentCommit(XAException e) { + return xidNoLongerKnownByResource(e); + } + + private boolean xidTerminatedInResourceByConcurrentRollback(XAException e) { + return xidNoLongerKnownByResource(e); + } + + private boolean xidNoLongerKnownByResource(XAException e) { + boolean ret = false; + switch (e.errorCode) { + case XAException.XAER_NOTA: + case XAException.XAER_INVAL: + ret = true; + } + return ret; + } + + private boolean handleHeuristicTerminationByResource(XID xid, + XAResource xaResource, XAException e, boolean commitDesired) { + boolean forgetInLog = true; + notifyLogOfHeuristic(xid, e, commitDesired); + if (e.errorCode != XAException.XA_HEURHAZ) { + forgetXidInXaResource(xid, xaResource); + } else { + forgetInLog = false; + } + return forgetInLog; + } + + private void forgetXidInXaResource(XID xid, XAResource xaResource) { + try { + xaResource.forget(xid); + } catch (XAException e) { + LOGGER.logWarning(XAExceptionHelper.formatLogMessage("Unexpected error during forget", e, "ignoring")); + // ignore: worst case, heuristic xid is presented again on next recovery scan + } + } + + private void notifyLogOfHeuristic(XID xid, XAException e, boolean commitDesired ) { + switch (e.errorCode) { + case XAException.XA_HEURHAZ: + fireTransactionHeuristicEvent(xid, TxState.HEUR_HAZARD); + break; + case XAException.XA_HEURCOM: + if(!commitDesired){ + fireTransactionHeuristicEvent(xid, TxState.HEUR_COMMITTED); + } + break; + case XAException.XA_HEURMIX: + fireTransactionHeuristicEvent(xid, TxState.HEUR_MIXED); + break; + case XAException.XA_HEURRB: + if(commitDesired) { + fireTransactionHeuristicEvent(xid, TxState.HEUR_ABORTED); + } + break; + default: + break; + } + } + + + private void fireTransactionHeuristicEvent(XID xid, TxState state) { + ParticipantHeuristicEvent event = new ParticipantHeuristicEvent(xid.getGlobalTransactionIdAsString(), xid.toString(), state); + EventPublisher.INSTANCE.publish(event); + } + + + private boolean presumedAbort(XID xid, XAResource xaResource) { + boolean ret = false; + if (LOGGER.isDebugEnabled()) LOGGER.logDebug(this + ": presumed abort of xid: " + xid); + try { + xaResource.rollback(xid); + ret = true; + } catch (XAException e) { + if (alreadyHeuristicallyTerminatedByResource(e)) { + ret = handleHeuristicTerminationByResource(xid, xaResource, e, false); + } else if (xidTerminatedInResourceByConcurrentRollback(e)) { + ret = true; + } else { + LOGGER.logWarning(XAExceptionHelper.formatLogMessage("Unexpected exception during recovery", e, "ignoring to retry later")); + } + } + + return ret; + } + + + private List retrievePreparedXidsFromXaResource(XAResource xaResource) throws XAException { + List ret = new ArrayList(); + try { + ret = RecoveryScan.recoverXids(xaResource, xidSelector); + } catch (XAException e) { + LOGGER.logWarning(XAExceptionHelper.formatLogMessage("Error while retrieving xids from resource", e, "will retry later")); + throw e; + } + return ret; + } + + @Override + public String toString() { + return "XARecoveryManager " + tmUniqueName; + } + + + public boolean hasPendingXids(String uniqueResourceName) { + return !getPreviousXidRepository(uniqueResourceName).isEmpty(); + } + +} diff --git a/public/transactions-jta/src/main/reference/OracleClasses12License.rtf b/public/transactions-jta/src/main/reference/OracleClasses12License.rtf new file mode 100644 index 000000000..3b4e07a2a --- /dev/null +++ b/public/transactions-jta/src/main/reference/OracleClasses12License.rtf @@ -0,0 +1,66 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural + +\f0\fs24 \cf0 ORACLE TECHNOLOGY NETWORK \ +DEVELOPMENT AND DISTRIBUTION LICENSE AGREEMENT\ +\ +\ +"We," "us," and "our" refers to Oracle USA, Inc., for and on behalf of itself and its subsidiaries and affiliates under common control. "You" and "your" refers to the individual or entity that wishes to use the programs from Oracle. "Programs" refers to the software product you wish to download and use and program documentation. "License" refers to your right to use the programs under the terms of this agreement. This agreement is governed by the substantive and procedural laws of California. You and Oracle agree to submit to the exclusive jurisdiction of, and venue in, the courts of San Francisco, San Mateo, or Santa Clara counties in California in any dispute arising out of or relating to this agreement.\ +\ +We are willing to license the programs to you only upon the condition that you accept all of the terms contained in this agreement. Read the terms carefully and select the "Accept" button at the bottom of the page to confirm your acceptance. If you are not willing to be bound by these terms, select the "Do Not Accept" button and the registration process will not continue.\ +\ +License Rights\ +We grant you a nonexclusive, nontransferable limited license to use the programs for purposes of developing your applications. You may also distribute the programs with your applications to your customers. If you want to use the programs for any purpose other than as expressly permitted under this agreement you must contact us, or an Oracle reseller, to obtain the appropriate license. We may audit your use of the programs. Program documentation is either shipped with the programs, or documentation may accessed online at http://otn.oracle.com/docs.\ +\ +Ownership and Restrictions\ +We retain all ownership and intellectual property rights in the programs. You may make a sufficient number of copies of the programs for the licensed use and one copy of the programs for backup purposes.\ +\ +You may not:\ +- use the programs for any purpose other than as provided above;\ +- distribute the programs unless accompanied with your applications;\ +- charge your end users for use of the programs;\ +- remove or modify any program markings or any notice of our proprietary rights;\ +- use the programs to provide third party training on the content and/or functionality of the programs, except for training your licensed users;\ +- assign this agreement or give the programs, program access or an interest in the programs to any individual or entity except as provided under this agreement;\ +- cause or permit reverse engineering (unless required by law for interoperability), disassembly or decompilation of the programs;\ +- disclose results of any program benchmark tests without our prior consent; or,\ +- use any Oracle name, trademark or logo.\ +\ +Program Distribution\ +We grant you a nonexclusive, nontransferable right to copy and distribute the programs to your end users provided that you do not charge your end users for use of the programs and provided your end users may only use the programs to run your applications for their business operations. Prior to distributing the programs you shall require your end users to execute an agreement binding them to terms consistent with those contained in this section and the sections of this agreement entitled "License Rights," "Ownership and Restrictions," "Export," "Disclaimer of Warranties and Exclusive Remedies," "No Technical Support," "End of Agreement," "Relationship Between the Parties," and "Open Source." You must also include a provision stating that your end users shall have no right to distribute the programs, and a provision specifying us as a third party beneficiary of the agreement. You are responsible for obtaining these agreements with your end users. \ +\ +You agree to: (a) defend and indemnify us against all claims and damages caused by your distribution of the programs in breach of this agreements and/or failure to include the required contractual provisions in your end user agreement as stated above; (b) keep executed end user agreements and records of end user information including name, address, date of distribution and identity of programs distributed; (c) allow us to inspect your end user agreements and records upon request; and, (d) enforce the terms of your end user agreements so as to effect a timely cure of any end user breach, and to notify us of any breach of the terms.\ +\ +Export\ +You agree that U.S. export control laws and other applicable export and import laws govern your use of the programs, including technical data; additional information can be found on Oracle's Global Trade Compliance web site located at http://www.oracle.com/products/export/index.html?content.html. You agree that neither the programs nor any direct product thereof will be exported, directly, or indirectly, in violation of these laws, or will be used for any purpose prohibited by these laws including, without limitation, nuclear, chemical, or biological weapons proliferation.\ +\ +Disclaimer of Warranty and Exclusive Remedies\ +\ +THE PROGRAMS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. WE FURTHER DISCLAIM ALL WARRANTIES, EXPRESS AND IMPLIED, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NONINFRINGEMENT.\ +\ +IN NO EVENT SHALL WE BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR DAMAGES FOR LOSS OF PROFITS, REVENUE, DATA OR DATA USE, INCURRED BY YOU OR ANY THIRD PARTY, WHETHER IN AN ACTION IN CONTRACT OR TORT, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR ENTIRE LIABILITY FOR DAMAGES HEREUNDER SHALL IN NO EVENT EXCEED ONE THOUSAND DOLLARS (U.S. $1,000).\ +\ +No Technical Support\ +Our technical support organization will not provide technical support, phone support, or updates to you for the programs licensed under this agreement.\ +\ +Restricted Rights\ +If you distribute a license to the United States government, the programs, including documentation, shall be considered commercial computer software and you will place a legend, in addition to applicable copyright notices, on the documentation, and on the media label, substantially similar to the following:\ +NOTICE OF RESTRICTED RIGHTS\ +"Programs delivered subject to the DOD FAR Supplement are 'commercial computer software' and use, duplication, and disclosure of the programs, including documentation, shall be subject to the licensing restrictions set forth in the applicable Oracle license agreement. Otherwise, programs delivered subject to the Federal Acquisition Regulations are 'restricted computer software' and use, duplication, and disclosure of the programs, including documentation, shall be subject to the restrictions in FAR 52.227-19, Commercial Computer Software-Restricted Rights (June 1987). Oracle USA, Inc., 500 Oracle Parkway, Redwood City, CA 94065."\ +\ +End of Agreement\ +You may terminate this agreement by destroying all copies of the programs. We have the right to terminate your right to use the programs if you fail to comply with any of the terms of this agreement, in which case you shall destroy all copies of the programs.\ +\ +Relationship Between the Parties\ +The relationship between you and us is that of licensee/licensor. Neither party will represent that it has any authority to assume or create any obligation, express or implied, on behalf of the other party, nor to represent the other party as agent, employee, franchisee, or in any other capacity. Nothing in this agreement shall be construed to limit either party's right to independently develop or distribute software that is functionally similar to the other party's products, so long as proprietary information of the other party is not included in such software.\ +\ +Open Source\ +"Open Source" software - software available without charge for use, modification and distribution - is often licensed under terms that require the user to make the user's modifications to the Open Source software or any software that the user 'combines' with the Open Source software freely available in source code form. If you use Open Source software in conjunction with the programs, you must ensure that your use does not: (i) create, or purport to create, obligations of us with respect to the Oracle programs; or (ii) grant, or purport to grant, to any third party any rights to or immunities under our intellectual property or proprietary rights in the Oracle programs. For example, you may not develop a software program using an Oracle program and an Open Source program where such use results in a program file(s) that contains code from both the Oracle program and the Open Source program (including without limitation libraries) if the Open Source program is licensed under a license that requires any "modifications" be made freely available. You also may not combine the Oracle program with programs licensed under the GNU General Public License ("GPL") in any manner that could cause, or could be interpreted or asserted to cause, the Oracle program or any modifications thereto to become subject to the terms of the GPL.\ +\ +Entire Agreement\ +You agree that this agreement is the complete agreement for the programs and licenses, and this agreement supersedes all prior or contemporaneous agreements or representations. If any term of this agreement is found to be invalid or unenforceable, the remaining provisions will remain effective.\ +\ +Last updated: 03/09/05} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/Sun Binary License.rtf b/public/transactions-jta/src/main/reference/Sun Binary License.rtf new file mode 100644 index 000000000..9857a475d --- /dev/null +++ b/public/transactions-jta/src/main/reference/Sun Binary License.rtf @@ -0,0 +1,50 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\froman\fcharset77 TimesNewRomanMS;\f1\froman\fcharset77 TimesNewRomanBdMS;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs32 \cf0 \ + \ +Licensing\ +\ +------------------------------------------------------------------------\ +\ +\ + +\f1\b\fs48 Sun Binary Code License Agreement +\f0\b0\fs32 \ +\ +Sun Microsystems, Inc.\ +Binary Code License Agreement \ +\ +READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA PACKAGE, YOU AGREE TO THE TERMS OF THIS AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE "DECLINE" BUTTON AT THE END OF THIS AGREEMENT. \ +\ +1. LICENSE TO USE. Sun grants you a non-exclusive and non-transferable license for the internal use only of the accompanying software and documentation and any error corrections provided by Sun (collectively "Software"), by the number of users and the class of computer hardware for which the corresponding fee has been paid. \ +\ +2. RESTRICTIONS. Software is confidential and copyrighted. Title to Software and all associated intellectual property rights is retained by Sun and/or its licensors. Except as specifically authorized in any Supplemental License Terms, you may not make copies of Software, other than a single copy of Software for archival purposes. Unless enforcement is prohibited by applicable law, you may not modify, decompile, or reverse engineer Software. Licensee acknowledges that Licensed Software is not designed or intended for use in the design, construction, operation or maintenance of any nuclear facility. Sun Microsystems, Inc. disclaims any express or implied warranty of fitness for such uses. No right, title or interest in or to any trademark, service mark, logo or trade name of Sun or its licensors is granted under this Agreement. \ +\ +3. LIMITED WARRANTY. Sun warrants to you that for a period of ninety (90) days from the date of purchase, as evidenced by a copy of the receipt, the media on which Software is furnished (if any) will be free of defects in materials and workmanship under normal use. Except for the foregoing, Software is provided "AS IS". Your exclusive remedy and Sun's entire liability under this limited warranty will be at Sun's option to replace Software media or refund the fee paid for Software. \ +\ +4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE DISCLAIMERS ARE HELD TO BE LEGALLY INVALID. \ +\ +5. LIMITATION OF LIABILITY. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. In no event will Sun's liability to you, whether in contract, tort (including negligence), or otherwise, exceed the amount paid by you for Software under this Agreement. The foregoing limitations will apply even if the above stated warranty fails of its essential purpose. \ +\ +6. Termination. This Agreement is effective until terminated. You may terminate this Agreement at any time by destroying all copies of Software. This Agreement will terminate immediately without notice from Sun if you fail to comply with any provision of this Agreement. Upon Termination, you must destroy all copies of Software. \ +\ +7. Export Regulations. All Software and technical data delivered under this Agreement are subject to US export control laws and may be subject to export or import regulations in other countries. You agree to comply strictly with all such laws and regulations and acknowledge that you have the responsibility to obtain such licenses to export, re-export, or import as may be required after delivery to you. \ +\ +8. U.S. Government Restricted Rights. If Software is being acquired by or on behalf of the U.S. Government or by a U.S. Government prime contractor or subcontractor (at any tier), then the Government's rights in Software and accompanying documentation will be only as set forth in this Agreement; this is in accordance with 48 CFR 227.7201 through 227.7202-4 (for Department of Defense (DOD) acquisitions) and with 48 CFR 2.101 and 12.212 (for non-DOD acquisitions). \ +\ +9. Governing Law. Any action related to this Agreement will be governed by California law and controlling U.S. federal law. No choice of law rules of any jurisdiction will apply. \ +\ +10. Severability. If any provision of this Agreement is held to be unenforceable, this Agreement will remain in effect with the provision omitted, unless omission would frustrate the intent of the parties, in which case this Agreement will immediately terminate. \ +\ +11. Integration. This Agreement is the entire agreement between you and Sun relating to its subject matter. It supersedes all prior or contemporaneous oral or written communications, proposals, representations and warranties and prevails over any conflicting or additional terms of any quote, order, acknowledgment, or other communication between the parties relating to its subject matter during the term of this Agreement. No modification of this Agreement will be binding, unless in writing and signed by an authorized representative of each party. \ +\ + \ +Licensing\ +\ +------------------------------------------------------------------------\ +\ +} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/1__#$!@%!#__a.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/1__#$!@%!#__a.gif new file mode 100644 index 000000000..35d42e808 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/1__#$!@%!#__a.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/1__#$!@%!#__v3_pixel-trans.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/1__#$!@%!#__v3_pixel-trans.gif new file mode 100644 index 000000000..bdf7a7704 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/1__#$!@%!#__v3_pixel-trans.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/2__#$!@%!#__a.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/2__#$!@%!#__a.gif new file mode 100644 index 000000000..35d42e808 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/2__#$!@%!#__a.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/2__#$!@%!#__v3_pixel-trans.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/2__#$!@%!#__v3_pixel-trans.gif new file mode 100644 index 000000000..bdf7a7704 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/2__#$!@%!#__v3_pixel-trans.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/3__#$!@%!#__a.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/3__#$!@%!#__a.gif new file mode 100644 index 000000000..35d42e808 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/3__#$!@%!#__a.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/3__#$!@%!#__v3_pixel-trans.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/3__#$!@%!#__v3_pixel-trans.gif new file mode 100644 index 000000000..bdf7a7704 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/3__#$!@%!#__v3_pixel-trans.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/4__#$!@%!#__a.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/4__#$!@%!#__a.gif new file mode 100644 index 000000000..35d42e808 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/4__#$!@%!#__a.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/4__#$!@%!#__v3_pixel-trans.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/4__#$!@%!#__v3_pixel-trans.gif new file mode 100644 index 000000000..bdf7a7704 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/4__#$!@%!#__v3_pixel-trans.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/5__#$!@%!#__a.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/5__#$!@%!#__a.gif new file mode 100644 index 000000000..35d42e808 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/5__#$!@%!#__a.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/TXT.rtf b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/TXT.rtf new file mode 100644 index 000000000..05ee8b4ea --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/TXT.rtf @@ -0,0 +1,421 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Helvetica;\f2\fswiss\fcharset77 Arial-BoldMS; +\f3\fnil\fcharset77 Verdana-Bold;\f4\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red221\green114\blue24;\red49\green54\blue101; +\red239\green40\blue21;\red102\green102\blue102;\red67\green107\blue137;} +\margl1440\margr1440\vieww12580\viewh9060\viewkind0 +\deftab720 + +\itap1\trowd \taflags0 \trgaph108\trleft-108 \tamart100 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil +\clvertalc \clshdrawnil \clwWidth1800\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx4320 +\clvertalc \clshdrawnil \clwWidth360\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx8640 +\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f0\fs24 \cf1 \cell +\pard\intbl\itap1\pardeftab720\ql\qnatural +\cf1 \cell \lastrow\row + +\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil \tapadt20 \tapadb20 +\clvmgf \clvertalb \clshdrawnil \clwWidth1920\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx1728 +\clmgf \clvertalb \clshdrawnil \clwWidth5000\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx3456 +\clmrg \clvertalb \clshdrawnil \clwWidth5000\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx5184 +\clmrg \clvertalb \clshdrawnil \clwWidth5000\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx6912 +\clmrg \clvertalb \clshdrawnil \clwWidth5000\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx8640 +\pard\intbl\itap1\pardeftab720\ql\qnatural +{\field{\*\fldinst{HYPERLINK "http://www.sun.com/"}}{\fldrslt +\f1 \cf0 {{\NeXTGraphic a.gif \width20 \height20 +}¬}}}\pard\intbl\itap1\pardeftab720\ql\qnatural +\cf2 \cell +\pard\intbl\itap1\pardeftab720\ql\qnatural +{\field{\*\fldinst{HYPERLINK "http://www.sun.com/download"}}{\fldrslt +\f1 \cf0 {{\NeXTGraphic 1__#$!@%!#__a.gif \width20 \height20 +}¬}}}\pard\intbl\itap1\pardeftab720\ql\qnatural +\cf2 \cell +\pard\intbl\itap1\cell +\pard\intbl\itap1\cell +\pard\intbl\itap1\cell \row + +\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil \tapadt20 \tapadb20 +\clvmrg \clvertalb \clshdrawnil \clwWidth1920\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx1728 +\clvertalb \clshdrawnil \clwWidth2480\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx3456 +\clmgf \clvertalb \clshdrawnil \clwWidth2480\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx5184 +\clmrg \clvertalb \clshdrawnil \clwWidth2480\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx6912 +\clmrg \clvertalb \clshdrawnil \clwWidth2480\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl20 \clpadr20 \gaph\cellx8640 +\pard\intbl\itap1\cell +\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 2__#$!@%!#__a.gif \width20 \height20 +}¬}\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f0 \cf2 \cell +\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 3__#$!@%!#__a.gif \width20 \height20 +}¬}\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f0 \cf2 \cell +\pard\intbl\itap1\cell +\pard\intbl\itap1\cell \lastrow\row +\pard\pardeftab720\ql\qnatural + +\f2\b\fs42 \cf3 License Agreement\ + +\itap1\trowd \taflags0 \trgaph108\trleft-108 \trwWidth18860\trftsWidth3 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil \tapadt200 \tapadb200 +\clvertalt \clshdrawnil \clwWidth3400\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl200 \clpadr200 \gaph\cellx4320 +\clvertalt \clshdrawnil \clwWidth14660\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl200 \clpadr200 \gaph\cellx8640 + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trcbpat1 \trwWidth3400\trftsWidth3 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil +\clvertalc \clshdrawnil \clwWidth3400\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0\b0\fs24 \cf2 \nestcell \lastrow\nestrow + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil +\clvertalc \clshdrawnil \clwWidth240\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx4320 +\clvertalb \clshdrawnil \clwWidth2900\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic v3_pixel-trans.gif \width20 \height20 +}¬}\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \nestcell +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f3\b\fs26 \cf4 Download Center +\f0\b0\fs24 \cf2 \nestcell \nestrow + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil +\clvertalc \clshdrawnil \clwWidth240\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx4320 +\clvertalc \clshdrawnil \clwWidth2900\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 1__#$!@%!#__v3_pixel-trans.gif \width20 \height20 +}¬}\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \nestcell +\pard\intbl\itap2\pardeftab720\ql\qnatural +{\field{\*\fldinst{HYPERLINK "https://sdlc4b.sun.com:443/ECom/EComActionServlet/LicensePage:~:com.sun.sunit.sdlc.content.LegalTextWebPageInfo;jsessionid=65616EE4B591890131FA935AA81C65C4;jsessionid=65616EE4B591890131FA935AA81C65C4?NavbarGotoPage=UpdateUserPage&NavbarAction=x&updateRegistrationLink_17="}}{\fldrslt +\f3\b\fs26 \cf4 - Update Account}}\nestcell \nestrow + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil +\clvertalc \clshdrawnil \clwWidth240\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx4320 +\clvertalc \clshdrawnil \clwWidth2900\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 2__#$!@%!#__v3_pixel-trans.gif \width20 \height20 +}¬}\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \nestcell +\pard\intbl\itap2\pardeftab720\ql\qnatural +{\field{\*\fldinst{HYPERLINK "https://sdlc4b.sun.com:443/ECom/EComActionServlet/LicensePage:~:com.sun.sunit.sdlc.content.LegalTextWebPageInfo;jsessionid=65616EE4B591890131FA935AA81C65C4;jsessionid=65616EE4B591890131FA935AA81C65C4?logOutLink_24=&NavbarGotoPage=LoggedOutPage&NavbarAction=x"}}{\fldrslt +\f3\b\fs26 \cf4 - Log Out}}\nestcell \nestrow + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil +\clvertalc \clshdrawnil \clwWidth240\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx4320 +\clvertalc \clshdrawnil \clwWidth2900\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl100 \clpadr100 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 3__#$!@%!#__v3_pixel-trans.gif \width20 \height20 +}¬}\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \nestcell +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 4__#$!@%!#__v3_pixel-trans.gif \width20 \height20 +}¬}\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \nestcell \lastrow\nestrow\cell + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trwWidth14660\trftsWidth3 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil \tapadt60 \tapadb60 +\clmgf \clvertalb \clshdrawnil \clwWidth14540\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl60 \clpadr60 \gaph\cellx4320 +\clmrg \clvertalb \clshdrawnil \clwWidth14540\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl60 \clpadr60 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f2\b \cf2 To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu.\nestcell +\pard\intbl\itap2\nestcell \nestrow + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trwWidth14660\trftsWidth3 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil \tapadt60 \tapadb60 +\clmgf \clvertalc \clshdrawnil \clwWidth14540\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl60 \clpadr60 \gaph\cellx4320 +\clmrg \clvertalc \clshdrawnil \clwWidth14540\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl60 \clpadr60 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic dot.gif \width20 \height20 +}¬}\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \ +\pard\intbl\itap2\pardeftab720\sa120\ql\qnatural +\cf2 \cb2 \nestcell +\pard\intbl\itap2\nestcell \lastrow\nestrow +\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f4 \cf6 \cb1 \ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. You acknowledge that Software\ + is not designed, licensed or intended for use in\ + the design, construction, operation or maintenance\ + of any nuclear facility. Sun disclaims any\ + express or implied warranty of fitness for such\ + uses. No right, title or interest in or to any\ + trademark, service mark, logo or trade name of Sun\ + or its licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) INTERFACE CLASSES\ + J2EE(TM) CONNECTOR ARCHITECTURE, VERSION 1.0\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of this\ + Agreement, including, but not limited to Section 3\ + (Java(TM) Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to Section 3 (Java Technology\ + Restrictions), Sun grants you a non-exclusive,\ + non-transferable, limited license to reproduce and\ + distribute the Software in binary form only,\ + provided that you (i) distribute the Software\ + complete and unmodified and only bundled as part\ + of your Programs, (ii) do not distribute\ + additional software intended to replace any\ + component(s) of the Software, (iii) do not remove\ + or alter any proprietary legends or notices\ + contained in the Software, (iv) only distribute\ + the Software subject to a license agreement that\ + protects Sun's interests consistent with the terms\ + contained in this Agreement, and (v) agree to\ + defend and indemnify Sun and its licensors from\ + and against any damages, costs, liabilities,\ + settlement amounts and/or expenses (including\ + attorneys' fees) incurred in connection with any\ + claim, lawsuit or action by any third party that\ + arises or results from the use or distribution of\ + any and all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + Platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks\ + and all SUN, SOLARIS, JAVA, JINI, FORTE, and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party\ + may terminate this Agreement immediately should\ + any Software become, or in either party's opinion\ + be likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc. 901 San Antonio Road, Palo Alto, California\ + 94303\ + (LFI#95674/Form ID#011801)\ +\ + +\itap2\trowd \taflags0 \trgaph108\trleft-108 \trwWidth14660\trftsWidth3 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil \tapadt60 \tapadb60 +\clvertalc \clshdrawnil \clwWidth14540\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl60 \clpadr60 \gaph\cellx8640 +\pard\intbl\itap2\pardeftab720\ql\qnatural + +\f0 \cf2 \nestcell \lastrow\nestrow\cell \row + +\itap1\trowd \taflags0 \trgaph108\trleft-108 \trwWidth18860\trftsWidth3 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil \tapadt200 \tapadb200 +\clvertalc \clshdrawnil \clwWidth3400\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl200 \clpadr200 \gaph\cellx4320 +\clvertalc \clshdrawnil \clwWidth14660\clftsWidth3 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadl200 \clpadr200 \gaph\cellx8640 +\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 4__#$!@%!#__a.gif \width20 \height20 +}¬}\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f0 \cf2 \cell +\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic 5__#$!@%!#__a.gif \width20 \height20 +}¬}\pard\intbl\itap1\pardeftab720\ql\qnatural + +\f0 \cf2 \cell \lastrow\row +\pard\pardeftab720\ql\qnatural +{\field{\*\fldinst{HYPERLINK "http://www.sun.com/contact/"}}{\fldrslt +\fs22 \cf7 Contact}} +\fs22 {\field{\*\fldinst{HYPERLINK "http://www.sun.com/company/"}}{\fldrslt \cf7 About Sun}} {\field{\*\fldinst{HYPERLINK "http://www.sun.com/aboutsun/media/"}}{\fldrslt \cf7 News}} {\field{\*\fldinst{HYPERLINK "http://www.sun.com/corp_emp/"}}{\fldrslt \cf7 Employment}} {\field{\*\fldinst{HYPERLINK "http://www.sun.com/privacy/"}}{\fldrslt \cf7 Privacy}} {\field{\*\fldinst{HYPERLINK "http://www.sun.com/share/text/termsofuse.html"}}{\fldrslt \cf7 Terms of Use}} {\field{\*\fldinst{HYPERLINK "http://www.sun.com/suntrademarks/"}}{\fldrslt \cf7 Trademarks}} Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ +\pard\pardeftab720\ql\qnatural + +\f1 \cf0 {{\NeXTGraphic s976981862111.gif \width40 \height40 +}¬}} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/a.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/a.gif new file mode 100644 index 000000000..35d42e808 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/a.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/dot.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/dot.gif new file mode 100644 index 000000000..1d11fa9ad Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/dot.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/s976981862111.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/s976981862111.gif new file mode 100644 index 000000000..f4a2493a1 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/s976981862111.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/v3_pixel-trans.gif b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/v3_pixel-trans.gif new file mode 100644 index 000000000..bdf7a7704 Binary files /dev/null and b/public/transactions-jta/src/main/reference/SunJca1.0License.rtfd/v3_pixel-trans.gif differ diff --git a/public/transactions-jta/src/main/reference/SunJca1.5License.rtf b/public/transactions-jta/src/main/reference/SunJca1.5License.rtf new file mode 100644 index 000000000..b650b1ae4 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJca1.5License.rtf @@ -0,0 +1,287 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. Licensee acknowledges that\ + Licensed Software is not designed or intended for\ + use in the design, construction, operation or\ + maintenance of any nuclear facility. Sun\ + Microsystems, Inc. disclaims any express or\ + implied warranty of fitness for such uses. No\ + right, title or interest in or to any trademark,\ + service mark, logo or trade name of Sun or its\ + licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) INTERFACE CLASSES\ + J2EE(TM) CONNECTOR ARCHITECTURE 1.5\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of this\ + Agreement, including, but not limited to Section 3\ + (Java(TM) Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to Section 3 (Java Technology\ + Restrictions), Sun grants you a non-exclusive,\ + non-transferable, limited license to reproduce and\ + distribute the Software in binary form only,\ + provided that you (i) distribute the Software\ + complete and unmodified and only bundled as part\ + of your Programs, (ii) do not distribute\ + additional software intended to replace any\ + component(s) of the Software, (iii) do not remove\ + or alter any proprietary legends or notices\ + contained in the Software, (iv) only distribute\ + the Software subject to a license agreement that\ + protects Sun's interests consistent with the terms\ + contained in this Agreement, and (v) agree to\ + defend and indemnify Sun and its licensors from\ + and against any damages, costs, liabilities,\ + settlement amounts and/or expenses (including\ + attorneys' fees) incurred in connection with any\ + claim, lawsuit or action by any third party that\ + arises or results from the use or distribution of\ + any and all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + Platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks\ + and all SUN, SOLARIS, JAVA, JINI, FORTE, and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party may\ + terminate this Agreement immediately should any\ + Software become, or in either party's opinion be\ + likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc. 4150 Network Circle, Santa Clara, California\ + 95054.\ + (LFI#136186/Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJcaSpecLicense.rtf b/public/transactions-jta/src/main/reference/SunJcaSpecLicense.rtf new file mode 100644 index 000000000..8c354c553 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJcaSpecLicense.rtf @@ -0,0 +1,7 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural + +\f0\fs24 \cf0 See the specs PDF for the license conditions concerning the specification.} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJdbc2.0ExtensionsLicense.rtf b/public/transactions-jta/src/main/reference/SunJdbc2.0ExtensionsLicense.rtf new file mode 100644 index 000000000..3b45b79d2 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJdbc2.0ExtensionsLicense.rtf @@ -0,0 +1,7 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\froman\fcharset77 TimesNewRomanMS;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs32 \cf0 See the 2.0 extensions PDF spec for the license: nothing is said about restrictions. } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJdbc3.0License.rtf b/public/transactions-jta/src/main/reference/SunJdbc3.0License.rtf new file mode 100644 index 000000000..d3e608120 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJdbc3.0License.rtf @@ -0,0 +1,65 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\froman\fcharset77 TimesNewRomanMS;\f1\froman\fcharset77 TimesNewRomanBdMS;\f2\froman\fcharset77 TimesNewRomanItMS; +} +{\colortbl;\red255\green255\blue255;\red51\green0\blue0;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs32 \cf0 \ + +\f1\b JDBC +\fs22 (TM) +\fs32 API Specification ("Specification") +\f0\b0 +\f1\b \ +Version: 3.0 +\f0\b0 +\f1\b \ +Status: Final Release\ +Release: December 1, 2001 +\f0\b0 \ +\ +Copyright 2001 Sun Microsystems, Inc. \ +901 San Antonio Road, Palo Alto, California 94303, U.S.A. \ +All rights reserved. \ +\ + +\f1\b NOTICE +\f0\b0 \ +The Specification is protected by copyright and the information described therein may be protected by one or more U.S. patents, foreign patents, or pending applications.\'ca Except as provided under the following license, no part of the Specification may be reproduced in any form by any means without the prior written authorization of Sun Microsystems, Inc. ("Sun") and its licensors, if any.\'ca Any use of the Specification and the information described therein will be governed by the terms and conditions of this license and the Export Control Guidelines as set forth in the Terms of Use on Sun's website.\'ca By viewing, downloading or otherwise copying the Specification, you agree that you have read, understood, and will comply with all of the terms and conditions set forth herein. \ +\ +Subject to the terms and conditions of this license, Sun hereby grants you a fully-paid, non-exclusive, non-transferable, worldwide, limited license (without the right to sublicense) under Sun's intellectual property rights to review the Specification internally solely for the purpose of \cf2 designing and developing your Java applets and applications intended to run on the Java platform\cf0 .\'ca Other than this limited license, you acquire no right, title or interest in or to the Specification or any other Sun intellectual property.\'ca The Specification contains the proprietary information of Sun and may only be used in accordance with the license terms set forth herein. This license\'ca will terminate immediately without notice from Sun if you fail to comply with any provision of this license.\'ca\'ca Upon termination or expiration of this license, you must cease use of or destroy the Specification. \ +\ + +\f1\b TRADEMARKS +\f0\b0 \ +No right, title, or interest in or to any trademarks, service marks, or trade names of Sun or Sun's licensors is granted hereunder.\'ca Sun, Sun Microsystems, the Sun logo, Java, the Java Coffee Cup Logo, and JDBC are trademarks or registeredtrademarks of Sun Microsystems, Inc. in the U.S. and other countries. \ +\ + +\f1\b DISCLAIMER OF WARRANTIES +\f0\b0 \ +THE SPECIFICATION IS PROVIDED "AS IS" AND IS EXPERIMENTAL AND MAY CONTAIN DEFECTS OR DEFICIENCIES WHICH CANNOT OR WILL NOT BE CORRECTED BY SUN.\'ca SUN MAKES NO REPRESENTATIONS OR WARRANTIES, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT THAT THE CONTENTS OF THE SPECIFICATION ARE SUITABLE FOR ANY PURPOSE OR THAT ANY PRACTICE OR IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADE SECRETS OR OTHER RIGHTS.\'ca This document does not represent any commitment to release or implement any portion of the Specification in any product. \ +\ +THE SPECIFICATION COULD INCLUDE TECHNICAL INACCURACIES OR TYPOGRAPHICAL ERRORS.\'ca CHANGES ARE PERIODICALLY ADDED TO THE INFORMATION THEREIN; THESE CHANGES WILL BE INCORPORATED INTO NEW VERSIONS OF THE SPECIFICATION, IF ANY.\'ca SUN MAY MAKE IMPROVEMENTS AND/OR CHANGES TO THE PRODUCT(S) AND/OR THE PROGRAM(S) DESCRIBED IN THE SPECIFICATION AT ANY TIME.\'ca Any use of such changes in the Specification will be governed by the then-current license for the applicable version of the Specification. \ +\ + +\f1\b LIMITATION OF LIABILITY +\f0\b0 \ +TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY DAMAGES, INCLUDING WITHOUT LIMITATION, LOST REVENUE, PROFITS OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO ANY FURNISHING, PRACTICING, MODIFYING OR ANY USE OF THE SPECIFICATION, EVEN IF SUN AND/OR ITS LICENSORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. \ +\ +You will indemnify, hold harmless, and defend Sun and its licensors from any claims arising or resulting from: (i) your use of the Specification; (ii) the use or distribution of your Java applications or applets; and/or (iii) any claims that later versions or releases of any Specification furnished to you are incompatible with the Specification provided to you under this license. \ +\ + +\f1\b RESTRICTED RIGHTS LEGEND +\f0\b0 \ +If this Software is being acquired by or on behalf of the U.S. Government or by a U.S. Government prime contractor or subcontractor (at any tier), then the Government's rights in the Software and accompanying \ +documentation shall be only as set forth in this license; this is in accordance with 48 C.F.R. 227.7201 through 227.7202-4 (for Department of Defense (DoD) acquisitions) and with 48 C.F.R. 2.101 and 12.212 (for non-DoD acquisitions). \ +\ + +\f1\b REPORT +\f0\b0 \ +You may wish to report any ambiguities, inconsistencies or inaccuracies you may find in connection with your evaluation of the Specification ("Feedback").\'ca To the extent that you provide Sun with any Feedback, you hereby: (i) agree that such Feedback is provided on a non-proprietary and non-confidential basis, and (ii) grant Sun a perpetual, non-exclusive, worldwide, fully paid-up, irrevocable license, with the right to sublicense through multiple levels of sublicensees, to incorporate, disclose, and use without limitation the Feedback for any purpose related to the Specification and future versions, implementations, and test suites thereof. +\f2\i \ +(LFI#95308/Form ID#011801) +\f0\i0 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJms1.0.2bLicense.rtf b/public/transactions-jta/src/main/reference/SunJms1.0.2bLicense.rtf new file mode 100644 index 000000000..365345bd2 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJms1.0.2bLicense.rtf @@ -0,0 +1,287 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. You acknowledge that Software\ + is not designed, licensed or intended for use in\ + the design, construction, operation or maintenance\ + of any nuclear facility. Sun disclaims any\ + express or implied warranty of fitness for such\ + uses. No right, title or interest in or to any\ + trademark, service mark, logo or trade name of Sun\ + or its licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) INTERFACE CLASSES\ + JAVA MESSAGE SERVICE (JMS), VERSION 1.0.2\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of this\ + Agreement, including, but not limited to Section 3\ + (Java(TM) Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to Section 3 (Java Technology\ + Restrictions), Sun grants you a non-exclusive,\ + non-transferable, limited license to reproduce and\ + distribute the Software in binary form only,\ + provided that you (i) distribute the Software\ + complete and unmodified and only bundled as part\ + of your Programs, (ii) do not distribute\ + additional software intended to replace any\ + component(s) of the Software, (iii) do not remove\ + or alter any proprietary legends or notices\ + contained in the Software, (iv) only distribute\ + the Software subject to a license agreement that\ + protects Sun's interests consistent with the terms\ + contained in this Agreement, and (v) agree to\ + defend and indemnify Sun and its licensors from\ + and against any damages, costs, liabilities,\ + settlement amounts and/or expenses (including\ + attorneys' fees) incurred in connection with any\ + claim, lawsuit or action by any third party that\ + arises or results from the use or distribution of\ + any and all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + Platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, STAROFFICE, STARPORTAL\ + and iPLANET trademarks and all SUN, SOLARIS, JAVA,\ + JINI, FORTE, STAROFFICE, STARPORTAL and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party\ + may terminate this Agreement immediately should\ + any Software become, or in either party's opinion\ + be likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc. 901 San Antonio Road, Palo Alto, California\ + 94303\ + (Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJms1.1License.rtf b/public/transactions-jta/src/main/reference/SunJms1.1License.rtf new file mode 100644 index 000000000..faef4b140 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJms1.1License.rtf @@ -0,0 +1,287 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. You acknowledge that Software\ + is not designed, licensed or intended for use in\ + the design, construction, operation or maintenance\ + of any nuclear facility. Sun disclaims any\ + express or implied warranty of fitness for such\ + uses. No right, title or interest in or to any\ + trademark, service mark, logo or trade name of Sun\ + or its licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) INTERFACE CLASSES\ + JAVA MESSAGE SERVICE (JMS), VERSION 1.1\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of this\ + Agreement, including, but not limited to Section 3\ + (Java Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to Section 3 (Java Technology\ + Restrictions), Sun grants you a non-exclusive,\ + non-transferable, limited license to reproduce and\ + distribute the Software in binary form only,\ + provided that you (i) distribute the Software\ + complete and unmodified and only bundled as part\ + of your Programs, (ii) do not distribute\ + additional software intended to replace any\ + component(s) of the Software, (iii) do not remove\ + or alter any proprietary legends or notices\ + contained in the Software, (iv) only distribute\ + the Software subject to a license agreement that\ + protects Sun's interests consistent with the terms\ + contained in this Agreement, and (v) agree to\ + defend and indemnify Sun and its licensors from\ + and against any damages, costs, liabilities,\ + settlement amounts and/or expenses (including\ + attorneys' fees) incurred in connection with any\ + claim, lawsuit or action by any third party that\ + arises or results from the use or distribution of\ + any and all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + Platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, STAROFFICE, STARPORTAL\ + and iPLANET trademarks and all SUN, SOLARIS, JAVA,\ + JINI, FORTE, STAROFFICE, STARPORTAL and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party may\ + terminate this Agreement immediately should any\ + Software become, or in either party's opinion be\ + likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc. 901 San Antonio Road, Palo Alto, California\ + 94303\ + (LFI#111755/Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJms1.1SpecLicense.rtf b/public/transactions-jta/src/main/reference/SunJms1.1SpecLicense.rtf new file mode 100644 index 000000000..b08d316a6 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJms1.1SpecLicense.rtf @@ -0,0 +1,201 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww14960\viewh10400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + SUN IS WILLING TO LICENSE THIS SPECIFICATION TO\ + YOU ONLY UPON THE CONDITION THAT YOU ACCEPT ALL OF\ + THE TERMS CONTAINED IN THIS LICENSE AGREEMENT\ + ("AGREEMENT"). PLEASE READ THE TERMS AND\ + CONDITIONS OF THIS LICENSE CAREFULLY. BY\ + DOWNLOADING THIS SPECIFICATION, YOU ACCEPT THE\ + TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT. IF\ + YOU ARE NOT WILLING TO BE BOUND BY ITS TERMS,\ + SELECT THE "DECLINE" BUTTON AT THE BOTTOM OF THIS\ + PAGE AND THE DOWNLOADING PROCESS WILL NOT\ + CONTINUE.\ +\ + Java(TM) Message Service Specification\ + ("Specification")\ + Version: 1.1\ + Status: FCS\ + Release: April 12, 2002\ +\ + Copyright 2002 Sun Microsystems, Inc.\ + 901 San Antonio Road, Palo Alto, California 94303,\ + U.S.A.\ + All rights reserved.\ +\ + NOTICE\ + The Specification is protected by copyright and\ + the information described therein may be protected\ + by one or more U.S. patents, foreign patents, or\ + pending applications. Except as provided under\ + the following license, no part of the\ + Specification may be reproduced in any form by any\ + means without the prior written authorization of\ + Sun Microsystems, Inc. ("Sun") and its licensors,\ + if any. Any use of the Specification and the\ + information described therein will be governed by\ + the terms and conditions of this license and the\ + Export Control Guidelines as set forth in the\ + Terms of Use on Sun's website. By viewing,\ + downloading or otherwise copying the\ + Specification, you agree that you have read,\ + understood, and will comply with all of the terms\ + and conditions set forth herein.\ +\ + Subject to the terms and conditions of this\ + license, Sun hereby grants you a fully-paid,\ + non-exclusive, non-transferable, worldwide,\ + limited license (without the right to sublicense)\ + under Sun's intellectual property rights to review\ + the Specification internally solely for the\ + purpose of designing and developing your Java\ + applets and applications intended to run on the\ + Java platform. Other than this limited license,\ + you acquire no right, title or interest in or to\ + the Specification or any other Sun intellectual\ + property. The Specification contains the\ + proprietary information of Sun and may only be\ + used in accordance with the license terms set\ + forth herein. This license will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this license. Upon\ + termination or expiration of this license, you\ + must cease use of or destroy the Specification.\ +\ + TRADEMARKS\ + No right, title, or interest in or to any\ + trademarks, service marks, or trade names of Sun\ + or Sun's licensors is granted hereunder. Sun, Sun\ + Microsystems, the Sun logo, Java, Jini, J2EE,\ + JavaServerPages, Enterprise JavaBeans,\ + JavaCompatible, JDK, JDBC, JavaBeans, JavaMail,\ + Write Once, Run Anywhere, and Java Naming and\ + Directory Interface are trademarks or registered\ + trademarks of Sun Microsystems, Inc. in the U.S.\ + and other countries.\ +\ + DISCLAIMER OF WARRANTIES\ + THE SPECIFICATION IS PROVIDED "AS IS". SUN MAKES\ + NO REPRESENTATIONS OR WARRANTIES, EITHER EXPRESS\ + OR IMPLIED, INCLUDING BUT NOT LIMITED TO,\ + WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE, OR NON-INFRINGEMENT THAT THE\ + CONTENTS OF THE SPECIFICATION ARE SUITABLE FOR ANY\ + PURPOSE OR THAT ANY PRACTICE OR IMPLEMENTATION OF\ + SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY\ + PATENTS, COPYRIGHTS, TRADE SECRETS OR OTHER\ + RIGHTS. This document does not represent any\ + commitment to release or implement any portion of\ + the Specification in any product.\ +\ + THE SPECIFICATION COULD INCLUDE TECHNICAL\ + INACCURACIES OR TYPOGRAPHICAL ERRORS. CHANGES ARE\ + PERIODICALLY ADDED TO THE INFORMATION THEREIN;\ + THESE CHANGES WILL BE INCORPORATED INTO NEW\ + VERSIONS OF THE SPECIFICATION, IF ANY. SUN MAY\ + MAKE IMPROVEMENTS AND/OR CHANGES TO THE PRODUCT(S)\ + AND/OR THE PROGRAM(S) DESCRIBED IN THE\ + SPECIFICATION AT ANY TIME. Any use of such\ + changes in the Specification will be governed by\ + the then-current license for the applicable\ + version of the Specification.\ +\ + LIMITATION OF LIABILITY\ + TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT\ + WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY\ + DAMAGES, INCLUDING WITHOUT LIMITATION, LOST\ + REVENUE, PROFITS OR DATA, OR FOR SPECIAL,\ + INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE\ + DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE\ + THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO\ + ANY FURNISHING, PRACTICING, MODIFYING OR ANY USE\ + OF THE SPECIFICATION, EVEN IF SUN AND/OR ITS\ + LICENSORS HAVE BEEN ADVISED OF THE POSSIBILITY OF\ + SUCH DAMAGES.\ +\ + You will indemnify, hold harmless, and defend Sun\ + and its licensors from any claims arising or\ + resulting from: (i) your use of the Specification;\ + (ii) the use or distribution of your Java\ + applications or applets; and/or (iii) any claims\ + that later versions or releases of any\ + Specification furnished to you are incompatible\ + with the Specification provided to you under this\ + license.\ +\ + RESTRICTED RIGHTS LEGEND\ + U.S. Government: If this Specification is being\ + acquired by or on behalf of the U.S. Government or\ + by a U.S. Government prime contractor or\ + subcontractor (at any tier), then the Government's\ + rights in the Software and accompanying\ + documentation shall be only as set forth in this\ + license; this is in accordance with 48 C.F.R.\ + 227.7201 through 227.7202-4 (for Department of\ + Defense (DoD) acquisitions) and with 48 C.F.R.\ + 2.101 and 12.212 (for non-DoD acquisitions).\ +\ + REPORT\ + You may wish to report any ambiguities,\ + inconsistencies or inaccuracies you may find in\ + connection with your use of the Specification\ + ("Feedback"). To the extent that you provide Sun\ + with any Feedback, you hereby: (i) agree that such\ + Feedback is provided on a non-proprietary and\ + non-confidential basis, and (ii) grant Sun a\ + perpetual, non-exclusive, worldwide, fully\ + paid-up, irrevocable license, with the right to\ + sublicense through multiple levels of\ + sublicensees, to incorporate, disclose, and use\ + without limitation the Feedback for any purpose\ + related to the Specification and future versions,\ + implementations, and test suites thereof.\ +\ + (LFI#111700/Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJmsSpecLicense.rtf b/public/transactions-jta/src/main/reference/SunJmsSpecLicense.rtf new file mode 100644 index 000000000..8c354c553 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJmsSpecLicense.rtf @@ -0,0 +1,7 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural + +\f0\fs24 \cf0 See the specs PDF for the license conditions concerning the specification.} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJmx1.1License.rtf b/public/transactions-jta/src/main/reference/SunJmx1.1License.rtf new file mode 100644 index 000000000..276207673 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJmx1.1License.rtf @@ -0,0 +1,288 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. Licensee acknowledges that\ + Licensed Software is not designed or intended for\ + use in the design, construction, operation or\ + maintenance of any nuclear facility. Sun\ + Microsystems, Inc. disclaims any express or\ + implied warranty of fitness for such uses. No\ + right, title or interest in or to any trademark,\ + service mark, logo or trade name of Sun or its\ + licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) OPTIONAL PACKAGE\ + JMX(TM), VERSION 1.1\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of\ + this Agreement, including, but not limited to\ + Section 3 (Java Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to, Section 3 (Java Technology\ + Restrictions) of these Supplemental Terms, Sun\ + grants you a non-exclusive, non-transferable,\ + limited license to reproduce and distribute the\ + Software in binary code form only, provided that\ + you (i) distribute the Software complete and\ + unmodified and only bundled as part of your\ + Programs, (ii) do not distribute additional\ + software intended to replace any component(s) of\ + the Software, (iii) do not remove or alter any\ + proprietary legends or notices contained in the\ + Software, (iv) only distribute the Software\ + subject to a license agreement that protects Sun's\ + interests consistent with the terms contained in\ + this Agreement, and (v) agree to defend and\ + indemnify Sun and its licensors from and against\ + any damages, costs, liabilities, settlement\ + amounts and/or expenses (including attorneys'\ + fees) incurred in connection with any claim,\ + lawsuit or action by any third party that arises\ + or results from the use or distribution of any and\ + all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks\ + and all SUN, SOLARIS, JAVA, JINI, FORTE, and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party may\ + terminate this Agreement immediately should any\ + Software become, or in either party's opinion be\ + likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc., 4150 Network Circle, Santa Clara, California\ + 95054, U.S.A.\ + (LFI#112552/Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJmx1.1SpecLicense.rtf b/public/transactions-jta/src/main/reference/SunJmx1.1SpecLicense.rtf new file mode 100644 index 000000000..b2a9eee02 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJmx1.1SpecLicense.rtf @@ -0,0 +1,239 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\froman\fcharset77 TimesNewRomanMS;\f1\fswiss\fcharset77 ArialMS;\f2\fswiss\fcharset77 Arial-BoldMS; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red83\green130\blue161;\red51\green51\blue51;\red102\green102\blue102; +\red102\green0\blue102;\red255\green0\blue0;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs32 \cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f1\fs26 \cf2 \cf3 \ + \cf4 Quick Links:\cf3 \'ca\'ca \cf5 What's New\cf3 \'ca\'ca \cf5 JCP 2\cf3 \'ca\'ca \cf5 Membership\cf3 \'ca\'ca \cf5 Spec Lead Guide\cf3 \'ca\'ca \cf5 Expert Group Log in\cf3 \'ca\'ca \cf5 java.sun.com\cf3 \ +\'ca\ +\'ca\ +\'ca\ +\'ca\ +\'ca\ +\'ca\ +\'ca\ + \'ca\ +\'ca\ +\'ca\ + \'ca \ +\ + +\f2\b\fs50 \cf0 License Agreement\ +\ + +\f1\b0\fs26 \cf3 \ + +\f2\b To Print : +\f1\b0 In most browsers, select \cf6 File\cf3 and then \cf6 Print\cf3 from your browser's menu. \ +\ +------------------------------------------------------------------------\ +\cf0 Accept License Agreement\cf3 \cf0 Decline License Agreement\cf3 \ +\ + +\f3 \cf4 \ +\ + SUN MICROSYSTEMS, INC. ("SUN") IS WILLING TO\ + LICENSE THIS SPECIFICATION TO YOU ONLY UPON THE\ + CONDITION THAT YOU ACCEPT ALL OF THE TERMS\ + CONTAINED IN THIS LICENSE AGREEMENT\ + ("AGREEMENT"). PLEASE READ THE TERMS AND\ + CONDITIONS OF THIS LICENSE CAREFULLY. BY\ + DOWNLOADING THIS SPECIFICATION, YOU ACCEPT THE\ + TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT. IF\ + YOU ARE NOT WILLING TO BE BOUND BY ITS TERMS,\ + SELECT THE "DECLINE" BUTTON AT THE BOTTOM OF THIS\ + PAGE AND THE DOWNLOADING PROCESS WILL NOT\ + CONTINUE.\ +\ + JMX(TM) API Specification ("Specification")\ + Version: 1.1 Maintenance Release\ + Status: FCS\ + Release: March 1, 2002\ +\ + Copyright 2002 Sun Microsystems, Inc. \ +\ + 4150 Network Circle, Santa Clara, California\ + 95054, U.S.A\ + All rights reserved. \ +\ + NOTICE; LIMITED LICENSE GRANTS\ +\ + Sun hereby grants you a fully-paid, non-exclusive,\ + non-transferable, worldwide, limited license\ + (without the right to sublicense), under the Sun's\ + applicable intellectual property rights to view,\ + download, use and reproduce the Specification only\ + for the purpose of internal evaluation, which\ + shall be understood to include developing\ + applications intended to run on an implementation\ + of the Specification provided that such\ + applications do not themselves implement any\ + portion(s) of the Specification.\ +\ + Sun also grants you a perpetual, non-exclusive,\ + worldwide, fully paid-up, royalty free, limited\ + license (without the right to sublicense) under\ + any applicable copyrights or patent rights it may\ + have in the Specification to create and/or\ + distribute an Independent Implementation of the\ + Specification that: (i) fully implements the\ + Spec(s) including all its required interfaces and\ + functionality; (ii) does not modify, subset,\ + superset or otherwise extend the Licensor Name\ + Space, or include any public or protected\ + packages, classes, Java interfaces, fields or\ + methods within the Licensor Name Space other than\ + those required/authorized by the Specification or\ + Specifications being implemented; and (iii) passes\ + the TCK (including satisfying the requirements of\ + the applicable TCK Users Guide) for such\ + Specification. The foregoing license is expressly\ + conditioned on your not acting outside its scope.\ + No license is granted hereunder for any other\ + purpose.\ +\ + You need not include limitations (i)-(iii) from\ + the previous paragraph or any other particular\ + "pass through" requirements in any license You\ + grant concerning the use of your Independent\ + Implementation or products derived from it.\ + However, except with respect to implementations of\ + the Specification (and products derived from them)\ + by the your licensee that satisfy limitations\ + (i)-(iii) from the previous paragraph, You may\ + neither: (a) grant or otherwise pass through to\ + your licensees any licenses under Sun's applicable\ + intellectual property rights; nor (b) authorize\ + your licensees to make any claims concerning their\ + implementation's compliance with the Spec in\ + question.\ +\ + For the purposes of this Agreement: "Independent\ + Implementation" shall mean an implementation of\ + the Specification that neither derives from any of\ + Sun's source code or binary code materials nor,\ + except with an appropriate and separate license\ + from Sun, includes any of Sun's source code or\ + binary code materials; and "Licensor Name Space"\ + shall mean the public class or interface\ + declarations whose names begin with "java",\ + "javax", "com.sun" or their equivalents in any\ + subsequent naming convention adopted by Sun\ + through the Java Community Process, or any\ + recognized successors or replacements thereof.\ +\ + This Agreement will terminate immediately without\ + notice from Sun if you fail to comply with any\ + material provision of or act outside the scope of\ + the licenses granted above.\ +\ + TRADEMARKS\ +\ + No right, title, or interest in or to any\ + trademarks, service marks, or trade names of Sun\ + or Sun's licensors is granted hereunder. Sun, Sun\ + Microsystems, the Sun logo, Java, JMX, and the\ + Java Coffee Cup logo are trademarks or registered\ + trademarks of Sun Microsystems, Inc. in the U.S.\ + and other countries.\ +\ + DISCLAIMER OF WARRANTIES\ +\ + THE SPECIFICATION IS PROVIDED "AS IS". SUN MAKES\ + NO REPRESENTATIONS OR WARRANTIES, EITHER EXPRESS\ + OR IMPLIED, INCLUDING BUT NOT LIMITED TO,\ + WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE, OR NON-INFRINGEMENT, THAT THE\ + CONTENTS OF THE SPECIFICATION ARE SUITABLE FOR ANY\ + PURPOSE OR THAT ANY PRACTICE OR IMPLEMENTATION OF\ + SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY\ + PATENTS, COPYRIGHTS, TRADE SECRETS OR OTHER\ + RIGHTS. This document does not represent any\ + commitment to release or implement any portion of\ + the Specification in any product.\ +\ + THE SPECIFICATION COULD INCLUDE TECHNICAL\ + INACCURACIES OR TYPOGRAPHICAL ERRORS. CHANGES ARE\ + PERIODICALLY ADDED TO THE INFORMATION THEREIN;\ + THESE CHANGES WILL BE INCORPORATED INTO NEW\ + VERSIONS OF THE SPECIFICATION, IF ANY. SUN MAY\ + MAKE IMPROVEMENTS AND/OR CHANGES TO THE PRODUCT(S)\ + AND/OR THE PROGRAM(S) DESCRIBED IN THE\ + SPECIFICATION AT ANY TIME. Any use of such\ + changes in the Specification will be governed by\ + the then-current license for the applicable\ + version of the Specification.\ +\ + LIMITATION OF LIABILITY\ +\ + TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT\ + WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY\ + DAMAGES, INCLUDING WITHOUT LIMITATION, LOST\ + REVENUE, PROFITS OR DATA, OR FOR SPECIAL,\ + INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE\ + DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE\ + THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO\ + ANY FURNISHING, PRACTICING, MODIFYING OR ANY USE\ + OF THE SPECIFICATION, EVEN IF SUN AND/OR ITS\ + LICENSORS HAVE BEEN ADVISED OF THE POSSIBILITY OF\ + SUCH DAMAGES.\ +\ + You will indemnify, hold harmless, and defend Sun\ + and its licensors from any claims arising or\ + resulting from: (i) your use of the Specification;\ + (ii) the use or distribution of your Java\ + application, applet and/or clean room\ + implementation; and/or (iii) any claims that later\ + versions or releases of any Specification\ + furnished to you are incompatible with the\ + Specification provided to you under this license.\ +\ + RESTRICTED RIGHTS LEGEND\ +\ + U.S. Government: If this Specification is being\ + acquired by or on behalf of the U.S. Government or\ + by a U.S. Government prime contractor or\ + subcontractor (at any tier), then the Government's\ + rights in the Software and accompanying\ + documentation shall be only as set forth in this\ + license; this is in accordance with 48 C.F.R.\ + 227.7201 through 227.7202-4 (for Department of\ + Defense (DoD) acquisitions) and with 48 C.F.R.\ + 2.101 and 12.212 (for non-DoD acquisitions).\ +\ + REPORT\ +\ + You may wish to report any ambiguities,\ + inconsistencies or inaccuracies you may find in\ + connection with your use of the Specification\ + ("Feedback"). To the extent that you provide Sun\ + with any Feedback, you hereby: (i) agree that such\ + Feedback is provided on a non-proprietary and\ + non-confidential basis, and (ii) grant Sun a\ + perpetual, non-exclusive, worldwide, fully\ + paid-up, irrevocable license, with the right to\ + sublicense through multiple levels of\ + sublicensees, to incorporate, disclose, and use\ + without limitation the Feedback for any purpose\ + related to the Specification and future versions,\ + implementations, and test suites thereof.\ +\ + (LFI#113662/Form ID#011801)\ +\ + +\f1 \cf3 \ +\cf0 Accept License Agreement\cf3 \cf0 Decline License Agreement\cf3 \ +\ +\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \ +\cf2 Introduction\cf3 \'ca|\'ca \cf2 JSRs\cf3 \'ca|\'ca \cf2 What's New\cf3 \'ca|\'ca \cf2 Participation\cf3 \ +\cf2 JCP Procedures\cf3 \'ca|\'ca \cf2 Press & Success\cf3 \'ca|\'ca \cf2 Community Resources\cf3 \ +\'ca\ +Site sponsored and powered by \cf2 Sun Microsystems\cf3 \ +Copyright \'a9 1995-2006. All Rights Reserved.\ +\cf2 Terms of Use\cf3 . \cf2 Privacy Policy\cf3 . } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJmx1.2License.rtf b/public/transactions-jta/src/main/reference/SunJmx1.2License.rtf new file mode 100644 index 000000000..e33ae55f3 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJmx1.2License.rtf @@ -0,0 +1,288 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. Licensee acknowledges that\ + Licensed Software is not designed or intended for\ + use in the design, construction, operation or\ + maintenance of any nuclear facility. Sun\ + Microsystems, Inc. disclaims any express or\ + implied warranty of fitness for such uses. No\ + right, title or interest in or to any trademark,\ + service mark, logo or trade name of Sun or its\ + licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) OPTIONAL PACKAGE\ + JMX(TM), VERSION 1.2\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of\ + this Agreement, including, but not limited to\ + Section 3 (Java Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to, Section 3 (Java Technology\ + Restrictions) of these Supplemental Terms, Sun\ + grants you a non-exclusive, non-transferable,\ + limited license to reproduce and distribute the\ + Software in binary code form only, provided that\ + you (i) distribute the Software complete and\ + unmodified and only bundled as part of your\ + Programs, (ii) do not distribute additional\ + software intended to replace any component(s) of\ + the Software, (iii) do not remove or alter any\ + proprietary legends or notices contained in the\ + Software, (iv) only distribute the Software\ + subject to a license agreement that protects Sun's\ + interests consistent with the terms contained in\ + this Agreement, and (v) agree to defend and\ + indemnify Sun and its licensors from and against\ + any damages, costs, liabilities, settlement\ + amounts and/or expenses (including attorneys'\ + fees) incurred in connection with any claim,\ + lawsuit or action by any third party that arises\ + or results from the use or distribution of any and\ + all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks\ + and all SUN, SOLARIS, JAVA, JINI, FORTE, and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party may\ + terminate this Agreement immediately should any\ + Software become, or in either party's opinion be\ + likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc., 4150 Network Circle, Santa Clara, California\ + 95054, U.S.A.\ + (LFI#135010Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJmx1.2SpecLicense.rtf b/public/transactions-jta/src/main/reference/SunJmx1.2SpecLicense.rtf new file mode 100644 index 000000000..500f7e255 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJmx1.2SpecLicense.rtf @@ -0,0 +1,239 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\froman\fcharset77 TimesNewRomanMS;\f1\fswiss\fcharset77 ArialMS;\f2\fswiss\fcharset77 Arial-BoldMS; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red83\green130\blue161;\red51\green51\blue51;\red102\green102\blue102; +\red102\green0\blue102;\red255\green0\blue0;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs32 \cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f1\fs26 \cf2 \cf3 \ + \cf4 Quick Links:\cf3 \'ca\'ca \cf5 What's New\cf3 \'ca\'ca \cf5 JCP 2\cf3 \'ca\'ca \cf5 Membership\cf3 \'ca\'ca \cf5 Spec Lead Guide\cf3 \'ca\'ca \cf5 Expert Group Log in\cf3 \'ca\'ca \cf5 java.sun.com\cf3 \ +\'ca\ +\'ca\ +\'ca\ +\'ca\ +\'ca\ +\'ca\ +\'ca\ + \'ca\ +\'ca\ +\'ca\ + \'ca \ +\ + +\f2\b\fs50 \cf0 License Agreement\ +\ + +\f1\b0\fs26 \cf3 \ + +\f2\b To Print : +\f1\b0 In most browsers, select \cf6 File\cf3 and then \cf6 Print\cf3 from your browser's menu. \ +\ +------------------------------------------------------------------------\ +\cf0 Accept License Agreement\cf3 \cf0 Decline License Agreement\cf3 \ +\ + +\f3 \cf4 \ +\ + SUN MICROSYSTEMS, INC. ("SUN") IS WILLING TO\ + LICENSE THIS SPECIFICATION TO YOU ONLY UPON THE\ + CONDITION THAT YOU ACCEPT ALL OF THE TERMS\ + CONTAINED IN THIS LICENSE AGREEMENT\ + ("AGREEMENT"). PLEASE READ THE TERMS AND\ + CONDITIONS OF THIS LICENSE CAREFULLY. BY\ + DOWNLOADING THIS SPECIFICATION, YOU ACCEPT THE\ + TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT. IF\ + YOU ARE NOT WILLING TO BE BOUND BY ITS TERMS,\ + SELECT THE "DECLINE" BUTTON AT THE BOTTOM OF THIS\ + PAGE AND THE DOWNLOADING PROCESS WILL NOT\ + CONTINUE.\ +\ + JMX(TM) API Specification ("Specification")\ + Version: 1.2 Maintenance Release\ + Status: FCS\ + Release: September 2, 2002\ +\ + Copyright 2002 Sun Microsystems, Inc. \ +\ + 4150 Network Circle, Santa Clara, California\ + 95054, U.S.A\ + All rights reserved. \ +\ + NOTICE; LIMITED LICENSE GRANTS\ +\ + Sun hereby grants you a fully-paid, non-exclusive,\ + non-transferable, worldwide, limited license\ + (without the right to sublicense), under the Sun's\ + applicable intellectual property rights to view,\ + download, use and reproduce the Specification only\ + for the purpose of internal evaluation, which\ + shall be understood to include developing\ + applications intended to run on an implementation\ + of the Specification provided that such\ + applications do not themselves implement any\ + portion(s) of the Specification.\ +\ + Sun also grants you a perpetual, non-exclusive,\ + worldwide, fully paid-up, royalty free, limited\ + license (without the right to sublicense) under\ + any applicable copyrights or patent rights it may\ + have in the Specification to create and/or\ + distribute an Independent Implementation of the\ + Specification that: (i) fully implements the\ + Spec(s) including all its required interfaces and\ + functionality; (ii) does not modify, subset,\ + superset or otherwise extend the Licensor Name\ + Space, or include any public or protected\ + packages, classes, Java interfaces, fields or\ + methods within the Licensor Name Space other than\ + those required/authorized by the Specification or\ + Specifications being implemented; and (iii) passes\ + the TCK (including satisfying the requirements of\ + the applicable TCK Users Guide) for such\ + Specification. The foregoing license is expressly\ + conditioned on your not acting outside its scope.\ + No license is granted hereunder for any other\ + purpose.\ +\ + You need not include limitations (i)-(iii) from\ + the previous paragraph or any other particular\ + "pass through" requirements in any license You\ + grant concerning the use of your Independent\ + Implementation or products derived from it.\ + However, except with respect to implementations of\ + the Specification (and products derived from them)\ + by the your licensee that satisfy limitations\ + (i)-(iii) from the previous paragraph, You may\ + neither: (a) grant or otherwise pass through to\ + your licensees any licenses under Sun's applicable\ + intellectual property rights; nor (b) authorize\ + your licensees to make any claims concerning their\ + implementation's compliance with the Spec in\ + question.\ +\ + For the purposes of this Agreement: "Independent\ + Implementation" shall mean an implementation of\ + the Specification that neither derives from any of\ + Sun's source code or binary code materials nor,\ + except with an appropriate and separate license\ + from Sun, includes any of Sun's source code or\ + binary code materials; and "Licensor Name Space"\ + shall mean the public class or interface\ + declarations whose names begin with "java",\ + "javax", "com.sun" or their equivalents in any\ + subsequent naming convention adopted by Sun\ + through the Java Community Process, or any\ + recognized successors or replacements thereof.\ +\ + This Agreement will terminate immediately without\ + notice from Sun if you fail to comply with any\ + material provision of or act outside the scope of\ + the licenses granted above.\ +\ + TRADEMARKS\ +\ + No right, title, or interest in or to any\ + trademarks, service marks, or trade names of Sun\ + or Sun's licensors is granted hereunder. Sun, Sun\ + Microsystems, the Sun logo, Java, JMX, and the\ + Java Coffee Cup logo are trademarks or registered\ + trademarks of Sun Microsystems, Inc. in the U.S.\ + and other countries.\ +\ + DISCLAIMER OF WARRANTIES\ +\ + THE SPECIFICATION IS PROVIDED "AS IS". SUN MAKES\ + NO REPRESENTATIONS OR WARRANTIES, EITHER EXPRESS\ + OR IMPLIED, INCLUDING BUT NOT LIMITED TO,\ + WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE, OR NON-INFRINGEMENT, THAT THE\ + CONTENTS OF THE SPECIFICATION ARE SUITABLE FOR ANY\ + PURPOSE OR THAT ANY PRACTICE OR IMPLEMENTATION OF\ + SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY\ + PATENTS, COPYRIGHTS, TRADE SECRETS OR OTHER\ + RIGHTS. This document does not represent any\ + commitment to release or implement any portion of\ + the Specification in any product.\ +\ + THE SPECIFICATION COULD INCLUDE TECHNICAL\ + INACCURACIES OR TYPOGRAPHICAL ERRORS. CHANGES ARE\ + PERIODICALLY ADDED TO THE INFORMATION THEREIN;\ + THESE CHANGES WILL BE INCORPORATED INTO NEW\ + VERSIONS OF THE SPECIFICATION, IF ANY. SUN MAY\ + MAKE IMPROVEMENTS AND/OR CHANGES TO THE PRODUCT(S)\ + AND/OR THE PROGRAM(S) DESCRIBED IN THE\ + SPECIFICATION AT ANY TIME. Any use of such\ + changes in the Specification will be governed by\ + the then-current license for the applicable\ + version of the Specification.\ +\ + LIMITATION OF LIABILITY\ +\ + TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT\ + WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY\ + DAMAGES, INCLUDING WITHOUT LIMITATION, LOST\ + REVENUE, PROFITS OR DATA, OR FOR SPECIAL,\ + INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE\ + DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE\ + THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO\ + ANY FURNISHING, PRACTICING, MODIFYING OR ANY USE\ + OF THE SPECIFICATION, EVEN IF SUN AND/OR ITS\ + LICENSORS HAVE BEEN ADVISED OF THE POSSIBILITY OF\ + SUCH DAMAGES.\ +\ + You will indemnify, hold harmless, and defend Sun\ + and its licensors from any claims arising or\ + resulting from: (i) your use of the Specification;\ + (ii) the use or distribution of your Java\ + application, applet and/or clean room\ + implementation; and/or (iii) any claims that later\ + versions or releases of any Specification\ + furnished to you are incompatible with the\ + Specification provided to you under this license.\ +\ + RESTRICTED RIGHTS LEGEND\ +\ + U.S. Government: If this Specification is being\ + acquired by or on behalf of the U.S. Government or\ + by a U.S. Government prime contractor or\ + subcontractor (at any tier), then the Government's\ + rights in the Software and accompanying\ + documentation shall be only as set forth in this\ + license; this is in accordance with 48 C.F.R.\ + 227.7201 through 227.7202-4 (for Department of\ + Defense (DoD) acquisitions) and with 48 C.F.R.\ + 2.101 and 12.212 (for non-DoD acquisitions).\ +\ + REPORT\ +\ + You may wish to report any ambiguities,\ + inconsistencies or inaccuracies you may find in\ + connection with your use of the Specification\ + ("Feedback"). To the extent that you provide Sun\ + with any Feedback, you hereby: (i) agree that such\ + Feedback is provided on a non-proprietary and\ + non-confidential basis, and (ii) grant Sun a\ + perpetual, non-exclusive, worldwide, fully\ + paid-up, irrevocable license, with the right to\ + sublicense through multiple levels of\ + sublicensees, to incorporate, disclose, and use\ + without limitation the Feedback for any purpose\ + related to the Specification and future versions,\ + implementations, and test suites thereof.\ +\ + (LFI#117292/Form ID#011801)\ +\ + +\f1 \cf3 \ +\cf0 Accept License Agreement\cf3 \cf0 Decline License Agreement\cf3 \ +\ +\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca\'ca \ +\cf2 Introduction\cf3 \'ca|\'ca \cf2 JSRs\cf3 \'ca|\'ca \cf2 What's New\cf3 \'ca|\'ca \cf2 Participation\cf3 \ +\cf2 JCP Procedures\cf3 \'ca|\'ca \cf2 Press & Success\cf3 \'ca|\'ca \cf2 Community Resources\cf3 \ +\'ca\ +Site sponsored and powered by \cf2 Sun Microsystems\cf3 \ +Copyright \'a9 1995-2006. All Rights Reserved.\ +\cf2 Terms of Use\cf3 . \cf2 Privacy Policy\cf3 . } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJta1.0.1SpecLicenseOnDownload.rtf b/public/transactions-jta/src/main/reference/SunJta1.0.1SpecLicenseOnDownload.rtf new file mode 100644 index 000000000..a9db243d6 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJta1.0.1SpecLicenseOnDownload.rtf @@ -0,0 +1,138 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +License Agreement\ +\ +SUN MICROSYSTEMS, INC. (``SUN'') IS WILLING TO LICENSE ITS Java Transaction API\ +SOFTWARE (``SOFTWARE'') TO YOU ("CUSTOMER") ONLY UPON THE CONDITION \ +THAT YOU ACCEPT ALL OF THE TERMS CONTAINED IN THIS LICENSE AGREEMENT \ +("AGREEMENT"). READ THE TERMS AND CONDITIONS OF THE AGREEMENT \ +CAREFULLY BEFORE SELECTING THE "ACCEPT" BUTTON AT THE BOTTOM OF THIS\ +PAGE. BY SELECTING THE "ACCEPT" BUTTON YOU AGREE TO THE TERMS AND \ +CONDITIONS OF THE AGREEMENT. IF YOU ARE NOT WILLING TO BE BOUND BY \ +ITS TERMS, SELECT THE "DO NOT ACCEPT" BUTTON AT THE BOTTOM OF THIS PAGE\ +AND THE INSTALLATION PROCESS WILL NOT CONTINUE.\ +\ +1. License to Distribute. Customer is granted a royalty-free,\ +non-transferable right to reproduce and use the Software\ +for the purpose of developing applications which run in\ +conjunction with the Software. Customer may not modify the Software\ +(including any APIs exposed by the Software) in any way.\ +\ +2. Restrictions. Software is confidential copyrighted information\ +of Sun and title to all copies is retained by Sun and/or its\ +licensors. Except to the extent enforcement of this provision is\ +prohibited by applicable law, if at all, Customer shall not decompile,\ +disassemble, decrypt, extract, or otherwise reverse engineer Software.\ +Software is not designed or intended for use in on-line control of\ +aircraft, air traffic, aircraft navigation or aircraft communications;\ +or in the design, construction, operation or maintenance of any nuclear\ +facility. Customer warrants that it will not use or redistribute the\ +Software for such purposes.\ +\ +3. Trademarks and Logos. This Agreement does not authorize\ +Customer to use any Sun name, trademark or logo. Customer acknowledges\ +that Sun owns the Java trademark and all Java-related trademarks, logos\ +and icons including the Coffee Cup and Duke (``Java Marks'') and agrees\ +to: (i) comply with the Java Trademark Guidelines at\ +http://java.sun.com/trademarks.html; (ii) not do anything harmful to or\ +inconsistent with Sun's rights in the Java Marks; and (iii) assist Sun\ +in protecting those rights, including assigning to Sun any rights\ +acquired by Customer in any Java Mark.\ +\ +4. Disclaimer of Warranty. Software is provided ``AS IS,'' without a\ +warranty of any kind. ALL EXPRESS OR IMPLIED REPRESENTATIONS AND\ +WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS\ +FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.\ +\ +5.Limitation of Liability. IN NO EVENT WILL SUN OR ITS LICENSORS\ +BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR SPECIAL,\ +INDIRECT, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES HOWEVER CAUSED\ +AND REGARDLESS OF THE THEORY OF LIABILITY ARISING OUT OF THE\ +DOWNLOADING OF, USE OF, OR INABILITY TO USE, SOFTWARE, EVEN IF SUN HAS\ +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\ +\ +6. Termination. Customer may terminate this Agreement at any time\ +by destroying all copies of Software. This Agreement will terminate \ +immediately without notice from Sun if Customer fails to comply with \ +any provision of this Agreement. Upon such termination, Customer must \ +destroy all copies of Software. Sections 4 and 5 above shall survive \ +termination of this Agreement.\ +\ +7. Export Regulations. Software, including technical data, is\ +subject to U.S. export control laws, including the U.S. Export\ +Administration Act and its associated regulations, and may be subject\ +to export or import regulations in other countries. Customer agrees to\ +comply strictly with all such regulations and acknowledges that it has\ +the responsibility to obtain licenses to export, re-export, or import\ +Software. Software may not be downloaded, or otherwise exported or\ +re-exported (i) into, or to a national or resident of, Cuba, Iraq,\ +Iran, North Korea, Libya, Sudan, Syria or any country to which the U.S.\ +has embargoed goods; or (ii) to anyone on the U.S. Treasury\ +Department's list of Specially Designated Nations or the U.S. Commerce\ +Department's Table of Denial Orders.\ +\ +8. Restricted Rights. Use, duplication or disclosure by the United\ +States government is subject to the restrictions as set forth in the\ +Rights in Technical Data and Computer Software Clauses in DFARS\ +252.227-7013(c) (1) (ii) and FAR 52.227-19(c) (2) as applicable.\ +\ +9. Governing Law. Any action related to this Agreement will be\ +governed by California law and controlling U.S. federal law. No choice\ +of law rules of any jurisdiction will apply.\ +\ +10. Severability. If any of the above provisions are held to be in\ +violation of applicable law, void, or unenforceable in any\ +jurisdiction, then such provisions are herewith waived or amended to\ +the extent necessary for the Agreement to be otherwise enforceable in\ +such jurisdiction. However, if in Sun's opinion deletion or amendment\ +of any provisions of the Agreement by operation of this paragraph\ +unreasonably compromises the rights or increase the liabilities of Sun\ +or its licensors, Sun reserves the right to terminate the Agreement.\ +\ +\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJtaLicense-1.0.1.B.rtf b/public/transactions-jta/src/main/reference/SunJtaLicense-1.0.1.B.rtf new file mode 100644 index 000000000..bc1d52e1b --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJtaLicense-1.0.1.B.rtf @@ -0,0 +1,288 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww12760\viewh10560\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. Licensee acknowledges that\ + Licensed Software is not designed or intended for\ + use in the design, construction, operation or\ + maintenance of any nuclear facility. Sun\ + Microsystems, Inc. disclaims any express or\ + implied warranty of fitness for such uses. No\ + right, title or interest in or to any trademark,\ + service mark, logo or trade name of Sun or its\ + licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA(TM) INTERFACE CLASSES\ + JAVA TRANSACTION API (JTA), VERSION 1.0.1B,\ + MAINTENANCE RELEASE\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of this\ + Agreement, including, but not limited to Section 3\ + (Java Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to Section 3 (Java Technology\ + Restrictions), Sun grants you a non-exclusive,\ + non-transferable, limited license to reproduce and\ + distribute the Software in binary form only,\ + provided that you (i) distribute the Software\ + complete and unmodified and only bundled as part\ + of your Programs, (ii) do not distribute\ + additional software intended to replace any\ + component(s) of the Software, (iii) do not remove\ + or alter any proprietary legends or notices\ + contained in the Software, (iv) only distribute\ + the Software subject to a license agreement that\ + protects Sun's interests consistent with the terms\ + contained in this Agreement, and (v) agree to\ + defend and indemnify Sun and its licensors from\ + and against any damages, costs, liabilities,\ + settlement amounts and/or expenses (including\ + attorneys' fees) incurred in connection with any\ + claim, lawsuit or action by any third party that\ + arises or results from the use or distribution of\ + any and all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + Platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks\ + and all SUN, SOLARIS, JAVA, JINI, FORTE, and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party may\ + terminate this Agreement immediately should any\ + Software become, or in either party's opinion be\ + likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc. 4150 Network Circle, Santa Clara, California\ + 95054.\ + (LFI#121049/Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJtaLicense19-7-2006.rtf b/public/transactions-jta/src/main/reference/SunJtaLicense19-7-2006.rtf new file mode 100644 index 000000000..a4eb9d169 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJtaLicense19-7-2006.rtf @@ -0,0 +1,43 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\froman\fcharset77 TimesNewRomanMS;\f1\froman\fcharset77 TimesNewRomanBdMS;} +{\colortbl;\red255\green255\blue255;\red157\green17\blue179;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs32 \cf0 \ + \ +Licensing\ +\ +------------------------------------------------------------------------\ +\ +\ + +\f1\b\fs48 License for Java Transaction API +\f0\b0\fs32 \ +\ +The Java Transaction API is licensed under the \cf2 \ul \ulc2 Sun Binary Code License Agreement\cf0 \ulnone with the addition of the following supplemental licensing terms. \ +\ +JAVATM INTERFACE CLASSES JAVA TRANSACTION API (JTA), VERSION 1.0.1B,\ +MAINTENANCE RELEASE SUPPLEMENTAL LICENSE TERMS \ +\ +These supplemental license terms ("Supplemental Terms") add to or modify the terms of the Binary Code License Agreement (collectively, the "Agreement"). Capitalized terms not defined in these Supplemental Terms shall have the same meanings ascribed to them in the Agreement. These Supplemental Terms shall supersede any inconsistent or conflicting terms in the Agreement, or in any license contained within the Software. \ +\ +1. Software Internal Use and Development License Grant. Subject to the terms and conditions of this Agreement, including, but not limited to Section 3 (Java Technology Restrictions) of these Supplemental Terms, Sun grants you a non-exclusive, non-transferable, limited license to reproduce internally and use internally the binary form of the Software, complete and unmodified, for the sole purpose of designing, developing and testing your Java applets and applications ("Programs"). \ +\ +2. License to Distribute Software. In addition to the license granted in Section 1 (Software Internal Use and Development License Grant) of these Supplemental Terms, subject to the terms and conditions of this Agreement, including but not limited to Section 3 (Java Technology Restrictions), Sun grants you a non-exclusive, non-transferable, limited license to reproduce and distribute the Software in binary form only, provided that you (i) distribute the Software complete and unmodified and only bundled as part of your Programs, (ii) do not distribute additional software intended to replace any component(s) of the Software, (iii) do not remove or alter any proprietary legends or notices contained in the Software, (iv) only distribute the Software subject to a license agreement that protects Sun's interests consistent with the terms contained in this Agreement, and (v) agree to defend and indemnify Sun and its licensors from and against any damages, costs, liabilities, settlement amounts and/or expenses (including attorneys' fees) incurred in connection with any claim, lawsuit or action by any third party that arises or results from the use or distribution of any and all Programs and/or Software. \ +\ +3. Java Technology Restrictions. You may not modify the Java Platform Interface ("JPI", identified as classes contained within the "java" package or any subpackages of the "java" package), by creating additional classes within the JPI or otherwise causing the addition to or modification of the classes in the JPI. In the event that you create an additional class and associated API(s) which (i) extends the functionality of the Java Platform, and (ii) is exposed to third party software developers for the purpose of developing additional software which invokes such additional API, you must promptly publish broadly an accurate specification for such API for free use by all developers. You may not create, or authorize your licensees to create additional classes, interfaces, or subpackages that are in any way identified as "java", "javax", "sun" or similar convention as specified by Sun in any naming convention designation. \ +\ +4. Trademarks and Logos. You acknowledge and agree as between you and Sun that Sun owns the SUN, SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks and all SUN, SOLARIS, JAVA, JINI, FORTE, and iPLANET-related trademarks, service marks, logos and other brand designations ("Sun Marks"), and you agree to comply with the Sun Trademark and Logo Usage Requirements currently located at http://www.sun.com/policies/trademarks. Any use you make of the Sun Marks inures to Sun's benefit. \ +\ +5. Source Code. Software may contain source code that is provided solely for reference purposes pursuant to the terms of this Agreement. Source code may not be redistributed unless expressly provided for in this Agreement. \ +\ +6. Termination for Infringement. Either party may terminate this Agreement immediately should any Software become, or in either party's opinion be likely to become, the subject of a claim of infringement of any intellectual property right. \ +\ +For inquiries please contact: Sun Microsystems, Inc. 4150 Network Circle, Santa Clara, California 95054. \ +\ +------------------------------------------------------------------------\ +\ + \ +Licensing\ +} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunJtaSpecLicense.rtf b/public/transactions-jta/src/main/reference/SunJtaSpecLicense.rtf new file mode 100644 index 000000000..8c354c553 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunJtaSpecLicense.rtf @@ -0,0 +1,7 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural + +\f0\fs24 \cf0 See the specs PDF for the license conditions concerning the specification.} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunServlet2.3License.rtf b/public/transactions-jta/src/main/reference/SunServlet2.3License.rtf new file mode 100644 index 000000000..c21b91cb4 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunServlet2.3License.rtf @@ -0,0 +1,286 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 ArialMS;\f1\fswiss\fcharset77 Arial-BoldMS;\f2\fnil\fcharset77 Verdana-Bold; +\f3\fnil\fcharset77 Monaco;} +{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red231\green111\blue0;\red51\green51\blue102; +\red255\green0\blue0;\red102\green102\blue102;\red62\green107\blue138;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\fs24 \cf2 \ +\ + \ +\ +\ +\ + +\f1\b\fs42 \cf3 License Agreement +\f0\b0\fs24 \cf2 \ + +\f2\b\fs26 \cf4 Download Center\ +- Update Account\ +- Log Out +\f0\b0\fs24 \cf2 \ + +\f1\b To Print : +\f0\b0 In most browsers, select \cf5 File\cf2 and then \cf5 Print\cf2 from your browser's menu. \ +\ +------------------------------------------------------------------------\ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\f3 \cf6 \ +\ + Sun Microsystems, Inc.\ + Binary Code License Agreement\ +\ + READ THE TERMS OF THIS AGREEMENT AND ANY PROVIDED\ + SUPPLEMENTAL LICENSE TERMS (COLLECTIVELY\ + "AGREEMENT") CAREFULLY BEFORE OPENING THE SOFTWARE\ + MEDIA PACKAGE. BY OPENING THE SOFTWARE MEDIA\ + PACKAGE, YOU AGREE TO THE TERMS OF THIS\ + AGREEMENT. IF YOU ARE ACCESSING THE SOFTWARE\ + ELECTRONICALLY, INDICATE YOUR ACCEPTANCE OF THESE\ + TERMS BY SELECTING THE "ACCEPT" BUTTON AT THE END\ + OF THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL\ + THESE TERMS, PROMPTLY RETURN THE UNUSED SOFTWARE\ + TO YOUR PLACE OF PURCHASE FOR A REFUND OR, IF THE\ + SOFTWARE IS ACCESSED ELECTRONICALLY, SELECT THE\ + "DECLINE" BUTTON AT THE END OF THIS AGREEMENT.\ +\ + 1. LICENSE TO USE. Sun grants you a\ + non-exclusive and non-transferable license for the\ + internal use only of the accompanying software and\ + documentation and any error corrections provided\ + by Sun (collectively "Software"), by the number of\ + users and the class of computer hardware for which\ + the corresponding fee has been paid.\ +\ + 2. RESTRICTIONS. Software is confidential and\ + copyrighted. Title to Software and all associated\ + intellectual property rights is retained by Sun\ + and/or its licensors. Except as specifically\ + authorized in any Supplemental License Terms, you\ + may not make copies of Software, other than a\ + single copy of Software for archival purposes.\ + Unless enforcement is prohibited by applicable\ + law, you may not modify, decompile, or reverse\ + engineer Software. You acknowledge that Software\ + is not designed, licensed or intended for use in\ + the design, construction, operation or maintenance\ + of any nuclear facility. Sun disclaims any\ + express or implied warranty of fitness for such\ + uses. No right, title or interest in or to any\ + trademark, service mark, logo or trade name of Sun\ + or its licensors is granted under this Agreement.\ +\ + 3. LIMITED WARRANTY. Sun warrants to you that for\ + a period of ninety (90) days from the date of\ + purchase, as evidenced by a copy of the receipt,\ + the media on which Software is furnished (if any)\ + will be free of defects in materials and\ + workmanship under normal use. Except for the\ + foregoing, Software is provided "AS IS". Your\ + exclusive remedy and Sun's entire liability under\ + this limited warranty will be at Sun's option to\ + replace Software media or refund the fee paid for\ + Software.\ +\ + 4. DISCLAIMER OF WARRANTY. UNLESS SPECIFIED IN\ + THIS AGREEMENT, ALL EXPRESS OR IMPLIED CONDITIONS,\ + REPRESENTATIONS AND WARRANTIES, INCLUDING ANY\ + IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A\ + PARTICULAR PURPOSE OR NON-INFRINGEMENT ARE\ + DISCLAIMED, EXCEPT TO THE EXTENT THAT THESE\ + DISCLAIMERS ARE HELD TO BE LEGALLY INVALID.\ +\ + 5. LIMITATION OF LIABILITY. TO THE EXTENT NOT\ + PROHIBITED BY LAW, IN NO EVENT WILL SUN OR ITS\ + LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT\ + OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL,\ + INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED\ + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT\ + OF OR RELATED TO THE USE OF OR INABILITY TO USE\ + SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE\ + POSSIBILITY OF SUCH DAMAGES. In no event will\ + Sun's liability to you, whether in contract, tort\ + (including negligence), or otherwise, exceed the\ + amount paid by you for Software under this\ + Agreement. The foregoing limitations will apply\ + even if the above stated warranty fails of its\ + essential purpose.\ +\ + 6. Termination. This Agreement is effective\ + until terminated. You may terminate this\ + Agreement at any time by destroying all copies of\ + Software. This Agreement will terminate\ + immediately without notice from Sun if you fail to\ + comply with any provision of this Agreement. Upon\ + Termination, you must destroy all copies of\ + Software.\ +\ + 7. Export Regulations. All Software and technical\ + data delivered under this Agreement are subject to\ + US export control laws and may be subject to\ + export or import regulations in other countries.\ + You agree to comply strictly with all such laws\ + and regulations and acknowledge that you have the\ + responsibility to obtain such licenses to export,\ + re-export, or import as may be required after\ + delivery to you.\ +\ + 8. U.S. Government Restricted Rights. If\ + Software is being acquired by or on behalf of the\ + U.S. Government or by a U.S. Government prime\ + contractor or subcontractor (at any tier), then\ + the Government's rights in Software and\ + accompanying documentation will be only as set\ + forth in this Agreement; this is in accordance\ + with 48 CFR 227.7201 through 227.7202-4 (for\ + Department of Defense (DOD) acquisitions) and with\ + 48 CFR 2.101 and 12.212 (for non-DOD\ + acquisitions).\ +\ + 9. Governing Law. Any action related to this\ + Agreement will be governed by California law and\ + controlling U.S. federal law. No choice of law\ + rules of any jurisdiction will apply.\ +\ + 10. Severability. If any provision of this\ + Agreement is held to be unenforceable, this\ + Agreement will remain in effect with the provision\ + omitted, unless omission would frustrate the\ + intent of the parties, in which case this\ + Agreement will immediately terminate.\ +\ + 11. Integration. This Agreement is the entire\ + agreement between you and Sun relating to its\ + subject matter. It supersedes all prior or\ + contemporaneous oral or written communications,\ + proposals, representations and warranties and\ + prevails over any conflicting or additional terms\ + of any quote, order, acknowledgment, or other\ + communication between the parties relating to its\ + subject matter during the term of this Agreement.\ + No modification of this Agreement will be binding,\ + unless in writing and signed by an authorized\ + representative of each party.\ +\ + JAVA^(TM) INTERFACE CLASSES\ + JAVASERVLET, VERSION 2.3\ + SUPPLEMENTAL LICENSE TERMS\ +\ + These supplemental license terms ("Supplemental\ + Terms") add to or modify the terms of the Binary\ + Code License Agreement (collectively, the\ + "Agreement"). Capitalized terms not defined in\ + these Supplemental Terms shall have the same\ + meanings ascribed to them in the Agreement. These\ + Supplemental Terms shall supersede any\ + inconsistent or conflicting terms in the\ + Agreement, or in any license contained within the\ + Software.\ +\ + 1. Software Internal Use and Development License\ + Grant. Subject to the terms and conditions of this\ + Agreement, including, but not limited to Section 3\ + (Java(TM) Technology Restrictions) of these\ + Supplemental Terms, Sun grants you a\ + non-exclusive, non-transferable, limited license\ + to reproduce internally and use internally the\ + binary form of the Software, complete and\ + unmodified, for the sole purpose of designing,\ + developing and testing your Java applets and\ + applications ("Programs").\ +\ + 2. License to Distribute Software. In addition to\ + the license granted in Section 1 (Software\ + Internal Use and Development License Grant) of\ + these Supplemental Terms, subject to the terms and\ + conditions of this Agreement, including but not\ + limited to Section 3 (Java Technology\ + Restrictions), Sun grants you a non-exclusive,\ + non-transferable, limited license to reproduce and\ + distribute the Software in binary form only,\ + provided that you (i) distribute the Software\ + complete and unmodified and only bundled as part\ + of your Programs, (ii) do not distribute\ + additional software intended to replace any\ + component(s) of the Software, (iii) do not remove\ + or alter any proprietary legends or notices\ + contained in the Software, (iv) only distribute\ + the Software subject to a license agreement that\ + protects Sun's interests consistent with the terms\ + contained in this Agreement, and (v) agree to\ + defend and indemnify Sun and its licensors from\ + and against any damages, costs, liabilities,\ + settlement amounts and/or expenses (including\ + attorneys' fees) incurred in connection with any\ + claim, lawsuit or action by any third party that\ + arises or results from the use or distribution of\ + any and all Programs and/or Software.\ +\ + 3. Java Technology Restrictions. You may not\ + modify the Java Platform Interface ("JPI",\ + identified as classes contained within the "java"\ + package or any subpackages of the "java" package),\ + by creating additional classes within the JPI or\ + otherwise causing the addition to or modification\ + of the classes in the JPI. In the event that you\ + create an additional class and associated API(s)\ + which (i) extends the functionality of the Java\ + Platform, and (ii) is exposed to third party\ + software developers for the purpose of developing\ + additional software which invokes such additional\ + API, you must promptly publish broadly an accurate\ + specification for such API for free use by all\ + developers. You may not create, or authorize your\ + licensees to create additional classes,\ + interfaces, or subpackages that are in any way\ + identified as "java", "javax", "sun" or similar\ + convention as specified by Sun in any naming\ + convention designation.\ +\ + 4. Trademarks and Logos. You acknowledge and agree\ + as between you and Sun that Sun owns the SUN,\ + SOLARIS, JAVA, JINI, FORTE, and iPLANET trademarks\ + and all SUN, SOLARIS, JAVA, JINI, FORTE, and\ + iPLANET-related trademarks, service marks, logos\ + and other brand designations ("Sun Marks"), and\ + you agree to comply with the Sun Trademark and\ + Logo Usage Requirements currently located at\ + http://www.sun.com/policies/trademarks. Any use\ + you make of the Sun Marks inures to Sun's benefit.\ +\ + 5. Source Code. Software may contain source code\ + that is provided solely for reference purposes\ + pursuant to the terms of this Agreement. Source\ + code may not be redistributed unless expressly\ + provided for in this Agreement.\ +\ + 6. Termination for Infringement. Either party\ + may terminate this Agreement immediately should\ + any Software become, or in either party's opinion\ + be likely to become, the subject of a claim of\ + infringement of any intellectual property right.\ +\ + For inquiries please contact: Sun Microsystems,\ + Inc. 901 San Antonio Road, Palo Alto, California\ + 94303\ + (LFI#95907/Form ID#011801)\ +\ + +\f0 \cf2 \ + +\f1\b\fs22 \cf1 Accept License Agreement +\f0\b0\fs24 \cf2 +\f1\b\fs22 \cf1 Decline License Agreement +\f0\b0\fs24 \cf2 \ +\ + +\fs22 \cf7 Contact\cf2 \cf7 About Sun\cf2 \cf7 News\cf2 \cf7 Employment\cf2 \cf7 Privacy\cf2 \cf7 Terms of Use\cf2 \cf7 Trademarks\cf2 Copyright 1994-2006 Sun Microsystems, Inc. +\fs24 \ + } \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/SunServlet2.3SpecLicense.rtf b/public/transactions-jta/src/main/reference/SunServlet2.3SpecLicense.rtf new file mode 100644 index 000000000..8c354c553 --- /dev/null +++ b/public/transactions-jta/src/main/reference/SunServlet2.3SpecLicense.rtf @@ -0,0 +1,7 @@ +{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf380 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural + +\f0\fs24 \cf0 See the specs PDF for the license conditions concerning the specification.} \ No newline at end of file diff --git a/public/transactions-jta/src/main/reference/j2ee_connector-1_0-fr-spec.pdf b/public/transactions-jta/src/main/reference/j2ee_connector-1_0-fr-spec.pdf new file mode 100644 index 000000000..5bed382dc Binary files /dev/null and b/public/transactions-jta/src/main/reference/j2ee_connector-1_0-fr-spec.pdf differ diff --git a/public/transactions-jta/src/main/reference/j2ee_connector-1_5-fr-spec.pdf b/public/transactions-jta/src/main/reference/j2ee_connector-1_5-fr-spec.pdf new file mode 100644 index 000000000..253a62384 Binary files /dev/null and b/public/transactions-jta/src/main/reference/j2ee_connector-1_5-fr-spec.pdf differ diff --git a/public/transactions-jta/src/main/reference/jdbc-3.0-spec.df.pdf b/public/transactions-jta/src/main/reference/jdbc-3.0-spec.df.pdf new file mode 100644 index 000000000..6ac85a733 Binary files /dev/null and b/public/transactions-jta/src/main/reference/jdbc-3.0-spec.df.pdf differ diff --git a/public/transactions-jta/src/main/reference/jdbc20.stdext.pdf b/public/transactions-jta/src/main/reference/jdbc20.stdext.pdf new file mode 100644 index 000000000..baa7cefa2 Binary files /dev/null and b/public/transactions-jta/src/main/reference/jdbc20.stdext.pdf differ diff --git a/public/transactions-jta/src/main/reference/jms-1_0_2b-spec.pdf b/public/transactions-jta/src/main/reference/jms-1_0_2b-spec.pdf new file mode 100644 index 000000000..37a195d22 Binary files /dev/null and b/public/transactions-jta/src/main/reference/jms-1_0_2b-spec.pdf differ diff --git a/public/transactions-jta/src/main/reference/jta-spec1_0_1.pdf b/public/transactions-jta/src/main/reference/jta-spec1_0_1.pdf new file mode 100644 index 000000000..430c27a67 Binary files /dev/null and b/public/transactions-jta/src/main/reference/jta-spec1_0_1.pdf differ diff --git a/public/transactions-jta/src/main/reference/jts1_0-spec.pdf b/public/transactions-jta/src/main/reference/jts1_0-spec.pdf new file mode 100644 index 000000000..abf346ce2 Binary files /dev/null and b/public/transactions-jta/src/main/reference/jts1_0-spec.pdf differ diff --git a/public/transactions-jta/src/main/resources/META-INF/services/com.atomikos.icatch.TransactionServicePlugin b/public/transactions-jta/src/main/resources/META-INF/services/com.atomikos.icatch.TransactionServicePlugin new file mode 100644 index 000000000..0e88f3f6b --- /dev/null +++ b/public/transactions-jta/src/main/resources/META-INF/services/com.atomikos.icatch.TransactionServicePlugin @@ -0,0 +1 @@ +com.atomikos.icatch.jta.JtaTransactionServicePlugin \ No newline at end of file diff --git a/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/AbstractXidFactoryTestCase.java b/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/AbstractXidFactoryTestCase.java new file mode 100644 index 000000000..38ba61455 --- /dev/null +++ b/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/AbstractXidFactoryTestCase.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import javax.transaction.xa.Xid; + +import junit.framework.TestCase; + +/** + * Common test logic for xid factory. + * + * + */ +public abstract class AbstractXidFactoryTestCase extends TestCase { + + private XidFactory factory; + + protected final void setUp() throws Exception { + super.setUp(); + factory = createXidFactory(); + } + + protected XidFactory getFactory() + { + return factory; + } + + public void testBranchesAreDifferentForSameTid() { + String tid = "mytid"; + String name = "name"; + Xid xid = factory.createXid ( tid , name , name ); + Xid xid2 = factory.createXid ( tid , name , name); + String bqual = new String ( xid.getBranchQualifier()); + String bqual2 = new String ( xid2.getBranchQualifier()); + assertFalse ( bqual.equals ( bqual2 ) ); + } + + public void testGtidsAreSameForSameTid() { + String tid = "mytid"; + String name = "name"; + Xid xid = factory.createXid ( tid , name, name ); + Xid xid2 = factory.createXid ( tid , name, name ); + String gtid = new String ( xid.getGlobalTransactionId() ); + String gtid2 = new String ( xid2.getGlobalTransactionId() ); + assertEquals ( gtid , gtid2 ); + } + + public void testGtidsAreDifferentForDifferentTid() { + String tid = "mytid"; + String tid2 = "mytid2"; + String name = "name"; + Xid xid = factory.createXid ( tid , name, name ); + Xid xid2 = factory.createXid ( tid2 , name, name); + String gtid = new String ( xid.getGlobalTransactionId() ); + String gtid2 = new String ( xid2.getGlobalTransactionId() ); + assertFalse ( gtid.equals ( gtid2 ) ); + } + + public void testUniqueResourceName() { + String tid = "mytid"; + String tid2 = "mytid2"; + String name = "name"; + String resName = "resName"; + XID xid = factory.createXid ( tid , name, resName ); + assertEquals(resName, xid.getUniqueResourceName()); + } + + protected abstract XidFactory createXidFactory(); + +} diff --git a/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/DefaultXidFactoryTestJUnit.java b/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/DefaultXidFactoryTestJUnit.java new file mode 100644 index 000000000..60a6e3e4d --- /dev/null +++ b/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/DefaultXidFactoryTestJUnit.java @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +public class DefaultXidFactoryTestJUnit extends AbstractXidFactoryTestCase { + + protected XidFactory createXidFactory() { + return new DefaultXidFactory(); + } + + + +} diff --git a/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/Issue10086TestJUnit.java b/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/Issue10086TestJUnit.java new file mode 100644 index 000000000..b816d7f4c --- /dev/null +++ b/public/transactions-jta/src/test/java/com/atomikos/datasource/xa/Issue10086TestJUnit.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.datasource.xa; + +import java.util.concurrent.atomic.AtomicLong; + +import junit.framework.TestCase; + +public class Issue10086TestJUnit extends TestCase { + + private XidFactory factory; + + protected void setUp() throws Exception { + super.setUp(); + //set counter to max value for max length of branch id + AbstractXidFactory.counter = new AtomicLong ( Long.MAX_VALUE -1) ; + factory = new DefaultXidFactory(); + } + + //assert that at least 45 characters are supported in branch identifier + public void testResourceNameLength45() + { + + String bname = "abcdefghijklmnopqrstuvwxyz1234567890123456789"; + factory.createXid ( "abc" , bname , "resource"); + } + +} diff --git a/public/transactions-jta/src/test/java/com/atomikos/icatch/jta/JtaTransactionServicePluginTestJUnit.java b/public/transactions-jta/src/test/java/com/atomikos/icatch/jta/JtaTransactionServicePluginTestJUnit.java new file mode 100644 index 000000000..c68dc1731 --- /dev/null +++ b/public/transactions-jta/src/test/java/com/atomikos/icatch/jta/JtaTransactionServicePluginTestJUnit.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta; + +import java.rmi.registry.LocateRegistry; +import java.util.Properties; +import java.util.ServiceLoader; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.atomikos.icatch.TransactionServicePlugin; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.imp.CompositeTransactionManagerImp; + +public class JtaTransactionServicePluginTestJUnit { + + private JtaTransactionServicePlugin plugin; + private Properties properties; + + @Before + public void setUp() throws Exception { + plugin = new JtaTransactionServicePlugin(); + properties = new Properties(); + properties.setProperty("com.atomikos.icatch.default_jta_timeout", "0"); + properties.setProperty("com.atomikos.icatch.serial_jta_transactions", "true"); + properties.setProperty("com.atomikos.icatch.tm_unique_name", "bla"); + properties.setProperty("com.atomikos.icatch.automatic_resource_registration", "true"); + try { + LocateRegistry.createRegistry ( 1099 ); + } catch (Exception ok){} + } + + @After + public void tearDown() throws Exception { + plugin.afterShutdown(); + } + + @Test + public void testCanBeLoadedViaServiceLoader() { + ServiceLoader loader = ServiceLoader.load(TransactionServicePlugin.class); + boolean found = false; + for(TransactionServicePlugin p : loader) { + if (p instanceof JtaTransactionServicePlugin) found = true; + } + Assert.assertTrue(found); + } + + @Test + public void testSetsDefaultJtaTimeout() { + Configuration.getConfigProperties().setProperty("com.atomikos.icatch.default_jta_timeout", "20000"); + plugin.beforeInit(); + Assert.assertEquals(20, TransactionManagerImp.getDefaultTimeout()); + } + + @Test + public void testSetSerialJtaTransactionsFalse() { + Configuration.getConfigProperties().setProperty("com.atomikos.icatch.serial_jta_transactions", "false"); + plugin.beforeInit(); + Assert.assertFalse(TransactionManagerImp.getDefaultSerial()); + } + + @Test + public void testSetsSerialJtaTransactionsTrue() { + Configuration.getConfigProperties().setProperty("com.atomikos.icatch.serial_jta_transactions", "true"); + plugin.beforeInit(); + Assert.assertTrue(TransactionManagerImp.getDefaultSerial()); + } + + + + + @Test + public void testAfterInitInstallsJtaTransactionManager() { + Configuration.installCompositeTransactionManager(new CompositeTransactionManagerImp()); + Configuration.init(); + plugin.afterInit(); + Assert.assertNotNull(TransactionManagerImp.getTransactionManager()); + Configuration.shutdown(true); + } +} \ No newline at end of file diff --git a/public/transactions-jta/src/test/java/com/atomikos/icatch/jta/template/TransactionTemplateTestJUnit.java b/public/transactions-jta/src/test/java/com/atomikos/icatch/jta/template/TransactionTemplateTestJUnit.java new file mode 100644 index 000000000..0efcc8e97 --- /dev/null +++ b/public/transactions-jta/src/test/java/com/atomikos/icatch/jta/template/TransactionTemplateTestJUnit.java @@ -0,0 +1,190 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.jta.template; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class TransactionTemplateTestJUnit { + + private TransactionTemplate template; + private TransactionManager mockedTm; + private TransactionTemplate recursiveTemplate; + + @Before + public void setUp() throws Exception { + mockedTm = Mockito.mock(TransactionManager.class); + template = new TransactionTemplate(mockedTm, 0); + } + + @Test + public void testRequiredPerformsCommitIfNoException() throws Exception { + template.required().execute(() -> {return null;}); + Mockito.verify(mockedTm).commit(); + Mockito.verify(mockedTm, Mockito.never()).rollback(); + Mockito.verify(mockedTm, Mockito.never()).setRollbackOnly(); + } + + @Test + public void testRequiredPerformsRollbackOnException() throws Exception { + try { + template.required().execute(() -> {throw new Exception();}); + } catch (Exception ok) {} + Mockito.verify(mockedTm).rollback(); + Mockito.verify(mockedTm, Mockito.never()).commit(); + } + + @Test + public void testRequiredWithExistingTransactionCallsRollbackOnlyForException() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + try { + template.required().execute(() -> {throw new Exception();}); + } catch (Exception ok) {} + Mockito.verify(mockedTm).setRollbackOnly(); + Mockito.verify(mockedTm, Mockito.never()).rollback(); + } + + @Test + public void testRequiredWithExistingTransactionDoesNotCommit() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + try { + template.required().execute(() -> {return null;}); + } catch (Exception ok) {} + Mockito.verify(mockedTm, Mockito.never()).commit(); + } + + @Test + public void testNestedCommitsSubtransaction() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + try { + template.nested().execute(() -> {return null;}); + } catch (Exception ok) {} + Mockito.verify(mockedTm).commit(); + Mockito.verify(mockedTm, Mockito.never()).rollback(); + Mockito.verify(mockedTm, Mockito.never()).setRollbackOnly(); + } + + @Test + public void testNestedRollsbackSubtransactionOnException() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + try { + template.nested().execute(() -> {throw new Exception();}); + } catch (Exception ok) {} + Mockito.verify(mockedTm).rollback(); + Mockito.verify(mockedTm, Mockito.never()).commit(); + } + + @Test + public void testRequiresNewSuspendsExistingTransaction() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + Mockito.when(mockedTm.suspend()).thenReturn(mockedTransaction); + template.requiresNew().execute(() -> {return null;}); + Mockito.verify(mockedTm).suspend(); + Mockito.verify(mockedTm).begin(); + Mockito.verify(mockedTm).commit(); + Mockito.verify(mockedTm).resume(Mockito.any()); + } + + @Test(expected=IllegalStateException.class) + public void testMandatoryThrowsWithoutExistingTransaction() throws Exception { + template.mandatory().execute(() -> {return null;}); + } + + @Test + public void testMandatory() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + template.mandatory().execute(() -> {return null;}); + Mockito.verify(mockedTm, Mockito.never()).begin(); + Mockito.verify(mockedTm, Mockito.never()).commit(); + } + + @Test(expected=IllegalStateException.class) + public void testNeverThrowsWithExistingTransaction() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + template.never().execute(() -> {return null;}); + } + + @Test + public void testNeverDoesNotStartTransaction() throws Exception { + template.never().execute(() -> {return null;}); + Mockito.verify(mockedTm, Mockito.never()).begin(); + } + + @Test + public void testSupportsReusesExistingTransaction() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + template.supports().execute(() -> {return null;}); + Mockito.verify(mockedTm, Mockito.never()).begin(); + Mockito.verify(mockedTm, Mockito.never()).suspend(); + Mockito.verify(mockedTm, Mockito.never()).commit(); + Mockito.verify(mockedTm, Mockito.never()).rollback(); + } + + @Test + public void testSupportsDoesNotStartTransaction() throws Exception { + template.supports().execute(() -> {return null;}); + Mockito.verify(mockedTm, Mockito.never()).begin(); + } + + @Test + public void testNotSupportedSuspendsExistingTransaction() throws Exception { + Transaction mockedTransaction = Mockito.mock(Transaction.class); + Mockito.when(mockedTm.getTransaction()).thenReturn(mockedTransaction); + Mockito.when(mockedTm.suspend()).thenReturn(mockedTransaction); + template.notSupported().execute(() -> {return null;}); + Mockito.verify(mockedTm).suspend(); + Mockito.verify(mockedTm).resume(Mockito.any()); + Mockito.verify(mockedTm, Mockito.never()).begin(); + Mockito.verify(mockedTm, Mockito.never()).commit(); + Mockito.verify(mockedTm, Mockito.never()).rollback(); + } + + @Test + public void testNotSupported() throws Exception { + template.notSupported().execute(() -> {return null;}); + Mockito.verify(mockedTm, Mockito.never()).begin(); + } + + @Test + public void testCallingWithTimeoutDoesNotChangeStrategy() { + TransactionTemplate required = template.required(); + assertSame(required, required.withTimeout(5)); + } + + @Test + public void testRecursiveTransactionScopesAreIsolatedFromEachOther() throws Exception { + TransactionTemplate required = template.required(); + required.execute(() -> { + recursiveTemplate = template.required(); + return null; + }); + assertNotSame(required, recursiveTemplate); + } + + @Test + public void testSubsequentCallsReturnDifferentTransactionScopes() throws Exception { + assertNotSame(template.required(), template.required()); + } + +} diff --git a/public/transactions-jta/src/test/java/com/atomikos/recovery/xa/InMemoryPreviousXidRepositoryTestJUnit.java b/public/transactions-jta/src/test/java/com/atomikos/recovery/xa/InMemoryPreviousXidRepositoryTestJUnit.java new file mode 100644 index 000000000..8fc72003c --- /dev/null +++ b/public/transactions-jta/src/test/java/com/atomikos/recovery/xa/InMemoryPreviousXidRepositoryTestJUnit.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.xa; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.atomikos.datasource.xa.XID; + +public class InMemoryPreviousXidRepositoryTestJUnit { + + private static final long EXPIRATION_DELAY = 10; + + private InMemoryPreviousXidRepository sut; + private XID xid; + + @Before + public void setUp() { + sut = new InMemoryPreviousXidRepository(); + xid = new XID("tid", "branch", "resource"); + + } + + @Test + public void testRemember() throws InterruptedException { + long expiration = System.currentTimeMillis() + EXPIRATION_DELAY; + sut.remember(xid, expiration); + List result = sut.findXidsExpiredAt(expiration + 1); + assertFalse(result.isEmpty()); + assertEquals(xid, result.get(0)); + } + + @Test + public void testRememberDoesNotOverwriteExistingContentWithSameExpiration() { + long expiration = System.currentTimeMillis() + EXPIRATION_DELAY; + sut.remember(xid, expiration); + sut.remember(new XID("tid2", "branch2", "resource2"), expiration); + List result = sut.findXidsExpiredAt(expiration + 1); + assertFalse(result.isEmpty()); + assertEquals(2, result.size()); + } + + @Test + public void testForget() { + long expiration = System.currentTimeMillis() + EXPIRATION_DELAY; + sut.remember(xid, expiration); + sut.forgetXidsExpiredAt(expiration); + List result = sut.findXidsExpiredAt(expiration + 1); + assertTrue(result.isEmpty()); + } + + +} diff --git a/public/transactions-osgi/osgi.bnd b/public/transactions-osgi/osgi.bnd new file mode 100644 index 000000000..a17d39e0e --- /dev/null +++ b/public/transactions-osgi/osgi.bnd @@ -0,0 +1,6 @@ +Bundle-Activator: com.atomikos.transactions.internal.AtomikosActivator +Embed-Transitive: true +Embed-Dependency: *;groupId=com.atomikos;inline=true +Import-Package: javax.transaction;version="1.0.1",javax.transaction.xa;version="1.0.1",org.osgi.framework,javax.management;resolution:=optional,javax.naming.*,*;resolution:=optional +Export-Package: com.atomikos.jdbc,com.atomikos.jdbc.nonxa,com.atomikos.jms,com.atomikos.jms.extra,com.atomikos.icatch.jta,com.atomikos.icatch.jta.template +DynamicImport-Package: * diff --git a/public/transactions-osgi/pom.xml b/public/transactions-osgi/pom.xml new file mode 100644 index 000000000..29cebf50a --- /dev/null +++ b/public/transactions-osgi/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-osgi + Transactions OSGi + + false + + + + + + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + + + bundle-manifest + compile + + manifest + + + true + + <_include>-osgi.bnd + + + + + + + + maven-dependency-plugin + + + unpack-sources + + unpack-dependencies + + + ${project.build.directory}/sources + sources + com.atomikos + + + + + + maven-assembly-plugin + + + source-assembly + package + + single + + + + + com.atomikos.transactions-osgi.source + ${project.version} + com.atomikos.transactions-osgi.source + com.atomikos.transactions-osgi;version="${project.version}";roots:="." + + + + src.xml + + + + + + + + + + com.atomikos + transactions-jdbc + 6.0.1-SNAPSHOT + true + + + com.atomikos + transactions-jms + 6.0.1-SNAPSHOT + true + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + true + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + org.osgi + org.osgi.core + 4.0.0 + provided + + + diff --git a/public/transactions-osgi/src.xml b/public/transactions-osgi/src.xml new file mode 100644 index 000000000..91825d32c --- /dev/null +++ b/public/transactions-osgi/src.xml @@ -0,0 +1,25 @@ + + + + + sources + false + + jar + + + + ${project.build.directory}/sources + / + true + + + \ No newline at end of file diff --git a/public/transactions-osgi/src/main/java/com/atomikos/transactions/internal/AtomikosActivator.java b/public/transactions-osgi/src/main/java/com/atomikos/transactions/internal/AtomikosActivator.java new file mode 100644 index 000000000..dc0c21468 --- /dev/null +++ b/public/transactions-osgi/src/main/java/com/atomikos/transactions/internal/AtomikosActivator.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.transactions.internal; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +import com.atomikos.icatch.jta.UserTransactionImp; +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * @author pascalleclercq When transactions-osgi bundle starts It register theses Impl in the service registry. + */ +public class AtomikosActivator implements BundleActivator { + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosActivator.class); + + private UserTransactionManager utm; + private ServiceRegistration utmRegistration; + private ServiceRegistration userTransactionRegistration; + private UserTransactionImp userTransaction; + + public void start(BundleContext context) throws Exception { + try { + // TransactionManager + utm = new UserTransactionManager(); + utm.init(); + Dictionary tmProps = new Hashtable(); + tmProps.put("osgi.jndi.service.name", "AtomikosV5"); + utmRegistration = context.registerService(TransactionManager.class.getName(), utm, tmProps); + // UserTransaction + userTransaction = new UserTransactionImp(); + Dictionary utmProps = new Hashtable(); + utmProps.put("osgi.jndi.service.name", "AtomikosV5"); + userTransactionRegistration = context.registerService(UserTransaction.class.getName(), userTransaction, utmProps); + } catch (Exception e) { + LOGGER.logFatal(e.getMessage(), e); + } + } + + public void stop(BundleContext context) throws Exception { + try { + if (utmRegistration != null) { + utmRegistration.unregister(); + utmRegistration = null; + } + + if (utm != null) { + utm.close(); + } + if (userTransactionRegistration != null) { + userTransactionRegistration.unregister(); + userTransactionRegistration = null; + } + + } catch (Exception e) { + LOGGER.logError(e.getMessage(), e); + } + + } + +} diff --git a/public/transactions-remoting/pom.xml b/public/transactions-remoting/pom.xml new file mode 100644 index 000000000..230dbdfd0 --- /dev/null +++ b/public/transactions-remoting/pom.xml @@ -0,0 +1,102 @@ + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions-remoting + + false + + + + + maven-surefire-plugin + + true + 1 + false + + **/*TestJUnit.java + + + ${project.build.testOutputDirectory} + + + + + + + com.atomikos + transactions-jta + 6.0.1-SNAPSHOT + provided + + + com.atomikos + transactions + 6.0.1-SNAPSHOT + + + org.apache.cxf + cxf-rt-frontend-jaxrs + 3.1.11 + provided + + + org.apache.cxf + cxf-rt-rs-client + 3.1.11 + provided + + + cxf-rt-rs-extension-providers + org.apache.cxf + 3.1.11 + provided + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + 2.9.4 + provided + + + org.apache.dubbo + dubbo + 2.7.6 + provided + + + io.grpc + grpc-api + 1.33.1 + provided + + + javax.ws.rs + javax.ws.rs-api + 2.0.1 + provided + + + org.springframework.hateoas + spring-hateoas + 0.23.0.RELEASE + provided + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + org.apache.geronimo.specs + geronimo-jta_1.0.1B_spec + 1.0 + provided + + + diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/CheckedExportingTransactionManager.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/CheckedExportingTransactionManager.java new file mode 100644 index 000000000..6c71519e8 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/CheckedExportingTransactionManager.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import java.util.HashMap; +import java.util.Map; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.ExportingTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + + /** + * Wrapper implementation that decorates another implementation with checking: + * at commit time, this instance makes sure that there are no outstanding calls. + * + * The original motivation for adding this functionality was due to HTTP 202 + * response codes: in such cases, our response filters are not invoked at all + * and the transaction will commit without extent for the remote work - + * which in turn means an unclear commit scope (since what your application commits + * is not what it may think it commits: the pending work of the remote 202 response is not part of the commit). + * + */ +public class CheckedExportingTransactionManager implements ExportingTransactionManager { + + private static final Logger LOGGER = LoggerFactory.createLogger(CheckedExportingTransactionManager.class); + + private static Map pendingRequestSynchronisation = new HashMap(); + + private ExportingTransactionManager delegate; + + public CheckedExportingTransactionManager(ExportingTransactionManager delegate) { + this.delegate = delegate; + } + + @Override + public Propagation getPropagation() throws SysException, IllegalStateException { + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + CompositeTransaction ct = ctm.getCompositeTransaction(); + if (ct == null) { + throw new IllegalStateException("This method requires a transaction but none was found"); + } + registerPendingRequestSynchronisation(ct); + return delegate.getPropagation(); + } + + @Override + public void addExtent(Extent extent) throws SysException, IllegalArgumentException, RollbackException { + delegate.addExtent(extent); + markRequestAsCompleted(extent.getParentTransactionId()); + } + + private void registerPendingRequestSynchronisation(CompositeTransaction ct) { + PendingRequestSynchronisation s = new PendingRequestSynchronisation(ct); + pendingRequestSynchronisation.put(ct.getTid(), s); + ct.registerSynchronization(s); + } + + + private static void markRequestAsCompleted(String tid) { + PendingRequestSynchronisation s = pendingRequestSynchronisation.get(tid); + if (s != null) { + s.markAsDone(); + pendingRequestSynchronisation.remove(tid); + } + } + + private static class PendingRequestSynchronisation implements Synchronization { + + private boolean done; + private CompositeTransaction ct; + + PendingRequestSynchronisation(CompositeTransaction ct) { + this.ct = ct; + } + + private void markAsDone() { + done = true; + } + + @Override + public void beforeCompletion() { + if (!done) { + LOGGER.logWarning( + "Pending outgoing remote request detected at transaction commit - forcing rollback since commit scope will not be as expected!\n" + + "Possible causes: attempting to commit a transaction with timed out remote calls, calling a remote service that returns HTTP 202 Accepted or an invalid extent in the return..."); + ct.setRollbackOnly(); + } + } + + @Override + public void afterCompletion(TxState txstate) { + markRequestAsCompleted(ct.getTid()); + } + + } + + + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/DefaultExportingTransactionManager.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/DefaultExportingTransactionManager.java new file mode 100644 index 000000000..06783aeb9 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/DefaultExportingTransactionManager.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import java.util.Stack; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.ExportingTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * Default implementation that should work with any remoting protocol as long as + * Strings can be sent/received. + * + */ + +public class DefaultExportingTransactionManager implements ExportingTransactionManager { + + private static final Logger LOGGER = LoggerFactory.createLogger(DefaultExportingTransactionManager.class); + + + @Override + public Propagation getPropagation() throws SysException, IllegalStateException { + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + CompositeTransaction ct = ctm.getCompositeTransaction(); + if (ct == null) { + throw new IllegalStateException("This method requires a transaction but none was found"); + } + String recoveryDomainName = Configuration.getConfigProperties().getTmUniqueName(); + String recoveryCoordinatorURI = null; + CompositeTransaction root = null; + if (ct.isRoot()) { + root = ct; + } else { + root = ct.getLineage().firstElement(); + } + Propagation p = new Propagation(recoveryDomainName, root, ct, ct.isSerial(), ct.getTimeout(), recoveryCoordinatorURI); + LOGGER.logDebug("Exporting propagation: " + p.toString()); + return p; + } + + + + @Override + public void addExtent(Extent extent) throws SysException, IllegalArgumentException, RollbackException { + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + CompositeTransaction ct = ctm.getCompositeTransaction(); + if (ct == null) { + throw new RollbackException("No transaction found - any remote work will not be committed by us."); + } + if (extent == null) { + throw new IllegalArgumentException("Expected an extent but found none. The remote work will not be committed by us."); + } + if (!ct.getTid().equals(extent.getParentTransactionId())) { + throw new IllegalArgumentException("The supplied extent is for a different transaction: found " + extent.getParentTransactionId()+ " but expected " + ct.getTid()); + } + ct.getExtent().add(extent); + Stack participants = extent.getParticipants(); + for (Participant p : participants) { + ct.addParticipant(p); //cf case 183884 + } + } + + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/DefaultImportingTransactionManager.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/DefaultImportingTransactionManager.java new file mode 100644 index 000000000..0b6d88948 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/DefaultImportingTransactionManager.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import java.net.URI; +import java.net.URISyntaxException; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.ImportingTransactionManager; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.twopc.AtomikosRestPort; +import com.atomikos.remoting.twopc.ParticipantAdapter; + +/** + * Default implementation that should work with any remoting protocol + * as long as Strings can be sent/received. + * + */ + +public class DefaultImportingTransactionManager implements ImportingTransactionManager { + + private static final Logger LOGGER = LoggerFactory.createLogger(DefaultImportingTransactionManager.class); + + private void assertRestPortUrlSet() { + if (AtomikosRestPort.getUrl() == null) { + LOGGER.logFatal("Not configured for remoting - see https://www.atomikos.com/Documentation/ConfiguringRemoting for details"); + } + } + + + @Override + public CompositeTransaction importTransaction(Propagation propagation) throws IllegalArgumentException, SysException { + assertRestPortUrlSet(); + if (propagation == null) { + throw new IllegalArgumentException("Propagation must not be null"); + } + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + return ctm.recreateCompositeTransaction(propagation); + } + + @Override + public Extent terminated(boolean commit) throws SysException, RollbackException { + Extent extent = null; + Extent ret = null; + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + CompositeTransaction ct = ctm.getCompositeTransaction(); + if(ct != null) { + try { + if (commit) { + extent = ct.getExtent(); + ct.commit(); + } else { + ct.rollback(); + return null; + } + + } catch (RollbackException rb) { + throw rb; + } catch (Throwable e) { + throw new SysException("Error in termination: " + e.getMessage(), e); + } + + } else { + throw new RollbackException("Attempting to terminate a transaction that no longer exists - probably due to a timeout?"); + } + + URI participantURI; + try { + participantURI = new URI(AtomikosRestPort.buildParticipantUrl(ct)); + } catch (URISyntaxException e) { + throw new SysException("Could not create URI for extent", e); + } + ret = new Extent(extent); // cf case 185558: create extent without any direct participants + ret.add(new ParticipantAdapter(participantURI), 1); + LOGGER.logDebug("Returning extent: " + ret); + return ret; + } + + + + + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/Parser.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/Parser.java new file mode 100644 index 000000000..3625a526a --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/Parser.java @@ -0,0 +1,217 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.imp.CompositeTransactionAdaptor; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.twopc.ParticipantAdapter; + + +public class Parser { + + private static final Logger LOGGER = LoggerFactory.createLogger(Parser.class); + + /** + * Parses an incoming propagation. This method should be forward and backward compatible: + * + *
    + *
  • New (extra) elements in the incoming propagationAsString are logged but ignored, and
  • + *
  • If new (extra) elements are expected but not found then this is also logged and ignored.
  • + *
+ * + * @param propagationAsString + * + * @return The parsed propagation. + */ + public Propagation parsePropagation(String propagationAsString) { + if (propagationAsString == null) { + throw new IllegalArgumentException("The supplied string must not be null"); + } + LOGGER.logDebug("Parsing incoming propagation: " + propagationAsString); + String[] properties = propagationAsString.split(","); + if (properties.length < 6) { + throw new IllegalArgumentException("The supplied propagation is incomplete"); + } + String version = parseProperty("version", properties[0]); + if (!Propagation.VERSION.equals(version)) { + throw new IllegalArgumentException("The supplied propagation is of a more recent, incompatible version: " + version); + } + String domain = parseProperty("domain", properties[1]); + String timeoutAsString = parseProperty("timeout", properties[2]); + long timeout = Long.parseLong(timeoutAsString); + String serialAsString = parseProperty("serial", properties[3]); + String recoveryCoordinatorURI = parseProperty("recoveryCoordinatorURI", properties[4]); + boolean serial = Boolean.parseBoolean(serialAsString); + + RecoveryCoordinator recoveryCoordinator = new RecoveryCoordinator() { + + @Override + public String getURI() { + return recoveryCoordinatorURI; + } + + @Override + public String getRecoveryDomainName() { + return domain; + } + + }; + + int lineageOffset = findFirstOccurrence("parent", properties, 5); + CompositeTransactionAdaptor rootTransaction = findNextAncestor(properties, serial, lineageOffset, recoveryCoordinator); + CompositeTransactionAdaptor parentTransaction = null; + lineageOffset = findLastOccurrence("parent", properties, lineageOffset + 1); + if (lineageOffset < properties.length) { + parentTransaction = findNextAncestor(properties, serial, lineageOffset, recoveryCoordinator); + } else {//propagation is for a root + parentTransaction = rootTransaction; + } + return new Propagation(domain, rootTransaction, parentTransaction, serial, timeout, recoveryCoordinatorURI); + } + + private CompositeTransactionAdaptor findNextAncestor(String[] properties, boolean serial, int lineageOffset, + RecoveryCoordinator recoveryCoordinator) { + CompositeTransactionAdaptor parentTransaction = null; + if (lineageOffset < properties.length) { + String parentId = parseProperty("parent", properties[lineageOffset]); + parentTransaction = new CompositeTransactionAdaptor(parentId, serial, recoveryCoordinator); + boolean nextParentFound = false; + for (int i = lineageOffset + 1; i < properties.length && !nextParentFound; i++) { + String[] property = parseAssignment(properties[i]); + if ("parent".equals(property[0])) { + nextParentFound = true; + } else { + String propertyKey = property[0].substring(9); //remove 'property.' prefix + parentTransaction.setProperty(propertyKey, property[1]); + } + } + } + return parentTransaction; + } + + private int findLastOccurrence(String propertyName, String[] properties, int startingPosition) { + int next = startingPosition; + int ret = properties.length; + while (next < properties.length) { + next = findFirstOccurrence(propertyName, properties, next); + if (next < properties.length) { + ret = next; + } + next++; + } + return ret; + } + + private int findFirstOccurrence(String propertyName, String[] properties, int startingPosition) { + int ret = properties.length; + boolean found = false; + for (int i = startingPosition; i < properties.length && !found; i++) { + try { + parseProperty(propertyName, properties[i]); + ret = i; + found = true; + } catch (IllegalArgumentException continueLoop) { + } + } + return ret; + } + + private String parseProperty(String expectedPropertyName, String propertyAssignmentExpression) { + String[] parsed = parseAssignment(propertyAssignmentExpression); + if (!parsed[0].equals(expectedPropertyName)) { + throw new IllegalArgumentException("Expected: " + expectedPropertyName + " but found: " + parsed[0]); + } + return parsed[1]; + } + + private String[] parseAssignment(String propertyAssignmentExpression) { + String[] parsed = propertyAssignmentExpression.split("="); + if (parsed.length != 2) { + throw new IllegalArgumentException(propertyAssignmentExpression + " is not a valid part"); + } + return parsed; + } + + /** + * Parses an incoming extent. This method should be forward and backward compatible: + * + *
    + *
  • New (extra) elements in the incoming extentAsString are logged but ignored, and
  • + *
  • If new (extra) elements are expected but not found then this is also logged and ignored.
  • + *
+ * + * @param extentAsString + * + * @return The parsed extent. + */ + public Extent parseExtent(String extentAsString) { + if (extentAsString == null) { + return null; + } + LOGGER.logDebug("Parsing incoming extent: " + extentAsString); + String[] properties = extentAsString.split(","); + if (properties.length < 5) { + throw new IllegalArgumentException("The supplied extent is incomplete: "+ extentAsString); + } + String version = parseProperty("version", properties[0]); + if (!Extent.VERSION.equals(version)) { + throw new IllegalArgumentException("The supplied extent is of a more recent, incompatible version: " + version); + } + String parentTransactionId = parseProperty("parent", properties[1]); + Extent ret = new Extent(parentTransactionId); + + int participantOffset = findFirstOccurrence("uri", properties, 2); + + if ((properties.length - participantOffset) / 3 == 0) { + throw new IllegalArgumentException("The supplied extent is incomplete: "+ extentAsString); + } + + while (participantOffset < properties.length) { + extractParticipantInfo(properties, ret, participantOffset); + participantOffset = findFirstOccurrence("uri", properties, participantOffset + 1); + } + + return ret; + } + + private void extractParticipantInfo(String[] properties, Extent ret, int offSet) { + Map remoteParticipants = new HashMap<>(); + String uri = parseProperty("uri", properties[offSet]); + String responseCountAsString = parseProperty("responseCount", properties[offSet+1]); + String directAsString = parseProperty("direct", properties[offSet+2]); + boolean direct = Boolean.parseBoolean(directAsString); + int responseCount = Integer.parseInt(responseCountAsString); + if (direct) { + try { + ParticipantAdapter p = new ParticipantAdapter(new URI(uri)); + ret.add(p, responseCount); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } else { + remoteParticipants.put(uri, responseCount); + ret.addRemoteParticipants(remoteParticipants); + } + int i = offSet + 3; + while (i < properties.length && !properties[i].startsWith("uri=")) { + LOGGER.logTrace("Ingoring unknown element in extent: " + properties[i]); + i++; + } + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/RemotingTransactionServicePlugin.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/RemotingTransactionServicePlugin.java new file mode 100644 index 000000000..2c7f7109b --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/RemotingTransactionServicePlugin.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import com.atomikos.icatch.TransactionServicePlugin; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.fs.RecoveryLogImp; +import com.atomikos.remoting.twopc.AtomikosRestPort; + +public class RemotingTransactionServicePlugin implements TransactionServicePlugin { + + + private static final Logger LOGGER = LoggerFactory.createLogger(RemotingTransactionServicePlugin.class); + + private ConfigProperties configProperties; + + @Override + public void beforeInit() { + configProperties = Configuration.getConfigProperties(); + } + + @Override + public void afterInit() { + AtomikosRestPort.init(findRestPortUrl()); + String registered = null; + try { + registered = configProperties.getProperty("com.atomikos.icatch.registered"); + } catch (Exception notRegistered) { + LOGGER.logTrace("Failed to check for registration property", notRegistered); + } + + if (Configuration.getRecoveryLog() instanceof RecoveryLogImp && registered == null) { + LOGGER.logWarning("Activating module transactions-remoting - this module is best combined with https://www.atomikos.com/Main/LogCloud for distributed recovery and distributed monitoring..."); + } + } + + private String findRestPortUrl() { + String ret = null; + try { + ret = configProperties.getProperty(AtomikosRestPort.REST_URL_PROPERTY_NAME); + } catch (Exception notFound) { + //do nothing: normal if not set in jta.properties + //our JAX-RS filter may still set this on the first incoming request + } + return ret; + } + + @Override + public void afterShutdown() { + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/dubbo/DubboConsumerTransactionPropagationFilter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/dubbo/DubboConsumerTransactionPropagationFilter.java new file mode 100644 index 000000000..086c76ca1 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/dubbo/DubboConsumerTransactionPropagationFilter.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.dubbo; + +import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER; + +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.Filter; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.RpcException; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ClientInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +@Activate(group = CONSUMER, order = 100) +public class DubboConsumerTransactionPropagationFilter implements Filter, Filter.Listener { + + private final Logger LOGGER = LoggerFactory.createLogger(DubboConsumerTransactionPropagationFilter.class); + + private ClientInterceptorTemplate template = new ClientInterceptorTemplate(); + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + LOGGER.logTrace("Filtering request..."); + String propagation = template.onOutgoingRequest(); + RpcContext.getContext().setAttachment(HeaderNames.PROPAGATION_HEADER_NAME, propagation); + return invoker.invoke(invocation); + } + + @Override + public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) { + LOGGER.logTrace("Filtering response..."); + String extent = appResponse.getAttachment(HeaderNames.EXTENT_HEADER_NAME); + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace("extent: " + extent); + } + template.onIncomingResponse(extent); + } + + @Override + public void onError(Throwable t, Invoker invoker, Invocation invocation) { + + } +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/dubbo/DubboProviderTransactionPropagationFilter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/dubbo/DubboProviderTransactionPropagationFilter.java new file mode 100644 index 000000000..da4c8cd70 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/dubbo/DubboProviderTransactionPropagationFilter.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.dubbo; + +import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER; + +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.Filter; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.RpcException; + +import com.atomikos.icatch.RollbackException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ContainerInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +@Activate(group = PROVIDER, order = 100) +public class DubboProviderTransactionPropagationFilter implements Filter, Filter.Listener { + + private final Logger LOGGER = LoggerFactory.createLogger(DubboProviderTransactionPropagationFilter.class); + + private ContainerInterceptorTemplate template = new ContainerInterceptorTemplate(); + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + LOGGER.logTrace("Filtering incoming request..."); + + try { + String propagation = RpcContext.getContext().getAttachment(HeaderNames.PROPAGATION_HEADER_NAME); + if ( LOGGER.isTraceEnabled() ) { + LOGGER.logTrace("propagation: " + propagation); + } + template.onIncomingRequest(propagation); + } catch (IllegalArgumentException e) { + LOGGER.logWarning("Detected invalid incoming transaction - aborting..."); + throw new RpcException(e); + } + return invoker.invoke(invocation); + } + + @Override + public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) { + String extent = terminateImportedTransaction(appResponse); + appResponse.setAttachment(HeaderNames.EXTENT_HEADER_NAME, extent); + } + + @Override + public void onError(Throwable t, Invoker invoker, Invocation invocation) { + LOGGER.logError("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + t.getClass().getName() + ": " + t.getMessage(), t); + } + + private String terminateImportedTransaction(Result result) { + String extent = null; + try { + if (result.hasException()) { + extent = template.onOutgoingResponse(true); + } else { + extent = template.onOutgoingResponse(false); + } + } catch (RollbackException e) { + String msg = "Transaction was rolled back - probably due to a timeout?"; + LOGGER.logWarning(msg, e); + } catch (Exception e) { + String msg = "Unexpected error while terminating transaction"; + LOGGER.logError(msg, e); + } + return extent; + } +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/grpc/TransactionAwareClientInterceptor.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/grpc/TransactionAwareClientInterceptor.java new file mode 100644 index 000000000..504067aad --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/grpc/TransactionAwareClientInterceptor.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.grpc; + +import com.atomikos.remoting.support.ClientInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.MethodDescriptor; +import io.grpc.Status; + +public class TransactionAwareClientInterceptor implements ClientInterceptor { + + private ClientInterceptorTemplate template = new ClientInterceptorTemplate(); + + + public ClientCall interceptCall(final MethodDescriptor methodDescriptor, final CallOptions callOptions, final Channel channel) { + return new ForwardingClientCall.SimpleForwardingClientCall(channel.newCall(methodDescriptor, callOptions)) { + @Override + public void start(final Listener responseListener, final Metadata headers) { + String propagation = template.onOutgoingRequest(); + if (propagation != null) { + headers.put(Key.of(HeaderNames.PROPAGATION_HEADER_NAME, Metadata.ASCII_STRING_MARSHALLER), propagation); + } + super.start( + new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + @Override + public void onClose(Status status, Metadata trailers) { + String extent = trailers.get(Key.of(HeaderNames.EXTENT_HEADER_NAME, Metadata.ASCII_STRING_MARSHALLER)); + template.onIncomingResponse(extent); + super.onClose(status, trailers); + } + }, + headers); + } + + + }; + } + +} \ No newline at end of file diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/grpc/TransactionAwareServerInterceptor.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/grpc/TransactionAwareServerInterceptor.java new file mode 100644 index 000000000..312c7bdde --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/grpc/TransactionAwareServerInterceptor.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.grpc; + +import java.io.IOException; + +import com.atomikos.icatch.RollbackException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ContainerInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +import io.grpc.ForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; + +public class TransactionAwareServerInterceptor implements ServerInterceptor { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionAwareServerInterceptor.class); + private ContainerInterceptorTemplate template = new ContainerInterceptorTemplate(); + + public Listener interceptCall(final ServerCall call, final Metadata metadata, + final ServerCallHandler next) { + + String propagation = metadata.get(Key.of(HeaderNames.PROPAGATION_HEADER_NAME, Metadata.ASCII_STRING_MARSHALLER)); + try { + template.onIncomingRequest(propagation); + } catch (IllegalArgumentException e) { + LOGGER.logWarning("Detected invalid incoming transaction - aborting..."); + call.close(Status.FAILED_PRECONDITION, metadata); + } + + return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void close(Status status, Metadata responseMetadata) { + try { + String extent = terminateImportedTransaction(status); + if (extent != null) { + Key extentHeaderName = Key.of(HeaderNames.EXTENT_HEADER_NAME, Metadata.ASCII_STRING_MARSHALLER); + metadata.put(extentHeaderName, extent); + responseMetadata.merge(metadata); + } + } catch (Exception e) { + String msg = "Unexpected error while terminating transaction"; + LOGGER.logError(msg, e); + } + + super.close(status, responseMetadata); + } + }, metadata); + + } + + + private String terminateImportedTransaction(Status status) throws IOException { + String extent = null; + boolean error = false; + try { + if (!status.isOk()) { + error = true; + } + extent = template.onOutgoingResponse(error); + } catch (RollbackException e) { + throw new IOException(e); + } catch (Exception e) { + String msg = "Unexpected error while terminating transaction"; + LOGGER.logError(msg, e); + throw new IOException(msg, e); + } + return extent; + } +} \ No newline at end of file diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/jaxrs/TransactionAwareRestClientFilter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/jaxrs/TransactionAwareRestClientFilter.java new file mode 100644 index 000000000..a6acdc2d2 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/jaxrs/TransactionAwareRestClientFilter.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.jaxrs; + +import java.io.IOException; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; + +import com.atomikos.remoting.support.ClientInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +/** + * Filter (interceptor) that adds the current thread's JTA transaction context + * to outgoing REST calls, and makes sure that the response is added to the + * commit scope of the active transaction. + * + * This class is only needed for REST clients that embed the Atomikos JTA + * transaction manager, and it automatically makes outgoing REST calls part of + * the transaction. + */ + +public class TransactionAwareRestClientFilter implements ClientRequestFilter, ClientResponseFilter { + + private ClientInterceptorTemplate template = new ClientInterceptorTemplate(); + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + String propagation = template.onOutgoingRequest(); + if (propagation != null) { + requestContext.getHeaders().add(HeaderNames.PROPAGATION_HEADER_NAME, propagation); + } + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + String extent = responseContext.getHeaderString(HeaderNames.EXTENT_HEADER_NAME); + template.onIncomingResponse(extent); + + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/jaxrs/TransactionAwareRestContainerFilter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/jaxrs/TransactionAwareRestContainerFilter.java new file mode 100644 index 000000000..4af2f7c2d --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/jaxrs/TransactionAwareRestContainerFilter.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.jaxrs; + +import java.io.IOException; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status.Family; +import javax.ws.rs.core.UriInfo; + +import com.atomikos.icatch.RollbackException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ContainerInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; +import com.atomikos.remoting.twopc.AtomikosRestPort; + +/** + * Filter (interceptor) that adds starts a local JTA transaction that becomes + * part of the remote client's transaction context. + * + * This class is only needed for REST services that embed the Atomikos JTA + * transaction manager, and it automatically makes incoming REST calls part of + * the caller's transaction. + * + * Note: this interceptor only works if: + *
    + *
  • the local transaction service (Atomikos) is running, AND
  • + *
  • a AtomikosRestPort instance is available for this JVM
  • + *
+ */ + +public class TransactionAwareRestContainerFilter implements ContainerRequestFilter, ContainerResponseFilter { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionAwareRestContainerFilter.class); + + private static final int UNPROCESSABLE_ENTITY = 422; + + private ContainerInterceptorTemplate template = new ContainerInterceptorTemplate(); + + @Context + UriInfo info; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + assertAtomikosRestPortUrlSet(); + String propagation = requestContext.getHeaderString(HeaderNames.PROPAGATION_HEADER_NAME); + try { + template.onIncomingRequest(propagation); + } catch (IllegalArgumentException e) { + LOGGER.logWarning("Detected invalid incoming transaction - aborting..."); + requestContext.abortWith(Response.status(UNPROCESSABLE_ENTITY).build()); + } + } + + private void assertAtomikosRestPortUrlSet() { + if (AtomikosRestPort.getUrl() == null ) { + // Not set in jta.properties => try to guess here... + AtomikosRestPort.setUrl(info.getBaseUriBuilder().path(AtomikosRestPort.class).build().toString()); + LOGGER.logWarning("Init property " + AtomikosRestPort.REST_URL_PROPERTY_NAME + " not set, guessing it. See https://www.atomikos.com/Documentation/ConfiguringRemoting for the implications..."); + } + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + String extent = terminateImportedTransaction(responseContext); + if (extent !=null) { + responseContext.getHeaders().add(HeaderNames.EXTENT_HEADER_NAME, extent); + } + } + + private String terminateImportedTransaction(ContainerResponseContext responseContext) throws IOException { + String extent = null; + boolean error = false; + try { + + if (responseContext.getStatusInfo().getFamily() != Family.SUCCESSFUL) { + error = true; + } + extent = template.onOutgoingResponse(error); + } catch (RollbackException e) { + throw new IOException(e); + } catch (Exception e) { + String msg = "Unexpected error while terminating transaction"; + LOGGER.logError(msg, e); + throw new IOException(msg, e); + } + return extent; + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/httpinvoker/TransactionalHttpInvokerRequestExecutor.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/httpinvoker/TransactionalHttpInvokerRequestExecutor.java new file mode 100644 index 000000000..527a8222d --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/httpinvoker/TransactionalHttpInvokerRequestExecutor.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.spring.httpinvoker; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import org.springframework.remoting.httpinvoker.HttpInvokerClientConfiguration; +import org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ClientInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +public class TransactionalHttpInvokerRequestExecutor extends SimpleHttpInvokerRequestExecutor { + + private final Logger LOGGER = LoggerFactory.createLogger(TransactionalHttpInvokerRequestExecutor.class); + + + private ClientInterceptorTemplate template = new ClientInterceptorTemplate(); + + @Override + protected void prepareConnection(HttpURLConnection con, int contentLength) + throws IOException { + LOGGER.logTrace("Filtering request..."); + String propagation = template.onOutgoingRequest(); + con.setRequestProperty(HeaderNames.PROPAGATION_HEADER_NAME, propagation); + super.prepareConnection(con, contentLength); + } + + @Override + protected void validateResponse(HttpInvokerClientConfiguration config, + HttpURLConnection con) throws IOException { + super.validateResponse(config, con); + LOGGER.logTrace("Filtering response..."); + String extent = con.getHeaderField(HeaderNames.EXTENT_HEADER_NAME); + template.onIncomingResponse(extent); + } + + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/httpinvoker/TransactionalHttpInvokerServiceExporter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/httpinvoker/TransactionalHttpInvokerServiceExporter.java new file mode 100644 index 000000000..05a7cafaf --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/httpinvoker/TransactionalHttpInvokerServiceExporter.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.spring.httpinvoker; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter; + +import com.atomikos.icatch.RollbackException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ContainerInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +public class TransactionalHttpInvokerServiceExporter extends HttpInvokerServiceExporter { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionalHttpInvokerServiceExporter.class); + + private ContainerInterceptorTemplate template = new ContainerInterceptorTemplate(); + + @Override + protected InputStream decorateInputStream(HttpServletRequest request, InputStream is) throws IOException { + LOGGER.logTrace("Filtering incoming request..."); + + try { + String propagation = request.getHeader(HeaderNames.PROPAGATION_HEADER_NAME); + template.onIncomingRequest(propagation); + } catch (IllegalArgumentException e) { + LOGGER.logWarning("Detected invalid incoming transaction - aborting..."); + throw new IOException(e); + } + + return super.decorateInputStream(request, is); + } + + @Override + protected OutputStream decorateOutputStream(HttpServletRequest request, HttpServletResponse response, + OutputStream os) throws IOException { + try { + String extent = null; + if (response.getStatus() >= 200 && response.getStatus() < 300) { + extent = template.onOutgoingResponse(false); + } else { + extent = template.onOutgoingResponse(true); + } + + if (extent != null) { + response.addHeader(HeaderNames.EXTENT_HEADER_NAME, extent); + } + + } catch (RollbackException e) { + String msg = "Transaction was rolled back - probably due to a timeout?"; + LOGGER.logWarning(msg, e); + throw new IOException(msg, e); + } catch (Exception e) { + LOGGER.logError("Unexpected error while terminating transaction", e); + throw e; + } + + + return super.decorateOutputStream(request, response, os); + } + + + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/rest/TransactionAwareRestClientInterceptor.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/rest/TransactionAwareRestClientInterceptor.java new file mode 100644 index 000000000..03e69bfc4 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/rest/TransactionAwareRestClientInterceptor.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.spring.rest; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import com.atomikos.remoting.support.ClientInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; + +/** + * Filter (interceptor) that adds the current thread's JTA transaction context to + * outgoing REST calls, and makes sure that the response is added to the commit scope + * of the active transaction. + * + * This class is only needed for REST clients that embed the Atomikos JTA transaction manager, + * and it automatically makes outgoing REST calls part of the transaction. + */ + +public class TransactionAwareRestClientInterceptor implements + ClientHttpRequestInterceptor { + + private ClientInterceptorTemplate template = new ClientInterceptorTemplate(); + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + String propagation = template.onOutgoingRequest(); + if (propagation!=null) { + request.getHeaders().set(HeaderNames.PROPAGATION_HEADER_NAME, propagation); + } + ClientHttpResponse response = execution.execute(request, body); + String extent = response.getHeaders().getFirst(HeaderNames.EXTENT_HEADER_NAME); + if (extent !=null) { + template.onIncomingResponse(extent); + } + return response; + } +} \ No newline at end of file diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/rest/TransactionAwareRestContainerFilter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/rest/TransactionAwareRestContainerFilter.java new file mode 100644 index 000000000..85a6fe483 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/spring/rest/TransactionAwareRestContainerFilter.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.spring.rest; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.atomikos.icatch.RollbackException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.ContainerInterceptorTemplate; +import com.atomikos.remoting.support.HeaderNames; +/** + * Filter (interceptor) that adds starts a local JTA transaction that becomes part + * of the remote client's transaction context. + * + * This class is only needed for REST services that embed the Atomikos JTA transaction manager, + * and it automatically makes incoming REST calls part of the caller's transaction. + * + * Note: this interceptor only works if: + *
    + *
  • the local transaction service (Atomikos) is running, AND
  • + *
  • a AtomikosRestPort instance is available for this JVM
  • + *
+ */ +public class TransactionAwareRestContainerFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionAwareRestContainerFilter.class); + + private ContainerInterceptorTemplate template = new ContainerInterceptorTemplate(); + + @Override + protected void doFilterInternal(HttpServletRequest request, final HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + String propagation = request.getHeader(HeaderNames.PROPAGATION_HEADER_NAME); + + try { + template.onIncomingRequest(propagation); + HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(response) { + @Override + public void setStatus(int sc) { + super.setStatus(sc); + String extent = terminateImportedTransaction(sc); + response.addHeader(HeaderNames.EXTENT_HEADER_NAME, extent); + } + }; + filterChain.doFilter(request, wrapper); + } catch (IllegalStateException e) { + LOGGER.logWarning("Detected invalid incoming transaction - aborting..."); + response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value()); + } + + + } + + private String terminateImportedTransaction(int httpCode) { + String extent = null; + try { + if(HttpStatus.valueOf(httpCode).is2xxSuccessful()) { + extent = template.onOutgoingResponse(false); + } else { + extent = template.onOutgoingResponse(true); + } + + } catch (RollbackException e) { + String msg = "Transaction was rolled back - probably due to a timeout?"; + LOGGER.logWarning(msg, e); + } catch (Exception e) { + String msg = "Unexpected error while terminating transaction"; + LOGGER.logError(msg, e); + } + return extent; + } + + + + +} \ No newline at end of file diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/ClientInterceptorTemplate.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/ClientInterceptorTemplate.java new file mode 100644 index 000000000..5a0a3a100 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/ClientInterceptorTemplate.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +import com.atomikos.icatch.ExportingTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.CheckedExportingTransactionManager; +import com.atomikos.remoting.DefaultExportingTransactionManager; +import com.atomikos.remoting.Parser; + +/** + * Common logic for client-side filters. + * + */ + +public class ClientInterceptorTemplate { + + private static final Logger LOGGER = LoggerFactory.createLogger(ClientInterceptorTemplate.class); + + private final ExportingTransactionManager exportingTransactionManager = new CheckedExportingTransactionManager(new DefaultExportingTransactionManager()); + + private final Parser parser = new Parser(); + /** + * Determines the propagation header. + * + * @return The header - null if no active transaction exists + */ + public String onOutgoingRequest() { + LOGGER.logTrace("onOutgoingRequest..."); + try { + Propagation propagation = exportingTransactionManager.getPropagation(); + return propagation.toString(); + } catch (SysException se) { + LOGGER.logError("System configuration problem - could not retrieve transaction propagation for outgoing request", se); + } catch (Exception e) { + LOGGER.logDebug("Failed to retrieve transaction propagation for outgoing request - request will not be transactional!", e); + } + + return null; + } + + /** + * Handles the transactional termination of an incoming response. + * + * @param extent The extent found in the response headers, can be null. + */ + public void onIncomingResponse(String extentAsString) { + LOGGER.logTrace("onIncomingResponse..."); + Extent extent = null; + try { + extent = parser.parseExtent(extentAsString); + exportingTransactionManager.addExtent(extent); + } catch (RollbackException e) { + if (extent != null) { + String message = "An extent was returned but no local transaction exists - any remote work will time out and rollback."; + LOGGER.logWarning(message, e); + //don't rethrow: CheckedExportingTransactionManager will prevent commit anyway + } + } catch (IllegalArgumentException invalidExtent) { + String message = "Invalid extent found - any remote work will time out and rollback."; + LOGGER.logWarning(message, invalidExtent); + //don't rethrow: CheckedExportingTransactionManager will prevent commit anyway + } + + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/ContainerInterceptorTemplate.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/ContainerInterceptorTemplate.java new file mode 100644 index 000000000..f6a8fb89e --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/ContainerInterceptorTemplate.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.ImportingTransactionManager; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.DefaultImportingTransactionManager; +import com.atomikos.remoting.Parser; + +/** + * Common logic for server-side filters. + * + */ + +public class ContainerInterceptorTemplate { + + private static final Logger LOGGER = LoggerFactory.createLogger(ContainerInterceptorTemplate.class); + private ImportingTransactionManager importingTransactionManager = new DefaultImportingTransactionManager(); + + private Parser parser = new Parser(); + /** + * Does nothing if propagation is null + * @param propagation + * @throws IllegalArgumentException if propagation is not understood + */ + public void onIncomingRequest(String propagationAsString) throws IllegalArgumentException { + if (propagationAsString != null) { + Propagation propagation = parser.parsePropagation(propagationAsString); + importingTransactionManager.importTransaction(propagation); + } else { + LOGGER.logTrace("No transaction context found in incoming request - this request will commit independently of any client transaction!"); + } + } + + /** + * Terminates an imported transaction. + * + * @param error If false then rollback, else commit. + * @return the extent, or null if no active transaction + * @throws RollbackException + */ + public String onOutgoingResponse(boolean error) throws RollbackException { + if (getCurrentTransaction() != null ) { + try { + Extent extent = importingTransactionManager.terminated(!error); + if (extent != null) { // null on error + return extent.toString(); + } + } catch (RollbackException e) { + if (!error) { + String msg = "Transaction was rolled back - probably due to a timeout?"; + LOGGER.logWarning(msg, e); + //don't re-throw: no extent will be added so let client detect that + } else { + LOGGER.logDebug("Transaction was rolled back after error"); + //don't re-throw: no extent will be added so let client detect that + } + } + } + return null; // client will fail due to missing extent... + } + + private CompositeTransaction getCurrentTransaction() { + return Configuration.getCompositeTransactionManager().getCompositeTransaction(); + } + + + void setImportingTransactionManager(ImportingTransactionManager importingTransactionManager) { + this.importingTransactionManager = importingTransactionManager; + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/HeaderNames.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/HeaderNames.java new file mode 100644 index 000000000..a53a9e239 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/support/HeaderNames.java @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +public class HeaderNames { + + public static class MimeType { + public static final String APPLICATION_VND_ATOMIKOS_JSON = "application/vnd.atomikos+json"; + } + + public static final String PROPAGATION_HEADER_NAME = "Atomikos-Propagation"; + public static final String EXTENT_HEADER_NAME = "Atomikos-Extent"; + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/RestTransactionService.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/RestTransactionService.java new file mode 100644 index 000000000..b6dc06c02 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/RestTransactionService.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.taas; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.RollbackException; +import com.atomikos.remoting.support.HeaderNames; + +@Path("atomikos") +@Produces(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +@Consumes(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +public interface RestTransactionService { + + @POST + @Path("/begin") + String begin(@QueryParam("timeout") Long timeout); + + @POST + @Path("/commit") + void commit(String... extents) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, RollbackException; + + @POST + @Path("/rollback") + void rollback(String... extents) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, RollbackException; + +} \ No newline at end of file diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/RestTransactionServiceImp.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/RestTransactionServiceImp.java new file mode 100644 index 000000000..a10c8d1c1 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/RestTransactionServiceImp.java @@ -0,0 +1,164 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.taas; + +import javax.annotation.PostConstruct; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.Parser; + +public class RestTransactionServiceImp implements RestTransactionService { + + private static Logger LOGGER = LoggerFactory.createLogger(RestTransactionServiceImp.class); + + private String recoveryDomainName; + + @PostConstruct + public void init() { + Configuration.init(); + recoveryDomainName = Configuration.getConfigProperties().getTmUniqueName(); + } + + @Override + public String begin(Long timeout) { + CompositeTransactionManager compositeTransactionManager = Configuration.getCompositeTransactionManager(); + if (compositeTransactionManager == null) { + throw new IllegalStateException("Transaction service not initialized !!!!"); + } + if (timeout == null) { + throw new IllegalArgumentException("Missing required argument: timeout"); + } + + assertNoTransactionForThread(compositeTransactionManager); + + CompositeTransaction root = compositeTransactionManager.createCompositeTransaction(timeout); + compositeTransactionManager.suspend(); + + if (root == null) { + throw new IllegalStateException("No transaction started"); + } + + TransactionManagerImp.markAsJtaTransaction(root); + + Propagation p = new Propagation(recoveryDomainName, root, root, root.isSerial(), root.getTimeout()); + LOGGER.logDebug("Returning propagation: " + p.toString()); + return p.toString(); + } + + private void assertNoTransactionForThread(CompositeTransactionManager compositeTransactionManager) { + CompositeTransaction existingTransaction = compositeTransactionManager.getCompositeTransaction(); + if (existingTransaction != null) { + LOGGER.logWarning("Found unexpected existing transaction: " + existingTransaction.getTid() + " rolling it back..."); + rollback(existingTransaction); + } + } + + private void rollback(CompositeTransaction existingTransaction) { + try { + existingTransaction.rollback(); + } catch (Exception e) { + LOGGER.logWarning("Unexpected error during rollback of pending transaction", e); + } + } + + Parser parser = new Parser(); + + @Override + public void commit(String... extentsAsString) + throws RollbackException, HeurMixedException, HeurHazardException, RollbackException { + CompositeTransactionManager compositeTransactionManager = Configuration.getCompositeTransactionManager(); + if (compositeTransactionManager == null) { + throw new IllegalStateException("Transaction service not initialized !!!!"); + } + + assertNoTransactionForThread(compositeTransactionManager); + + Extent extent = null; + try { + extent = parseExtents(extentsAsString); + } catch (IllegalArgumentException e) { + LOGGER.logWarning(e.getMessage()); + throw new RollbackException(e.getMessage()); + } + + CompositeTransaction ct = compositeTransactionManager.getCompositeTransaction(extent.getParentTransactionId()); + addExtent(extent, ct); + try { + ct.commit(); + } catch (RuntimeException e) { + LOGGER.logWarning("Unexpected exception on commit: " + e); + throw e; + } + } + + protected void addExtent(Extent extent, CompositeTransaction ct) { + ct.getExtent().add(extent); + for (Participant p : extent.getParticipants()) { + ct.addParticipant(p); + } + } + + private Extent parseExtents(String[] extents) { + Extent extent = null; + String parentTransactionId = null; + for (int i = 0; i < extents.length; i++) { + Extent parsed = parser.parseExtent(extents[i]); + if (i==0) { + parentTransactionId = parsed.getParentTransactionId(); + extent = new Extent(parentTransactionId); + } else { + if (!parentTransactionId.equals(parsed.getParentTransactionId())) { + throw new IllegalArgumentException("The supplied extents are for different parent transactions"); + } + } + extent.add(parsed); + } + return extent; + } + + @Override + public void rollback(String... extentsAsString) + throws HeurRollbackException, HeurMixedException, HeurHazardException, RollbackException { + CompositeTransactionManager compositeTransactionManager = Configuration.getCompositeTransactionManager(); + if (compositeTransactionManager == null) { + throw new IllegalStateException("Transaction service not initialized !!!!"); + } + + assertNoTransactionForThread(compositeTransactionManager); + + Extent extent = null; + try { + extent = parseExtents(extentsAsString); + } catch (IllegalArgumentException e) { + LOGGER.logWarning(e.getMessage()); + throw new RollbackException(e.getMessage()); + } + + CompositeTransaction ct = compositeTransactionManager.getCompositeTransaction(extent.getParentTransactionId()); + addExtent(extent, ct); + try { + ct.rollback(); + } catch (RuntimeException e) { + LOGGER.logWarning("Unexpected exception on rollback: " + e); + } + + } +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/TransactionProvider.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/TransactionProvider.java new file mode 100644 index 000000000..54f8680b3 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/taas/TransactionProvider.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.taas; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.regex.Pattern; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.Providers; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.HeaderNames; + +/** + * Custom marshalling / unmarshalling of transaction data in a request / response. + */ + +@Consumes(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +@Produces(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +@Provider +public class TransactionProvider implements MessageBodyWriter, MessageBodyReader { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionProvider.class); + + @Context + protected Providers providers; + + public long getSize(Object l, Class type, Type genericType, Annotation[] annotations, MediaType mt) { + return -1; + } + + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mt) { + return true; + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return true; + } + + @Override + public Object readFrom(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + String content = getStringFromInputStream(entityStream); + + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("Incoming REST request payload:\n" + content); + } + if(content.contains("|")) { + return content.split(Pattern.quote("|")); + } + return content; + } + + private String getStringFromInputStream(InputStream is) { + + BufferedReader br = null; + StringBuilder sb = new StringBuilder(); + + String line; + try { + + br = new BufferedReader(new InputStreamReader(is)); + while ((line = br.readLine()) != null) { + sb.append(line); + } + + } catch (IOException e) { + LOGGER.logTrace("Failed to read REST payload.", e); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // ignore + } + } + } + + return sb.toString(); + + } + + @Override + public void writeTo(Object t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, + WebApplicationException { + + if(t.getClass().isArray()) { + Object[] array = (Object[])t; + for (Object content : array) { + entityStream.write(content.toString().getBytes()); + entityStream.write("|".getBytes()); + } + } else { + entityStream.write(t.toString().getBytes()); + } + + + } +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/AtomikosRestPort.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/AtomikosRestPort.java new file mode 100644 index 000000000..0606922b6 --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/AtomikosRestPort.java @@ -0,0 +1,246 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.twopc; + +import java.util.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionService; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.imp.CoordinatorImp; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.RecoveryLog; +import com.atomikos.recovery.TxState; +import com.atomikos.remoting.support.HeaderNames; + + /** + * A REST port for making the local JVM's invocations part of the transaction scope + * of the calling client (if any). An instance needs to be running if you want to + * accept incoming transactional REST calls. + */ + +@Path("/atomikos") +@Consumes(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +public class AtomikosRestPort { + + public static final String REST_URL_PROPERTY_NAME = "com.atomikos.icatch.rest_port_url"; + + private static final Logger LOGGER = LoggerFactory.createLogger(AtomikosRestPort.class); + + private static String atomikosRestPortUrl; + + /** + * Sets the URL for this port - either from jta.properties or as detected on first invocation of our interceptor. + * Needed because JAX-RS does not have an easy why of finding out on which URL we are bound. + * + * @param url Can be null in which case this method does nothing. If the URL was already set then a second invocation will not have any effect. + */ + public static void setUrl(String url) { + if (atomikosRestPortUrl == null && url != null) { //cf case 181280 + atomikosRestPortUrl = url; + if (!atomikosRestPortUrl.endsWith("/")) + atomikosRestPortUrl = atomikosRestPortUrl + "/"; + } + } + + public static String getUrl() { + return atomikosRestPortUrl; + } + + public static String buildParticipantUrl(CompositeTransaction ct) throws SysException { + assertRestPortUrlSet(); + return getUrl()+ct.getCompositeCoordinator().getRootId()+"/" + ct.getCompositeCoordinator().getCoordinatorId(); + } + + private static void assertRestPortUrlSet() { + if (getUrl() == null) { + throw new SysException("Please set property " + AtomikosRestPort.REST_URL_PROPERTY_NAME + " - see https://www.atomikos.com/Documentation/ConfiguringRemoting for details"); + } + } + + private static RecoveryLog recoveryLog; + + public static void init(String url) { + recoveryLog = Configuration.getRecoveryLog(); + setUrl(url); + } + + private static String buildParticipantUrl(String root, String coordinatorId) { + return atomikosRestPortUrl + root + "/" + coordinatorId; + } + + @GET + public String ping() { + return "Hello from Atomikos!"; + } + + @GET + @Path("{coordinatorId}") + public String getOutcome(@PathParam("coordinatorId") String coordinatorId) { + TxState ret = TxState.TERMINATED; + PendingTransactionRecord record = null; + try { + record = recoveryLog.get(coordinatorId); + } catch (LogReadException e) { + LOGGER.logWarning("Unexpected log exception", e); + throw409(e); + } + if (record != null) { + ret = record.state; + } + return ret.toString(); + } + + @POST + @Path("{rootId}/{coordinatorId}") + public Response prepare(@PathParam("rootId") String rootId, @PathParam("coordinatorId") String coordinatorId, Map cascadeList) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("prepare ( ... ) received for root " + rootId); + } + TransactionService service = Configuration.getTransactionService(); + String extentUri = buildParticipantUrl(rootId, coordinatorId); + Integer count = cascadeList.get(extentUri); + Participant part = service.getParticipant(rootId); + if (part == null) { + return Response.status(Status.NOT_FOUND).entity(rootId).build(); + } + part.setGlobalSiblingCount(count); + part.setCascadeList(cascadeList); + int result = -1; + try { + result = part.prepare(); + } catch (RollbackException rb) { + LOGGER.logWarning("Error in prepare for root " + rootId, rb); + throw404(); + } catch (Exception e) { + LOGGER.logWarning("Error in prepare for root " + rootId, e); + throw409(e); + } + + return Response.status(Status.CREATED).entity(result).build(); + } + + private void throw404() { + Response response = Response.status(Status.NOT_FOUND) + .entity("Transaction has timed out and was rolledback") + .type(MediaType.TEXT_PLAIN).build(); + throw new WebApplicationException(response); + } + + private void throw409(Exception e) { + Response response = Response.status(Status.CONFLICT).entity(e.getMessage()).type(MediaType.TEXT_PLAIN).build(); + throw new WebApplicationException(response); + } + + @PUT + @Path("{rootId}/{coordinatorId}/{onePhase}") + public Response commit(@PathParam("rootId") String rootId, @PathParam("coordinatorId") String coordinatorId, @PathParam("onePhase") boolean onePhase) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("commit() received for root " + rootId + " onePhase = " + onePhase); + } + TransactionService service = Configuration.getTransactionService(); + CoordinatorImp part = (CoordinatorImp) service.getParticipant(rootId); + + if (part != null) { + if (!part.getState().isFinalState()) { //ignore remote commit retries that worked here the first time + if (!part.getState().transitionAllowedTo(TxState.COMMITTING)) { + if(!onePhase) { + LOGGER.logWarning("Commit no longer allowed for root " + rootId + " - probably due to heuristic rollback?"); + return Response.status(Status.CONFLICT).entity(rootId).build(); + } + } else { + try { + part.commit(onePhase); + } catch (RollbackException rb) { + LOGGER.logWarning("Error in commit for root " + rootId, rb); + throw404(); + } catch (Exception e) { + LOGGER.logWarning("Error in commit for root " + rootId, e); + throw409(e); + } + } + } + } else { //abandoned in OLTP? => delegate to log and recovery + + if (!onePhase) { + try { + delegateToRecovery(coordinatorId, true); + } catch (LogException e) { + LOGGER.logWarning("Error in commit for root " + rootId, e); + throw409(e); + } + } else { + Response response = Response.status(Status.CONFLICT).entity(rootId).type(MediaType.TEXT_PLAIN).build(); + throw new WebApplicationException(response); + } + + } + + + return Response.status(Status.NO_CONTENT).build(); + } + + private void delegateToRecovery(String coordinatorId, boolean commit) throws LogException { + if (recoveryLog == null) { + recoveryLog = Configuration.getRecoveryLog(); + } + if (commit) { + recoveryLog.recordAsCommitting(coordinatorId); + } else { + recoveryLog.forget(coordinatorId); + } + } + + @DELETE + @Path("{rootId}/{coordinatorId}") + public Response rollback(@PathParam("rootId") String rootId, @PathParam("coordinatorId") String coordinatorId) { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("rollback() received for root " + rootId); + } + TransactionService service = Configuration.getTransactionService(); + Participant part = service.getParticipant(rootId); + if (part != null) { //null for remote retry of rollback that worked here the first time + try { + part.rollback(); + } catch (Exception e) { + LOGGER.logWarning("Error in rollback for root " + rootId, e); + throw409(e); + } + } else { //abandoned in OLTP? => delegate to log and recovery + try { + delegateToRecovery(coordinatorId, false); + } catch (LogException e) { + LOGGER.logWarning("Error in rollback for root " + rootId, e); + throw409(e); + } + } + + return Response.status(Status.NO_CONTENT).build(); + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/ParticipantAdapter.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/ParticipantAdapter.java new file mode 100644 index 000000000..7081ec28e --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/ParticipantAdapter.java @@ -0,0 +1,178 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.twopc; + +import static javax.ws.rs.client.ClientBuilder.newClient; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status.Family; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.HeaderNames; + +/** + * A utility adapter class for easily adding REST participants to the core + * transaction engine. + */ + +public class ParticipantAdapter implements Participant { + + private static final Logger LOGGER = LoggerFactory.createLogger(ParticipantAdapter.class); + + private final WebTarget target; + + private final Map cascadeList = new HashMap<>(); + + public ParticipantAdapter(URI uri) { + Client client = newClient(); + client.property("jersey.config.client.suppressHttpComplianceValidation", true); + client.register(ParticipantsProvider.class); + target = client.target(uri); + } + + @Override + public String getURI() { + return target.getUri().toASCIIString(); + } + + @Override + public void setCascadeList(Map allParticipants) throws SysException { + this.cascadeList.putAll(allParticipants); + } + + @Override + public void setGlobalSiblingCount(int count) { + this.cascadeList.put(getURI(), count); + } + + @Override + public int prepare() throws RollbackException, HeurHazardException, HeurMixedException, SysException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("Calling prepare on " + getURI()); + } + try { + int result = target.request() + .buildPost(Entity.entity(cascadeList, HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON)) + .invoke(Integer.class); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("Prepare returned " + result); + } + return result; + } catch (WebApplicationException e) { + int status = e.getResponse().getStatus(); + if (status == 404) { + LOGGER.logWarning("Remote participant not available - any remote work will rollback...", e); + throw new RollbackException(); + } else { + LOGGER.logWarning("Unexpected error during prepare - see stacktrace for more details...", e); + throw new HeurHazardException(); + } + } + } + + @Override + public void commit(boolean onePhase) + throws HeurRollbackException, HeurHazardException, HeurMixedException, RollbackException, SysException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("Calling commit on " + getURI()); + } + + Response r = target.path(String.valueOf(onePhase)).request().buildPut(Entity.entity("", HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON)).invoke(); + + if (r.getStatusInfo().getFamily() != Family.SUCCESSFUL) { + int status = r.getStatus(); + switch (status) { + case 404: + if (onePhase) { + LOGGER.logWarning("Remote participant not available - default outcome will be rollback"); + throw new RollbackException(); + } + case 409: + LOGGER.logWarning("Unexpected 409 error on commit"); + throw new HeurMixedException(); + default: + LOGGER.logWarning("Unexpected error on commit: " + status); + throw new HeurHazardException(); + } + } + + } + + @Override + public void rollback() throws HeurCommitException, HeurMixedException, HeurHazardException, SysException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug("Calling rollback on " + getURI()); + } + + Response r = target.request().header(HttpHeaders.CONTENT_TYPE, HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON).delete(); + + if (r.getStatusInfo().getFamily() != Family.SUCCESSFUL) { + int status = r.getStatus(); + switch (status) { + case 409: + LOGGER.logWarning("Unexpected 409 error on rollback"); + throw new HeurMixedException(); + case 404: + LOGGER.logDebug("Unexpected 404 error on rollback - ignoring..."); + break; + default: + LOGGER.logWarning("Unexpected error on rollback: " + status); + throw new HeurHazardException(); + } + } + + } + + @Override + public void forget() { + } + + @Override + public String getResourceName() { + return null; + } + + @Override + public boolean equals(Object o) { + boolean ret = false; + if (o instanceof ParticipantAdapter) { + ParticipantAdapter other = (ParticipantAdapter) o; + ret = getURI().equals(other.getURI()); + } + return ret; + } + + @Override + public int hashCode() { + return getURI().hashCode(); + } + + @Override + public String toString() { + return "ParticipantAdapter for: " + getURI(); + } + +} diff --git a/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/ParticipantsProvider.java b/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/ParticipantsProvider.java new file mode 100644 index 000000000..70a68638f --- /dev/null +++ b/public/transactions-remoting/src/main/java/com/atomikos/remoting/twopc/ParticipantsProvider.java @@ -0,0 +1,132 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.twopc; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.Providers; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.remoting.support.HeaderNames; +import com.atomikos.remoting.taas.TransactionProvider; + +@Consumes(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +@Produces(HeaderNames.MimeType.APPLICATION_VND_ATOMIKOS_JSON) +@Provider +public class ParticipantsProvider implements MessageBodyWriter>, MessageBodyReader> { + + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionProvider.class); + + @Context + protected Providers providers; + + public long getSize(Map l, Class type, Type genericType, Annotation[] annotations, MediaType mt) { + return -1; + } + + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mt) { + return true; + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return true; + } + + @Override + public HashMap readFrom(Class> type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + String content = getStringFromInputStream(entityStream); + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace("Incoming REST request payload:\n" + content); + + } + HashMap result = new HashMap<>(); + String keyValues =content.replaceAll("\\{", ""); + keyValues = keyValues.replaceAll("\\}", ""); + String[] array = keyValues.split(","); + for (String entry : array) { //"key"="value" + entry = entry.replaceAll("\"", ""); //get git of " + String[] pair = entry.split("="); + result.put(pair[0], Integer.valueOf(pair[1])); + } + + + return result; + } + + private String getStringFromInputStream(InputStream is) { + + BufferedReader br = null; + StringBuilder sb = new StringBuilder(); + + String line; + try { + + br = new BufferedReader(new InputStreamReader(is)); + while ((line = br.readLine()) != null) { + sb.append(line); + } + + } catch (IOException e) { + LOGGER.logTrace("Failed to read REST payload.", e); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // ignore + } + } + } + + return sb.toString(); + + } + + @Override + public void writeTo(Map t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, + WebApplicationException { + + StringBuffer buffer = new StringBuffer(); + buffer.append("{"); + String comma=""; + for (Entry entry : t.entrySet()) { + buffer.append(comma); + buffer.append('{').append('"').append(entry.getKey()).append('"').append('=').append(entry.getValue()).append('}'); + comma=","; + } + buffer.append("}"); + entityStream.write(buffer.toString().getBytes()); + + } + + +} diff --git a/public/transactions-remoting/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter b/public/transactions-remoting/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter new file mode 100644 index 000000000..e07b330b7 --- /dev/null +++ b/public/transactions-remoting/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter @@ -0,0 +1,2 @@ +com.atomikos.remoting.dubbo.DubboConsumerTransactionPropagationFilter +com.atomikos.remoting.dubbo.DubboProviderTransactionPropagationFilter \ No newline at end of file diff --git a/public/transactions-remoting/src/main/resources/META-INF/services/com.atomikos.icatch.TransactionServicePlugin b/public/transactions-remoting/src/main/resources/META-INF/services/com.atomikos.icatch.TransactionServicePlugin new file mode 100644 index 000000000..5154c7407 --- /dev/null +++ b/public/transactions-remoting/src/main/resources/META-INF/services/com.atomikos.icatch.TransactionServicePlugin @@ -0,0 +1 @@ +com.atomikos.remoting.RemotingTransactionServicePlugin \ No newline at end of file diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/DefaultExportingTransactionManagerTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/DefaultExportingTransactionManagerTestJUnit.java new file mode 100644 index 000000000..c7cc2fb10 --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/DefaultExportingTransactionManagerTestJUnit.java @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import static org.junit.Assert.assertEquals; + +import java.util.Properties; +import java.util.Stack; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.icatch.provider.ConfigProperties; + +public class DefaultExportingTransactionManagerTestJUnit { + + private CheckedExportingTransactionManager sut = new CheckedExportingTransactionManager(new DefaultExportingTransactionManager()); + + @Mock + CompositeTransactionManager mockedCTM; + @Mock + CompositeTransaction mockedCT; + @Mock + CompositeTransaction mockedRootCT; + + private String givenCoordinatorId = "1234"; + private String givenTid = "4567"; + private String givenParentTid = "98"; + + private long anyTimout = 1000l; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Configuration.getConfigProperties().setProperty(ConfigProperties.TM_UNIQUE_NAME_PROPERTY_NAME, "tmUniqueName"); + Configuration.installCompositeTransactionManager(mockedCTM); + } + + @Test + public void testJtaTransactionYieldsPendingRequestSynchronization() throws Exception { + givenExistingTransaction(); + sut.getPropagation(); + Mockito.verify(mockedCT).registerSynchronization(Mockito.any(Synchronization.class)); + } + + @Test + public void testPropgationRecoveryCoordinatorURI() { + givenExistingTransaction(); + Propagation p = sut.getPropagation(); + assertEquals(givenCoordinatorId, p.getRecoveryCoordinatorURI()); + } + + private CompositeTransaction givenExistingTransaction() { + + Mockito.when(mockedCTM.getCompositeTransaction()).thenReturn(mockedCT); + Mockito.when(mockedCTM.recreateCompositeTransaction(Mockito.any())).thenReturn(mockedCT); + Mockito.when(mockedCT.getExtent()).thenReturn(new Extent()); + Mockito.when(mockedRootCT.getTid()).thenReturn(givenParentTid); + Mockito.when(mockedRootCT.getProperties()).thenReturn(new Properties()); + Stack lineage = new Stack<>(); + lineage.push(mockedRootCT); + Mockito.when(mockedCT.getLineage()).thenReturn(lineage); + Mockito.when(mockedCT.getProperty(TransactionManagerImp.JTA_PROPERTY_NAME)).thenReturn("true"); + Mockito.when(mockedCT.getTimeout()).thenReturn(anyTimout); + CompositeCoordinator compositeCoordinator = Mockito.mock(CompositeCoordinator.class); + Mockito.when(compositeCoordinator.getCoordinatorId()).thenReturn(givenCoordinatorId); + Mockito.when(mockedCT.getCompositeCoordinator()).thenReturn(compositeCoordinator); + Mockito.when(mockedCT.isSerial()).thenReturn(true); + Mockito.when(mockedCT.getTid()).thenReturn(givenTid); + Mockito.when(mockedCT.getProperties()).thenReturn(new Properties()); + return mockedCT; + } +} diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/DefaultImportingTransactionManagerTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/DefaultImportingTransactionManagerTestJUnit.java new file mode 100644 index 000000000..81a19d65e --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/DefaultImportingTransactionManagerTestJUnit.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.remoting.twopc.AtomikosRestPort; + +public class DefaultImportingTransactionManagerTestJUnit { + + private static final String BASE_URL ="http://localhost:8088/"; + private static final String PROPAGATION = + "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT,parent=PARENT,property.key=value";; + + private Propagation propagation; + DefaultImportingTransactionManager sut; + + @Mock CompositeTransactionManager mockedCTM; + @Mock CompositeTransaction mockedCT; + @Mock CompositeCoordinator mockedCC; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Mockito.when(mockedCTM.getCompositeTransaction()).thenReturn(mockedCT); + Mockito.when(mockedCTM.recreateCompositeTransaction(Mockito.any())).thenReturn(mockedCT); + Mockito.when(mockedCT.getExtent()).thenReturn(new Extent()); + Mockito.when(mockedCT.getCompositeCoordinator()).thenReturn(mockedCC); + Mockito.when(mockedCC.getRootId()).thenReturn("ROOT"); + Configuration.installCompositeTransactionManager(mockedCTM); + sut = new DefaultImportingTransactionManager(); + propagation = new Parser().parsePropagation(PROPAGATION); + AtomikosRestPort.setUrl(BASE_URL); + } + + @Test + public void testImportRecreatesCompositeTransaction() { + sut.importTransaction(propagation); + Mockito.verify(mockedCTM).recreateCompositeTransaction(Mockito.any()); + } + + @Test + public void testTerminatedWithSuccessCompletesExtent() throws SysException, RollbackException { + sut.importTransaction(propagation); + Extent completedExtent = sut.terminated(true); + assertTrue(completedExtent.toString().indexOf(BASE_URL) >=0); + } + + @Test(expected=IllegalArgumentException.class) + public void testImportThrowsForNullPropagation() { + sut.importTransaction(null); + } + + @Test + public void testTerminatedReturnsNullForFailure() throws Exception { + sut.importTransaction(propagation); + + Extent extent = sut.terminated(false); + assertNull(extent); + Mockito.verify(mockedCT).rollback(); + } + + @Test(expected=RollbackException.class) + public void testTerminatedThrowsIfNoActiveTransaction() throws Exception { + Mockito.when(mockedCTM.getCompositeTransaction()).thenReturn(null); + sut.terminated(true); + } +} diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/ParserTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/ParserTestJUnit.java new file mode 100644 index 000000000..8c6e3e7eb --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/ParserTestJUnit.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.Propagation; + +public class ParserTestJUnit { + + private static final String PROPAGATION_OF_ROOT_WITHOUT_PROPERTIES = + "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT"; + + + private static final String PROPAGATION_WITHOUT_PROPERTIES = + "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT,parent=PARENT"; + + private static final String PROPAGATION_WITH_FUTURE_EXTRA_LINEAGE = + "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT,parent=FUTURE_PARENT,parent=PARENT"; + + private static final String PROPAGATION_WITH_INCOMPATIBLE_VERSION = + "version=2020,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT,parent=PARENT"; + + private static final String PROPAGATION_WITH_FUTURE_EXTRA_PROPERTIES = + "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,extra=bla,extra2=blabla,parent=ROOT,parent=PARENT,property.key=value,property.key=bla"; + + private static final String PROPAGATION_WITH_PROPERTIES = + "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT,parent=PARENT,property.key=value"; + + private static final String EXTENT = + "version=2019,parent=PARENT,uri=URI_1,responseCount=1,direct=true,uri=URI_2,responseCount=2,direct=false"; + + private static final String EXTENT_WITH_FUTURE_EXTRA_PROPERTIES = + "version=2019,parent=PARENT,extra=bla,extra2=blabla,uri=URI_1,responseCount=1,direct=true,extra=bla,uri=URI_2,responseCount=2,direct=false"; + + private static final String EXTENT_INCOMPLETE ="version=2019,parent=PARENT,uri=URI_1,responseCount=1"; + + private static final String EXTENT_WITH_INCOMPATIBLE_VERSION = + "version=2020,parent=PARENT,uri=URI_1,responseCount=1,direct=true,uri=URI_2,responseCount=2,direct=false"; + + private static final String PROPAGATION_FROM_EXAMPLES = "version=2019,domain=client,timeout=9750,serial=true,recoveryCoordinatorURI=client155888493530000001,parent=client155888493530000001,property.com.atomikos.icatch.jta.transaction=true"; + + private Parser parser; + + @Before + public void setUp() throws Exception { + parser = new Parser(); + } + + @Test + public void testParsePropagationWithoutProperties() { + Propagation p = parser.parsePropagation(PROPAGATION_WITHOUT_PROPERTIES); + assertEquals(PROPAGATION_WITHOUT_PROPERTIES, p.toString()); + } + + @Test + public void testParsePropagationWithProperties() { + Propagation p = parser.parsePropagation(PROPAGATION_WITH_PROPERTIES); + assertEquals(PROPAGATION_WITH_PROPERTIES, p.toString()); + } + + @Test + public void testParseExtent() { + Extent e = parser.parseExtent(EXTENT); + assertEquals(EXTENT, e.toString()); + } + + @Test + public void testParsePropagationWithFutureExtraProperties() { + Propagation p = parser.parsePropagation(PROPAGATION_WITH_FUTURE_EXTRA_PROPERTIES); + assertEquals(PROPAGATION_WITH_PROPERTIES, p.toString()); + } + + @Test + public void testParseExtentWithFutureExtraProperties() { + Extent e = parser.parseExtent(EXTENT_WITH_FUTURE_EXTRA_PROPERTIES); + assertEquals(EXTENT, e.toString()); + } + + @Test(expected=IllegalArgumentException.class) + public void testParseIncompleteExtent() { + Extent e = parser.parseExtent(EXTENT_INCOMPLETE); + } + + @Test(expected=IllegalArgumentException.class) + public void testParsePropagationWithIncompatibleVersionThrows() { + parser.parsePropagation(PROPAGATION_WITH_INCOMPATIBLE_VERSION); + } + + @Test(expected=IllegalArgumentException.class) + public void testParseExtentWithIncompatibleVersionThrows() { + parser.parsePropagation(EXTENT_WITH_INCOMPATIBLE_VERSION); + } + + @Test + public void testParsePropagationForRoot() { + Propagation p = parser.parsePropagation(PROPAGATION_OF_ROOT_WITHOUT_PROPERTIES); + assertEquals(PROPAGATION_OF_ROOT_WITHOUT_PROPERTIES, p.toString()); + } + + @Test + public void testParsePropagationWithFutureExtraAncestors() { + Propagation p = parser.parsePropagation(PROPAGATION_WITH_FUTURE_EXTRA_LINEAGE); + assertEquals(PROPAGATION_WITHOUT_PROPERTIES, p.toString()); + } + + @Test + public void testParsePropagationFromExamples() { + Propagation p = parser.parsePropagation(PROPAGATION_FROM_EXAMPLES); + } +} diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ClientInterceptorOnIncomingResponseTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ClientInterceptorOnIncomingResponseTestJUnit.java new file mode 100644 index 000000000..5d4fce373 --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ClientInterceptorOnIncomingResponseTestJUnit.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +import java.util.Properties; +import java.util.Stack; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.icatch.provider.ConfigProperties; + +public class ClientInterceptorOnIncomingResponseTestJUnit { + ClientInterceptorTemplate sut = new ClientInterceptorTemplate(); + + long anyTimout = 1000l; + + @Mock + CompositeTransactionManager mockedCTM; + @Mock + CompositeTransaction mockedCT; + @Mock + CompositeTransaction mockedRootCT; + + + + private static final String EXTENT = + "version=2019,parent=PARENT,uri=URI_1,responseCount=1,direct=true,uri=URI_2,responseCount=2,direct=false"; + + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Configuration.getConfigProperties().setProperty(ConfigProperties.TM_UNIQUE_NAME_PROPERTY_NAME, "tmUniqueName"); + Configuration.installCompositeTransactionManager(mockedCTM); + } + + + @Test + public void testTransactionWithExtentWorks() throws Exception { + givenExistingTransaction(); + sut.onIncomingResponse(EXTENT); + } + + + @Test + public void testNoTransactionWithNullExtentWorks() throws Exception { + sut.onIncomingResponse(null); + } + + private CompositeTransaction givenExistingTransaction() { + + Mockito.when(mockedCTM.getCompositeTransaction()).thenReturn(mockedCT); + Mockito.when(mockedCTM.recreateCompositeTransaction(Mockito.any())).thenReturn(mockedCT); + Mockito.when(mockedCT.getExtent()).thenReturn(new Extent()); + Mockito.when(mockedRootCT.getTid()).thenReturn("ROOT"); + Stack lineage = new Stack<>(); + lineage.push(mockedRootCT); + Mockito.when(mockedCT.getLineage()).thenReturn(lineage); + Mockito.when(mockedCT.getProperty(TransactionManagerImp.JTA_PROPERTY_NAME)).thenReturn("true"); + Mockito.when(mockedCT.getTimeout()).thenReturn(anyTimout); + CompositeCoordinator compositeCoordinator = Mockito.mock(CompositeCoordinator.class); + Mockito.when(mockedCT.getCompositeCoordinator()).thenReturn(compositeCoordinator); + Mockito.when(mockedCT.isSerial()).thenReturn(true); + Mockito.when(mockedCT.getTid()).thenReturn("PARENT"); + Mockito.when(mockedCT.getProperties()).thenReturn(new Properties()); + return mockedCT; + } +} diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ClientInterceptorOnOutgoingRequestTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ClientInterceptorOnOutgoingRequestTestJUnit.java new file mode 100644 index 000000000..62a78020a --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ClientInterceptorOnOutgoingRequestTestJUnit.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Properties; +import java.util.Stack; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.icatch.provider.ConfigProperties; + +public class ClientInterceptorOnOutgoingRequestTestJUnit { + + + @Mock CompositeTransactionManager mockedCTM; + @Mock CompositeTransaction mockedCT; + @Mock CompositeTransaction mockedRootCT; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Configuration.getConfigProperties().setProperty(ConfigProperties.TM_UNIQUE_NAME_PROPERTY_NAME, "tmUniqueName"); + Configuration.installCompositeTransactionManager(mockedCTM); + } + + + ClientInterceptorTemplate sut = new ClientInterceptorTemplate(); + + String givenCoordinatorId = "1234"; + + String givenParentTid = "4567"; + long anyTimout = 1000l; + + @Test + public void testTransactionYieldsPropagation() throws Exception { + givenExistingTransaction(true); + String propagation = sut.onOutgoingRequest(); + assertNotNull(propagation); + } + + + @Test + public void testNoTransactionYieldsNullPropagation() throws Exception { + String propagation = sut.onOutgoingRequest(); + assertNull(propagation); + } + + private CompositeTransaction givenExistingTransaction(boolean jta) { + + Mockito.when(mockedCTM.getCompositeTransaction()).thenReturn(mockedCT); + Mockito.when(mockedCTM.recreateCompositeTransaction(Mockito.any())).thenReturn(mockedCT); + Mockito.when(mockedCT.getExtent()).thenReturn(new Extent()); + Mockito.when(mockedRootCT.getTid()).thenReturn(givenParentTid); + Mockito.when(mockedRootCT.getProperties()).thenReturn(new Properties()); + + + Stack lineage = new Stack<>(); + + lineage.push(mockedRootCT); + + Mockito.when(mockedCT.getLineage()).thenReturn(lineage); + + + if (jta) { + Mockito.when(mockedCT.getProperty(TransactionManagerImp.JTA_PROPERTY_NAME)).thenReturn("true"); + } + Mockito.when(mockedCT.getTimeout()).thenReturn(anyTimout); + CompositeCoordinator compositeCoordinator = Mockito.mock(CompositeCoordinator.class); + // + Mockito.when(mockedCT.getCompositeCoordinator()).thenReturn(compositeCoordinator); + Mockito.when(mockedCT.isSerial()).thenReturn(true); + Mockito.when(mockedCT.getTid()).thenReturn(givenCoordinatorId); + Mockito.when(mockedCT.getProperties()).thenReturn(new Properties()); + return mockedCT; + } + +} diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ContainerInterceptorOnIncomingRequestTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ContainerInterceptorOnIncomingRequestTestJUnit.java new file mode 100644 index 000000000..58f3f5042 --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ContainerInterceptorOnIncomingRequestTestJUnit.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.atomikos.icatch.ImportingTransactionManager; + +public class ContainerInterceptorOnIncomingRequestTestJUnit { + + private static final String PROPAGATION_WITHOUT_PROPERTIES = "version=2019,domain=DOMAIN,timeout=1234,serial=true,recoveryCoordinatorURI=PARENT,parent=ROOT,parent=PARENT"; + + ContainerInterceptorTemplate sut = new ContainerInterceptorTemplate(); + + ImportingTransactionManager mockedImportingTransactionManager; + + @Before + public void setup() { + mockedImportingTransactionManager = Mockito.mock(ImportingTransactionManager.class); + sut.setImportingTransactionManager(mockedImportingTransactionManager); + } + + @Test + public void testNullPropagationIsIgnored() throws Exception { + String propagation = null; + sut.onIncomingRequest(propagation); + Mockito.verify(mockedImportingTransactionManager, Mockito.never()).importTransaction(Mockito.any()); + } + + @Test + public void testValidPropagationHeaderIsImported() throws Exception { + sut.onIncomingRequest(PROPAGATION_WITHOUT_PROPERTIES); + Mockito.verify(mockedImportingTransactionManager).importTransaction(Mockito.any()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInValidPropagationHeaderThrows() throws Exception { + sut.onIncomingRequest("bla"); + } +} diff --git a/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ContainerInterceptorOnOutgoingResponseTestJUnit.java b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ContainerInterceptorOnOutgoingResponseTestJUnit.java new file mode 100644 index 000000000..e52332296 --- /dev/null +++ b/public/transactions-remoting/src/test/java/com/atomikos/remoting/support/ContainerInterceptorOnOutgoingResponseTestJUnit.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.remoting.support; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Properties; +import java.util.Stack; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.jta.TransactionManagerImp; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.remoting.twopc.AtomikosRestPort; + +public class ContainerInterceptorOnOutgoingResponseTestJUnit { + ContainerInterceptorTemplate sut = new ContainerInterceptorTemplate(); + + + @Mock CompositeTransactionManager mockedCTM; + @Mock CompositeTransaction mockedCT; + @Mock CompositeTransaction mockedRootCT; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + Configuration.getConfigProperties().setProperty(ConfigProperties.TM_UNIQUE_NAME_PROPERTY_NAME, "tmUniqueName"); + Configuration.installCompositeTransactionManager(mockedCTM); + AtomikosRestPort.setUrl("http://bla"); + } + String transactionId = "1234"; + long anyTimout = 1000l; + String givenParentTid = "4567"; + String givenCoordinatorId = "1234"; + + @Test + public void testNoTransactionReturnsNull() throws Exception { + String extent = sut.onOutgoingResponse(false); + assertNull(extent); + } + + @Test + public void testTransactionWithoutErrorCommits() throws Exception { + givenExistingTransaction(); + sut.onOutgoingResponse(false); + Mockito.verify(mockedCT).commit(); + } + + @Test + public void testTransactionWithErrorAborts() throws Exception { + givenExistingTransaction(); + sut.onOutgoingResponse(true); + Mockito.verify(mockedCT).rollback(); + } + + + @Test + public void testTransactionWithoutErrorReturnsExtent() throws Exception { + givenExistingTransaction(); + String extent = sut.onOutgoingResponse(false); + assertNotNull(extent); + } + + @Test + public void testTransactionWithErrorsReturnsNull() throws Exception { + givenExistingTransaction(); + String extent = sut.onOutgoingResponse(true); + assertNull(extent); + } + + private CompositeTransaction givenExistingTransaction() { + + Mockito.when(mockedCTM.getCompositeTransaction()).thenReturn(mockedCT); + Mockito.when(mockedCTM.recreateCompositeTransaction(Mockito.any())).thenReturn(mockedCT); + Mockito.when(mockedCT.getExtent()).thenReturn(new Extent()); + Mockito.when(mockedRootCT.getTid()).thenReturn(givenParentTid); + + Stack lineage = new Stack<>(); + + lineage.push(mockedRootCT); + + Mockito.when(mockedCT.getLineage()).thenReturn(lineage); + + Mockito.when(mockedCT.getProperty(TransactionManagerImp.JTA_PROPERTY_NAME)).thenReturn("true"); + Mockito.when(mockedCT.getTimeout()).thenReturn(anyTimout); + CompositeCoordinator compositeCoordinator = Mockito.mock(CompositeCoordinator.class); + // + Mockito.when(mockedCT.getCompositeCoordinator()).thenReturn(compositeCoordinator); + Mockito.when(mockedCT.isSerial()).thenReturn(true); + Mockito.when(mockedCT.getTid()).thenReturn(givenCoordinatorId); + Mockito.when(mockedCT.getProperties()).thenReturn(new Properties()); + return mockedCT; + } +} diff --git a/public/transactions/pom.xml b/public/transactions/pom.xml new file mode 100644 index 000000000..9a5f4bcd1 --- /dev/null +++ b/public/transactions/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + transactions + Transactions Core + + + + com.atomikos + transactions-api + 6.0.1-SNAPSHOT + + + com.atomikos + atomikos-util + 6.0.1-SNAPSHOT + + + diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSM.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSM.java new file mode 100644 index 000000000..e963b230c --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSM.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + +//$Id: FSM.java,v 1.1.1.1 2006/08/29 10:01:15 guy Exp $ +//$Log: FSM.java,v $ +//Revision 1.1.1.1 2006/08/29 10:01:15 guy +//Import of 3.0 essentials edition. +// +//Revision 1.1.1.1 2006/04/29 08:55:51 guy +//Initial import. +// +//Revision 1.1.1.1 2006/03/29 13:21:41 guy +//Imported. +// +//Revision 1.1.1.1 2006/03/23 16:25:37 guy +//Imported. +// +//Revision 1.1.1.1 2006/03/22 13:47:03 guy +//Import. +// +//Revision 1.1.1.1 2006/03/09 14:59:43 guy +//Imported 3.0 development into CVS repository. +// +//Revision 1.4 2005/08/09 15:24:57 guy +//Updated javadoc. +// +//Revision 1.3 2004/10/12 13:04:22 guy +//Updated docs (changed Guy Pardon to Atomikos in many places). +// +//Revision 1.2 2002/01/29 11:29:58 guy +//Updated to latest state: repository seemed outdated? +// +//Revision 1.3 2001/03/23 10:50:44 pardon +//Changed Stateful NOT to have setState; this is in StateMutable. +// +//Revision 1.2 2001/03/08 18:18:50 pardon +//Made FSM a real state machine. +// + +package com.atomikos.finitestates; + + + +public interface FSM extends StateMutable, + FSMEnterEventSource, + FSMTransitionEventSource +{ + + +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterEvent.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterEvent.java new file mode 100644 index 000000000..66aedd316 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import java.util.EventObject; + +import com.atomikos.recovery.TxState; + +public class FSMEnterEvent extends EventObject{ + + private static final long serialVersionUID = -7910459829127232977L; + + protected final TxState newState; + + public FSMEnterEvent(Object source, TxState state){ + super(source); + newState=state; + } + + public TxState getState(){ + return newState; + } +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterEventSource.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterEventSource.java new file mode 100644 index 000000000..02dc9ee99 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterEventSource.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import com.atomikos.recovery.TxState; + + +public interface FSMEnterEventSource extends Stateful +{ + + /** + *Add an enter event listener. + *@param l The listener. + *@param state The state to listen on. + * + */ + + public void addFSMEnterListener(FSMEnterListener l, TxState state); + +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterListener.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterListener.java new file mode 100644 index 000000000..fab864612 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMEnterListener.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import java.util.EventListener; + + + +public interface FSMEnterListener extends EventListener +{ + + /** + *Called BEFORE the FSM enters the new state, so that + *the callee is sure that nobody has seen the new state yet. + * + *@exception IllegalStateException on failure. + *The callee can use this to prevent the state change from + *happening. + */ + + public void preEnter(FSMEnterEvent e) throws IllegalStateException; + + /** + *Called when the FSM has entered a new state. + * + */ + + public void entered(FSMEnterEvent e); +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMImp.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMImp.java new file mode 100644 index 000000000..5f0cb81d3 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMImp.java @@ -0,0 +1,226 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; + +import com.atomikos.recovery.TxState; + + +/** + * + * + * Implementation of a finite state machine. The following + * consistency is provided: + *
    + *
  • getState returns a snapshot (state may change continuously)
  • + *
  • FSMPreEnterListeners have the guarantee that getState() returns the same + * value each time during the preEnter notification.
  • + *
  • FSMEnterListeners have no guarantee that the getState() method will + * return the state that was entered - this state may have changed since.
  • + *
+ * + */ + +public class FSMImp implements FSM +{ + + private TxState state_ = null; + + private final Hashtable> enterlisteners_ = new Hashtable<>(); + + private final Hashtable> transitionlisteners_ = new Hashtable<>(); + + private Object eventsource_ = null; + + private final Object stateLatch_ = new Object(); + + + /** + *Constructor. + * + *@param transitiontable The transitiontable with valid + *transitions. + * + *@param initialstate The initial state of the FSM. + */ + + public FSMImp ( TxState initialstate ) + { + this ( null, initialstate ); + eventsource_ = this; + } + + /** + *Creates a new instance with a given event source. + *Useful for cases where finite state machine behaviour is modelled + *by delegation to an instance of this class. + * + *@param eventsource The object to be used as source of events. + *@param transitiontable The transitiontable for state changes. + *@param initialstate The initial state of the FSM. + */ + + public FSMImp ( Object eventsource, TxState initialstate ) + { + state_ = initialstate; + eventsource_ = eventsource; + } + + + + /** + *Notify the enter listeners. + * + *@param state The state about to enter (or entered). + *@param pre True iff before entering. + */ + + protected void notifyListeners(TxState state, + boolean pre) + { + Set lstnrs = null; + FSMEnterEvent event = new FSMEnterEvent (eventsource_, state); + synchronized ( this ) { + lstnrs = enterlisteners_.get ( state ); + if ( lstnrs == null ) return; + //clone to avoid concurrency effects outside synch block + //during iteration hereafter + lstnrs = new HashSet(lstnrs); + } + //notify OUTSIDE SYNCH to minimize deadlocks + for (FSMEnterListener listener : lstnrs) { + if ( pre ) { + listener.preEnter(event); + } else { + listener.entered(event); + } + } + } + + /** + *Notify transition listeners. + * + *@param transition + *@param pre True iff before transition. + */ + + protected void notifyListeners(Transition transition, boolean pre) { + FSMTransitionEvent event = new FSMTransitionEvent (eventsource_, transition); + Set lstnrs = null; + synchronized ( this ) { + lstnrs = transitionlisteners_.get( transition ); + if ( lstnrs == null ) { + return; + } + //clone to avoid concurrency effects + //during iteration outside synch block + lstnrs = new HashSet(lstnrs); + } + + //iterator outside synch to avoid deadlocks + for (FSMTransitionListener listener : lstnrs) { + if ( pre ) { + listener.beforeTransition(event); + } else { + listener.transitionPerformed(event); + } + } + } + + /** + *@see com.atomikos.finitestates.FSM + */ + + public TxState getState() + { + //Note: this method should NOT be synchronized on the FSM itself, to avoid deadlocks + //in re-entrant 2PC calls! + TxState ret = null; + //don't synch on FSM -> use latch object instead + synchronized ( stateLatch_ ) { + ret = state_; + } + return ret; + } + + private void setStateObject ( TxState state ) + { + //synchronize on stateLatch ONLY to make sure that getState + //returns the latest (non-cached) value + synchronized ( stateLatch_ ) { + this.state_ = state; + } + } + + + /** + *@see com.atomikos.finitestates.StateMutable + */ + + public void setState(TxState state) + throws IllegalStateException + { + TxState oldstate = null; + Transition transition = null; + synchronized ( this ) { + if (!state_.transitionAllowedTo(state)) { + throw new IllegalStateException("Transition not allowed: "+state_ +" to "+state); + } + oldstate = state_; + transition = new Transition(oldstate, state); + notifyListeners(state , true); + notifyListeners(transition , true); + setStateObject ( state ); + } + //ENTER EVENTS ARE OUTSIDE SYNCH BLOCK TO MINIMIZE DEADLOCKS!!! + notifyListeners(state , false); + notifyListeners(transition, false); + } + + + /** + *@see com.atomikos.finitestates.FSMEnterEventSource + */ + + public synchronized void addFSMEnterListener(FSMEnterListener lstnr, TxState state) + { + Set lstnrs = enterlisteners_.get(state); + if ( lstnrs == null ) { + lstnrs = new HashSet(); + enterlisteners_.put( state , lstnrs ); + } + if ( !lstnrs.contains(lstnr) ) { + lstnrs.add(lstnr); + } + } + + /** + *@see com.atomikos.finitestates.FSMTransitionEventSource + */ + + + public synchronized void addFSMTransitionListener(FSMTransitionListener listener, + TxState from, TxState to) { + Transition transition = new Transition(from, to); + Set lstnrs = transitionlisteners_.get(transition); + if (lstnrs == null) { + lstnrs = new HashSet(); + transitionlisteners_.put(transition, lstnrs); + } + if (!lstnrs.contains(listener)) { + lstnrs.add(listener); + } + } + + +} + diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionEvent.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionEvent.java new file mode 100644 index 000000000..89e32611d --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionEvent.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import java.util.EventObject; + +import com.atomikos.recovery.TxState; + +public class FSMTransitionEvent extends EventObject{ + + private static final long serialVersionUID = 7629493293234798149L; + + protected final Transition transition; + + public FSMTransitionEvent(Object source,Transition transition){ + super(source); + this.transition = transition; + } + + public TxState fromState(){ + return transition.from; + } + + public TxState toState(){ + return transition.to; + } +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionEventSource.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionEventSource.java new file mode 100644 index 000000000..8d7737d6e --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionEventSource.java @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import com.atomikos.recovery.TxState; + + + +public interface FSMTransitionEventSource extends Stateful +{ + public void addFSMTransitionListener(FSMTransitionListener l, + TxState from, TxState to); + +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionListener.java b/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionListener.java new file mode 100644 index 000000000..a36d20ef2 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/FSMTransitionListener.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import java.util.EventListener; + +public interface FSMTransitionListener extends EventListener +{ + /** + *A method to be called BEFORE the specified transition takes place. + *Since the transition still has to happen, no listener can be sure + *that the event notification eventually leads to the transition. + *This is because the state machine process can fail after the notice, + *or the target state can be prevented somehow. + * + *@param e The transition that will be attempted. + *@exception IllegalStateException on failure. + */ + + public void beforeTransition(FSMTransitionEvent e) + throws IllegalStateException; + + + /** + *A method to be called AFTER the specified transition is done. + * + *@param e The transition that was made. + * + */ + + public void transitionPerformed(FSMTransitionEvent e); + +} diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/StateMutable.java b/public/transactions/src/main/java/com/atomikos/finitestates/StateMutable.java new file mode 100644 index 000000000..9f3bef4c7 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/StateMutable.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import com.atomikos.recovery.TxState; + + +public interface StateMutable extends Stateful{ + + /** + *@exception IllegalStateException if the new state transition to + *the new state is not allowed. + */ + + public void setState(TxState s) throws IllegalStateException; +} + diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/Stateful.java b/public/transactions/src/main/java/com/atomikos/finitestates/Stateful.java new file mode 100644 index 000000000..2f326eaa1 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/Stateful.java @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import com.atomikos.recovery.TxState; + + +public interface Stateful{ + /** + *@return The object representing the state. + */ + public TxState getState(); +} + diff --git a/public/transactions/src/main/java/com/atomikos/finitestates/Transition.java b/public/transactions/src/main/java/com/atomikos/finitestates/Transition.java new file mode 100644 index 000000000..ce391e57a --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/finitestates/Transition.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import com.atomikos.recovery.TxState; + +public class Transition { + + public final TxState from; + public final TxState to; + public Transition(TxState from, TxState to) { + super(); + this.from = from; + this.to = to; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((from == null) ? 0 : from.hashCode()); + result = prime * result + ((to == null) ? 0 : to.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Transition other = (Transition) obj; + if (from != other.from) + return false; + if (to != other.to) + return false; + return true; + } + + + + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/AbstractCompositeTransaction.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/AbstractCompositeTransaction.java new file mode 100644 index 000000000..c994bb753 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/AbstractCompositeTransaction.java @@ -0,0 +1,354 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Properties; +import java.util.Stack; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.recovery.TxState; + +/** + * + * + * An abstract base implementation of CompositeTransaction, for common behaviour + * of both proxy and local instances. + */ + +public abstract class AbstractCompositeTransaction implements CompositeTransaction, + java.io.Serializable +{ + + private static final long serialVersionUID = 3522422565305065464L; + + + protected Stack lineage_; + + protected String tid_; + + protected boolean serial_; + + + protected Properties properties_; + + /** + * Required for externalization of subclasses + */ + + public AbstractCompositeTransaction () + { + } + + /** + * Constructor. + * + */ + + public AbstractCompositeTransaction ( String tid , Stack lineage , + boolean serial ) + { + tid_ = tid; + lineage_ = lineage; + if ( lineage_ == null ) { + lineage_ = new Stack (); + properties_ = new Properties(); + } + else { + if ( ! lineage_.empty() ) { + CompositeTransaction parent = ( CompositeTransaction ) lineage_.peek(); + properties_ = parent.getProperties(); + } + } + if ( properties_ == null ) properties_ = new Properties(); + serial_ = serial; + + } + + /** + * @see CompositeTransaction. + */ + + public String getTid () + { + return tid_; + } + + + + /** + * @see CompositeTransaction. + */ + + public boolean isSerial () + { + return serial_; + } + + /** + * @see CompositeTransaction. + * + * Defaults to false. + */ + + public boolean isLocal () + { + return false; + } + + /** + * @see CompositeTransaction + */ + + public RecoveryCoordinator addParticipant ( Participant participant ) + throws SysException, java.lang.IllegalStateException + + { + throw new UnsupportedOperationException(); + } + + /** + * @see CompositeTransaction + */ + + public void registerSynchronization ( Synchronization sync ) + throws IllegalStateException, UnsupportedOperationException, SysException + { + throw new UnsupportedOperationException(); + } + + /** + * @see CompositeTransaction. + */ + @SuppressWarnings("unchecked") + public Stack getLineage () + { + return (Stack) lineage_.clone (); + } + + /** + * @see CompositeTransaction. + */ + + public boolean isRoot () + { + return ( lineage_ == null || lineage_.size () == 0); + // for non-roots, this is at least one + } + + /** + * @see CompositeTransaction. + */ + + public boolean isAncestorOf ( CompositeTransaction ct ) + { + return ct.isDescendantOf ( this ); + } + + /** + * @see CompositeTransaction. + */ + + public boolean isDescendantOf ( CompositeTransaction ct ) + { + CompositeTransaction parent = null; + if ( lineage_ != null && (!lineage_.empty ()) ) + parent = (CompositeTransaction) lineage_.peek (); + + return (isSameTransaction ( ct ) || (parent != null && parent + .isDescendantOf ( ct ))); + } + + /** + * @see CompositeTransaction. + */ + @SuppressWarnings("unchecked") + public boolean isRelatedTransaction ( CompositeTransaction ct ) + { + Stack lineage = null; + if ( lineage_ == null ) + lineage = new Stack (); + else + lineage = (Stack) lineage_.clone (); + + if ( lineage.empty () ) + return isAncestorOf ( ct ); + + CompositeTransaction root = null; + while ( !lineage.empty () ) + root = (CompositeTransaction) lineage.pop (); + return root.isAncestorOf ( ct ); + } + + /** + * @see CompositeTransaction. + */ + + public boolean isSameTransaction ( CompositeTransaction ct ) + { + return (equals ( ct )); + + // lock inheritance is determined by resource using this; + // OK if same invocation or if sibling, but serial mode. + } + + + public int hashCode() + { + int ret = 0; + + if ( tid_ == null ) { + ret = super.hashCode(); + } else { + ret = getTid().hashCode(); + } + + return ret; + } + + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof AbstractCompositeTransaction) ) + return false; + AbstractCompositeTransaction other = (AbstractCompositeTransaction) obj; + if (tid_ == null) { + if (other.tid_ != null) + return false; + } else if (!tid_.equals(other.tid_)) + return false; + return true; + } + + /** + * @see CompositeTransaction. + */ + + public CompositeCoordinator getCompositeCoordinator () throws SysException, + UnsupportedOperationException + { + throw new UnsupportedOperationException(); + } + + /** + * @see CompositeTransaction + */ + + public void addSubTxAwareParticipant ( SubTxAwareParticipant subtxaware ) + throws SysException, UnsupportedOperationException, + java.lang.IllegalStateException + { + throw new UnsupportedOperationException (); + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#createSubTransaction() + */ + public CompositeTransaction createSubTransaction () throws SysException, + IllegalStateException + { + throw new UnsupportedOperationException(); + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#setSerial() + */ + public void setSerial () throws IllegalStateException, SysException + { + throw new UnsupportedOperationException(); + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#getExtent() + */ + public Extent getExtent () + { + throw new UnsupportedOperationException(); + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#getTimeout() + */ + public long getTimeout () + { + return 0; + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#setRollbackOnly() + */ + public void setRollbackOnly () + { + throw new UnsupportedOperationException(); + + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#commit() + */ + public void commit () throws HeurMixedException, + HeurHazardException, SysException, SecurityException, + RollbackException + { + throw new UnsupportedOperationException(); + + } + + + + /** + * @see com.atomikos.icatch.CompositeTransaction#rollback() + */ + + public void rollback () throws IllegalStateException, SysException + { + throw new UnsupportedOperationException(); + + } + + + public void setProperty ( String name , String value ) + { + if ( getProperty ( name ) == null ) + properties_.setProperty ( name , value ); + } + + public String getProperty ( String name ) + { + return properties_.getProperty ( name ); + } + + public Properties getProperties() + { + return ( Properties ) properties_.clone(); + } + + + /** + * @see com.atomikos.finitestates.Stateful. + */ + + public TxState getState () + { + throw new UnsupportedOperationException(); + } + + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/ActiveStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/ActiveStateHandler.java new file mode 100644 index 000000000..0afc18b47 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/ActiveStateHandler.java @@ -0,0 +1,313 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Enumeration; +import java.util.Vector; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; +import com.atomikos.thread.InterruptedExceptionHelper; + +/** + * A state handler for the active coordinator state. + */ + +class ActiveStateHandler extends CoordinatorStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(ActiveStateHandler.class); + + private long rollbackTicks_; + // how many timeout events have happened? + // if max allowed -> rollback on timeout + + private int globalSiblingCount_; + + private boolean wasSetToRollbackOnly; + + ActiveStateHandler ( CoordinatorImp coordinator ) + { + super ( coordinator ); + rollbackTicks_ = 0; + wasSetToRollbackOnly = false; + } + + + protected long getRollbackTicks () + { + return rollbackTicks_; + } + + protected TxState getState () + { + return TxState.ACTIVE; + } + + protected void onTimeout () + { + + try { + if ( rollbackTicks_ < getCoordinator ().getMaxRollbackTicks () ) + rollbackTicks_++; + else { + // first check if we are still the current state! + // otherwise, a COMMITTING tx could be rolled back + // in case of 1PC!!! + if ( getCoordinator().getState() == TxState.ACTIVE) { + if ( getCoordinator().hasActiveSiblings() ) { + //cf case 71748 + if (!wasSetToRollbackOnly) { + LOGGER.logWarning ( "Transaction " + getCoordinator().getCoordinatorId() + " has timed out and will rollback."); + getCoordinator().timedout(true); + wasSetToRollbackOnly = true; + } + } else { + LOGGER.logWarning ( "Transaction " + getCoordinator().getCoordinatorId() + " has timed out - rolling back..."); + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + getCoordinator().timedout(false); + rollbackFromWithinCallback(false,false); + }}); + } + } else if (getCoordinator().getState().isOneOf(TxState.PREPARING, TxState.COMMITTING, TxState.ABORTING)) { + //pending coordinator after failed prepare: cleanup to remove from TransactionServiceImp + removePendingOltpCoordinatorFromTransactionService(); + } + } + } catch ( Exception e ) { + LOGGER.logDebug( "Error in timeout: " + e.getMessage () + + " for transaction " + getCoordinator ().getCoordinatorId () ); + } + } + + protected void setGlobalSiblingCount ( int count ) + { + globalSiblingCount_ = count; + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + + int count = 0; // number of participants + PrepareResult result = null; // synchronization + boolean allReadOnly = true; // if still true at end-> readonly vote + int ret = 0; // return value + Vector participants = getCoordinator ().getParticipants (); + CoordinatorStateHandler nextStateHandler = null; + + if ( orphansExist() ) { + try { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Orphans detected: " + + getCoordinator ().getLocalSiblingCount () + " vs " + + globalSiblingCount_ + " - forcing rollback." ); + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + rollbackFromWithinCallback(false,false); + }}); + + } catch ( HeurCommitException hc ) { + throw new HeurMixedException(); + } + + throw new RollbackException ( "Orphans detected." ); + } + + try { + try { + getCoordinator().setState ( TxState.PREPARING ); + } catch ( RuntimeException error ) { + //See case 23334 + String msg = "Error in preparing: " + error.getMessage() + " - rolling back instead"; + LOGGER.logWarning ( msg , error ); + try { + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + rollbackFromWithinCallback(false,false); + }}); + throw new RollbackException ( msg , error); + } catch ( HeurCommitException e ) { + LOGGER.logError ( "Illegal heuristic commit during rollback before prepare:" + e ); + throw new HeurMixedException(); + } + } + count = participants.size (); + result = new PrepareResult ( count ); + Enumeration enumm = participants.elements (); + while ( enumm.hasMoreElements () ) { + Participant p = (Participant) enumm.nextElement (); + PrepareMessage pm = new PrepareMessage ( p, result ); + if ( getCascadeList () != null && p.getURI () != null ) { //null for OTS + Integer sibnum = (Integer) getCascadeList ().get ( p.getURI () ); + if ( sibnum != null ) { // null for local participant! + p.setGlobalSiblingCount ( sibnum.intValue () ); + } + p.setCascadeList ( getCascadeList () ); + } + + getPropagator ().submitPropagationMessage ( pm ); + } // while + + result.waitForReplies (); + + boolean voteOK = result.allYes (); + setReadOnlyTable ( result.getReadOnlyTable () ); + allReadOnly = result.allReadOnly (); + + if ( !voteOK ) { + int res = result.getResult (); + Exception cause = result.findFirstOriginalException(); + try { + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + rollbackFromWithinCallback(true,false); + }}); + } catch ( HeurCommitException hc ) { + // should not happen: + // means that ALL subordinate work committed heuristically. + // this is impossible since it assumes that ALL + // participants voted YES in the first place, + // which contradicts the fact that we are dealing with + // !voteOK + throw new SysException ( "Unexpected heuristic: " + + hc.getMessage (), hc ); + } + // let recovery clean up in the background + String msg = "Prepare failed because one or more resources refused to commit. This transaction has been rolled back instead. The cause could be either:\n" + + "1. a transaction timeout (in which case you should see additional timeout warnings in this log file), or\n" + + "2. inability to reach the resource (in which case you should see network errors), or\n" + + "3. a resource-internal cause that we can’t inspect"; + throw new RollbackException ( msg, cause ); + } + } catch ( RuntimeException runerr ) { + throw new SysException ( "Error in prepare: " + runerr.getMessage (), runerr ); + } catch ( InterruptedException err ) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( err ); + throw new SysException ( "Error in prepare: " + err.getMessage (), err ); + } + // here we are if all yes. + if (discardCoordinatorAfterPrepare(allReadOnly)) { + nextStateHandler = new TerminatedStateHandler ( this ); + getCoordinator ().setStateHandler ( nextStateHandler ); + ret = Participant.READ_ONLY; + notifySynchronizationsAfterCompletion(TxState.COMMITTING,TxState.TERMINATED); //cf bug 127485 + } else { + nextStateHandler = new IndoubtStateHandler ( this ); + getCoordinator ().setStateHandler ( nextStateHandler ); + ret = Participant.READ_ONLY + 1; + } + + return ret; + } + + private boolean discardCoordinatorAfterPrepare(boolean allReadOnly) { + boolean ret = false; + if (allReadOnly) { + if (getCoordinator().isRoot() || getCoordinator().getLocalSiblingCount() == 1) { + //cf case 197506 + ret = true; + } + } + return ret; + } + + + private boolean orphansExist() { + return globalSiblingCount_ != getCoordinator ().getLocalSiblingCount(); + } + + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + if ( !onePhase ) + throw new IllegalStateException ( + "Illegal state for commit: ACTIVE!" ); + + if ( getCoordinator ().getParticipants ().size () > 1 ) { + int prepareResult = Participant.READ_ONLY + 1; + + // happens if client has one remote participant + // and hence decides for 1PC, but the remote + // instance (this one) has more participants registered + // in that case: first prepare and then do normal + // 2PC commit + + setGlobalSiblingCount ( 1 ); + prepareResult = prepare (); + + if ( prepareResult != Participant.READ_ONLY ) { + commitWithAfterCompletionNotification ( new CommitCallback() { + public void doCommit() + throws HeurRollbackException, HeurMixedException, + HeurHazardException, IllegalStateException, + RollbackException, SysException { + commitFromWithinCallback ( false, false ); + } + }); + } + } else { + commitWithAfterCompletionNotification ( new CommitCallback() { + public void doCommit() + throws HeurRollbackException, HeurMixedException, + HeurHazardException, IllegalStateException, + RollbackException, SysException { + commitFromWithinCallback ( false, true ); + } + }); + } + + } + + protected void rollback () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException + { + + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + rollbackFromWithinCallback(false,false); + }}); + } + + protected Boolean replayCompletion ( Participant participant ) + throws IllegalStateException + { + + throw new IllegalStateException ( "No prepares sent yet." ); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CommitCallback.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CommitCallback.java new file mode 100644 index 000000000..be4f8b914 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CommitCallback.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; + +/** + * Callback interface for logic inside the commit template. + */ + +interface CommitCallback { + + public void doCommit() throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException; + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CommitMessage.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CommitMessage.java new file mode 100644 index 000000000..ed739078f --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CommitMessage.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * A commit message implementation. + */ + +class CommitMessage extends PropagationMessage +{ + private static final Logger LOGGER = LoggerFactory.createLogger(CommitMessage.class); + + private boolean onephase_ = false; + + + public CommitMessage ( Participant participant , Result result , + boolean onephase ) + { + super ( participant , result ); + onephase_ = onephase; + } + + /** + * A commit message. + * + * @return Boolean null. + * @exception PropagationException + * If problems. If heuristics, this will be a heuristic + * exception; otherwise, commit has to be retried since + * participant can be indoubt. Hence, if not heuristic in + * nature, then the error is transient. + */ + + protected Boolean send () throws PropagationException + { + Participant part = getParticipant (); + try { + part.commit ( onephase_ ); + return null; + } catch ( RollbackException rb ) { + throw new PropagationException ( rb, false ); + } catch ( HeurMixedException heurm ) { + throw new PropagationException ( heurm, false ); + } catch ( HeurRollbackException heurr ) { + throw new PropagationException ( heurr, false ); + } catch ( Exception e ) { + // heuristic hazard or not, participant might be indoubt. + String msg = "Unexpected error in commit"; + LOGGER.logError ( msg, e ); + HeurHazardException heurh = new HeurHazardException(); + boolean retry = !onephase_; // cf case 175451 + throw new PropagationException ( heurh, retry ); + } + } + + public String toString () + { + return ("CommitMessage to " + getParticipant ()); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionAdaptor.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionAdaptor.java new file mode 100644 index 000000000..4e0aa5928 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionAdaptor.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.SysException; + +/** + * A composite transaction adaptor for inter-position on an imported instance. + * This allows substitution of the recovery coordinator adaptor. + */ + +public class CompositeTransactionAdaptor extends AbstractCompositeTransaction + implements CompositeCoordinator +{ + + private static final long serialVersionUID = 6361601412982044104L; + + private RecoveryCoordinator adaptorForReplayRequests; + + private String root; + + private String coordinatorId; + + + /** + * Constructor for testin. + * @param root + * @param serial + * @param adaptor + */ + + public CompositeTransactionAdaptor ( String root , boolean serial , + RecoveryCoordinator adaptor ) + { + super ( root , null , serial ); + adaptorForReplayRequests = adaptor; + this.root = root; + } + + /** + * @see CompositeCoordinator. + */ + + public CompositeCoordinator getCompositeCoordinator () throws SysException + { + return this; + } + + /** + * @see CompositeCoordinator. + */ + + public String getRootId () + { + return root; + } + + /** + * @see CompositeCoordinator. + */ + + public RecoveryCoordinator getRecoveryCoordinator () + { + return adaptorForReplayRequests; + } + + @Override + public String getCoordinatorId() { + return coordinatorId; + } + + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionImp.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionImp.java new file mode 100644 index 000000000..ff8286012 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionImp.java @@ -0,0 +1,368 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Map; +import java.util.Stack; + +import com.atomikos.finitestates.FSMEnterEvent; +import com.atomikos.finitestates.FSMEnterListener; +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * A complete composite transaction implementation for use in the local VM. + */ + +class CompositeTransactionImp extends AbstractCompositeTransaction implements FSMEnterListener +{ + private static final long serialVersionUID = 975317723773209940L; + + private static final Logger LOGGER = LoggerFactory.createLogger(CompositeTransactionImp.class); + + private CoordinatorImp coordinator = null; + + private TransactionServiceImp txservice; + + private Extent extent = null; + + protected boolean noLocalAncestors; + + private TransactionStateHandler stateHandler; + + /** + * This constructor is kept for compatibility with the test classes. + */ + + CompositeTransactionImp ( Stack lineage , String tid , boolean serial , + CoordinatorImp coordinator ) + { + this ( null , lineage , tid , serial , coordinator ); + } + + /** + * Constructor. + * + * @param txservice + * The Transaction Service this is for. + * @param lineage + * The ancestor information. + * @param tid + * The identifier for this one. + * @param serial + * If true, no parallel calls allowed. + * @param coordinator + * The coordinator to use. + * @exception IllegalStateException + * If coordinator no longer activatable. + */ + + CompositeTransactionImp ( TransactionServiceImp txservice , + Stack lineage , String tid , boolean serial , + CoordinatorImp coordinator ) throws IllegalStateException + { + + super ( tid , lineage , serial ); + this.coordinator = coordinator; + this.txservice = txservice; + this.extent = null; + this.noLocalAncestors = true; + this.stateHandler = new TxActiveStateHandler ( this ); + coordinator.addFSMEnterListener ( this, TxState.TERMINATED ); + + } + + synchronized void localSetTransactionStateHandler ( TransactionStateHandler handler ) + { + stateHandler = handler; + } + + synchronized void localTestAndSetTransactionStateHandler ( TransactionStateHandler expected , TransactionStateHandler newHandler ) + { + if ( stateHandler != expected ) throw new IllegalStateException ( "State is no longer " + expected.getState() + " but " + newHandler.getState() ); + localSetTransactionStateHandler( newHandler ); + } + + synchronized TransactionStateHandler localGetTransactionStateHandler() + { + return stateHandler; + } + + boolean isLocalRoot () + { + return noLocalAncestors; + } + + TransactionServiceImp getTransactionService () + { + return txservice; + } + + CoordinatorImp getCoordinatorImp () + { + return coordinator; + } + + public synchronized void setSerial () throws IllegalStateException, + SysException + { + if ( !isRoot () ) throw new IllegalStateException ( "Not a root tx." ); + serial_ = true; + } + + + /** + * @see TransactionControl. + */ + + public CompositeTransaction createSubTransaction () throws SysException, + IllegalStateException + { + CompositeTransaction ret = localGetTransactionStateHandler().createSubTransaction (); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("createSubTransaction(): created new SUBTRANSACTION " + + ret.getTid () + " for existing transaction " + getTid () + " with coordinatorId " + ret.getCompositeCoordinator().getCoordinatorId()); + } + + return ret; + } + + /** + * @see CompositeTransaction + */ + + public RecoveryCoordinator addParticipant ( Participant participant ) + throws SysException, java.lang.IllegalStateException + { + + RecoveryCoordinator ret = localGetTransactionStateHandler().addParticipant ( participant ); + return ret; + } + + /** + * @see CompositeTransaction + */ + + public void registerSynchronization ( Synchronization sync ) throws // RollbackException, + IllegalStateException, UnsupportedOperationException, SysException + { + localGetTransactionStateHandler().registerSynchronization ( sync ); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("registerSynchronization ( " + sync + " ) for transaction " + + getTid ()); + } + } + + /** + * @see CompositeTransaction + */ + + public void addSubTxAwareParticipant ( SubTxAwareParticipant subtxaware ) + throws SysException, java.lang.IllegalStateException + { + localGetTransactionStateHandler().addSubTxAwareParticipant ( subtxaware ); + } + + /** + * @see TransactionControl. + */ + + protected void doRollback () throws java.lang.IllegalStateException, + SysException + { + localGetTransactionStateHandler().rollbackWithStateCheck (); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("rollback() done of transaction " + getTid ()); + } + + } + + /** + * @see CompositeTransaction. + */ + + public CompositeCoordinator getCompositeCoordinator () throws SysException + { + return coordinator; + } + + /** + * @see CompositeTransaction. + */ + + public boolean isLocal () + { + return true; + } + + /** + * Successfully end the composite transaction. Marks it as inactive. Called + * by Terminator implementation only! NOTE: this does NOT commit the + * participants, but rather only marks the (sub)transaction as being + * ELIGIBLE FOR PREPARE IN 2PC. + * + * @exception IllegalStateException + * If no longer active. + * @exception SysException + * Unexpected failure. + */ + + protected void doCommit () throws SysException, + java.lang.IllegalStateException, RollbackException + { + + localGetTransactionStateHandler().commit (); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("commit() done (by application) of transaction " + getTid ()); + } + } + + public long getTimeout () + { + return coordinator.getTimeOut (); + } + + public synchronized Extent getExtent() { + if (extent == null) { + if (isRoot()) { + extent = new Extent(); + } else { + String parentTransactionId = getLineage().peek().getTid(); + extent = new Extent(parentTransactionId); + } + } + return extent; + } + + public void setRollbackOnly () + { + localGetTransactionStateHandler().setRollbackOnly (); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("setRollbackOnly() called for transaction " + getTid ()); + } + + + } + + /** + * @see com.atomikos.icatch.CompositeTransaction#commit() + */ + public void commit () throws HeurMixedException, + HeurHazardException, SysException, SecurityException, + RollbackException + { + doCommit (); + setSiblingInfoForIncoming1pcRequestFromRemoteClient(); + + try { + if (isRoot()) { + coordinator.terminate(true); + } else { + coordinator.incLocalSiblingsTerminated(); + } + } catch (HeurRollbackException rb) { + throw new RollbackException("Transaction was rolled back", rb); + } catch ( RollbackException rb) { + throw rb; + } catch ( HeurHazardException | HeurMixedException h ) { + //no need to log: coordinator already logs on heuristics + if (throwOnHeuristic()) { + throw h; + } + } catch ( SysException se ) { + throw se; + } catch ( Exception e ) { + throw new SysException ( + "Unexpected error: " + e.getMessage (), e ); + } + + } + + private boolean throwOnHeuristic() { + return Configuration.getConfigProperties().getThrowOnHeuristic(); + } + + private void setSiblingInfoForIncoming1pcRequestFromRemoteClient() { + Map cascadelist = getExtent().getRemoteParticipants (); + coordinator.setGlobalSiblingCount ( coordinator.getLocalSiblingCount () ); + coordinator.setCascadeList ( cascadelist ); + } + + + /** + * @see com.atomikos.icatch.CompositeTransaction#rollback() + */ + public void rollback() throws IllegalStateException, SysException + { + doRollback(); + try { + if (isRoot()) { + coordinator.terminate(false); + } else { + coordinator.incLocalSiblingsTerminated(); + } + } catch ( Exception e ) { + throw new SysException ( "Unexpected error in rollback: " + e.getMessage (), e ); + } + + } + + /** + * @see com.atomikos.finitestates.Stateful. + */ + + public TxState getState () + { + return localGetTransactionStateHandler().getState (); + } + + /** + * @see com.atomikos.finitestates.FSMEnterListener#preEnter(com.atomikos.finitestates.FSMEnterEvent) + */ + public void entered ( FSMEnterEvent coordinatorTerminatedEvent ) + { + if (getState().isOneOf(TxState.ACTIVE,TxState.MARKED_ABORT )) { + // our coordinator terminated and we did not -> coordinator must have seen a timeout/abort + try { + if (!( stateHandler instanceof TxTerminatedStateHandler ) ) { + // note: check for TxTerminatedStateHandler differentiates regular rollback from timeout/rollback !!! + setRollbackOnly(); // see case 27857: keep tx context for thread on timeout + } else { + rollback(); + } + + } catch ( Exception e ) { + // ignore but log + LOGGER.logTrace("Ignoring error during event callback",e); + } + } + + } + + + @Override + public void preEnter(FSMEnterEvent e) throws IllegalStateException { + } + + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionManagerImp.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionManagerImp.java new file mode 100644 index 000000000..1595992ae --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CompositeTransactionManagerImp.java @@ -0,0 +1,390 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionService; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * Reusable (generic) composite transaction manager implementation. + */ + +public class CompositeTransactionManagerImp implements CompositeTransactionManager, + SubTxAwareParticipant +{ + private static final Logger LOGGER = LoggerFactory.createLogger(CompositeTransactionManagerImp.class); + + private Map> threadtotxmap_ = null; + private Map txtothreadmap_ = null; + + + public CompositeTransactionManagerImp () + { + threadtotxmap_ = new HashMap> (); + txtothreadmap_ = new HashMap (); + } + + + + /** + * Get the thread associated with a given transaction. + */ + + private Thread getThread ( CompositeTransaction ct ) + { + Thread thread = null; + + synchronized ( txtothreadmap_ ) { + thread = (Thread) txtothreadmap_.get ( ct ); + } + return thread; + } + + /** + * Remove mappings for given thread. + * + * @return Stack The tx stack that was for current thread. + */ + + private Stack removeThreadMappings ( Thread thread ) + { + + Stack ret = null; + synchronized ( threadtotxmap_ ) { + ret = threadtotxmap_.remove ( thread ); + CompositeTransaction tx = ret.peek (); + txtothreadmap_.remove ( tx ); + } + return ret; + } + + /** + * Set mappings for current thread. + * + * @param ct + * The transaction to map to. Also sets the coordinator mapping + * by getting ct's coordinator. + */ + + private void setThreadMappings ( CompositeTransaction ct , Thread thread ) + throws IllegalStateException, SysException + { + //case 21806: callbacks to ct to be made outside synchronized block + ct.addSubTxAwareParticipant ( this ); //step 1 + + synchronized ( threadtotxmap_ ) { + //between step 1 and here, intermediate timeout/rollback of the ct + //may have happened; make sure to check or we add a thread mapping + //that will never be removed! + if ( TxState.ACTIVE.equals ( ct.getState() )) { + Stack txs = threadtotxmap_.get ( thread ); + if ( txs == null ) + txs = new Stack(); + txs.push ( ct ); + threadtotxmap_.put ( thread, txs ); + txtothreadmap_.put ( ct, thread ); + } + } + + + } + + private void restoreThreadMappings ( Stack stack , Thread thread ) + throws IllegalStateException + { + //case 21806: callbacks to ct to be made outside synchronized block + CompositeTransaction tx = stack.peek (); + tx.addSubTxAwareParticipant(this); //step 1 + + synchronized ( threadtotxmap_ ) { + //between step 1 and here, intermediate timeout/rollback of the ct + //may have happened; make sure to check or we add a thread mapping + //that will never be removed! + TxState state = tx.getState(); + + if ( state.isOneOf(TxState.ACTIVE, TxState.MARKED_ABORT) ) { + //also resume for marked abort - see case 26398 + Stack txs = threadtotxmap_.get ( thread ); + if ( txs != null ) { + throw new IllegalStateException ("Thread already has subtx stack" ); + } + threadtotxmap_.put ( thread, stack ); + txtothreadmap_.put ( tx, thread ); + } + } + } + + private CompositeTransaction getCurrentTx () + { + Thread thread = Thread.currentThread (); + synchronized ( threadtotxmap_ ) { + Stack txs = threadtotxmap_.get ( thread ); + if ( txs == null ) + return null; + else + return txs.peek (); + + } + } + + private TransactionService getTransactionService() { + TransactionService ret = Configuration.getTransactionService(); + if (ret == null) throw new IllegalStateException("Not initialized"); + return ret; + } + + + + /** + * Called if a tx is ended successfully. In order to remove the tx from the + * mapping. + * + * @see SubTxAwareParticipant + */ + + public void committed ( CompositeTransaction tx ) + { + removeTransaction ( tx ); + } + + /** + * Called if a tx is ended with failure. In order to remove tx from mapping. + * + * @see SubTxAwareParticipant + */ + + public void rolledback ( CompositeTransaction tx ) + { + removeTransaction ( tx ); + + } + + /** + * @see CompositeTransactionManager + */ + + public CompositeTransaction getCompositeTransaction () throws SysException + { + + CompositeTransaction ct = null; + ct = getCurrentTx (); + if ( ct != null ) { + if(LOGGER.isTraceEnabled()){ + LOGGER.logTrace("getCompositeTransaction() returning instance with id " + + ct.getTid ()); + } + } else{ + if(LOGGER.isTraceEnabled()){ + LOGGER.logTrace("getCompositeTransaction() returning NULL!"); + } + } + + return ct; + } + + /** + * @see CompositeTransactionManager + */ + + public CompositeTransaction getCompositeTransaction ( String tid ) + throws SysException + { + CompositeTransaction ret = getTransactionService().getCompositeTransaction ( tid ); + if ( ret != null ) { + if(LOGGER.isTraceEnabled()){ + LOGGER.logTrace("getCompositeTransaction ( " + tid + + " ) returning instance with tid " + ret.getTid ()); + } + } else { + if(LOGGER.isTraceEnabled()){ + LOGGER.logTrace( "getCompositeTransaction ( " + tid + + " ) returning null"); + } + } + return ret; + } + + + + + /** + * Recreate a composite transaction based on an imported context. Needed by + * the application's communication layer. + * + * @param context + * The propagationcontext. + * + * @return CompositeTransaction The recreated local instance. + * @exception SysException + * Failure. + */ + + public synchronized CompositeTransaction recreateCompositeTransaction( Propagation context )throws SysException { + CompositeTransaction ct = null; + + + ct = getCurrentTx(); + if ( ct != null ) { + LOGGER.logWarning("Recreating a transaction with existing transaction: " + ct.getTid()); + } + ct = getTransactionService().recreateCompositeTransaction(context); + Thread t = Thread.currentThread (); + setThreadMappings ( ct, t ); + return ct; + } + + /** + * @see CompositeTransactionManager + */ + + public CompositeTransaction suspend () throws SysException + { + CompositeTransaction ret = getCurrentTx (); + if ( ret != null ) { + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("suspend() for transaction " + ret.getTid ()); + } + removeThreadMappings(Thread.currentThread()); + } else { + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("suspend() called without a transaction context"); + } + } + return ret; + + } + + + /** + * @see CompositeTransactionManager + */ + @SuppressWarnings("unchecked") + public void resume ( CompositeTransaction ct ) + throws IllegalStateException, SysException + { + Stack ancestors = new Stack(); + Stack tmp = new Stack(); + Stack lineage = (Stack) ct.getLineage ().clone (); + boolean done = false; + while ( !lineage.isEmpty () && !done ) { + CompositeTransaction parent = lineage.pop (); + if ( !parent.isLocal () ) + done = true; + else + tmp.push ( parent ); + } + while ( !tmp.isEmpty () ) { + ancestors.push ( tmp.pop () ); + } + ancestors.push ( ct ); + + restoreThreadMappings(ancestors, Thread.currentThread()); + if(LOGGER.isDebugEnabled()) { + LOGGER.logDebug("resume ( " + ct + " ) done for transaction " + ct.getTid ()); + } + + } + + /** + * Shut down the server in a clean way. + * + * @param force + * If true, shutdown will not wait for possibly indoubt txs to + * finish. Calling shutdown with force being true implies that + * shutdown will not fail, but there may be remaining timer + * threads that stay asleep until there timeouts expire. Such + * remaining active transactions will NOT be able to finish, + * because the recovery manager will be shutdown by that time. + * New transactions will not be allowed. + * + * @exception SysException + * For unexpected errors. + * @exception IllegalStateException + * If active txs exist, and not force. + */ + + public void shutdown ( boolean force ) throws SysException, + IllegalStateException + { + getTransactionService().shutdown ( force ); + } + + protected void startlistening ( CompositeTransaction transaction ) + throws SysException + { + transaction.addSubTxAwareParticipant ( this ); + } + + /** + * Removes the tx associated with calling thread. Restores context to the + * last locally started parent, if any. Does nothing if no thread found or + * if ct null. + * + * @param ct + * The transaction to remove. + */ + + private void removeTransaction ( CompositeTransaction ct ) + { + if ( ct == null ) return; + + Thread thread = getThread ( ct ); + if ( thread == null ) return; + + Stack mappings = removeThreadMappings ( thread ); + if ( mappings != null && !mappings.empty() ) { + mappings.pop(); + if ( !mappings.empty()) { + restoreThreadMappings(mappings, thread); + } + } + + } + + /** + * @see CompositeTransactionManager + */ + + public CompositeTransaction createCompositeTransaction ( long timeout ) throws SysException + { + CompositeTransaction ct = null , ret = null; + + ct = getCurrentTx (); + if ( ct == null ) { + ret = getTransactionService().createCompositeTransaction ( timeout ); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("createCompositeTransaction ( " + timeout + " ): " + + "created new ROOT transaction with id " + ret.getTid ()); + } + } else { + if(LOGGER.isDebugEnabled()) { + LOGGER.logDebug("createCompositeTransaction ( " + timeout + " )"); + } + ret = ct.createSubTransaction (); + + } + Thread thread = Thread.currentThread (); + setThreadMappings ( ret, thread ); + + return ret; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/ConditionalWaiter.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/ConditionalWaiter.java new file mode 100644 index 000000000..f05efc830 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/ConditionalWaiter.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.thread.InterruptedExceptionHelper; + +class ConditionalWaiter { + + private static final Logger LOGGER = LoggerFactory.createLogger(ConditionalWaiter.class); + + private long maxWaitTime; + + ConditionalWaiter(long maxWaitTime) { + this.maxWaitTime = maxWaitTime; + } + + /** + * Waits while the condition evaluates to true, or until maxWaitTime has passed. + * @param condition + * @return True if timed out. + */ + boolean waitWhile(Condition condition) { + long accumulatedWaitTime = 0; + int waitTime = 1000; + boolean evaluation = condition.evaluate(); + while (evaluation && (accumulatedWaitTime < maxWaitTime)) { + LOGGER.logInfo("Waiting for condition to become true..."); + synchronized(this) { + try { + this.wait(waitTime); + } catch (InterruptedException ex) { + InterruptedExceptionHelper.handleInterruptedException ( ex ); + // ignore + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( this + ": interrupted during wait" , ex ); + } + } + accumulatedWaitTime = accumulatedWaitTime + waitTime; + evaluation = condition.evaluate(); + } + return evaluation; + } + + @FunctionalInterface + static interface Condition { + boolean evaluate(); + } +} + diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CoordinatorImp.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CoordinatorImp.java new file mode 100644 index 000000000..7264b48e9 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CoordinatorImp.java @@ -0,0 +1,796 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import com.atomikos.finitestates.FSM; +import com.atomikos.finitestates.FSMEnterEvent; +import com.atomikos.finitestates.FSMEnterListener; +import com.atomikos.finitestates.FSMImp; +import com.atomikos.finitestates.FSMTransitionEvent; +import com.atomikos.finitestates.FSMTransitionListener; +import com.atomikos.finitestates.Stateful; +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.event.transaction.TransactionHeuristicEvent; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.persistence.RecoverableCoordinator; +import com.atomikos.publish.EventPublisher; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; +import com.atomikos.thread.TaskManager; +import com.atomikos.timing.AlarmTimer; +import com.atomikos.timing.AlarmTimerListener; +import com.atomikos.timing.PooledAlarmTimer; + +/** + * + * All things related to termination logic. + * + */ + +public class CoordinatorImp implements CompositeCoordinator, Participant, + RecoveryCoordinator, RecoverableCoordinator, AlarmTimerListener, Stateful, + FSMEnterListener, FSMTransitionListener +{ + private static final Logger LOGGER = LoggerFactory.createLogger(CoordinatorImp.class); + + static long DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS = 150; + // SHOULD NOT BE BIG, otherwise lots of sleeping threads -> OUT OF MEMORY! + + private static final int MAX_NUMBER_OF_TIMEOUT_TICKS_FOR_INDOUBTS = 30; + private static final int MAX_NUMBER_OF_TIMEOUT_TICKS_BEFORE_ROLLBACK_OF_ACTIVES = 30; + + private int localSiblingsStarted = 0; + private int localSiblingsTerminated = 0; + private AlarmTimer timer_ = null; + + private long maxNumberOfTimeoutTicksBeforeHeuristicDecision_ = MAX_NUMBER_OF_TIMEOUT_TICKS_FOR_INDOUBTS; + private long maxNumberOfTimeoutTicksBeforeRollback_ = MAX_NUMBER_OF_TIMEOUT_TICKS_BEFORE_ROLLBACK_OF_ACTIVES; + + private String root_ = null; + private String coordinatorId = null; + private FSM fsm_ = null; + private Vector participants_ = new Vector(); + private RecoveryCoordinator superiorCoordinator_ = null; + + private CoordinatorStateHandler stateHandler_; + private boolean single_threaded_2pc_; + private transient List synchronizations; + private boolean timedout = false; + + private String recoveryDomainName; + + /** + * Constructor for testing only. + */ + + protected CoordinatorImp ( String root) + { + root_ = root; + this.coordinatorId = root; + initFsm(TxState.ACTIVE); + + setStateHandler ( new ActiveStateHandler ( this ) ); + startThreads ( DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS ); + single_threaded_2pc_ = false; + synchronizations = new ArrayList(); + } + + private void initFsm(TxState initialState) { + fsm_ = new FSMImp ( this, initialState ); + fsm_.addFSMEnterListener(this, TxState.TERMINATED); + fsm_.addFSMEnterListener(this, TxState.HEUR_COMMITTED ); + fsm_.addFSMEnterListener(this, TxState.HEUR_ABORTED ); + fsm_.addFSMEnterListener(this, TxState.HEUR_MIXED ); + fsm_.addFSMEnterListener(this, TxState.HEUR_HAZARD ); + fsm_.addFSMEnterListener(this, TxState.ABANDONED); + fsm_.addFSMTransitionListener ( this, TxState.COMMITTING, TxState.TERMINATED ); + fsm_.addFSMTransitionListener ( this, TxState.ABORTING, TxState.TERMINATED ); + fsm_.addFSMTransitionListener ( this, TxState.PREPARING, TxState.TERMINATED); + } + + /** + * Constructor. + * + * @param recoveryDomainName + * + * @param coordinatorId + * + * @param root + * The root tid. + * @param coord + * The RecoverCoordinator, null if root. + * @param console + * The console to log to, or null if none. + * @param timeout + * The timeout in milliseconds for indoubts before a heuristic + * decision is made. + * + * @param single_threaded_2pc + * If true then commit is done in the same thread as the one that + * started the tx. + */ + + protected CoordinatorImp ( String recoveryDomainName, String coordinatorId, String root , RecoveryCoordinator coord , + long timeout , boolean single_threaded_2pc ) + { + root_ = root; + this.coordinatorId = coordinatorId; + this.recoveryDomainName = recoveryDomainName; + single_threaded_2pc_ = single_threaded_2pc; + initFsm(TxState.ACTIVE ); + + superiorCoordinator_ = coord; + if ( timeout > DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS ) { + // If timeout is smaller than the default timeout, then + // there is no need to re-adjust the next two fields + // since the defaults will be used. + maxNumberOfTimeoutTicksBeforeHeuristicDecision_ = timeout / DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS; + maxNumberOfTimeoutTicksBeforeRollback_ = maxNumberOfTimeoutTicksBeforeHeuristicDecision_; + } + + setStateHandler ( new ActiveStateHandler ( this ) ); + startThreads ( DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS ); + synchronizations = new ArrayList(); + } + + /** + * No argument constructor as required by Recoverable interface. + */ + + public CoordinatorImp () + { + + initFsm(TxState.ACTIVE ); + + single_threaded_2pc_ = false; + synchronizations = new ArrayList(); + + } + + + + boolean prefersSingleThreaded2PC() + { + return single_threaded_2pc_; + } + + /** + * Mark the tx as committed. Needed for testing. + */ + + void setCommitted () + { + stateHandler_.setCommitted (); + } + + /** + * Set the state handler. This method should always be preferred over + * calling setState directly. + * + * @param stateHandler + * The next state handler. + */ + + void setStateHandler ( CoordinatorStateHandler stateHandler ) + { + // NB: if this method is synchronized then deadlock happens on heuristic mixed! + TxState state = stateHandler.getState (); + stateHandler_ = stateHandler; + setState ( state ); + } + + + RecoveryCoordinator getSuperiorRecoveryCoordinator () + { + return superiorCoordinator_; + } + + public Vector getParticipants () + { + return participants_; + } + + + int getLocalSiblingCount () + { + return localSiblingsStarted; + } + + long getMaxIndoubtTicks () + { + return maxNumberOfTimeoutTicksBeforeHeuristicDecision_; + } + + long getMaxRollbackTicks () + { + return maxNumberOfTimeoutTicksBeforeRollback_; + } + + + /** + * Tests if the transaction was committed or not. + * + * @return boolean True iff committed. + */ + + public boolean isCommitted () + { + return stateHandler_.isCommitted (); + } + + /** + * Start threads, propagator and timer logic. Needed on construction AND by + * replay request events: timers have stopped by then! + * + * @param timeout + * The timeout for the thread wakeup interval. + * @param console + * The console, null if none. + */ + + private void startThreads ( long timeout ) + { + synchronized ( fsm_ ) { + if ( timer_ == null ) { //not null for repeated recovery + stateHandler_.activate (); + timer_ = new PooledAlarmTimer(timeout); + timer_.addAlarmTimerListener(this); + submitTimer(timer_); + } + } + + } + + + private void submitTimer(AlarmTimer timer) { + TaskManager.SINGLETON.executeTask (timer); + } + + protected long getTimeOut () + { + return (maxNumberOfTimeoutTicksBeforeRollback_ - stateHandler_.getRollbackTicks ()) + * DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS; + } + + + void setState ( TxState state ) throws IllegalStateException + { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Coordinator " + getCoordinatorId () + + " entering state: " + state.toString () ); + fsm_.setState ( state ); + + } + + /** + * @see Stateful + */ + + public TxState getState () + { + // this method should NOT be synchronized to avoid + // recursive 2PC deadlocks! + return fsm_.getState (); + } + + + /** + * @see FSMEnterEventSource. + */ + + public void addFSMEnterListener ( FSMEnterListener l, TxState state ) + { + fsm_.addFSMEnterListener ( l, state ); + + } + + + /** + * @see CompositeCoordinator. + */ + + public RecoveryCoordinator getRecoveryCoordinator () + { + return this; + } + + /** + * @see CompositeCoordinator. + */ + + public Participant getParticipant () throws UnsupportedOperationException + { + return this; + } + + /** + * @see com.atomikos.icatch.CompositeCoordinator. + */ + + public String getCoordinatorId () + { + return coordinatorId; + } + + RecoveryCoordinator addParticipant ( + Participant participant ) throws SysException, + java.lang.IllegalStateException, RollbackException + { + synchronized ( fsm_ ) { + if ( !getState ().equals ( TxState.ACTIVE ) ) + throw new IllegalStateException ( + getCoordinatorId() + + " is no longer active but in state " + + getState ().toString () ); + + //FIRST add participant, THEN set state to support active recovery + if ( !participants_.contains ( participant ) ) { + participants_.add ( participant ); + } + //make sure that aftercompletion notification is done. + setState ( TxState.ACTIVE ); + } + + + return this; + + } + + /** + * Called when a tx import is being done. + */ + + protected void incLocalSiblingsStarted () + { + synchronized ( fsm_ ) { + localSiblingsStarted++; + } + } + + protected void incLocalSiblingsTerminated() throws HeurRollbackException, HeurMixedException, SysException, SecurityException, HeurCommitException, HeurHazardException, IllegalStateException, RollbackException { + synchronized ( fsm_ ) { + localSiblingsTerminated++; + if (hasTimedOut() && !hasActiveSiblings()) { + terminate(false); + } + } + } + + boolean hasTimedOut() { + synchronized ( fsm_ ) { + return timedout; + } + } + + public boolean hasActiveSiblings() { + return localSiblingsStarted > localSiblingsTerminated; + } + + void registerSynchronization ( Synchronization sync ) + throws RollbackException, IllegalStateException, + UnsupportedOperationException, SysException + + { + + synchronized ( fsm_ ) { + if ( !getState ().equals ( TxState.ACTIVE ) ) + throw new IllegalStateException ( "wrong state: " + getState () ); + rememberSychronizationForAfterCompletion(sync); + } + } + + + private void rememberSychronizationForAfterCompletion(Synchronization sync) { + getSynchronizations().add(sync); + } + + private List getSynchronizations() { + synchronized(fsm_) { + if (synchronizations == null) synchronizations = new ArrayList(); + return synchronizations; + } + } + + private List cloneAndReverseSynchronizationsForAfterCompletion() { + List src = getSynchronizations(); + List ret = new ArrayList<>(src.size()); + synchronized(fsm_) { + ret.addAll(src); + Collections.reverse(ret); // cf case 20711 + } + return ret; + } + + void notifySynchronizationsAfterCompletion(TxState... successiveStates) { + for ( TxState state : successiveStates ) { + for (Synchronization s : cloneAndReverseSynchronizationsForAfterCompletion()) { + try { + s.afterCompletion(state); + } catch (Throwable t) { + LOGGER.logWarning("Unexpected error in afterCompletion", t); + } + } + } + } + + /** + * @see FSMEnterListener. + */ + public void preEnter ( FSMEnterEvent event ) throws IllegalStateException + { + TxState state = event.getState (); + if (state.isHeuristic() && requiresHeuristics()) { + //if logcloud: recovery will take care of this so don't publish event + TransactionHeuristicEvent the = new TransactionHeuristicEvent(getCoordinatorId(), superiorCoordinatorId(), state); + EventPublisher.INSTANCE.publish(the); + } + if (state.isFinalStateForOltp()) { + dispose (); + } + + } + + boolean requiresHeuristics() { + boolean ret = false; + if (recoveryDomainName != null) { // can be null for testing + String recoveryDomainName = Configuration.getConfigProperties().getTmUniqueName(); + PendingTransactionRecord record = getPendingTransactionRecord(getState()); + if (record != null) { + ret = record.allowsHeuristicTermination(recoveryDomainName); + } + } + return ret; + } + + /** + * @see Participant + */ + + public String getURI () + { + return getCoordinatorId (); + } + + /** + * @see Participant. + */ + + public void forget () + { + stateHandler_.forget (); + } + + /** + * @see Participant. + */ + + public void setCascadeList ( Map allParticipants ) + throws SysException + { + stateHandler_.setCascadeList ( allParticipants ); + } + + /** + * @see Participant. + */ + + public void setGlobalSiblingCount ( int count ) + { + stateHandler_.setGlobalSiblingCount ( count ); + } + + /** + * @see Participant. + */ + + public int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + // FIRST, TAKE CARE OF DUPLICATE PREPARES + + // Recursive prepare-calls should be avoided for not deadlocking rollback/commit methods + // If a recursive prepare re-enters, then it will see a voting state -> reject. + // Note that this may also avoid some legal prepares, but only rarely + if ( getState ().equals ( TxState.PREPARING ) ) + throw new RollbackException ( "Recursion detected" ); + + int ret = Participant.READ_ONLY + 1; + synchronized ( fsm_ ) { + ret = stateHandler_.prepare (); + if ( ret == Participant.READ_ONLY ) { + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "prepare() of Coordinator " + getCoordinatorId () + + " returning READONLY" ); + } else { + + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "prepare() of Coordinator " + getCoordinatorId () + + " returning YES vote"); + } + } + return ret; + + } + + /** + * @see Participant. + */ + + public void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + synchronized ( fsm_ ) { + stateHandler_.commit(onePhase); + } + } + + /** + * @see Participant. + */ + + public void rollback () throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + java.lang.IllegalStateException + { + + if ( getState ().equals ( TxState.ABORTING ) ) { + // this method is ONLY called for EXTERNAL events -> by remote coordinators + // therefore, state aborting means either a recursive + // call or a concurrent rollback by two different coordinators. + // Recursion can be detected by this state, because the + // original call will still be in its propagation phase, + // where the state is set to ABORTING. + // Returning immediately will make sure no + // deadlock happens during 2PC, especially for recursion! + return; + } + + // here, we are certain that no RECURSIVE call is going on, + // so we can safely lock this instance. + + synchronized ( fsm_ ) { + stateHandler_.rollback(); + } + } + + + void rollbackHeuristically () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException + { + synchronized ( fsm_ ) { + stateHandler_.rollbackHeuristically(); + } + } + + void commitHeuristically () throws HeurMixedException, + SysException, HeurRollbackException, HeurHazardException, + java.lang.IllegalStateException, RollbackException + { + synchronized ( fsm_ ) { + stateHandler_.commitHeuristically(); + } + } + + + /** + * @see RecoveryCoordinator. + */ + + public Boolean replayCompletion ( Participant participant ) + throws IllegalStateException + { + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("replayCompletion ( " + participant + + " ) received by coordinator " + getCoordinatorId () + + " for participant " + participant.toString ()); + } + Boolean ret = null; + synchronized ( fsm_ ) { + ret = stateHandler_.replayCompletion ( participant ); + } + return ret; + } + + + private boolean excludedFromLogging(TxState state) { + boolean ret = false; + if (!state.isRecoverableState() ) { + ret = true; + } else if ( superiorCoordinator_ == null) { + if ( state.equals( TxState.IN_DOUBT )) { + ret = true; //see case 23693: don't log prepared state for roots + } else if ( participants_.isEmpty() ) { + ret = true; //see case 84851: avoid logging overhead for empty transactions + } + } + + if (state.isHeuristic()) { + //new recovery: don't log heuristics - let recovery clean them up + ret = true; + } + + return ret; + } + + + public void alarm ( AlarmTimer timer ) + { + try { + stateHandler_.onTimeout (); + } catch ( Exception e ) { + LOGGER.logWarning( "Exception on timeout of coordinator " + root_ , e ); + } + } + + protected void dispose () + { + synchronized ( fsm_ ) { + if ( timer_ != null ) { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Coordinator " + getCoordinatorId() + " : stopping timer..." ); + timer_.stopTimer (); + } + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Coordinator " + getCoordinatorId() + " : disposing statehandler " + stateHandler_.getState() + "..." ); + stateHandler_.dispose (); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Coordinator " + getCoordinatorId() + " : disposed." ); + } + } + + /** + * Terminate the work, on behalf of Terminator. + * + * @param commit + * True iff commit termination is asked. + */ + + protected void terminate ( boolean commit ) throws HeurRollbackException, + HeurMixedException, SysException, java.lang.SecurityException, + HeurCommitException, HeurHazardException, RollbackException, + IllegalStateException + + { + synchronized ( fsm_ ) { + if ( commit ) { + if ( participants_.size () <= 1 ) { + commit ( true ); + } else { + int prepareResult = prepare (); + // make sure to only do commit if NOT read only + if ( prepareResult != Participant.READ_ONLY ) + commit ( false ); + } + } else { + rollback (); + } + } + } + + void setRollbackOnly() { + + RollbackOnlyParticipant p = new RollbackOnlyParticipant ( ); + + try { + addParticipant ( p ); + } catch ( IllegalStateException alreadyTerminated ) { + //happens in rollback after timeout - see case 27857; ignore but log + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Error during setRollbackOnly" , alreadyTerminated ); + } catch ( RollbackException e ) { + //ignore: corresponds to desired outcome + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Error during setRollbackOnly" , e ); + } + } + + public TxState getStateWithTwoPhaseCommitDecision() { + TxState ret = getState(); + if (TxState.TERMINATED.equals(getState())) { + if (isCommitted()) ret = TxState.COMMITTED; + else ret = TxState.ABORTED; + } else if (TxState.HEUR_ABORTED.equals(getState())) { + ret = TxState.ABORTED; + } else if (TxState.HEUR_COMMITTED.equals(getState())) { + ret = TxState.COMMITTED; + } else if (TxState.HEUR_HAZARD.equals(getState())) { + if (isCommitted()) ret = TxState.COMMITTING; + else ret = TxState.ABORTING; + } + return ret; + } + + + + @Override + public void transitionPerformed(FSMTransitionEvent e) { + //nothing to do in open source + } + + @Override + public PendingTransactionRecord getPendingTransactionRecord(TxState state) { + synchronized ( fsm_ ) { + if ( excludedFromLogging(state)) { + //merely return null to avoid logging overhead + return null; + } + else { + return new PendingTransactionRecord(this.getCoordinatorId(), state, this.getExpires(), recoveryDomainName, superiorCoordinatorId()); + } + } + } + + private String superiorCoordinatorId() { + String ret = null; + if (getSuperiorRecoveryCoordinator()!=null) { + ret = this.getSuperiorRecoveryCoordinator().getURI(); + } + return ret; + } + + + + private long getExpires() { + return System.currentTimeMillis() + getTimeOut(); + } + + + @Override + public String getResourceName() { + return null; + } + + @Override + public String getRootId() { + return root_; + } + + public void timedout(boolean rollbackOnly) { + synchronized ( fsm_ ) { + timedout = true; + if (rollbackOnly) { + setRollbackOnly(); + } + } + + } + + public boolean isRoot() { + return superiorCoordinator_ == null; + } + + @Override + public String getRecoveryDomainName() { + return recoveryDomainName; + } + + @Override + public void beforeTransition(FSMTransitionEvent e) throws IllegalStateException { + } + + @Override + public void entered(FSMEnterEvent e) { + } + + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/CoordinatorStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/CoordinatorStateHandler.java new file mode 100644 index 000000000..94e1bacf9 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/CoordinatorStateHandler.java @@ -0,0 +1,639 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.Vector; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; +import com.atomikos.thread.InterruptedExceptionHelper; + +/** + * Application of the state pattern to the transaction coordinator: each + * important state has a handler and this class is the superclass that holds + * common logic. + * + * Note: this class and it subclasses should not use synchronized blocks; + * the coordinator (owner) class is responsible for synchronizing access to + * this class. + */ + +abstract class CoordinatorStateHandler +{ + + private static final Logger LOGGER = LoggerFactory.createLogger(CoordinatorStateHandler.class); + + private final CoordinatorImp coordinator_; + // the coordinator instance whose state we represent + + private Set readOnlyTable_; + // a hash table that keeps track of which participants are readonly + // needed on prepare, commit and rollback + + private Propagator propagator_; + // The propagator for propagation of messages + + private final Stack replayStack_; + // where replay requests are queued + + private Boolean committed_; + // True iff commit, False iff rollback, otherwise null + + private Map cascadeList_; + // The participants to cascade prepare to + + + /** + * Creates a new instance. + * + * @param coordinator + * The coordinator to represent. + * + */ + protected CoordinatorStateHandler ( CoordinatorImp coordinator ) + { + coordinator_ = coordinator; + replayStack_ = new Stack(); + readOnlyTable_ = new HashSet (); + committed_ = null; + } + + /** + * For use in this class or subclasses only. This constructor creates a new + * instance based on a previous state handler's attributes. In this case, + * activate or recover should NOT be called! + * + * @param other + * The previous instance whose attributes should be used. + */ + + protected CoordinatorStateHandler ( CoordinatorStateHandler other ) + { + coordinator_ = other.coordinator_; + propagator_ = other.propagator_; + replayStack_ = other.replayStack_; + readOnlyTable_ = other.readOnlyTable_; + committed_ = other.committed_; + cascadeList_ = other.cascadeList_; + } + + /** + * For testing only. + */ + + void setCommitted () + { + committed_ = Boolean.TRUE; + } + + /** + * Get the coordinator whose state we handle. + * + * @return CoordinatorImp The coordinator. + */ + + protected CoordinatorImp getCoordinator () + { + return coordinator_; + } + + protected long getRollbackTicks () + { + return 0; + } + + /** + * Get the replay stack for replay completion requests. + * + * @return Stack The stack with replay requests, or an empty stack if none + * are present. + */ + + protected Stack getReplayStack () + { + return replayStack_; + } + + /** + * Get the cascade list. + * + * @return Map The cascade list. + */ + + protected Map getCascadeList () + { + return cascadeList_; + } + + /** + * Get the propagator for sending messages in the subclasses. + * + * @return Propagator The propagator. + */ + + protected Propagator getPropagator () + { + return propagator_; + } + + /** + * Test if the result was commit. + * + * @return Boolean Null if not known yet, True if commit, False if rollback. + */ + + protected Boolean getCommitted () + { + return committed_; + } + + /** + * Tests if commit has happened. + * + * @return boolean True iff commit happened. + */ + + protected boolean isCommitted () + { + if ( committed_ == null ) + return false; + else + return committed_.booleanValue (); + } + + + + /** + * Sets the table of readonly participants. + * + * @param table + * The table. + */ + + protected void setReadOnlyTable ( Set table ) + { + readOnlyTable_ = table; + } + + /** + * Start the threads. This method should be called when the state handler + * should start being active, as the first method for recovered instances or + * when the constructor without a propagator argument is called. + */ + + protected void activate () + { + boolean threaded = !coordinator_.prefersSingleThreaded2PC(); + if ( propagator_ == null ) + propagator_ = new Propagator ( threaded ); + } + + /** + * Notification of shutdown; this method triggers the stopping of all active + * threads for propagation. + */ + + protected void dispose () + { + propagator_ = null; + } + + /** + * Handle a replay request for a participant. This method makes the + * participant eligible for replay on the next timer event, but does nothing + * else. Subclasses should take care of checking preconditions! + * + * @return Boolean Indication of the termination decision, null if not known yet. + */ + + protected Boolean replayCompletion ( Participant participant ) + throws IllegalStateException + { + if ( !replayStack_.contains ( participant ) ) { + // check needed to be idempotent + replayStack_.push ( participant ); + } + return committed_; + } + + /** + * Utility method for subclasses. + * + * @param participants + */ + protected void addAllForReplay ( Collection participants ) + { + Iterator it = participants.iterator(); + while ( it.hasNext() ) { + Participant p = it.next(); + replayCompletion ( p ); + } + } + + /** + * The corresponding 2PC method is delegated hereto. + */ + + protected void setCascadeList ( Map allParticipants ) + { + cascadeList_ = allParticipants; + } + + /** + * Callback method on timeout event of the coordinator. The interpretation + * of timeout will typically be different for each state handler; some may + * rollback while others need to inquire about completion. This method + * should also check any replay requests. + */ + + protected abstract void onTimeout (); + + /** + * Get the (non-pseudo) coordinator state to which this handler belongs. + * + * @return Object The object that represents the corresponding coordinator + * state. + */ + + abstract TxState getState (); + + /** + * The corresponding 2PC method is delegated hereto. + */ + + abstract void setGlobalSiblingCount ( int count ); + + /** + * The corresponding 2PC method is delegated hereto. + */ + + protected abstract int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException; + + /** + * The corresponding 2PC method is delegated hereto. Subclasses should + * override this, and may use the auxiliary commit method provided by this + * class (in addition to their state-specific preconditions). + * + */ + + protected abstract void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException; + + /** + * The corresponding 2PC method is delegated hereto. Subclasses should + * override this, and may use the auxiliary rollback method provided by this + * class (in addition to their state-specific preconditions). + */ + + protected abstract void rollback () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException; + + /** + * Auxiliary method for committing. This method can be reused in subclasses + * in order to process commit. + * + * @param heuristic + * True iff a heuristic commit should be done. + * @param onePhase + * True iff one-phase commit. + */ + + protected void commitFromWithinCallback ( boolean heuristic , + boolean onePhase ) throws HeurRollbackException, + HeurMixedException, HeurHazardException, + java.lang.IllegalStateException, RollbackException, SysException + { + CoordinatorStateHandler nextStateHandler = null; + + try { + + Vector participants = coordinator_.getParticipants(); + int count = (participants.size () - readOnlyTable_.size ()); + TerminationResult commitresult = new TerminationResult ( count ); + + // cf bug 64546: avoid committed_ being null upon recovery! + committed_ = Boolean.TRUE; + // for replaying completion: commit decision was reached + // otherwise, replay requests might only see TERMINATED! + + try { + coordinator_.setState ( TxState.COMMITTING ); + } catch ( RuntimeException error ) { + //happens if interleaving recovery has done rollback, or if disk is full and log cannot be written + String msg = "Error in committing: " + error.getMessage() + " - recovery will clean up in the background"; + LOGGER.logWarning ( msg , error ); + throw new RollbackException ( msg , error ); + } + + + // start messages + Enumeration enumm = participants.elements (); + while ( enumm.hasMoreElements () ) { + Participant p = enumm.nextElement (); + if ( !readOnlyTable_.contains ( p ) ) { + CommitMessage cm = new CommitMessage ( p, commitresult, + onePhase ); + + // if onephase: set cascadelist anyway, because if the + // participant is a REMOTE one, then it might have + // multiple participants that are not visible here! + + if ( onePhase && cascadeList_ != null ) { + Integer sibnum = cascadeList_.get ( p.getURI() ); + if ( sibnum != null ) // null for local participant! + p.setGlobalSiblingCount ( sibnum.intValue () ); + p.setCascadeList ( cascadeList_ ); + } + propagator_.submitPropagationMessage ( cm ); + } + } // while + + commitresult.waitForReplies (); + int res = commitresult.getResult (); + + if ( res != TerminationResult.ALL_OK ) { + + if ( res == TerminationResult.HEUR_MIXED ) { + Set hazards = commitresult.getPossiblyIndoubts (); + nextStateHandler = new HeurMixedStateHandler ( this, hazards ); + coordinator_.setStateHandler ( nextStateHandler ); + throw new HeurMixedException(); + } + + else if ( res == TerminationResult.ROLLBACK ) { + // 1PC and rolled back before commit arrived. + nextStateHandler = new TerminatedStateHandler ( this ); + coordinator_.setStateHandler ( nextStateHandler ); + Exception cause = commitresult.findFirstOriginalException(); + if (cause != null) { + throw new RollbackException ( "Rolled back already.", cause); + } else { + throw new RollbackException ( "Rolled back already." ); + } + } else if ( res == TerminationResult.HEUR_ROLLBACK ) { + nextStateHandler = new HeurAbortedStateHandler ( this ); + coordinator_.setStateHandler ( nextStateHandler ); + // Here, we do NOT need to add extra information, since ALL + // participants agreed to rollback. + // Therefore, we need not worry about who aborted and who committed. + throw new HeurRollbackException(); + + } + + else if ( res == TerminationResult.HEUR_HAZARD ) { + Set hazards = commitresult.getPossiblyIndoubts (); + nextStateHandler = new HeurHazardStateHandler ( this, hazards ); + coordinator_.setStateHandler ( nextStateHandler ); + throw new HeurHazardException(); + } + + } else { + // all OK + if ( heuristic ) { + nextStateHandler = new HeurCommittedStateHandler ( this ); + // again, here we do NOT need to preserve extra per-participant + // state mappings, since ALL participants were heur. committed. + } else + nextStateHandler = new TerminatedStateHandler ( this ); + + coordinator_.setStateHandler ( nextStateHandler ); + } + } catch ( RuntimeException runerr ) { + throw new SysException ( "Error in commit: " + runerr.getMessage (), runerr ); + } + + catch ( InterruptedException intr ) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( intr ); + throw new SysException ( "Error in commit" + intr.getMessage (), intr ); + } + } + + + /** + * Auxiliary method for rollback. This method can be reused in subclasses in + * order to process rollback. + * + * @param indoubt + * True iff some participants may already have voted YES. + * @param heuristic + * True iff a heuristic rollback should be done. + */ + + protected void rollbackFromWithinCallback ( boolean indoubt , + boolean heuristic ) throws HeurCommitException, HeurMixedException, + SysException, HeurHazardException, java.lang.IllegalStateException + { + + CoordinatorStateHandler nextStateHandler = null; + try { + + coordinator_.setState ( TxState.ABORTING ); + + // mark decision for replay requests; since these might only + // see TERMINATED state! + committed_ = new Boolean ( false ); + + Vector participants = coordinator_.getParticipants (); + int count = (participants.size () - readOnlyTable_.size ()); + + TerminationResult rollbackresult = new TerminationResult ( count ); + + Enumeration enumm = participants.elements (); + while ( enumm.hasMoreElements () ) { + Participant p = enumm.nextElement (); + if ( !readOnlyTable_.contains ( p ) ) { + RollbackMessage rm = new RollbackMessage ( p, + rollbackresult, indoubt ); + propagator_.submitPropagationMessage ( rm ); + } + } + + rollbackresult.waitForReplies (); + int res = rollbackresult.getResult (); + + // check results, but we only care if we are indoubt. + // otherwise, we don't mind any remaining indoubts. + if ( indoubt && res != TerminationResult.ALL_OK ) { + if ( res == TerminationResult.HEUR_MIXED ) { + Set hazards = rollbackresult.getPossiblyIndoubts (); + nextStateHandler = new HeurMixedStateHandler ( this, hazards ); + coordinator_.setStateHandler ( nextStateHandler ); + throw new HeurMixedException(); + } else if ( res == TerminationResult.HEUR_COMMIT ) { + nextStateHandler = new HeurCommittedStateHandler ( this ); + coordinator_.setStateHandler ( nextStateHandler ); + // NO extra per-participant state mappings, since ALL + // participants are heuristically committed. + throw new HeurCommitException(); + } else if ( res == TerminationResult.HEUR_HAZARD ) { + Set hazards = rollbackresult.getPossiblyIndoubts (); + nextStateHandler = new HeurHazardStateHandler ( this, hazards ); + coordinator_.setStateHandler ( nextStateHandler ); + throw new HeurHazardException(); + } + } + + else { + // all answers OK + if ( heuristic ) { + nextStateHandler = new HeurAbortedStateHandler ( this ); + // NO per-participant state mapping needed, since ALL agree + // on same heuristic outcome. + } else + nextStateHandler = new TerminatedStateHandler ( this ); + + coordinator_.setStateHandler ( nextStateHandler ); + } + + } + + catch ( RuntimeException runerr ) { + throw new SysException ( "Error in rollback: " + runerr.getMessage (), runerr ); + } + + catch ( InterruptedException e ) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( e ); + throw new SysException ( "Error in rollback: " + e.getMessage (), e ); + } + } + + protected void forget () + { + // NOTE: no need to add synchronized -> don't + // do it, you never know if recursion happens here + + // NOTE: this is of secondary importance; failures are not + // problematic since forget is mainly for log efficiency. + // Therefore, this does not affect the final TERMINATED state + // NOTE: remote participants are notified as well, but they + // are themselves responsible for deciding whether or not + // to react to the forget notification. + + CoordinatorStateHandler nextStateHandler = null; + + Vector participants = coordinator_.getParticipants (); + int count = (participants.size () - readOnlyTable_.size ()); + Enumeration enumm = participants.elements (); + ForgetResult result = new ForgetResult ( count ); + while ( enumm.hasMoreElements () ) { + Participant p = (Participant) enumm.nextElement (); + if ( !readOnlyTable_.contains ( p ) ) { + ForgetMessage fm = new ForgetMessage ( p, result ); + propagator_.submitPropagationMessage ( fm ); + } + + } + try { + result.waitForReplies (); + } catch ( InterruptedException inter ) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( inter ); + // some might be left in heuristic state -- that's OK. + } + nextStateHandler = new TerminatedStateHandler ( this ); + coordinator_.setStateHandler ( nextStateHandler ); + } + + public void rollbackWithAfterCompletionNotification(RollbackCallback cb) throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + java.lang.IllegalStateException { + try { + cb.doRollback(); + coordinator_.notifySynchronizationsAfterCompletion(TxState.ABORTING, TxState.TERMINATED); + } catch (HeurCommitException hc) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.COMMITTING, TxState.TERMINATED); + throw hc; + } catch (HeurMixedException hm) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.ABORTING, TxState.TERMINATED); + throw hm; + } catch (HeurHazardException hh) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.ABORTING,TxState.TERMINATED); + throw hh; + } + } + + void commitWithAfterCompletionNotification(CommitCallback cb) throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException { + try { + cb.doCommit(); + coordinator_.notifySynchronizationsAfterCompletion(TxState.COMMITTING,TxState.TERMINATED); + } catch (RollbackException rb) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.ABORTING,TxState.TERMINATED); + throw rb; + } catch (HeurMixedException hm) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.COMMITTING,TxState.TERMINATED); + throw hm; + } catch (HeurHazardException hh) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.COMMITTING,TxState.TERMINATED); + throw hh; + } catch (HeurRollbackException hr) { + coordinator_.notifySynchronizationsAfterCompletion(TxState.ABORTING,TxState.TERMINATED); + throw hr; + } + } + + protected void notifySynchronizationsAfterCompletion(TxState... successiveStates) { + coordinator_.notifySynchronizationsAfterCompletion(successiveStates); + } + + public void rollbackHeuristically () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException + { + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + IllegalStateException { + rollbackFromWithinCallback(true, true); + } + }); + } + + public void commitHeuristically () throws HeurMixedException, + SysException, HeurRollbackException, HeurHazardException, + java.lang.IllegalStateException, RollbackException + { + commitWithAfterCompletionNotification(new CommitCallback() { + public void doCommit() throws HeurRollbackException, + HeurMixedException, HeurHazardException, IllegalStateException, + RollbackException, SysException { + commitFromWithinCallback(true,false); + } + }); + } + + protected void removePendingOltpCoordinatorFromTransactionService() { + LOGGER.logDebug("Abandoning "+getCoordinator().getCoordinatorId()+" in state "+getState()+" after timeout - recovery will cleanup in the background"); + getCoordinator().setState(TxState.ABANDONED); + getCoordinator().dispose(); //needed to stop all threads - see case 189264 + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/ForgetMessage.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/ForgetMessage.java new file mode 100644 index 000000000..1dfe2207e --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/ForgetMessage.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.Participant; + +/** + * A forget message implementation. + */ + +class ForgetMessage extends PropagationMessage +{ + + ForgetMessage ( Participant p , ForgetResult result ) + { + super ( p , result ); + } + + /** + * A forget message. + * + * @return Object The participant to whom this was sent. + * @exception PropagationException + * Never returned; we don't care now. + */ + + protected Object send () throws PropagationException + { + try { + Participant part = getParticipant (); + part.forget (); + + } catch ( Exception e ) { + } + + return getParticipant (); + } + + public String toString () + { + return ("ForgetMessage to " + getParticipant ()); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/ForgetResult.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/ForgetResult.java new file mode 100644 index 000000000..fe04a4778 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/ForgetResult.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +class ForgetResult extends Result +{ + + public ForgetResult ( int numberOfRepliesToWaitFor ) + { + super ( numberOfRepliesToWaitFor ); + + } + + protected synchronized void calculateResultFromAllReplies() throws IllegalStateException, + InterruptedException + + { + // nothing to do here + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurAbortedStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurAbortedStateHandler.java new file mode 100644 index 000000000..823e8143a --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurAbortedStateHandler.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.recovery.TxState; + +/** + * A state handler for the heuristic abort coordinator state. + */ + +class HeurAbortedStateHandler extends CoordinatorStateHandler +{ + + private long timeoutTicks = 0; + private long maxTimeoutTicks = 0; + + HeurAbortedStateHandler ( CoordinatorStateHandler previous ) + { + super ( previous ); + this.maxTimeoutTicks = Configuration.getConfigProperties().getMaxTimeout() / CoordinatorImp.DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS + 1; + } + + protected TxState getState () + { + return TxState.HEUR_ABORTED; + } + + protected void onTimeout () + { + if (timeoutTicks < maxTimeoutTicks) { + //stay around for a while so incoming commit requests find out about heuristic abort + timeoutTicks++; + } else { + removePendingOltpCoordinatorFromTransactionService(); + } + } + + protected void setGlobalSiblingCount ( int count ) + { + // nothing to do here + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + + throw new HeurHazardException(); + } + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + + throw new HeurRollbackException(); + } + + protected void rollback () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException + { + + // if global rollback coincides with heuristic outcome -> terminated + TerminatedStateHandler termStateHandler = new TerminatedStateHandler ( + this ); + getCoordinator ().setStateHandler ( termStateHandler ); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurCommittedStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurCommittedStateHandler.java new file mode 100644 index 000000000..cf9c3f1b8 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurCommittedStateHandler.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.recovery.TxState; + +/** + * A state handler for the heuristic committed coordinator state. + */ + +class HeurCommittedStateHandler extends CoordinatorStateHandler +{ + private long timeoutTicks = 0; + private long maxTimeoutTicks = 0; + + HeurCommittedStateHandler ( CoordinatorStateHandler previous ) + { + super ( previous ); + this.maxTimeoutTicks = Configuration.getConfigProperties().getMaxTimeout() / CoordinatorImp.DEFAULT_MILLIS_BETWEEN_TIMER_WAKEUPS + 1; + + } + + protected TxState getState () + { + return TxState.HEUR_COMMITTED; + } + + protected void onTimeout () + { + if (timeoutTicks < maxTimeoutTicks) { + //stay around for a while so incoming rollback requests find out about heuristic abort + timeoutTicks++; + } else { + removePendingOltpCoordinatorFromTransactionService(); + } + } + + protected void setGlobalSiblingCount ( int count ) + { + // nothing to do here + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + + throw new HeurHazardException (); + } + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + + // heur outcome same as global outcome ->terminated state + TerminatedStateHandler termStateHandler = new TerminatedStateHandler(this); + getCoordinator().setStateHandler ( termStateHandler ); + + } + + protected void rollback () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException + { + + throw new HeurCommitException(); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurHazardStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurHazardStateHandler.java new file mode 100644 index 000000000..00a0af0a8 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurHazardStateHandler.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Enumeration; +import java.util.Set; +import java.util.Stack; +import java.util.Vector; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.recovery.TxState; +import com.atomikos.thread.InterruptedExceptionHelper; + +/** + * A state handler for the heuristic hazard coordinator state. + */ + +class HeurHazardStateHandler extends CoordinatorStateHandler +{ + private Vector hazards_; + private long timeoutTicks = 0; + + HeurHazardStateHandler ( CoordinatorStateHandler previous , + Set hazards ) + { + super ( previous ); + hazards_ = new Vector(hazards); + + } + + protected TxState getState () + { + return TxState.HEUR_HAZARD; + } + + protected void onTimeout () + { + // this state can only be reached through COMMITTING or ABORTING + // so getCommitted can not be null + // or it can be: cf case 72990 + Boolean commitDecided = getCommitted(); + boolean committed = false; + + addAllForReplay ( hazards_ ); + timeoutTicks++; + + // get Stack to avoid overwriting effects of + // intermediate recovery calls + Stack replayStack = getReplayStack (); + if ( !replayStack.empty () && commitDecided != null ) { + committed = commitDecided.booleanValue (); + int count = replayStack.size (); + TerminationResult result = new TerminationResult ( count ); + + while ( !replayStack.empty () ) { + Participant part = replayStack.pop (); + if ( committed ) { + CommitMessage cm = new CommitMessage ( part, result, false ); + getPropagator ().submitPropagationMessage ( cm ); + } else { + RollbackMessage rm = new RollbackMessage ( part, result, + true ); + getPropagator ().submitPropagationMessage ( rm ); + } + } + try { + result.waitForReplies (); + Stack replies = result.getReplies (); + Enumeration enumm = replies.elements (); + while ( enumm.hasMoreElements () ) { + Reply reply = enumm.nextElement (); + + if ( !reply.hasFailed () ) { + hazards_.remove ( reply.getParticipant () ); + } + } + // TODO if overall result failed: check if heuristic state + // should change? + // for instance: if mixed replies -> change state to HEURMIXED + // NOTE: this can happen on recovery with late registration + // where the resource has ended in mixed mode + + + } catch ( InterruptedException inter ) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( inter ); + // return silently; + // worst case is some remaining indoubt participants + } + + } + + if ( hazards_.isEmpty () ) { + TerminatedStateHandler termStateHandler = new TerminatedStateHandler ( + this ); + getCoordinator ().setStateHandler ( termStateHandler ); + } + + if (timeoutTicks > getCoordinator().getMaxIndoubtTicks()) { //we give up after a while + removePendingOltpCoordinatorFromTransactionService(); + } + } + + protected void setGlobalSiblingCount ( int count ) + { + // nothing to do here + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + + throw new HeurHazardException(); + } + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + + throw new HeurHazardException(); + } + + protected void rollback () throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + java.lang.IllegalStateException + { + + throw new HeurHazardException(); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurMixedStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurMixedStateHandler.java new file mode 100644 index 000000000..bf37fff4b --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/HeurMixedStateHandler.java @@ -0,0 +1,146 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.recovery.TxState; +import com.atomikos.thread.InterruptedExceptionHelper; + +/** + * A state handler for the heuristic mixed coordinator state. + */ + +class HeurMixedStateHandler extends CoordinatorStateHandler +{ + + private Set hazards_; + private long timeoutTicks = 0; + + HeurMixedStateHandler ( CoordinatorStateHandler previous , Set hazards ) + { + super ( previous ); + hazards_ = new HashSet(hazards); + } + + protected TxState getState () + { + return TxState.HEUR_MIXED; + } + + protected void onTimeout () + { + timeoutTicks++; + // this state can only be reached through COMMITTING or ABORTING + // so getCommitted can not be null + Boolean commitDecided = getCommitted(); + + //replay does remove -> re-add hazards each time + addAllForReplay ( hazards_ ); + + Stack replayStack = getReplayStack (); + if ( !replayStack.empty () && commitDecided != null ) { + boolean committed = commitDecided.booleanValue (); + int count = replayStack.size (); + TerminationResult result = new TerminationResult ( count ); + + while ( !replayStack.empty () ) { + Participant part = replayStack.pop (); + if ( committed ) { + CommitMessage cm = new CommitMessage ( part, result, false ); + getPropagator ().submitPropagationMessage ( cm ); + } else { + RollbackMessage rm = new RollbackMessage ( part, result, + true ); + getPropagator ().submitPropagationMessage ( rm ); + } + } + try { + result.waitForReplies (); + + // remove OK replies from hazards_ list and change state if + // hazard_ is empty. + + Stack replies = result.getReplies (); + + Enumeration enumm = replies.elements (); + while ( enumm.hasMoreElements () ) { + Reply reply = enumm.nextElement (); + + if ( !reply.hasFailed () ) { + hazards_.remove ( reply.getParticipant () ); + } + } + + if ( hazards_.isEmpty () ) { + TerminatedStateHandler termStateHandler = new TerminatedStateHandler ( + this ); + getCoordinator().setStateHandler(termStateHandler); + } + + } catch ( InterruptedException inter ) { + // cf bug 67457 + InterruptedExceptionHelper.handleInterruptedException ( inter ); + // return silently; + // worst case is some remaining indoubt participants + } + + } + if (timeoutTicks > getCoordinator().getMaxIndoubtTicks()) { //we give up after a while + removePendingOltpCoordinatorFromTransactionService(); + } + } + + protected void setGlobalSiblingCount ( int count ) + { + // nothing to do here + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + + // check heuristic state: during prepare, there can be no global commit + // decision yet. + // therefore, this prepare call is NOT allowed to return anything else + // then a + // heuristic hazard exception. + // thus, no matter what the heuristic really is, report it as hazard. + + throw new HeurHazardException(); + } + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + + throw new HeurMixedException(); + } + + protected void rollback () throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + java.lang.IllegalStateException + { + + throw new HeurMixedException(); + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/IndoubtStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/IndoubtStateHandler.java new file mode 100644 index 000000000..71f69d134 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/IndoubtStateHandler.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * A state handler for the indoubt coordinator state. + */ + +class IndoubtStateHandler extends CoordinatorStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(IndoubtStateHandler.class); + + private long timeoutTicks = 0; + + IndoubtStateHandler ( ActiveStateHandler previous ) + { + super ( previous ); + timeoutTicks = previous.getRollbackTicks(); + } + + protected TxState getState () + { + return TxState.IN_DOUBT; + } + + protected void onTimeout () + { + // first check if we are still the current state! + // otherwise, a COMMITTING tx could be rolled back if it + // times out in between (i.e. a commit can come in while + // this state handler gets a timeout event and rolls back) + if ( !getCoordinator().getState().equals(getState())) { + return; + } + + if (timeoutTicks < getCoordinator().getMaxIndoubtTicks()) { + timeoutTicks++; + } else { + try { + if (getCoordinator().requiresHeuristics()) { + //local recovery will automatically do presumed abort after max_timeout + //for a root this is OK but for imported transactions this means a heuristic + LOGGER.logWarning ( "Transaction " + getCoordinator().getCoordinatorId() + " has timed out - performing heuristic rollback. See https://www.atomikos.com/Documentation/HeuristicExceptions for more details or try https://www.atomikos.com/Main/ExtremeTransactions for self-healing recovery."); + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + rollbackFromWithinCallback(true, true); + }}); + } else { + //no heuristics => pending coordinator after failed commit or rollback: + //cleanup to remove from TransactionServiceImp and let recovery work in the background + removePendingOltpCoordinatorFromTransactionService(); + } + + } catch ( Exception e ) { + LOGGER.logWarning("Error in timeout of INDOUBT state: " + e.getMessage () ); + } + } + + } + + + + protected void setGlobalSiblingCount ( int count ) + { + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + // if we have a repeated prepare in this state, then the + // first prepare must have been a YES vote, otherwise we + // would not be in-doubt! -> repeat the same vote + // (required for WS-AT) + return Participant.READ_ONLY + 1; + } + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + + commitWithAfterCompletionNotification ( new CommitCallback() { + public void doCommit() + throws HeurRollbackException, HeurMixedException, + HeurHazardException, IllegalStateException, + RollbackException, SysException { + commitFromWithinCallback ( false, false ); + } + }); + + } + + protected void rollback () + throws HeurCommitException, HeurMixedException, SysException, + HeurHazardException, java.lang.IllegalStateException + { + rollbackWithAfterCompletionNotification(new RollbackCallback() { + public void doRollback() + throws HeurCommitException, + HeurMixedException, SysException, + HeurHazardException, IllegalStateException { + rollbackFromWithinCallback(true,false); + }}); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/PrepareMessage.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/PrepareMessage.java new file mode 100644 index 000000000..037cd80f5 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/PrepareMessage.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; + +/** + * A prepare message implementation. + */ + +class PrepareMessage extends PropagationMessage +{ + + public PrepareMessage ( Participant participant , Result result ) + { + super ( participant , result ); + } + + /** + * A prepare message. + * + * @return Boolean True if YES vote, False if NO vote, null if + * readonly vote. + */ + + protected Boolean send () throws PropagationException + { + Participant part = getParticipant (); + int ret = 0; + Boolean result = null; + try { + ret = part.prepare (); + if ( ret == Participant.READ_ONLY ) + result = null; + else + result = new Boolean ( true ); + } catch ( HeurHazardException heurh ) { + throw new PropagationException ( heurh, false ); + } catch ( RollbackException jtr ) { + // NO vote. + result = new Boolean ( false ); + } catch ( Exception e ) { + // here, participant might be indoubt! + HeurHazardException heurh = new HeurHazardException (); + throw new PropagationException ( heurh, false ); + + } + return result; + } + + public String toString () + { + return ("PrepareMessage to " + getParticipant ()); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/PrepareResult.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/PrepareResult.java new file mode 100644 index 000000000..be4a8bdb3 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/PrepareResult.java @@ -0,0 +1,164 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.Participant; + +/** + * A result for prepare messages. + */ + +class PrepareResult extends Result +{ + + private Set readonlytable_ = new HashSet (); + // for read only voters + private Set indoubts_ = new HashSet(); + // for indoubt participants + // should be rolled back in case of failure! + + private boolean analyzed_ = false; + + /** + * Constructor. + * + * @param count + * The number of replies to deal with. + */ + + public PrepareResult ( int count ) + { + super ( count ); + } + + protected synchronized void calculateResultFromAllReplies () throws IllegalStateException, + InterruptedException + { + if ( analyzed_ ) + return; + + boolean allReadOnly = true; + boolean allYes = true; + boolean heurmixed = false; + boolean heurhazards = false; + boolean heurcommits = false; + Stack replies = getReplies (); + Enumeration enumm = replies.elements (); + + while ( enumm.hasMoreElements () ) { + boolean yes = false; + boolean readonly = false; + + Reply reply = (Reply) enumm.nextElement (); + + if ( reply.hasFailed () ) { + yes = false; + readonly = false; + + Exception err = reply.getException (); + if ( err instanceof HeurMixedException ) { + heurmixed = true; + } else if ( err instanceof HeurCommitException ) { + heurcommits = true; + heurmixed = (heurmixed || heurhazards); + } else if ( err instanceof HeurHazardException ) { + heurhazards = true; + heurmixed = (heurmixed || heurcommits); + indoubts_.add ( reply.getParticipant ()); + // REMEMBER: might be indoubt, so HAS to be notified + // during rollback! + } + + }// if failed + + else { + readonly = (reply.getResponse () == null); + Boolean answer = new Boolean ( false ); + if ( !readonly ) { + answer = (Boolean) reply.getResponse (); + } + yes = (readonly || answer.booleanValue ()); + + // if readonly: remember this fact for logging and second phase + if ( readonly ) readonlytable_.add ( reply.getParticipant () ); + else indoubts_.add ( reply.getParticipant ()); + } + + allYes = (allYes && yes); + allReadOnly = (allReadOnly && readonly); + + } + + if ( heurmixed ) + result_ = HEUR_MIXED; + else if ( heurcommits ) + result_ = HEUR_COMMIT; + else if ( heurhazards ) + result_ = HEUR_HAZARD; + else if ( allReadOnly ) + result_ = ALL_READONLY; + else if ( allYes ) + result_ = ALL_OK; + + analyzed_ = true; + } + + /** + * Test if all answers represent a yes vote. Blocks until all results + * arrived. + * + * @return boolean True if all are yes, false if not. + * @exception InterruptedException + * If interrupt on wait. + */ + + public boolean allYes () throws InterruptedException + { + calculateResultFromAllReplies (); + return (result_ == ALL_OK || result_ == ALL_READONLY); + + } + + /** + * Test if all answers were readonly votes. Blocks till all results known. + * + * @return boolean True if all readonly, false otherwise. + * @exception InterruptedException + * If interrupted. + */ + + public boolean allReadOnly () throws InterruptedException + { + calculateResultFromAllReplies (); + return (result_ == ALL_READONLY); + } + + /** + * Get a table of readonly voting participants. + * + * @return Set Contains readonly participant. + * @exception InterruptedException + * If interrupted on wait. + */ + + public Set getReadOnlyTable () throws InterruptedException + { + calculateResultFromAllReplies (); + return readonlytable_; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/PropagationException.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/PropagationException.java new file mode 100644 index 000000000..b0b1aa4aa --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/PropagationException.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + + +/** + * + * + * An error of propagation messages. Some errors are transient, leading to a + * retry of the message send. Others are fatal, and reported back to the sender. + */ + +class PropagationException extends java.io.IOException +{ + + private boolean transient_ = false; + + private Exception detail_ = null; + // wrapped exception + + /** + * Constructor. + * + * @param detail + * The wrapped exception. + * @param trans + * If true, then the failure will NOT be reported to the Sender, + * but rather be dealt with in the Propagator by retrying it. + */ + + public PropagationException ( Exception detail , boolean trans ) + { + super (); + transient_ = trans; + detail_ = detail; + } + + /** + * Get transient flag. + * + * @return boolean True if the error is transient and its message can be + * retried. + */ + + public boolean isTransient () + { + return transient_; + } + + /** + * Get detail. + * + * @return Exception The underlying error cause. + */ + + public Exception getDetail () + { + return detail_; + } + + public void printStackTrace () + { + super.printStackTrace (); + if ( detail_ != null ) + detail_.printStackTrace (); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/PropagationMessage.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/PropagationMessage.java new file mode 100644 index 000000000..04fd7c50b --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/PropagationMessage.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.config.Configuration; + +/** + * A PropagationMessage is used for propagation of 2pc communication. + */ + +abstract class PropagationMessage +{ + + /** + * How many times is message re-sent if comm. failure? + */ + + private static final int MAX_RETRIES_ON_COMM_FAILURE = Configuration.getConfigProperties().getOltpMaxRetries(); + + private Participant participant_; + private int retrycount_ = 0; + private Result result_ = null; + + public PropagationMessage ( Participant participant , Result result ) + { + participant_ = participant; + result_ = result; + } + + public Participant getParticipant () + { + return participant_; + } + + /** + * @exception PropagationException + * If any. If the exception is transient, then this instance + * will retry calling send() until it succeeds. Otherwise, a + * failure is reported to the Result object. + */ + + protected abstract Object send() throws PropagationException; + + /** + * Called by system to process message. This will call the send() method and + * return a reply to the result object. + * + * @return boolean If true, then it should be tried again on failure. + */ + + protected boolean submit () + { + boolean failed = false; + boolean transienterr = false; + Exception exception = null; + Object result = null; + boolean retried = false; + + try { + result = send (); + } catch ( PropagationException e ) { + failed = true; + transienterr = e.isTransient (); + exception = e.getDetail (); + } finally { + if ( failed && transienterr && retrycount_ < MAX_RETRIES_ON_COMM_FAILURE ) { + retried = true; + retrycount_++; + } + if ( result_ != null ) { + result_.addReply ( new Reply ( result, exception, + getParticipant (), retried ) ); + } + } + return retried; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/Propagator.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/Propagator.java new file mode 100644 index 000000000..3588c6c75 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/Propagator.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.thread.TaskManager; + +/** + * A propagator sends PropagationMessages to participants. + */ + +class Propagator +{ + private static final Logger LOGGER = LoggerFactory.createLogger(Propagator.class); + + static long RETRY_INTERVAL = Configuration.getConfigProperties().getOltpRetryInterval(); + + + private boolean threaded_ = true; + + + Propagator ( boolean threaded ) + { + threaded_ = threaded; + } + + + public synchronized void submitPropagationMessage ( PropagationMessage msg ) + { + PropagatorThread t = new PropagatorThread ( msg ); + if ( threaded_ ) { + TaskManager.SINGLETON.executeTask ( t ); + } else { + t.run(); + } + + } + + + + private static class PropagatorThread implements Runnable + { + private PropagationMessage msg; + + PropagatorThread ( PropagationMessage msg ) + { + this.msg = msg; + } + + public void run() + { + try { + boolean tryAgain = true; + do { + tryAgain = msg.submit(); + if ( tryAgain ) { + //wait a little before retrying + Thread.sleep ( RETRY_INTERVAL ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Propagator: retrying " + "message: " + msg ); + } + } while ( tryAgain ); + } + catch ( Exception e ) { + LOGGER.logWarning ( "ERROR in propagator: " + e.getMessage () + + (msg != null ? " while sending message: " + msg : "") , e ); + } + } + + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/ReadOnlyParticipant.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/ReadOnlyParticipant.java new file mode 100644 index 000000000..c2c211e12 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/ReadOnlyParticipant.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Map; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; + + +class ReadOnlyParticipant implements Participant { + + //keep coordinator ID for equality + private final String coordinatorId; + public ReadOnlyParticipant() { + this.coordinatorId = null; + } + + ReadOnlyParticipant ( CoordinatorImp coordinator ) + { + this.coordinatorId = coordinator.getCoordinatorId(); + } + + public String getURI() { + return null; + } + + public void setCascadeList(Map allParticipants) throws SysException { + } + + public void setGlobalSiblingCount(int count) { + } + + public int prepare() throws RollbackException, HeurHazardException, + HeurMixedException, SysException { + return Participant.READ_ONLY; + } + + public void commit(boolean onePhase) + throws HeurRollbackException, HeurHazardException, + HeurMixedException, RollbackException, SysException { + } + + public void rollback() throws HeurCommitException, + HeurMixedException, HeurHazardException, SysException { + } + + public void forget() { + } + + public boolean equals ( Object o ) { + boolean ret = false; + if ( o instanceof ReadOnlyParticipant && coordinatorId != null ) { + ReadOnlyParticipant other = ( ReadOnlyParticipant ) o; + ret = coordinatorId.equals ( other.coordinatorId ); + } + return ret; + } + + public int hashCode() + { + int ret = 1; + if ( coordinatorId != null ) ret = coordinatorId.hashCode(); + return ret; + } + + @Override + public String toString() { + return "ReadOnlyParticipant"; + } + + @Override + public String getResourceName() { + return null; + } + + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/RecoveryDomainService.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/RecoveryDomainService.java new file mode 100644 index 000000000..e829920ab --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/RecoveryDomainService.java @@ -0,0 +1,180 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.atomikos.datasource.RecoverableResource; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.event.transaction.TransactionHeuristicEvent; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.publish.EventPublisher; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.RecoveryLog; +import com.atomikos.recovery.TxState; +import com.atomikos.thread.TaskManager; +import com.atomikos.timing.AlarmTimer; +import com.atomikos.timing.AlarmTimerListener; +import com.atomikos.timing.PooledAlarmTimer; + +public class RecoveryDomainService { + + private static final Logger LOGGER = LoggerFactory.createLogger(RecoveryDomainService.class); + + private RecoveryLog recoveryLog; + private boolean stopped; // for case 189603: avoid API-based recovery attempts after shutdown + + public RecoveryDomainService(RecoveryLog recoveryLog) { + this.recoveryLog = recoveryLog; + } + + private long maxTimeout; + private PooledAlarmTimer recoveryTimer; + private String recoveryDomainName; + + public void init() { + + long recoveryDelay = Configuration.getConfigProperties().getRecoveryDelay(); + setMaxTimeout(Configuration.getConfigProperties().getMaxTimeout()); + recoveryDomainName = Configuration.getConfigProperties().getTmUniqueName(); + recoveryTimer = new PooledAlarmTimer(recoveryDelay); + + recoveryTimer.addAlarmTimerListener(new AlarmTimerListener() { + + @Override + public void alarm(AlarmTimer timer) { + performRecovery(); + } + }); + TaskManager.SINGLETON.executeTask(recoveryTimer); + + } + + public void setMaxTimeout(long maxTimeout) { + this.maxTimeout = maxTimeout; + } + + protected synchronized boolean performRecovery() { + boolean perform = !stopped && recoveryLog.isActive(); + if (perform) { + try { + boolean allOk = true; + long startOfRecovery = System.currentTimeMillis(); + Set resourcesToRecover = getResourcesForRecovery(); + Collection indoubtCoordinators = recoveryLog.getIndoubtTransactionRecords(); + Collection foreignIndoubtCoordinators = extractForeignRecords(indoubtCoordinators); + Collection foreignCoordinatorsForHeuristicAbort = extractForeignIndoubtCoordinatorsForHeuristicAbort(foreignIndoubtCoordinators, startOfRecovery); + Collection expiredCommittingCoordinators = recoveryLog.getExpiredPendingCommittingTransactionRecordsAt(startOfRecovery); + + for (RecoverableResource recoverableResource : resourcesToRecover) { + try { + allOk = allOk && recoverableResource.recover(startOfRecovery, expiredCommittingCoordinators, foreignIndoubtCoordinators); + } catch (Throwable e) { + allOk = false; + LOGGER.logError(e.getMessage(), e); + } + } + + Collection recordsToDelete = new HashSet<>(); + if (allOk) { + recordsToDelete.addAll(expiredCommittingCoordinators); + Collection expiredNativeIndoubtCoordinators = extractNativeIndoubtCoordinatorsExpiredSince(startOfRecovery - maxTimeout, indoubtCoordinators); + recordsToDelete.addAll(expiredNativeIndoubtCoordinators); + } + recordsToDelete.addAll(foreignCoordinatorsForHeuristicAbort); + recoveryLog.forgetTransactionRecords(recordsToDelete); + + } catch (Throwable e) { + LOGGER.logError(e.getMessage(), e); + } + } + return perform; + } + + + private Collection extractNativeIndoubtCoordinatorsExpiredSince(long momentInThePast, + Collection collection) { + return PendingTransactionRecord.collectLineages( + (PendingTransactionRecord r) -> r.isLocalRoot(recoveryDomainName) && !r.isForeignInDomain(recoveryDomainName) && r.expires < momentInThePast && r.state == TxState.IN_DOUBT , + collection); + } + + private Collection extractForeignRecords( + Collection collection) { + return PendingTransactionRecord.collectLineages( + (PendingTransactionRecord r) -> r.isForeignInDomain(recoveryDomainName), + collection); + } + + private Collection extractForeignIndoubtCoordinatorsForHeuristicAbort( + Collection foreignIndoubtCoordinators, long startOfRecovery) { + HashSet ret = new HashSet<>(); + Iterator it = foreignIndoubtCoordinators.iterator(); + while (it.hasNext()) { + PendingTransactionRecord record = it.next(); + if (record.expires + maxTimeout < startOfRecovery) { + if (record.allowsHeuristicTermination(recoveryDomainName)) { + ret.add(record); + } else { + //pending expired in-doubt => generate warning + TransactionHeuristicEvent event = new TransactionHeuristicEvent(record.id, record.superiorId, TxState.IN_DOUBT); + EventPublisher.INSTANCE.publish(event); + } + } + for (PendingTransactionRecord entry : ret) { + foreignIndoubtCoordinators.remove(entry); //remove - so presumed abort will terminate this one + PendingTransactionRecord.removeAllDescendants(entry, foreignIndoubtCoordinators); //make sure that local descendants also abort + TransactionHeuristicEvent event = new TransactionHeuristicEvent(record.id, record.superiorId, TxState.HEUR_ABORTED); + EventPublisher.INSTANCE.publish(event); + } + } + return ret; + } + + + private Set getResourcesForRecovery() { + Collection resources = null; + resources = Configuration.getResources(); + return filterDuplicates(resources); //cf case 170618 + } + + private Set filterDuplicates(Collection resources) { + return new HashSet(resources); + } + + public synchronized void stop() { + if (recoveryTimer != null) { + recoveryTimer.stopTimer(); + recoveryTimer = null; + } + stopped = true; + } + + /** + * + * @return False if nothing more to do. + */ + public synchronized boolean hasPendingParticipantsFromLastRecoveryScan() { + if (!recoveryLog.isActive()) { + // another instance has taken over => we're off the hook + return false; + } + for (RecoverableResource res : getResourcesForRecovery()) { + if (res.hasPendingParticipantsFromLastRecoveryScan()) { + return true; + } + } + return false; + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/Reply.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/Reply.java new file mode 100644 index 000000000..e83aa59bb --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/Reply.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.Participant; + +/** + * A reply for propagation messages. + */ + +class Reply +{ + + private Exception exception_ = null; + + private Object response_ = null; + + private Participant participant_ = null; + + private boolean retried_ = false; + + /** + * Constructor. + * + * @param response + * The response value. + * @param exception + * The exception, or null if none. + * @param Participant + * The participant whose reply this is. + * @param retried + * If true, then the original messages has failed but is + * reschuled for retry. + */ + + public Reply ( Object response , Exception exception , + Participant participant , boolean retried ) + { + response_ = response; + exception_ = exception; + participant_ = participant; + retried_ = retried; + } + + /** + * Check if response ok. + * + * @return boolean True if response is invalid. In that case, getException() + * returns the error. + */ + + public boolean hasFailed () + { + return exception_ != null; + } + + /** + * To check if retried for failure case. + * + * @return boolean True if the original message will be retried. + */ + + public boolean isRetried () + { + return hasFailed () && retried_; + } + + /** + * Get any errors. Not null if hasFailed() is true. + * + * @return Exception The exception. + */ + + public Exception getException () + { + return exception_; + } + + /** + * Get the response. OK if hasFailed() returns false. + * + * @return Object Application specific; can be null. + */ + + public Object getResponse () + { + + return response_; + } + + /** + * Get the participant who replied this. + * + * @return Participant. + */ + + public Participant getParticipant () + { + return participant_; + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/Result.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/Result.java new file mode 100644 index 000000000..914c53970 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/Result.java @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import com.atomikos.icatch.Participant; + +/** + * A Result is responsible for collecting the replies of a termination round. + */ + +abstract class Result +{ + + public static final int ALL_OK = 0 , HEUR_HAZARD = 1 , HEUR_MIXED = 2 , + HEUR_ROLLBACK = 3 , HEUR_COMMIT = 4 , ALL_READONLY = 5 , + ROLLBACK = 6; + + protected int result_ = -1; + // should be set by analyze() + + protected int numberOfMissingReplies_ = 0; + protected Stack replies_ = new Stack(); + private Set repliedlist_ = new HashSet(); + + public Result ( int numberOfRepliesToWaitFor ) + { + numberOfMissingReplies_ = numberOfRepliesToWaitFor; + } + + /** + * Get the overall result for this communication round. + * + * @return int One of the static codes. + * + * @exception IllegalStateException + * If active msgs exist. + * @exception InterruptedException + * If interrupted during wait. + */ + + public int getResult() throws IllegalStateException, InterruptedException + { + calculateResultFromAllReplies(); + return result_; + } + + + /** + * Abstract method: analyze the results for this message round. + * + * @exception IllegalStateException + * If not done yet. + * @exception InterruptedException + * If interruption during result wait. + */ + + protected abstract void calculateResultFromAllReplies() throws IllegalStateException, + InterruptedException; + + + private boolean ignoreReply ( Reply reply ) { + // retried messages are not counted in result + // and duplicate entries per participant neither + // otherwise duplicates arise if a participant sends replay + return reply.isRetried() || repliedlist_.contains(reply.getParticipant()); + } + + /** + * Add a reply to the result. + * + * @param reply + * The reply to add. + */ + + public synchronized void addReply(Reply reply) + { + if ( !ignoreReply(reply) ) { + repliedlist_.add(reply.getParticipant()); + replies_.push(reply); + numberOfMissingReplies_--; + notifyAll(); + } + } + + /** + * Get all replies for this result's message round. Block until ready. + * + * @return Stack All replies in a stack. + * @exception IllegalStateException + * If not all replies are in yet. + * @exception InterruptedException + * During waiting interrupt. + */ + + public Stack getReplies() throws IllegalStateException, + InterruptedException + { + waitForReplies(); + return replies_; + } + + /** + * Wait until all replies arrived. + * + * @exception InterruptedException + * If the wait is interrupted. + */ + + synchronized void waitForReplies() throws InterruptedException + { + while ( numberOfMissingReplies_ > 0 ) wait(); + } + + /** + * Finds the first exception in the results. + * + * @return The exception, or null if none. + */ + Exception findFirstOriginalException() { + Exception ret = null; + for (Reply reply : replies_) { + ret = reply.getException(); + if (ret != null) break; + } + return ret; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackCallback.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackCallback.java new file mode 100644 index 000000000..bbebd7793 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackCallback.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.SysException; + +/** + * Callback interface for logic inside the rollback template. + * + */ + +interface RollbackCallback { + + public void doRollback() throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + java.lang.IllegalStateException; + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackMessage.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackMessage.java new file mode 100644 index 000000000..e15c09d26 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackMessage.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.Participant; + +/** + * A rollback message implementation. + */ + +class RollbackMessage extends PropagationMessage +{ + private boolean indoubt_ = false; + // true if participant can be indoubt. + + public RollbackMessage ( Participant participant , Result result , + boolean indoubt ) + { + super ( participant , result ); + indoubt_ = indoubt; + } + + /** + * A rollback message. + * + * @return Boolean null + * @exception PropagationException + * If problems. If heuristics, this will be a fatal + * exception; otherwise, rollback has to be retried since + * participant can be indoubt. In that case, the error is + * transient in nature. + */ + + protected Boolean send () throws PropagationException + { + Participant part = getParticipant (); + try { + part.rollback (); + + } catch ( HeurCommitException heurc ) { + throw new PropagationException ( heurc, false ); + } catch ( HeurMixedException heurm ) { + throw new PropagationException ( heurm, false ); + } + + catch ( Exception e ) { + // only retry if might be indoubt. Otherwise ignore. + if ( indoubt_ ) { + // here, participant might be indoubt! + // fill in exact heuristic msgs by using buffered effect of proxies + HeurHazardException heurh = new HeurHazardException(); + throw new PropagationException ( heurh, true ); + } + } + return null; + } + + public String toString () + { + return ("RollbackMessage to " + getParticipant ()); + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackOnlyParticipant.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackOnlyParticipant.java new file mode 100644 index 000000000..98e5658f1 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/RollbackOnlyParticipant.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Map; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * A participant to add in case setRollbackOnly is called. This participant will + * never allow commit. + */ + +class RollbackOnlyParticipant implements Participant +{ + + private static final Logger LOG = LoggerFactory.createLogger(RollbackOnlyParticipant.class); + + /** + * @see Participant + */ + + public void setGlobalSiblingCount ( int count ) + { + } + + /** + * @see Participant + */ + + public String getURI () + { + return null; + } + + /** + * @see Participant + */ + + public int prepare () throws RollbackException, HeurHazardException, + HeurMixedException, SysException + { + // prepare MUST fail: rollback only! + throw new RollbackException(); + } + + /** + * @see Participant + */ + + public void commit ( boolean onePhase ) + throws HeurRollbackException, HeurHazardException, + HeurMixedException, RollbackException, SysException + { + if (onePhase) throw new RollbackException(); + else LOG.logError("Unexpected 2-phase commit: outcome should be rollback!"); + } + + /** + * @see Participant + */ + + public void rollback () throws HeurCommitException, + HeurMixedException, HeurHazardException, SysException + { + } + + /** + * @see Participant + */ + + public void forget () + { + } + + + @Override + public String toString() { + return "RollbackOnlyParticipant"; + } + + + @Override + public String getResourceName() { + return null; + } + + @Override + public void setCascadeList(Map cascadeList) + throws SysException { + + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/SubTransactionCoordinatorParticipant.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/SubTransactionCoordinatorParticipant.java new file mode 100644 index 000000000..afa895fb7 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/SubTransactionCoordinatorParticipant.java @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Map; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; + +/** + * A participant for registering a subtx coordinator as a subordinate in 2PC of + * the parent transaction coordinator. + */ + +public class SubTransactionCoordinatorParticipant implements Participant +{ + + private transient Participant subordinateCoordinator; + // the participant role of the subtx coordinator + // NOT serializable -> recover by ID + + private String subordinateId; + // the id to recover the participant of the subordinate + + + private boolean prepareCalled; + // if true: heuristics on failure of rollback + + public SubTransactionCoordinatorParticipant ( + CoordinatorImp subordinateCoordinator ) + { + this.subordinateCoordinator = subordinateCoordinator; + this.subordinateId = subordinateCoordinator.getCoordinatorId (); + this.prepareCalled = false; + } + + /** + * @see com.atomikos.icatch.Participant#getURI() + */ + public String getURI () + { + return subordinateId; + } + + + public void setCascadeList ( Map allParticipants ) + throws SysException + { + // delegate to subordinate, in order to propagate to remote + // work (even though the subordinate itself is local, its + // registered participants may be remote!) + subordinateCoordinator.setCascadeList ( allParticipants ); + + } + + /** + * @see com.atomikos.icatch.Participant#setGlobalSiblingCount(int) + */ + public void setGlobalSiblingCount ( int count ) + { + // delegate to subordinate, in order to propagate + // to remote work + subordinateCoordinator.setGlobalSiblingCount ( count ); + + } + + /** + * @see com.atomikos.icatch.Participant#prepare() + */ + public int prepare () throws RollbackException, HeurHazardException, + HeurMixedException, SysException + { + prepareCalled = true; + return subordinateCoordinator.prepare (); + } + + /** + * @see com.atomikos.icatch.Participant#commit(boolean) + */ + public void commit ( boolean onePhase ) + throws HeurRollbackException, HeurHazardException, + HeurMixedException, RollbackException, SysException + { + if ( subordinateCoordinator != null ) + subordinateCoordinator.commit ( onePhase ); + else if ( prepareCalled ) { + throw new HeurHazardException (); + } else { + // prepare NOT called -> subordinate timed out + // and must have rolled back + throw new RollbackException (); + } + } + + /** + * @see com.atomikos.icatch.Participant#rollback() + */ + public void rollback () throws HeurCommitException, + HeurMixedException, HeurHazardException, SysException + { + if ( subordinateCoordinator != null ) { + subordinateCoordinator.rollback (); + } else if ( prepareCalled ) { + // heuristic: coordinator not recovered?! + throw new HeurHazardException(); + } + // if prepare not called then the subordinate will + // not be committed, so rollback is correct then + } + + /** + * @see com.atomikos.icatch.Participant#forget() + */ + public void forget () + { + if ( subordinateCoordinator != null ) subordinateCoordinator.forget (); + } + + @Override + public String getResourceName() { + return null; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/SubTransactionRecoveryCoordinator.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/SubTransactionRecoveryCoordinator.java new file mode 100644 index 000000000..bc48f0a63 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/SubTransactionRecoveryCoordinator.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.RecoveryCoordinator; + +class SubTransactionRecoveryCoordinator implements RecoveryCoordinator { + + private String superiorCoordinatorId; + private String recoveryDomainName; + + public SubTransactionRecoveryCoordinator(String superiorCoordinatorId, String recoveryDomainName) { + this.superiorCoordinatorId = superiorCoordinatorId; + this.recoveryDomainName = recoveryDomainName; + } + + + @Override + public String getURI() { + return superiorCoordinatorId; + } + + + @Override + public String getRecoveryDomainName() { + return recoveryDomainName; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TerminatedStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TerminatedStateHandler.java new file mode 100644 index 000000000..4d070b032 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TerminatedStateHandler.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.recovery.TxState; + +/** + * + * + * A state handler for the terminated coordinator state. + */ + +class TerminatedStateHandler extends CoordinatorStateHandler +{ + + TerminatedStateHandler ( CoordinatorStateHandler previous ) + { + super ( previous ); + // VERY important: stop all active threads + dispose (); + } + + protected TxState getState () + { + return TxState.TERMINATED; + } + + protected void onTimeout () + { + // nothing to do here + } + + protected void setGlobalSiblingCount ( int count ) + { + // nothing to do here + } + + protected int prepare () throws RollbackException, + java.lang.IllegalStateException, HeurHazardException, + HeurMixedException, SysException + { + + // prepare in terminated state happens for JOIN cases where a second + // client TM propagates prepare. In that case, reply readonly + // to avoid double commits later. This only works if prepares + // themselves are NEVER retried on failure! + + return Participant.READ_ONLY; + } + + protected void commit ( boolean onePhase ) + throws HeurRollbackException, HeurMixedException, + HeurHazardException, java.lang.IllegalStateException, + RollbackException, SysException + { + + if ( !onePhase ) { + // respond consistently on multiple commits + // but only if NOT 1PC: in that case, terminated + // means that we have rolled back! + return; + } else { + // 1PC -> commit causes rolled back exception, + // and this ONLY works if at most 1 commit message is + // sent (otherwise, a second commit will see + // terminated state and assume tx was rolled back) + // ! => propagator thread should NOT retry + // commit for 1PC! + // The implication is that, if the rolled back + // exception does not arrive due to masking + // errors, then the client has no certainty about + // the outcome. + // In order to have certainty, a client participant + // wrapper should be used to force 2PC and + // logging. + + throw new RollbackException ( "Transaction was rolled back." ); + } + + } + + protected void rollback () throws HeurCommitException, + HeurMixedException, SysException, HeurHazardException, + java.lang.IllegalStateException + { + } + + protected void forget () + { + // OVERRIDE TO DO NOTHING SINCE PROPAGATOR THREADS NO LONGER RUN + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TerminationResult.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TerminationResult.java new file mode 100644 index 000000000..d9446400a --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TerminationResult.java @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RollbackException; + + +class TerminationResult extends Result +{ + private boolean allRepliesProcessed; + private Set heuristicparticipants_; + private Set possiblyIndoubts_; + + public TerminationResult ( int numberOfRepliesToWaitFor ) + { + super ( numberOfRepliesToWaitFor ); + allRepliesProcessed = false; + heuristicparticipants_ = new HashSet(); + possiblyIndoubts_ = new HashSet(); + } + + /** + * @exception IllegalStateException + * If not done yet. + */ + + public Set getHeuristicParticipants () throws IllegalStateException, + InterruptedException + { + calculateResultFromAllReplies(); + return heuristicparticipants_; + } + + /** + * + * @exception IllegalStateException + * If comm. not done yet. + */ + + public Set getPossiblyIndoubts () throws IllegalStateException, + InterruptedException + { + calculateResultFromAllReplies (); + return possiblyIndoubts_; + } + + protected synchronized void calculateResultFromAllReplies () throws IllegalStateException, + InterruptedException + + { + if (allRepliesProcessed) return; + + boolean atLeastOneHeuristicMixedException = false; + boolean atLeastOneHeuristicRollbackException = false; + boolean atLeastOneHeuristicCommitException = false; + boolean atLeastOneHeuristicHazardException = false; + boolean noFailedReplies = true; + boolean onePhaseCommitWithRollbackException = false; + + Stack replies = getReplies(); + Enumeration enumm = replies.elements (); + + while ( enumm.hasMoreElements () ) { + + Reply reply = (Reply) enumm.nextElement (); + + if ( reply.hasFailed () ) { + noFailedReplies = false; + Exception err = reply.getException (); + if ( err instanceof RollbackException ) { + onePhaseCommitWithRollbackException = true; + } else if ( err instanceof HeurMixedException ) { + atLeastOneHeuristicMixedException = true; + heuristicparticipants_.add ( reply.getParticipant ()); + } else if ( err instanceof HeurCommitException ) { + atLeastOneHeuristicCommitException = true; + atLeastOneHeuristicMixedException = (atLeastOneHeuristicMixedException || atLeastOneHeuristicRollbackException || atLeastOneHeuristicHazardException); + heuristicparticipants_.add ( reply.getParticipant () ); + + } else if ( err instanceof HeurRollbackException ) { + atLeastOneHeuristicRollbackException = true; + atLeastOneHeuristicMixedException = (atLeastOneHeuristicMixedException || atLeastOneHeuristicCommitException || atLeastOneHeuristicHazardException); + heuristicparticipants_.add ( reply.getParticipant ()); + + } else { + + atLeastOneHeuristicHazardException = true; + atLeastOneHeuristicMixedException = (atLeastOneHeuristicMixedException || atLeastOneHeuristicRollbackException || atLeastOneHeuristicCommitException); + heuristicparticipants_.add ( reply.getParticipant ()); + possiblyIndoubts_.add ( reply.getParticipant ()); + + } + } + } + + if ( onePhaseCommitWithRollbackException ) + result_ = ROLLBACK; + else if ( atLeastOneHeuristicMixedException || atLeastOneHeuristicRollbackException + && heuristicparticipants_.size () != replies.size () + || atLeastOneHeuristicCommitException + && heuristicparticipants_.size () != replies.size () ) + result_ = HEUR_MIXED; + else if ( atLeastOneHeuristicHazardException ) { + // heur hazard BEFORE heur abort or commit! + // see OTS definitions: hazard ASA some unknown, + // but ALL KNOWN ARE COMMIT OR ALL ARE ABORT + result_ = HEUR_HAZARD; + } else if ( atLeastOneHeuristicRollbackException ) { + result_ = HEUR_ROLLBACK; + // here, there can be no heur commits as well, since otherwise mixed + // would have fitted. Same for hazards. + + } else if ( atLeastOneHeuristicCommitException ) { + // here, there can be no heur aborts as well, since mixed would have + // fitted. no hazards either, since hazards would have fitted. + result_ = HEUR_COMMIT; + + } else if ( noFailedReplies ) + result_ = ALL_OK; + + allRepliesProcessed = true; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TransactionServiceImp.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TransactionServiceImp.java new file mode 100644 index 000000000..d660e20a9 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TransactionServiceImp.java @@ -0,0 +1,688 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Stack; + +import com.atomikos.finitestates.FSMEnterEvent; +import com.atomikos.finitestates.FSMEnterListener; +import com.atomikos.icatch.CompositeCoordinator; +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.Propagation; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RecoveryService; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.TransactionService; +import com.atomikos.icatch.TransactionServicePlugin; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.provider.TransactionServiceProvider; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.persistence.StateRecoveryManager; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.RecoveryLog; +import com.atomikos.recovery.TxState; +import com.atomikos.recovery.fs.RecoveryLogImp; +import com.atomikos.thread.InterruptedExceptionHelper; +import com.atomikos.thread.TaskManager; +import com.atomikos.util.UniqueIdMgr; + +/** + * General implementation of Transaction Service. + */ + +public class TransactionServiceImp implements TransactionServiceProvider, + FSMEnterListener, SubTxAwareParticipant, RecoveryService +{ + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionServiceImp.class); + private static final int NUMLATCHES = 97; + private static final Object shutdownSynchronizer = new Object(); + + + private long maxTimeout_; + private Object[] rootLatches_ = null; + private Hashtable tidToTransactionMap_ = null; + private Map recreatedCoordinatorsByRootId = new HashMap<>(); + private Map allCoordinatorsByCoordinatorId = new HashMap<>(); + private boolean shutdownInProgress_ = false; + private UniqueIdMgr tidmgr_ = null; + private StateRecoveryManager recoverymanager_ = null; + private boolean initialized_ = false; + + + private Set tsListeners = new HashSet<>(); + private int maxNumberOfActiveTransactions_; + private String tmUniqueName_; + private boolean single_threaded_2pc_; + private RecoveryLog recoveryLog; + private RecoveryDomainService recoveryDomainService; + + + /** + * Create a new instance, with orphan checking set. + * + * @param name + * The unique name of this TM. + * @param recoverymanager + * The recovery manager to use. + * @param tidmgr + * The String manager to use. + * @param maxtimeout + * The max timeout for new or imported txs. + * + * @param maxActives + * The max number of active txs, or negative if unlimited. + * even for creation requests that ask for checks. This + * mode may be needed for being compatible with certain + * configurations that do not support orphan detection. + * @param single_threaded_2pc + * Whether 2PC commit should happen in the same thread that started the tx. + * @param recoveryLog2 + * + */ + + public TransactionServiceImp ( String name , + StateRecoveryManager recoverymanager , UniqueIdMgr tidmgr , + long maxtimeout , + int maxActives , boolean single_threaded_2pc, RecoveryLog recoveryLog ) + { + maxNumberOfActiveTransactions_ = maxActives; + + initialized_ = false; + recoverymanager_ = recoverymanager; + tidmgr_ = tidmgr; + tidToTransactionMap_ = new Hashtable(); + rootLatches_ = new Object[NUMLATCHES]; + for (int i = 0; i < NUMLATCHES; i++) { + rootLatches_[i] = new Object(); + } + + maxTimeout_ = maxtimeout; + + tmUniqueName_ = name; + single_threaded_2pc_ = single_threaded_2pc; + this.recoveryLog = recoveryLog; + this.recoveryDomainService = new RecoveryDomainService(recoveryLog); + } + + /** + * Get an object to lock for the given root. To increase concurrency and + * still provide atomic operations within the scope of one root. + * + * @return Object The object to lock for the given root. + */ + private Object getLatch ( String root ) + { + return rootLatches_[Math.abs ( root.toString().hashCode() % NUMLATCHES )]; + } + + /** + * Set the map to ct for this tid. + * + * @param tid + * The tx id to map. + * @param ct + * The tx to map to. + * @exception IllegalStateException + * If the tid is already mapped. + */ + + private void setTidToTx ( String tid , CompositeTransaction ct ) + throws IllegalStateException + { + synchronized ( tidToTransactionMap_ ) { + if ( tidToTransactionMap_.containsKey ( tid.intern () ) ) + throw new IllegalStateException ( "Already mapped: " + tid ); + tidToTransactionMap_.put ( tid.intern (), ct ); + ct.addSubTxAwareParticipant(this); // for GC purposes + } + } + + /** + * Removes the coordinator from the root map. + * + * @param coord + * The coordinator to remove. + */ + + private void removeCoordinator ( CompositeCoordinator coord ) + { + + synchronized ( shutdownSynchronizer ) { + synchronized ( getLatch ( coord.getRootId()) ) { + recreatedCoordinatorsByRootId.remove (coord.getRootId()); + allCoordinatorsByCoordinatorId.remove(coord.getCoordinatorId()); + } + + // notify any waiting threads for shutdown + if ( allCoordinatorsByCoordinatorId.isEmpty() ) + shutdownSynchronizer.notifyAll (); + } + } + + /** + * Removes the tx from the map. + * + * Does nothing if not found or if ct null. + * + * @param ct + * The transaction to remove. + */ + + private void removeTransaction ( CompositeTransaction ct ) + { + if ( ct == null ) + return; + tidToTransactionMap_.remove ( ct.getTid ().intern () ); + + } + + /** + * Creation method for composite transactions. + * + * @return CompositeTransaction. + */ + + private CompositeTransactionImp createCT ( String tid , + CoordinatorImp coordinator , Stack lineage , boolean serial ) + throws SysException + { + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Creating composite transaction: " + tid ); + CompositeTransactionImp ct = new CompositeTransactionImp ( this, + lineage, tid, serial, coordinator ); + + setTidToTx ( ct.getTid (), ct ); + coordinator.incLocalSiblingsStarted(); //orphan detection and timeout handling + return ct; + } + + /** + * Creation method for composite coordinators. + * + * @param recoveryDomainName The recovery domain of the superior of this coordinator. + * + * @param RecoveryCoordinator + * An existing coordinator for the given root. Null if not a + * subtx, or an adaptor in other cases. + * @param lineage + * The ancestor information. + * @param root + * The root id. + * @param timeout + * The timeout for indoubt states. After this time, indoubts are + * terminated heuristically according to the given strategy. + * + * @return CoordinatorImp. + */ + + private CoordinatorImp createCC (String recoveryDomainName, RecoveryCoordinator adaptor , + String root, long timeout ) + { + CoordinatorImp cc = null; + + if (maxTimeout_ > 0 && timeout > maxTimeout_ ) { + timeout = maxTimeout_; + //FIXED 20188 + LOGGER.logWarning ( "Attempt to create a transaction with a timeout that exceeds maximum - truncating to: " + maxTimeout_ ); + } + + synchronized ( shutdownSynchronizer ) { + // check if shutting down -> do not allow new coordinator objects + // to be added, so that shutdown will eventually succeed. + if ( shutdownInProgress_ ) + throw new IllegalStateException ( "Server is shutting down..." ); + + + String coordinatorId = root; + boolean subTransaction = (adaptor != null); + if (subTransaction) { //not a root + coordinatorId = tidmgr_.get(); + } + cc = new CoordinatorImp (recoveryDomainName, coordinatorId, root, adaptor, timeout, single_threaded_2pc_ ); + + recoverymanager_.register ( cc ); + + // now, add to root map, since we are sure there are not too many active txs + synchronized ( getLatch ( root) ) { + CoordinatorImp entryForRoot = recreatedCoordinatorsByRootId.get(root); + if (entryForRoot == null) { //cf case 178075 + recreatedCoordinatorsByRootId.put(root, cc); + } + allCoordinatorsByCoordinatorId.put(coordinatorId, cc); + } + startlistening ( cc ); + } + + return cc; + } + + /** + * Start listening for terminated states, so coordinator can be removed. + * + * @param coordinator + * The coordinator to listen on. + * + */ + + private void startlistening ( CoordinatorImp coordinator ) + { + Set forgetStates = new HashSet(); + for (TxState txState : TxState.values()) { + if(txState.isFinalStateForOltp()) { + forgetStates.add(txState); + } + } + + for (TxState txState : forgetStates) { + coordinator.addFSMEnterListener ( this, txState ); + } + + // on recovery, the end states might have been reached + // BEFORE listener added -> check and remove if so. + if ( forgetStates.contains ( coordinator.getState () ) ) + removeCoordinator ( coordinator ); + } + + private CoordinatorImp getCoordinatorImpForRoot ( String root ) + throws SysException + { + root = root.intern (); + if ( !initialized_ ) + throw new IllegalStateException ( "Not initialized" ); + + CoordinatorImp cc = null; + synchronized ( shutdownSynchronizer ) { + // Synch on shutdownSynchronizer_ first to avoid + // deadlock, even if we don't seem to need it here + + synchronized ( getLatch ( root ) ) { + cc = recreatedCoordinatorsByRootId.get(root); + } + } + + return cc; + } + + + public String getName () + { + return tmUniqueName_; + } + + + + /** + * @see TransactionService + */ + + public CompositeCoordinator getCompositeCoordinator ( String root ) + throws SysException + { + return getCoordinatorImpForRoot ( root ); + } + + /** + * @see TransactionService + */ + + public void addTSListener ( TransactionServicePlugin listener ) + throws IllegalStateException + { + + // NOTE: we do NOT synchronize with init, + // because compensators will call this method + // during recovery, and recovery happens inside + // init! + + tsListeners.add( listener ); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Added TSListener: " + listener ); + + } + + /** + * @see TransactionService + */ + + public void removeTSListener ( TransactionServicePlugin listener ) + { + + tsListeners.remove(listener); + if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Removed TSListener: " + listener ); + + } + + + /** + * @see TransactionService + */ + + public synchronized void init ( Properties properties ) throws SysException + { + shutdownInProgress_ = false; + recoveryDomainService.init();; + initialized_ = true; + } + + /** + * @see TransactionService + */ + + public Participant getParticipant ( String root ) throws SysException + { + return getCoordinatorImpForRoot ( root ); + } + + /** + * @see FSMEnterListener. + */ + + public void entered ( FSMEnterEvent event ) + { + CoordinatorImp cc = (CoordinatorImp) event.getSource (); + removeCoordinator(cc); + } + + /** + * Called if a tx is ended successfully. In order to remove the tx from the + * mapping. + * + * @see SubTxAwareParticipant + */ + + public void committed ( CompositeTransaction tx ) + { + removeTransaction ( tx ); + } + + /** + * Called if a tx is ended with failure. In order to remove tx from mapping. + * + * @see SubTxAwareParticipant + */ + + public void rolledback ( CompositeTransaction tx ) + { + removeTransaction ( tx ); + + } + + /** + * @see TransactionService + */ + + public CompositeTransaction getCompositeTransaction ( String tid ) + { + CompositeTransaction ret = null; + + synchronized ( tidToTransactionMap_ ) { + ret = (CompositeTransaction) tidToTransactionMap_.get ( tid.intern () ); + } + + return ret; + } + + + + /** + * Creates a subtransaction for the given parent + * + * @param parent + * @return + */ + @SuppressWarnings("unchecked") + CompositeTransaction createSubTransaction ( CompositeTransaction parent ) + { + if (Configuration.getConfigProperties().getAllowSubTransactions()) { + CompositeTransactionImp ret = null; + Stack lineage = (Stack) parent.getLineage ().clone (); + lineage.push ( parent ); + String tid = tidmgr_.get (); + CoordinatorImp ccParent = (CoordinatorImp) parent + .getCompositeCoordinator (); + SubTransactionRecoveryCoordinator rc = new SubTransactionRecoveryCoordinator(ccParent.getCoordinatorId(), tmUniqueName_); + // create NEW coordinator for subtx, with most of the parent settings + // but without orphan checks since subtxs have no orphans + CoordinatorImp cc = createCC ( tmUniqueName_, rc, parent.getCompositeCoordinator().getRootId(), parent.getTimeout () ); + ret = createCT ( tid, cc, lineage, parent.isSerial () ); + ret.noLocalAncestors = false; + return ret; + } else { + throw new SysException("Subtransactions not allowed - set config property com.atomikos.icatch.allow_subtransactions=true to enable"); + } + + } + + /** + * @see TransactionService + */ + + public synchronized CompositeTransaction recreateCompositeTransaction (Propagation context) throws SysException { + if ( !initialized_ ) + throw new IllegalStateException ( "Not initialized" ); + + if (!tmUniqueName_.equals(context.getRecoveryDomainName()) && !usesDefaultRecovery()) { + throw new IllegalArgumentException("Cannot import a transaction from a different recovery domain: " + + context.getRecoveryDomainName() + ".\n" + + "Only transactions within the same domain (a.k.a. LogCloud) are allowed!"); + } + + if ( maxNumberOfActiveTransactions_ >= 0 && tidToTransactionMap_.size () >= maxNumberOfActiveTransactions_ ) + throw new IllegalStateException ( + "Max number of active transactions reached:" + maxNumberOfActiveTransactions_ ); + + CoordinatorImp cc = null; + CompositeTransaction ct = null; + + try { + String tid = tidmgr_.get (); + boolean serial = context.isSerial (); + + CompositeTransaction root = context.getRootTransaction(); + CompositeTransaction parent = context.getParentTransaction(); + + synchronized ( shutdownSynchronizer ) { + synchronized ( getLatch ( root.getTid () ) ) { + cc = getCoordinatorImpForRoot ( root.getTid () ); + if ( cc == null ) { + RecoveryCoordinator coord = parent + .getCompositeCoordinator () + .getRecoveryCoordinator (); + cc = createCC (context.getRecoveryDomainName(), coord, root.getTid (), context.getTimeout () ); + } + } + } + ct = createCT ( tid, cc, context.getLineage(), serial ); + + } catch ( Exception e ) { + throw new SysException ( "Error in recreate.", e ); + } + + return ct; + } + + private boolean usesDefaultRecovery() { + return Configuration.getRecoveryLog() instanceof RecoveryLogImp; + } + + /** + * @see TransactionService + */ + + public void shutdown(boolean force) { + + boolean wasShuttingDown = false; + LOGGER.logInfo ( "Entering shutdown (" + force + ")..." ); + + // following moved out of synch block to avoid deadlock on immediate + // shutdown with interleaving entered notification of a terminating + // coordinator state-handler + if ( !wasShuttingDown && force ) { + // If we were already shutting down, then the FIRST thread + // to enter this method will do the following. Don't do + // it twice. + + for (String next : allCoordinatorsByCoordinatorId.keySet()) { + LOGGER.logTrace ( "Stopping thread for coordinatorId " + + next + "..." ); + CoordinatorImp c = allCoordinatorsByCoordinatorId.get(next); + if (c != null) { // null on concurrent termination / removal + c.dispose (); // needed for forced shutdown + } + LOGGER.logTrace ( "Thread stopped." ); + } + + + + } + + + synchronized ( shutdownSynchronizer ) { + LOGGER.logTrace ( "Shutdown acquired lock on waiter." ); + wasShuttingDown = shutdownInProgress_; + shutdownInProgress_ = true; + + if (!force) { + boolean timeout = waitForActiveCoordinatorsToFinish(); + if (!timeout) { + performRecoveryPass(); + } + if (usesDefaultRecovery()) { + if (timeout || recoveryDomainService.hasPendingParticipantsFromLastRecoveryScan()) { + LOGGER.logWarning("Shutdown leaves pending transactions in log - do NOT delete logfiles!"); + } else { + // if we are here then there are NO pending XIDs in any resource + // => whatever is left in the transaction log files: we don't it any more + LOGGER.logInfo("Shutdown leaves no pending transactions - ok to delete logfiles"); + } + } else { + recoveryLog.closing(); // allow other cluster node to take over + } + } + + initialized_ = false; + if ( !wasShuttingDown ) { + // If we were already shutting down, then the FIRST thread + // to enter this method will do the following. Don't do + // it twice. + try { + recoverymanager_.close (); + } catch ( LogException le ) { + throw new SysException ( "Error in shutdown: " + + le.getMessage (), le ); + } + recoveryDomainService.stop(); + recoveryLog.closed(); + } + + } + + shutdownSystemExecutors(); + } + + private boolean waitForActiveCoordinatorsToFinish() { + ConditionalWaiter waiter = new ConditionalWaiter(maxTimeout_); + boolean timeout = waiter.waitWhile(() -> { + boolean allCoordinatorsDone = allCoordinatorsByCoordinatorId.isEmpty(); + if (!allCoordinatorsDone) { + LOGGER.logWarning("Shutdown; waiting for all active transactions to finish..."); + } + return !allCoordinatorsDone; + }); + return timeout; + } + + private void shutdownSystemExecutors() { + TaskManager exec = TaskManager.SINGLETON; + if ( exec != null ) { + exec.shutdown(); + } + } + + public synchronized void finalize () throws Throwable + { + + try { + if ( !shutdownInProgress_ && initialized_ ) shutdown ( true ); + } catch ( Exception e ) { + LOGGER.logWarning( "Error in GC of TransactionServiceImp" , e ); + } finally { + super.finalize (); + } + } + + public CompositeTransaction createCompositeTransaction ( long timeout ) throws SysException + { + if ( !initialized_ ) throw new IllegalStateException ( "Not initialized" ); + + if ( maxNumberOfActiveTransactions_ >= 0 && + tidToTransactionMap_.size () >= maxNumberOfActiveTransactions_ ) { + throw new IllegalStateException ( "Max number of active transactions reached:" + maxNumberOfActiveTransactions_ ); + } + + String tid = tidmgr_.get (); + Stack lineage = new Stack(); + // create a CC with heuristic preference set to false, + // since it does not really matter anyway (since we are + // creating a root) + CoordinatorImp cc = createCC(tmUniqueName_, null, tid, timeout); + CompositeTransaction ct = createCT ( tid, cc, lineage, false ); + return ct; + } + + @Override + public RecoveryService getRecoveryService() { + return this; + } + + @Override + public RecoveryLog getRecoveryLog() { + return this.recoveryLog; + } + + @Override + public boolean performRecovery() { + boolean perform = performRecoveryPass(); + if (perform) { + try { + Thread.currentThread().sleep(maxTimeout_ + 1000); + } catch (InterruptedException e) { + InterruptedExceptionHelper.handleInterruptedException(e); + } + performRecoveryPass(); + } + return perform; + } + + protected boolean performRecoveryPass() { + boolean ret = false; + RecoveryDomainService rds = recoveryDomainService; + if (rds != null) { // null on concurrent shutdown + ret = rds.performRecovery(); + } + return ret; + } + + @Override + public boolean performRecovery(boolean lax) { + return performRecovery(); + } + + @Override + public void preEnter(FSMEnterEvent e) throws IllegalStateException { + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TransactionStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TransactionStateHandler.java new file mode 100644 index 000000000..af1671f2b --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TransactionStateHandler.java @@ -0,0 +1,314 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Stack; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Extent; +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * The state pattern applied to the CompositeTransaction classes. + */ + +abstract class TransactionStateHandler implements SubTxAwareParticipant +{ + private static final Logger LOGGER = LoggerFactory.createLogger(TransactionStateHandler.class); + + private int subtxs_; + private List synchronizations_; // FIFO - cf case 20711 + private List subtxawares_; + private CompositeTransactionImp ct_; + + protected TransactionStateHandler ( CompositeTransactionImp ct ) + { + ct_ = ct; + subtxs_ = 0; + subtxawares_ = new ArrayList(); + synchronizations_ = new Stack(); + } + + protected TransactionStateHandler ( CompositeTransactionImp ct , + TransactionStateHandler handler ) + { + subtxs_ = handler.getSubTransactionCount(); + synchronizations_ = handler.getSynchronizations(); + subtxawares_ = handler.getSubtxawares(); + ct_ = ct; + + } + + private synchronized void localDecSubTxCount() + { + subtxs_--; + } + + private synchronized void localIncSubTxCount() + { + subtxs_++; + } + + private synchronized int localGetSubTxCount() + { + return subtxs_; + } + + private synchronized void localPushSynchronization ( Synchronization sync ) + { + synchronizations_.add ( sync ); + } + + /** + * Should be called instead of iterator: commit can add more synchronizations + * so an iterator would give ConcurrentModificationException! + */ + private Synchronization localPopSynchronization() { + Synchronization ret = null; + if (!synchronizations_.isEmpty()) ret = synchronizations_.remove(0); + return ret; + } + + private synchronized void localAddSubTxAwareParticipant ( SubTxAwareParticipant p ) + { + if (!subtxawares_.contains(p)) { + subtxawares_.add(p); + } + } + + protected CompositeTransaction createSubTransaction () + throws SysException, IllegalStateException + { + CompositeTransaction ct = null; + ct = ct_.getTransactionService ().createSubTransaction ( ct_ ); + // we want to be notified of subtx commit for handling extents + ct.addSubTxAwareParticipant ( this ); + localIncSubTxCount(); + return ct; + } + + // this method should NOT be synchronized, to avoid deadlocks in JBoss + // termination handling at remote servers! + protected RecoveryCoordinator addParticipant ( Participant participant ) + throws SysException, java.lang.IllegalStateException + { + + CoordinatorImp coord = ct_.getCoordinatorImp (); + try { + coord.addParticipant ( participant ); + if(LOGGER.isDebugEnabled()){ + LOGGER.logDebug("addParticipant ( " + participant + " ) for transaction " + + getCT().getTid ()); + } + + } catch ( RollbackException e ) { + throw new IllegalStateException ( "Transaction already rolled back" ); + } + return coord; + } + + protected void registerSynchronization ( Synchronization sync ) + throws IllegalStateException, UnsupportedOperationException, SysException + { + if ( sync != null ) { + try { + ct_.getCoordinatorImp().registerSynchronization(sync); + } catch ( RollbackException e ) { + throw new IllegalStateException ( + "Transaction already rolled back" ); + } + localPushSynchronization ( sync ); + } + } + + protected void addSubTxAwareParticipant ( + SubTxAwareParticipant subtxaware ) throws SysException, + java.lang.IllegalStateException + { + + localAddSubTxAwareParticipant ( subtxaware ); + } + + private void rollback() throws IllegalStateException, SysException + { + for ( int i = 0; i < subtxawares_.size (); i++ ) { + SubTxAwareParticipant subtxaware = (SubTxAwareParticipant) subtxawares_.get ( i ); + subtxaware.rolledback ( ct_ ); + } + + Extent extent = ct_.getExtent (); + if ( extent != null ) { + Enumeration enumm = extent.getParticipants ().elements (); + while ( enumm.hasMoreElements () ) { + Participant part = (Participant) enumm.nextElement(); + addParticipant ( part ); + } + } + + ct_.localSetTransactionStateHandler ( new TxTerminatedStateHandler ( ct_, this, false ) ); + + try { + ct_.getCoordinatorImp().rollback(); + } catch ( HeurCommitException e ) { + throw new SysException ( "Unexpected error in rollback", e ); + } catch ( HeurMixedException e ) { + throw new SysException ( "Unexpected error in rollback", e ); + } catch ( HeurHazardException e ) { + throw new SysException ( "Unexpected error in rollback", e ); + } + } + + protected void rollbackWithStateCheck () throws java.lang.IllegalStateException, + SysException + { + //prevent concurrent commits - relevant if this is a timeout thread + ct_.localTestAndSetTransactionStateHandler ( this , new TxTerminatingStateHandler ( false , ct_ , this ) ); + rollback(); + + } + + // IMPORTANT: don't synchronize the commit method, because it causes deadlocks + // (since this method also indirectly locks the coordinator and the FSM) + // This deadlock happens in particular when application commit interleaves + // with timeout-driven rollback (during preEnter, the rollback of this same + // handler is invoked) + protected void commit() throws SysException, + java.lang.IllegalStateException, RollbackException + { + //prevent concurrent rollback due to timeout + ct_.localTestAndSetTransactionStateHandler(this , new TxTerminatingStateHandler(true , ct_ , this)); + + if ( subtxs_ > 0 ) throw new IllegalStateException ( "Active subtransactions exist" ); + + // BEFORE calling SubTxAwares, make sure that synchronizations + // are called. This is because the calling thread must still be + // associated with the tx at beforeCompletion, and the + // TM is listening as a SubTxAware! + // NOTE: doing this at the very beginning of commit + // also makes sure that the tx can still get new Participants + // from beforeCompletion work being done! This is required. + Throwable cause = notifyBeforeCompletion(); + + if ( ct_.getState().equals ( TxState.MARKED_ABORT ) ) { + // happens if synchronization has called setRollbackOnly + rollback(); + throw new RollbackException ( "The transaction was set to rollback only" , cause ); + } + + // for loop to make sure that new registrations are possible during callback + for ( int i = 0; i < subtxawares_.size (); i++ ) { + SubTxAwareParticipant subtxaware = (SubTxAwareParticipant) subtxawares_.get ( i ); + subtxaware.committed ( ct_ ); + } + + ct_.localSetTransactionStateHandler ( new TxTerminatedStateHandler ( ct_, this, true ) ); + + + + } + + private Throwable notifyBeforeCompletion() { + Throwable cause = null; + Synchronization sync = localPopSynchronization(); + while ( sync != null ) { + try { + sync.beforeCompletion (); + } catch ( RuntimeException error ) { + // see case 24246: rollback only + setRollbackOnly(); + // see case 115604 + // transport the first exception here as return value + if (cause == null) { + cause = error; + } else { + // log the others which may still happen as error - cf. case 115604 + LOGGER.logError("Unexpected error in beforeCompletion: ", error); + } + } + sync = localPopSynchronization(); + } + return cause; + } + + protected void setRollbackOnly () + { + ct_.getCoordinatorImp().setRollbackOnly(); + synchronized ( this ) { + ct_.localSetTransactionStateHandler ( new TxRollbackOnlyStateHandler ( ct_,this ) ); + } + + } + + public void committed ( CompositeTransaction subtx ) + { + CompositeTransactionImp ct = (CompositeTransactionImp) subtx; + Extent toAdd = subtx.getExtent(); + Extent target = ct_.getExtent(); + target.add ( toAdd ); + CoordinatorImp subTxCoordinator = ct.getCoordinatorImp(); + if (!subTxCoordinator.getParticipants().isEmpty()) { //case 175440: avoid unnecessary 2PC so non-XA works smoother + SubTransactionCoordinatorParticipant part = new SubTransactionCoordinatorParticipant ( subTxCoordinator ); + addParticipant ( part ); + } + + localDecSubTxCount(); + } + + public void rolledback ( CompositeTransaction subtx ) + { + localDecSubTxCount(); + } + + protected abstract TxState getState(); + + + protected List getSubtxawares() + { + return subtxawares_; + } + + protected CompositeTransactionImp getCT() + { + return ct_; + } + + + protected int getSubTransactionCount() + { + return localGetSubTxCount(); + } + + protected List getSynchronizations() + { + return synchronizations_; + } + + protected void addSynchronizations ( Stack synchronizations ) + { + while ( !synchronizations.isEmpty() ) { + Synchronization next = synchronizations.pop(); + localPushSynchronization ( next ); + } + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TxActiveStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxActiveStateHandler.java new file mode 100644 index 000000000..00da857ce --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxActiveStateHandler.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.recovery.TxState; + +/** + * An active state handler. + */ + +class TxActiveStateHandler extends TransactionStateHandler +{ + + protected TxActiveStateHandler ( CompositeTransactionImp ct ) + { + super ( ct ); + + } + + protected TxState getState () + { + + return TxState.ACTIVE; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TxRollbackOnlyStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxRollbackOnlyStateHandler.java new file mode 100644 index 000000000..f59d408da --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxRollbackOnlyStateHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * A rollback only state handler. + */ + +class TxRollbackOnlyStateHandler extends TransactionStateHandler +{ + private static final Logger LOGGER = LoggerFactory.createLogger(TxRollbackOnlyStateHandler.class); + + protected TxRollbackOnlyStateHandler ( CompositeTransactionImp ct , + TransactionStateHandler handler ) + { + super ( ct , handler ); + } + + protected RecoveryCoordinator addParticipant ( Participant participant ) + throws SysException, java.lang.IllegalStateException + { + // see case 28843: accept the participant, but call rollback immediately + try { + participant.rollback(); + } catch ( Exception ignore ) { + LOGGER.logTrace("Ignoring exception on participant rollback",ignore); + } + + return getCT().getCoordinatorImp(); + } + + protected CompositeTransaction createSubTransaction () throws SysException, + IllegalStateException + { + // creating a new subtx is not allowed to avoid that people keep adding work to that one. + throw new IllegalStateException ( "Transaction is marked for rollback" ); + } + + + protected void commit () throws SysException, + java.lang.IllegalStateException, RollbackException + { + rollbackWithStateCheck(); + throw new RollbackException ( "Transaction set to rollback only" ); + } + + protected TxState getState() + { + return TxState.MARKED_ABORT; + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TxTerminatedStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxTerminatedStateHandler.java new file mode 100644 index 000000000..7a90983b1 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxTerminatedStateHandler.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.Synchronization; +import com.atomikos.icatch.SysException; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.TxState; + +/** + * A transaction terminated state handler. + */ + +class TxTerminatedStateHandler extends TransactionStateHandler +{ + private static Logger LOGGER = LoggerFactory.createLogger(TxTerminatedStateHandler.class); + + private boolean transactionCommitted; + + protected TxTerminatedStateHandler ( CompositeTransactionImp ct , + TransactionStateHandler handler , boolean transactionCommitted ) + { + super ( ct , handler ); + this.transactionCommitted = transactionCommitted; + } + + protected CompositeTransaction createSubTransaction () throws SysException, + IllegalStateException + { + throw new IllegalStateException ( "Transaction no longer active" ); + } + + protected RecoveryCoordinator addParticipant ( Participant participant ) + throws SysException, java.lang.IllegalStateException + { + + if ( !transactionCommitted ) { + // can happen after resuming a timedout transaction; + // accept the participant, but call rollback immediately + // cf JBoss + try { + participant.rollback(); + } catch ( Exception ignore ) { + LOGGER.logTrace("Ignoring error on participant rollback",ignore); + } + } else { + // transaction already committed, possibly with 2PC + // so adding more work is unacceptable + throw new IllegalStateException ( "Transaction no longer active" ); + } + + return getCT().getCoordinatorImp(); + } + + protected void registerSynchronization ( Synchronization sync ) + throws IllegalStateException, UnsupportedOperationException, SysException + { + throw new IllegalStateException ( "Transaction no longer active" ); + } + + protected void addSubTxAwareParticipant ( SubTxAwareParticipant subtxaware ) + throws SysException, java.lang.IllegalStateException + { + + if ( transactionCommitted ) + throw new IllegalStateException ( "Transaction no longer active" ); + else { + // accept the participant, but call rollback immediately + // needed to allow JBoss integration for marked aborts + subtxaware.rolledback ( getCT() ); + } + } + + protected void rollbackWithStateCheck () throws java.lang.IllegalStateException, SysException + + { + if (transactionCommitted) throw new IllegalStateException ( "Transaction no longer active" ); + } + + protected void commit () throws SysException, + java.lang.IllegalStateException, RollbackException + { + if (!transactionCommitted) throw new IllegalStateException ( "Transaction no longer active" ); + } + + protected TxState getState() + { + if ( transactionCommitted ) return getCT().getCoordinatorImp().getStateWithTwoPhaseCommitDecision(); + else { + // Because we have no rolled back state, we return marked abort. + // This should be indistinguishable for the client: a later rollback + // will fail, but that will seem like an intermediate timeout rollback + // of the transaction service + return TxState.MARKED_ABORT; + } + + } +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/imp/TxTerminatingStateHandler.java b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxTerminatingStateHandler.java new file mode 100644 index 000000000..0934753c4 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/imp/TxTerminatingStateHandler.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import java.util.Stack; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.Participant; +import com.atomikos.icatch.RecoveryCoordinator; +import com.atomikos.icatch.SubTxAwareParticipant; +import com.atomikos.icatch.Synchronization; +import com.atomikos.recovery.TxState; + + +/** + * + * + * A transaction terminating state handler. The purpose of this + * state is to detect and prevent concurrency effects when + * a timout/rollback thread interleaves with an application/commit + * thread. Due to the testAndSet functionality, the first such + * interleaving thread will be allowed to proceed, whereas the + * second thread will see this state and its safety checks. + */ + +class TxTerminatingStateHandler extends TransactionStateHandler +{ + + + private boolean committing; + + public TxTerminatingStateHandler ( boolean committing , CompositeTransactionImp ct, + TransactionStateHandler handler ) + { + super ( ct , handler ); + this.committing = committing; + } + + private void reject() throws IllegalStateException + { + if ( committing ) + throw new IllegalStateException ( "Transaction is committing - adding a new participant is not allowed" ); + else + throw new IllegalStateException ( "Transaction is rolling back - adding a new participant is not allowed" ); + } + + /** + * @return ACTIVE or JPA implementations like EclipseJPA will not attempt to flush changes before commit! + */ + protected TxState getState() + { + return TxState.ACTIVE; + } + + protected RecoveryCoordinator addParticipant ( Participant p ) + { + if ( ! committing ) reject(); + + return super.addParticipant ( p ); + } + + protected void addSubTxAwareParticipant ( SubTxAwareParticipant p ) + { + if ( ! committing ) reject(); + + super.addSubTxAwareParticipant ( p ); + } + + protected void addSynchronizations ( Stack s ) + { + reject(); + } + + protected void commit() + { + //reject in all cases, even if committing: the application thread should commit, and only once + reject(); + } + + protected CompositeTransaction createSubTransaction() + { + reject(); + return null; + } + + protected void registerSynchronization() + { + reject(); + } + + protected void rollbackWithStateCheck() + { + if ( committing ) reject(); + + //return silently if rolling back already: rollback twice should be the same as once + } + + protected void setRollbackOnly() + { + + if ( committing ) { + //happens legally if synchronizations call this method! + super.setRollbackOnly(); + } //else ignore: already rolling back; this is consistent with what is asked + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/icatch/provider/imp/AssemblerImp.java b/public/transactions/src/main/java/com/atomikos/icatch/provider/imp/AssemblerImp.java new file mode 100644 index 000000000..fea364877 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/icatch/provider/imp/AssemblerImp.java @@ -0,0 +1,259 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.provider.imp; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.ServiceLoader; + +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.SysException; +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.event.EventListener; +import com.atomikos.icatch.imp.CompositeTransactionManagerImp; +import com.atomikos.icatch.imp.TransactionServiceImp; +import com.atomikos.icatch.provider.Assembler; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.icatch.provider.TransactionServiceProvider; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.persistence.imp.StateRecoveryManagerImp; +import com.atomikos.publish.EventPublisher; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.OltpLog; +import com.atomikos.recovery.OltpLogFactory; +import com.atomikos.recovery.RecoveryLog; +import com.atomikos.recovery.fs.CachedRepository; +import com.atomikos.recovery.fs.FileSystemRepository; +import com.atomikos.recovery.fs.InMemoryRepository; +import com.atomikos.recovery.fs.OltpLogImp; +import com.atomikos.recovery.fs.RecoveryLogImp; +import com.atomikos.recovery.fs.Repository; +import com.atomikos.util.Atomikos; +import com.atomikos.util.ClassLoadingHelper; +import com.atomikos.util.UniqueIdMgr; + +public class AssemblerImp implements Assembler { + + private static final String DEFAULT_PROPERTIES_FILE_NAME = "transactions-defaults.properties"; + + private static final String JTA_PROPERTIES_FILE_NAME = "jta.properties"; + + private static final String TRANSACTIONS_PROPERTIES_FILE_NAME = "transactions.properties"; + + private static final int MAX_TID_LENGTH = 64; //XID limitation + + private static com.atomikos.logging.Logger LOGGER = LoggerFactory.createLogger(AssemblerImp.class); + + + private void loadPropertiesFromClasspath(Properties p, String fileName){ + URL url = null; + + //first look in application classpath (cf ISSUE 10091) + url = ClassLoadingHelper.loadResourceFromClasspath(getClass(), fileName); + if (url == null) { + url = getClass().getClassLoader().getSystemResource ( fileName ); + } + if (url != null) { + loadPropertiesFromUrl(p, url); + } else { + LOGGER.logTrace("Could not find expected property file: " + fileName); + } + } + + private void loadPropertiesFromUrl(Properties p, URL url) { + InputStream in; + try { + in = url.openStream(); + p.load(in); + in.close(); + LOGGER.logInfo("Loaded " + url.toString()); + } catch (IOException e) { + LOGGER.logTrace("Failed to load property file: " + url.toString(), e); + } + } + + /** + * Called by ServiceLoader. + */ + public AssemblerImp() { + } + + @Override + public ConfigProperties initializeProperties() { + Properties defaults = new Properties(); + loadPropertiesFromClasspath(defaults, DEFAULT_PROPERTIES_FILE_NAME); + Properties transactionsProperties = new Properties(defaults); + loadPropertiesFromClasspath(transactionsProperties, TRANSACTIONS_PROPERTIES_FILE_NAME); + Properties jtaProperties = new Properties(transactionsProperties); + loadPropertiesFromClasspath(jtaProperties, JTA_PROPERTIES_FILE_NAME); + Properties customProperties = new Properties(jtaProperties); + loadPropertiesFromCustomFilePath(customProperties); + Properties finalProperties = new Properties(customProperties); + ConfigProperties configProperties = new ConfigProperties(finalProperties); + checkRegistration(configProperties); + return configProperties; + } + private void checkRegistration(ConfigProperties configProperties) { + if (configProperties.getCompletedProperties().getProperty("com.atomikos.icatch.registered") == null) { + String message = + "Thanks for using Atomikos! This installation is not registered yet. \n" + + "REGISTER FOR FREE at http://www.atomikos.com/Main/RegisterYourDownload and receive:\n" + + "- tips & advice \n" + + "- working demos \n" + + "- access to the full documentation \n" + + "- special exclusive bonus offers not available to others \n" + + "- everything you need to get the most out of using Atomikos!"; + + LOGGER.logWarning(message); + System.out.println(message); + } + if (Atomikos.isEvaluationVersion()) { + String message ="This product (ExtremeTransactions) is licensed for DEVELOPMENT ONLY - for production use you need to purchase a subscription via https://www.atomikos.com/Main/BuyOnline"; + LOGGER.logWarning(message); + System.err.println(message); + } + } + + private void loadPropertiesFromCustomFilePath(Properties customProperties) { + String customFilePath = System.getProperty(ConfigProperties.FILE_PATH_PROPERTY_NAME); + if (customFilePath != null) { + File file = new File(customFilePath); + URL url; + try { + url = file.toURL(); + loadPropertiesFromUrl(customProperties, url); + } catch (MalformedURLException e) { + LOGGER.logFatal("File not found: " + customFilePath); + } + } + } + + + private void logProperties(Properties properties) { + LOGGER.logInfo("USING core version: " + Atomikos.VERSION); + for (Entry entry : properties.entrySet()) { + LOGGER.logInfo("USING: " + entry.getKey() + " = " + entry.getValue()); + } + } + + private void findAllEventListenersInClassPath() { + ServiceLoader loader = ServiceLoader.load(EventListener.class, EventPublisher.class.getClassLoader()); + for (EventListener l : loader) { + EventPublisher.INSTANCE.registerEventListener(l); + } + } + + @Override + public TransactionServiceProvider assembleTransactionService() { + findAllEventListenersInClassPath(); + RecoveryLog recoveryLog =null; + ConfigProperties configProperties = Configuration.getConfigProperties(); + logProperties(configProperties.getCompletedProperties()); + String tmUniqueName = configProperties.getTmUniqueName(); + + long maxTimeout = configProperties.getMaxTimeout(); + int maxActives = configProperties.getMaxActives(); + + OltpLog oltpLog = createOltpLogFromClasspath(); + if (oltpLog == null) { + LOGGER.logInfo("Using default (local) logging and recovery..."); + Repository repository = createRepository(configProperties); + oltpLog = createOltpLog(repository); + //??? Assemble recoveryLog + recoveryLog = createRecoveryLog(repository); + } else { + //logcloud + if (oltpLog instanceof RecoveryLog) { + recoveryLog = (RecoveryLog) oltpLog; + } + } + StateRecoveryManagerImp recoveryManager = new StateRecoveryManagerImp(); + recoveryManager.setOltpLog(oltpLog); + UniqueIdMgr idMgr = new UniqueIdMgr ( tmUniqueName ); + int overflow = idMgr.getMaxIdLengthInBytes() - MAX_TID_LENGTH; + if ( overflow > 0 ) { + // see case 73086 + String msg = "Value too long : " + tmUniqueName; + LOGGER.logFatal ( msg ); + throw new SysException(msg); + } + return new TransactionServiceImp(tmUniqueName, recoveryManager, idMgr, maxTimeout, maxActives, true, recoveryLog); + } + + private Repository createRepository(ConfigProperties configProperties) { + boolean enableLogging = configProperties.getEnableLogging(); + Repository repository; + if (enableLogging) { + try { + repository = createCoordinatorLogEntryRepository(); + } catch ( LogException le ) { + throw new SysException ( "Error in init: " + le.getMessage (), le ); + } + } else { + repository = createInMemoryCoordinatorLogEntryRepository(configProperties); + } + return repository; + } + + private OltpLog createOltpLogFromClasspath() { + OltpLog ret = null; + ServiceLoader loader = ServiceLoader.load(OltpLogFactory.class,Configuration.class.getClassLoader()); + int i = 0; + for (OltpLogFactory l : loader ) { + ret = l.createOltpLog(); + i++; + } + if (i > 1) { + String msg = "More than one OltpLogFactory found in classpath - error in configuration!"; + LOGGER.logFatal(msg); + throw new SysException(msg); + } + return ret; + } + + private Repository createInMemoryCoordinatorLogEntryRepository( + ConfigProperties configProperties) { + InMemoryRepository inMemoryCoordinatorLogEntryRepository = new InMemoryRepository(); + inMemoryCoordinatorLogEntryRepository.init(); + return inMemoryCoordinatorLogEntryRepository; + } + + private RecoveryLog createRecoveryLog(Repository repository) { + RecoveryLogImp recoveryLog = new RecoveryLogImp(); + recoveryLog.setRepository(repository); + return recoveryLog; + } + + private OltpLog createOltpLog(Repository repository) { + OltpLogImp oltpLog = new OltpLogImp(); + oltpLog.setRepository(repository); + return oltpLog; + } + + private CachedRepository createCoordinatorLogEntryRepository() throws LogException { + InMemoryRepository inMemoryCoordinatorLogEntryRepository = new InMemoryRepository(); + inMemoryCoordinatorLogEntryRepository.init(); + FileSystemRepository backupCoordinatorLogEntryRepository = new FileSystemRepository(); + backupCoordinatorLogEntryRepository.init(); + CachedRepository repository = new CachedRepository(inMemoryCoordinatorLogEntryRepository, backupCoordinatorLogEntryRepository); + repository.init(); + return repository; + } + + + @Override + public CompositeTransactionManager assembleCompositeTransactionManager() { + return new CompositeTransactionManagerImp(); + } +} diff --git a/public/transactions/src/main/java/com/atomikos/persistence/RecoverableCoordinator.java b/public/transactions/src/main/java/com/atomikos/persistence/RecoverableCoordinator.java new file mode 100644 index 000000000..ab0217379 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/persistence/RecoverableCoordinator.java @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.persistence; + +import com.atomikos.finitestates.FSMEnterEventSource; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; + +/** + * A type of stateful objects whose state is guaranteed to be recoverable. The + * logging is done based on PreEnter events. The guarantee offered is the + * following: IF a recoverable state is reached by the instance, then its image + * is GUARANTEED to be recoverable. The inverse does NOT hold: the fact that an + * object is recovered in some state does NOT mean that the state was reached. + * Indeed, other PreEnter listeners may still have prevented the transition in + * the last moment. However, this should not be a real problem; applications + * should take this into account. + */ + +public interface RecoverableCoordinator extends FSMEnterEventSource +{ + + PendingTransactionRecord getPendingTransactionRecord(TxState state); + +} diff --git a/public/transactions/src/main/java/com/atomikos/persistence/StateRecoveryManager.java b/public/transactions/src/main/java/com/atomikos/persistence/StateRecoveryManager.java new file mode 100644 index 000000000..b8df3d499 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/persistence/StateRecoveryManager.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.persistence; + +import com.atomikos.recovery.LogException; +/** + * A state recovery manager is responsible for reconstructing StateRecoverable + * instances based on the history. + */ + +public interface StateRecoveryManager +{ + + /** + * Register a staterecoverable with the recovery manager service. + * + * @param staterecoverable + * The object that wants recoverable states. + */ + + public void register ( RecoverableCoordinator staterecoverable ); + + /** + * Shutdown. + * + * @exception LogException + * For underlying log failure. + */ + + public void close () throws com.atomikos.recovery.LogException; + + +} diff --git a/public/transactions/src/main/java/com/atomikos/persistence/imp/LogFileLock.java b/public/transactions/src/main/java/com/atomikos/persistence/imp/LogFileLock.java new file mode 100644 index 000000000..cf03ad3d0 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/persistence/imp/LogFileLock.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.persistence.imp; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.util.concurrent.TimeUnit; + +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.LogException; +import com.atomikos.thread.InterruptedExceptionHelper; + +public class LogFileLock { + + private static Logger LOGGER = LoggerFactory.createLogger(LogFileLock.class); + private static final String FILE_SEPARATOR = String.valueOf(File.separatorChar); + private File lockfileToPreventDoubleStartup_; + private FileOutputStream lockfilestream_ = null; + private FileLock lock_ = null; + private String dir; + private String fileName; + private int lockAcquisitionMaxAttempts; + private long lockAcquisitionRetryDelay; + + public LogFileLock(String dir, String fileName) { + if(!dir.endsWith(FILE_SEPARATOR)) { + dir += FILE_SEPARATOR; + } + this.dir = dir; + this.fileName = fileName; + this.lockAcquisitionMaxAttempts = Configuration.getConfigProperties().getLockAcquisitionMaxAttempts(); + this.lockAcquisitionRetryDelay = Configuration.getConfigProperties().getLockAcquisitionRetryDelay(); + } + + public void acquireLock() throws LogException { + try { + File parent = new File(dir); + if(!parent.exists()) { + parent.mkdirs(); + } + lockfileToPreventDoubleStartup_ = new File(dir, fileName + ".lck"); + lockfilestream_ = new FileOutputStream(lockfileToPreventDoubleStartup_); + FileLock tryLock = tryAcquiringLock(1); + if (tryLock != null) { + lock_ = tryLock; + lockfileToPreventDoubleStartup_.deleteOnExit(); + return; + } + } catch (IOException failedToGetLock) { + // fail like below... + } + String msg = "The specified log seems to be in use already: " + fileName + " in "+ dir+". Make sure that no other instance is running, or kill any pending process if needed."; + LOGGER.logFatal(msg); + throw new LogException(msg); + } + + private FileLock tryAcquiringLock(int currentTryCount) throws IOException { + try { + FileLock fileLock = lockfilestream_.getChannel().tryLock(); + if(fileLock != null) { + return fileLock; + } + } catch (OverlappingFileLockException | IOException failedToGetLock) { + LOGGER.logTrace("Trying to get lock failed with exception...", failedToGetLock); + } + // if we are here: either fileLock was null, OR we got an exception => outcome is the same: retry or give up + if (currentTryCount < lockAcquisitionMaxAttempts) { + LOGGER.logWarning("Couldn't acquire lock, will try again in " + lockAcquisitionRetryDelay + " millis..."); + try { + TimeUnit.MILLISECONDS.sleep(lockAcquisitionRetryDelay); + } catch (InterruptedException ie) { + InterruptedExceptionHelper.handleInterruptedException(ie); + } + return tryAcquiringLock(currentTryCount + 1); + } + // out of retries and still no prior return with a lock => give up and fail + throw new IOException("The file lock couldn't be acquired and no more retries allowed."); + } + + public void releaseLock() { + try { + if (lock_ != null) { + lock_.release(); + } + if (lockfilestream_ != null) + lockfilestream_.close(); + } catch (IOException e) { + LOGGER.logWarning("Error releasing file lock: " + e.getMessage()); + } finally { + lock_ = null; + } + + if (lockfileToPreventDoubleStartup_ != null) { + lockfileToPreventDoubleStartup_.delete(); + lockfileToPreventDoubleStartup_ = null; + } + } +} diff --git a/public/transactions/src/main/java/com/atomikos/persistence/imp/StateRecoveryManagerImp.java b/public/transactions/src/main/java/com/atomikos/persistence/imp/StateRecoveryManagerImp.java new file mode 100644 index 000000000..cd85fd2ee --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/persistence/imp/StateRecoveryManagerImp.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.persistence.imp; + +import com.atomikos.finitestates.FSMEnterEvent; +import com.atomikos.finitestates.FSMEnterListener; +import com.atomikos.persistence.RecoverableCoordinator; +import com.atomikos.persistence.StateRecoveryManager; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.OltpLog; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; +import com.atomikos.util.Assert; + +/** + * Default implementation of a state recovery manager. + */ + +public class StateRecoveryManagerImp implements StateRecoveryManager, FSMEnterListener +{ + /** + * @see StateRecoveryManager + */ + public void register(RecoverableCoordinator staterecoverable) { + Assert.notNull("illegal attempt to register null staterecoverable", staterecoverable); + TxState[] states = TxState.values(); + for (TxState txState : states) { + if (txState.isRecoverableState()) { + staterecoverable.addFSMEnterListener(this, txState); + } + } + } + + /** + * @see FSMPreEnterListener + */ + public void preEnter(FSMEnterEvent event) throws IllegalStateException { + TxState state = event.getState(); + + RecoverableCoordinator source = (RecoverableCoordinator) event.getSource(); + PendingTransactionRecord pendingTransactionRecord = source.getPendingTransactionRecord(state); + if (pendingTransactionRecord != null) { + // null images are not logged as per the Recoverable contract + try { + oltpLog.write(pendingTransactionRecord); + + } catch (Exception le) { + throw new IllegalStateException("could not flush state image " + le.getMessage() + " " + le.getClass().getName(), le); + } + } + + } + + /** + * @see StateRecoveryManager + */ + public void close() throws LogException { + oltpLog.close(); + } + + private OltpLog oltpLog; + + public void setOltpLog(OltpLog oltpLog) { + this.oltpLog = oltpLog; + } + + @Override + public void entered(FSMEnterEvent e) {} + + + + +} diff --git a/public/transactions/src/main/java/com/atomikos/recovery/fs/CachedRepository.java b/public/transactions/src/main/java/com/atomikos/recovery/fs/CachedRepository.java new file mode 100644 index 000000000..f6d43b012 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/recovery/fs/CachedRepository.java @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.fs; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.LogWriteException; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; + +public class CachedRepository implements Repository { + + private static final Logger LOGGER = LoggerFactory.createLogger(CachedRepository.class); + private boolean corrupt = false; + private final InMemoryRepository inMemoryCoordinatorLogEntryRepository; + + private final Repository backupCoordinatorLogEntryRepository; + + private volatile long numberOfPutsSinceLastCheckpoint = 0; + private long checkpointInterval; + private long forgetOrphanedLogEntriesDelay; + public CachedRepository( + InMemoryRepository inMemoryCoordinatorLogEntryRepository, + Repository backupCoordinatorLogEntryRepository) { + this.inMemoryCoordinatorLogEntryRepository = inMemoryCoordinatorLogEntryRepository; + this.backupCoordinatorLogEntryRepository = backupCoordinatorLogEntryRepository; + } + + @Override + public void init() { + //populate inMemoryCoordinatorLogEntryRepository with backup data + + ConfigProperties configProperties = Configuration.getConfigProperties(); + checkpointInterval = configProperties.getCheckpointInterval(); + forgetOrphanedLogEntriesDelay = configProperties.getForgetOrphanedLogEntriesDelay(); + + try { + Collection coordinatorLogEntries = backupCoordinatorLogEntryRepository.getAllCoordinatorLogEntries(); + for (PendingTransactionRecord coordinatorLogEntry : coordinatorLogEntries) { + inMemoryCoordinatorLogEntryRepository.put(coordinatorLogEntry.id, coordinatorLogEntry); + } + + performCheckpoint(); + } catch (LogException e) { + LOGGER.logFatal("Corrupted transaction log cache - restart JVM", e); + corrupt = true; + } + + } + + @Override + public synchronized void put(String id, PendingTransactionRecord coordinatorLogEntry) + throws IllegalArgumentException, LogWriteException { + + try { + if(needsCheckpoint()){ + performCheckpoint(); + } + backupCoordinatorLogEntryRepository.put(id, coordinatorLogEntry); + inMemoryCoordinatorLogEntryRepository.put(id, coordinatorLogEntry); + numberOfPutsSinceLastCheckpoint++; + } catch (Exception e) { + performCheckpoint(); + } + } + + private synchronized void performCheckpoint() throws LogWriteException { + try { + Collection coordinatorLogEntries = purgeExpiredCoordinatorLogEntriesInStateAborting(); + backupCoordinatorLogEntryRepository.writeCheckpoint(coordinatorLogEntries); + inMemoryCoordinatorLogEntryRepository.writeCheckpoint(coordinatorLogEntries); + numberOfPutsSinceLastCheckpoint=0; + corrupt = false; + } catch (LogWriteException corrupted) { + LOGGER.logWarning("Failed to write checkpoint - will try again later", corrupted); + corrupt = true; + throw corrupted; + } catch (Exception corrupted) { + LOGGER.logWarning("Failed to write checkpoint - will try again later", corrupted); + corrupt = true; + throw new LogWriteException(corrupted); + } + } + + private Collection purgeExpiredCoordinatorLogEntriesInStateAborting() { + Set ret = new HashSet(); + long now = System.currentTimeMillis(); + Collection coordinatorLogEntries = inMemoryCoordinatorLogEntryRepository.getAllCoordinatorLogEntries(); + for (PendingTransactionRecord coordinatorLogEntry : coordinatorLogEntries) { + if (!canBeForgotten(now, coordinatorLogEntry)){ + ret.add(coordinatorLogEntry); + } + } + return ret; + } + + protected boolean canBeForgotten(long now, + PendingTransactionRecord coordinatorLogEntry) { + boolean ret = false; + if ((coordinatorLogEntry.expires+forgetOrphanedLogEntriesDelay) < now) { + TxState entryState = coordinatorLogEntry.state; + if (!entryState.isHeuristic()) { + //can happen for commits or aborts where the 'terminated' does not make it to the log after the resource aborted/committed + LOGGER.logWarning("Purging orphaned entry from log: " + coordinatorLogEntry); + ret = true; + } + + } + return ret; + } + + private boolean needsCheckpoint() { + return numberOfPutsSinceLastCheckpoint>=checkpointInterval || corrupt; + } + + @Override + public PendingTransactionRecord get(String coordinatorId) throws LogReadException { + return inMemoryCoordinatorLogEntryRepository.get(coordinatorId); + } + + + @Override + public Collection findAllCommittingCoordinatorLogEntries() throws LogReadException { + return inMemoryCoordinatorLogEntryRepository.findAllCommittingCoordinatorLogEntries(); + } + + + + @Override + public void close() { + backupCoordinatorLogEntryRepository.close(); + inMemoryCoordinatorLogEntryRepository.close(); + } + + @Override + public Collection getAllCoordinatorLogEntries() { + return inMemoryCoordinatorLogEntryRepository.getAllCoordinatorLogEntries(); + } + + @Override + public void writeCheckpoint( + Collection checkpointContent) { + throw new UnsupportedOperationException(); + } +} diff --git a/public/transactions/src/main/java/com/atomikos/recovery/fs/FileSystemRepository.java b/public/transactions/src/main/java/com/atomikos/recovery/fs/FileSystemRepository.java new file mode 100644 index 000000000..ea265d199 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/recovery/fs/FileSystemRepository.java @@ -0,0 +1,229 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.fs; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectStreamException; +import java.io.StreamCorruptedException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.atomikos.icatch.config.Configuration; +import com.atomikos.icatch.provider.ConfigProperties; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.persistence.imp.LogFileLock; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.LogWriteException; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.util.VersionedFile; + +public class FileSystemRepository implements Repository { + + private static final Logger LOGGER = LoggerFactory.createLogger(FileSystemRepository.class); + private VersionedFile file; + private FileChannel rwChannel = null; + private LogFileLock lock_; + + @Override + public void init() throws LogException { + ConfigProperties configProperties = Configuration.getConfigProperties(); + String baseDir = configProperties.getLogBaseDir(); + String baseName = configProperties.getLogBaseName(); + LOGGER.logDebug("baseDir " + baseDir); + LOGGER.logDebug("baseName " + baseName); + lock_ = new LogFileLock(baseDir, baseName); + LOGGER.logDebug("LogFileLock " + lock_); + lock_.acquireLock(); + file = new VersionedFile(baseDir, baseName, ".log"); + + } + + @Override + public void put(String id, PendingTransactionRecord pendingTransactionRecord) + throws IllegalArgumentException, LogWriteException { + + try { + initChannelIfNecessary(); + write(pendingTransactionRecord, true); + } catch (IOException e) { + throw new LogWriteException(e); + } + } + + private synchronized void initChannelIfNecessary() + throws FileNotFoundException { + if (rwChannel == null) { + rwChannel = file.openNewVersionForNioWriting(); + } + } + private void write(PendingTransactionRecord pendingTransactionRecord, + boolean flushImmediately) throws IOException { + String str = pendingTransactionRecord.toRecord(); + byte[] buffer = str.getBytes(); + ByteBuffer buff = ByteBuffer.wrap(buffer); + writeToFile(buff, flushImmediately); + } + + private synchronized void writeToFile(ByteBuffer buff, boolean force) + throws IOException { + rwChannel.write(buff); + if (force) { + rwChannel.force(false); + } + } + + @Override + public PendingTransactionRecord get(String coordinatorId) throws LogReadException { + throw new UnsupportedOperationException(); + } + + @Override + public Collection findAllCommittingCoordinatorLogEntries() throws LogReadException { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getAllCoordinatorLogEntries() throws LogReadException { + FileInputStream fis = null; + try { + fis = file.openLastValidVersionForReading(); + } catch (FileNotFoundException firstStart) { + // the file could not be opened for reading; + // merely return the default empty vector + } + if (fis != null) { + return readFromInputStream(fis); + } + //else + return Collections.emptyList(); + } + + public static Collection readFromInputStream( + InputStream in) throws LogReadException { + Map coordinatorLogEntries = new HashMap(); + BufferedReader br = null; + try { + InputStreamReader isr = new InputStreamReader(in); + br = new BufferedReader(isr); + coordinatorLogEntries = readContent(br); + } catch (Exception e) { + LOGGER.logFatal("Error in recover", e); + throw new LogReadException(e); + } finally { + closeSilently(br); + } + return coordinatorLogEntries.values(); + } + + static Map readContent(BufferedReader br) + throws IOException { + + Map coordinatorLogEntries = new HashMap(); + String line = null; + try { + + while ((line = br.readLine()) != null) { + if (line.startsWith("{\"id\"")) { + String msg = "Detected old log file format - please terminate all transactions under the old release first!"; + throw new IOException(msg); + } + PendingTransactionRecord coordinatorLogEntry = PendingTransactionRecord.fromRecord(line); + coordinatorLogEntries.put(coordinatorLogEntry.id,coordinatorLogEntry); + } + + } catch (java.io.EOFException unexpectedEOF) { + LOGGER.logTrace( + "Unexpected EOF - logfile not closed properly last time?", + unexpectedEOF); + // merely return what was read so far... + } catch (StreamCorruptedException unexpectedEOF) { + LOGGER.logTrace( + "Unexpected EOF - logfile not closed properly last time?", + unexpectedEOF); + // merely return what was read so far... + } catch (ObjectStreamException unexpectedEOF) { + LOGGER.logTrace( + "Unexpected EOF - logfile not closed properly last time?", + unexpectedEOF); + // merely return what was read so far... + } catch (IllegalArgumentException couldNotParseLastRecord) { + LOGGER.logTrace( + "Unexpected record format - logfile not closed properly last time?", + couldNotParseLastRecord); + // merely return what was read so far... + } catch (RuntimeException unexpectedEOF) { + LOGGER.logWarning("Unexpected EOF - logfile not closed properly last time? " +line +" " + + unexpectedEOF); + } + return coordinatorLogEntries; + } + private static void closeSilently(BufferedReader fis) { + try { + if (fis != null) + fis.close(); + } catch (IOException io) { + LOGGER.logWarning("Fail to close logfile after reading - ignoring"); + } + } + + @Override + public void writeCheckpoint(Collection checkpointContent) throws LogWriteException { + + try { + closeOutput(); + + rwChannel = file.openNewVersionForNioWriting(); + for (PendingTransactionRecord coordinatorLogEntry : checkpointContent) { + write(coordinatorLogEntry, false); + } + rwChannel.force(false); + file.discardBackupVersion(); + } catch (FileNotFoundException firstStart) { + // the file could not be opened for reading; + // merely return the default empty vector + } catch (Exception e) { + LOGGER.logFatal("Failed to write checkpoint", e); + throw new LogWriteException(e); + } + } + + protected void closeOutput() throws IllegalStateException { + try { + if (file != null) { + file.close(); + } + } catch (IOException e) { + throw new IllegalStateException("Error closing previous output", e); + } + } + + @Override + public void close() { + try { + closeOutput(); + } catch (Exception e) { + LOGGER.logWarning("Error closing file - ignoring", e); + } finally { + lock_.releaseLock(); + } + + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/recovery/fs/InMemoryRepository.java b/public/transactions/src/main/java/com/atomikos/recovery/fs/InMemoryRepository.java new file mode 100644 index 000000000..8bfb6c013 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/recovery/fs/InMemoryRepository.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.fs; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; + +public class InMemoryRepository implements Repository { + +private Map storage = new ConcurrentHashMap(); + + + private boolean closed = true; + @Override + public void init() { + closed=false; + } + + + @Override + public synchronized void put(String id, PendingTransactionRecord coordinatorLogEntry) + throws IllegalArgumentException { + PendingTransactionRecord existing = storage.get(id); + if (existing != null && existing == coordinatorLogEntry) { + throw new IllegalArgumentException("cannot put the same coordinatorLogEntry twice"); + } + if(coordinatorLogEntry.state.isFinalState()){ + storage.remove(id); + } else { + storage.put(id, coordinatorLogEntry); + } + } + + @Override + public synchronized PendingTransactionRecord get(String coordinatorId) { + return storage.get(coordinatorId); + } + + @Override + public synchronized Collection findAllCommittingCoordinatorLogEntries() { + Set res = new HashSet(); + Collection allCoordinatorLogEntry = storage.values(); + for (PendingTransactionRecord coordinatorLogEntry : allCoordinatorLogEntry) { + TxState state = coordinatorLogEntry.state; + if(state == TxState.COMMITTING) { + res.add(coordinatorLogEntry); + } else if (state == TxState.IN_DOUBT) { + if(hasCommittingSuperiorCoordnator(coordinatorLogEntry)) { + res.add(coordinatorLogEntry); + } + + } + } + return res; + } + + private boolean hasCommittingSuperiorCoordnator(PendingTransactionRecord coordinatorLogEntry) { + if(coordinatorLogEntry.superiorId == null) return false; + + PendingTransactionRecord superiorCoordinator = storage.get(coordinatorLogEntry.superiorId); + if (superiorCoordinator == null) { + return false; + } + if (superiorCoordinator.state == TxState.COMMITTING) { + return true; + } else { + return hasCommittingSuperiorCoordnator(superiorCoordinator); + } + } + + @Override + public void close() { + storage.clear(); + closed=true; + } + + @Override + public Collection getAllCoordinatorLogEntries() { + return storage.values(); + } + + @Override + public void writeCheckpoint( + Collection checkpointContent) { + storage.clear(); + for (PendingTransactionRecord coordinatorLogEntry : checkpointContent) { + storage.put(coordinatorLogEntry.id, coordinatorLogEntry); + } + + } + + + + public boolean isClosed() { + return closed; + } + +} diff --git a/public/transactions/src/main/java/com/atomikos/recovery/fs/OltpLogImp.java b/public/transactions/src/main/java/com/atomikos/recovery/fs/OltpLogImp.java new file mode 100644 index 000000000..ba1b19a32 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/recovery/fs/OltpLogImp.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.fs; + + +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.LogWriteException; +import com.atomikos.recovery.OltpLog; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.TxState; + +public class OltpLogImp implements OltpLog { + + + private static final Logger LOGGER = LoggerFactory.createLogger(OltpLogImp.class); + private Repository repository; + private String recoveryDomainName; + + public void setRepository(Repository repository) { + this.repository = repository; + this.recoveryDomainName = Configuration.getConfigProperties().getTmUniqueName(); + } + + + @Override + public void write(PendingTransactionRecord pendingTransactionRecord) throws LogWriteException { + TxState state = pendingTransactionRecord.state; + if (pendingTransactionRecord.expires < System.currentTimeMillis()) { + if (state == TxState.IN_DOUBT) { + throw new IllegalArgumentException("Transaction has expired - " + state + " no longer allowed"); + } else if (state == TxState.COMMITTING && pendingTransactionRecord.isRecoveredByDomain(recoveryDomainName) + ){ + throw new IllegalArgumentException("Transaction may have been subject to abort by recovery - COMMITTING no longer allowed"); + } + + } + if(pendingTransactionRecord.state.isRecoverableState()) { + repository.put(pendingTransactionRecord.id, pendingTransactionRecord); + } else { + LOGGER.logWarning("Attempt to log a record with unexpected state : " + pendingTransactionRecord.state); + } + + + } + + @Override + public void close() { + repository.close(); + } + + +} diff --git a/public/transactions/src/main/java/com/atomikos/recovery/fs/RecoveryLogImp.java b/public/transactions/src/main/java/com/atomikos/recovery/fs/RecoveryLogImp.java new file mode 100644 index 000000000..99a9e7770 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/recovery/fs/RecoveryLogImp.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.fs; + + +import java.util.Collection; + +import com.atomikos.icatch.config.Configuration; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.PendingTransactionRecord; +import com.atomikos.recovery.RecoveryLog; +import com.atomikos.recovery.TxState; + +public class RecoveryLogImp implements RecoveryLog { + + private static final Logger LOGGER = LoggerFactory.createLogger(RecoveryLogImp.class); + + private Repository repository; + + private String recoveryDomainName; + + public RecoveryLogImp() { + recoveryDomainName = Configuration.getConfigProperties().getTmUniqueName(); + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public Collection getPendingTransactionRecords() throws LogReadException { + return repository.getAllCoordinatorLogEntries(); + + } + + + @Override + public void closing() { + //don't close repository: OltpLog responsibility. + } + + @Override + public Collection getExpiredPendingCommittingTransactionRecordsAt(long time) throws LogReadException { + Collection allCoordinatorLogEntries = repository.findAllCommittingCoordinatorLogEntries(); + + return PendingTransactionRecord.collectLineages( + (PendingTransactionRecord r) -> r.isLocalRoot(recoveryDomainName) && r.expires < time && r.state == TxState.COMMITTING, + allCoordinatorLogEntries); + } + + + @Override + public Collection getIndoubtTransactionRecords() + throws LogReadException { + Collection allCoordinatorLogEntries = repository.getAllCoordinatorLogEntries(); + return PendingTransactionRecord.collectLineages( + (PendingTransactionRecord r) -> r.state == TxState.IN_DOUBT, + allCoordinatorLogEntries); + } + + @Override + public boolean isActive() { + return true; + } + + @Override + public void forgetTransactionRecords(Collection coordinators) { + try { + for(PendingTransactionRecord entry : coordinators) { + PendingTransactionRecord terminated = entry.markAsTerminated(); + repository.put(terminated.id, terminated); + } + } catch (Exception e) { + LOGGER.logDebug("Unexpected exception - ignoring...", e); + } + } + + @Override + public void recordAsCommitting(String coordinatorId) throws LogException { + PendingTransactionRecord entry = repository.get(coordinatorId); + if (entry != null) { + PendingTransactionRecord committing = entry.markAsCommitting(); + repository.put(committing.id, committing); + } else { + LOGGER.logWarning("Entry for coordinator " + coordinatorId + " no longer found - consider https://www.atomikos.com/Main/LogCloud for full distributed recovery among microservices"); + } + } + + @Override + public void forget(String coordinatorId) { + try { + PendingTransactionRecord rec = repository.get(coordinatorId); + if (rec != null) { // case 179295 + PendingTransactionRecord terminated = rec.markAsTerminated(); + repository.put(terminated.id, terminated); + } + } catch (Exception e) { + LOGGER.logDebug("Unexpected exception - ignoring...", e); + } + } + + @Override + public PendingTransactionRecord get(String coordinatorId) throws LogReadException { + return repository.get(coordinatorId); + } + + @Override + public void closed() { + + } + +} \ No newline at end of file diff --git a/public/transactions/src/main/java/com/atomikos/recovery/fs/Repository.java b/public/transactions/src/main/java/com/atomikos/recovery/fs/Repository.java new file mode 100644 index 000000000..252cb2447 --- /dev/null +++ b/public/transactions/src/main/java/com/atomikos/recovery/fs/Repository.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.recovery.fs; + +import java.util.Collection; + +import com.atomikos.recovery.LogException; +import com.atomikos.recovery.LogReadException; +import com.atomikos.recovery.LogWriteException; +import com.atomikos.recovery.PendingTransactionRecord; + +public interface Repository { + + void init() throws LogException; + + void put(String id,PendingTransactionRecord pendingTransactionRecord) throws LogWriteException; + + PendingTransactionRecord get(String coordinatorId) throws LogReadException; + + Collection findAllCommittingCoordinatorLogEntries() throws LogReadException; + + Collection getAllCoordinatorLogEntries() throws LogReadException; + + void writeCheckpoint(Collection checkpointContent) throws LogWriteException; + + void close(); +} diff --git a/public/transactions/src/main/resources/META-INF/services/com.atomikos.icatch.provider.Assembler b/public/transactions/src/main/resources/META-INF/services/com.atomikos.icatch.provider.Assembler new file mode 100644 index 000000000..d747674b3 --- /dev/null +++ b/public/transactions/src/main/resources/META-INF/services/com.atomikos.icatch.provider.Assembler @@ -0,0 +1 @@ +com.atomikos.icatch.provider.imp.AssemblerImp \ No newline at end of file diff --git a/public/transactions/src/main/resources/transactions-defaults.properties b/public/transactions/src/main/resources/transactions-defaults.properties new file mode 100644 index 000000000..f1c2113c9 --- /dev/null +++ b/public/transactions/src/main/resources/transactions-defaults.properties @@ -0,0 +1,26 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +com.atomikos.icatch.enable_logging=true +com.atomikos.icatch.force_shutdown_on_vm_exit=false +com.atomikos.icatch.checkpoint_interval=500 +com.atomikos.icatch.serial_jta_transactions=true +com.atomikos.icatch.default_jta_timeout=10000 +com.atomikos.icatch.max_timeout=300000 +com.atomikos.icatch.log_base_dir=./ +com.atomikos.icatch.max_actives=50 +com.atomikos.icatch.log_base_name=tmlog +com.atomikos.icatch.forget_orphaned_log_entries_delay=86400000 +com.atomikos.icatch.recovery_delay=${com.atomikos.icatch.default_jta_timeout} +com.atomikos.icatch.oltp_max_retries=5 +com.atomikos.icatch.oltp_retry_interval=10000 +com.atomikos.icatch.allow_subtransactions=true +com.atomikos.icatch.logcloud_datasource_name=logCloudDS +com.atomikos.icatch.throw_on_heuristic=false +com.atomikos.icatch.log_lock_acquisition_max_attempts=3 +com.atomikos.icatch.log_lock_acquisition_retry_delay=1000 diff --git a/public/transactions/src/test/java/com/atomikos/finitestates/FSMImpTestJUnit.java b/public/transactions/src/test/java/com/atomikos/finitestates/FSMImpTestJUnit.java new file mode 100644 index 000000000..8c5de7245 --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/finitestates/FSMImpTestJUnit.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +import com.atomikos.recovery.TxState; + +import junit.framework.TestCase; + +public class FSMImpTestJUnit extends TestCase +{ + + public static TxState INITIAL=TxState.ACTIVE; + public static TxState MIDDLE=TxState.COMMITTING; + public static TxState END=TxState.TERMINATED; + + + private FSM fsm; + private TestListener lstnr1, lstnr2, lstnr3, lstnr4; + + public FSMImpTestJUnit ( String name ) + { + super ( name ); + } + + protected void setUp() + { + fsm = new FSMImp (new Object(), INITIAL); + lstnr1 =new TestListener(); + lstnr2=new TestListener(); + lstnr3=new TestListener(); + lstnr4=new TestListener(); + fsm.addFSMEnterListener(lstnr1, MIDDLE); + fsm.addFSMTransitionListener(lstnr2, INITIAL,MIDDLE); + fsm.addFSMEnterListener(lstnr3, MIDDLE); + fsm.addFSMTransitionListener(lstnr4, INITIAL,MIDDLE); + } + + public void testIllegalTransition() + { + try { + fsm.setState(END); + //should cause exception since not allowed + fail ("ERROR: transition checking not ok"); + } + catch (IllegalStateException ok ) { + } + } + + public void testEnterListenerNotification() + { + fsm.setState(MIDDLE); + if (!lstnr1.isNotified()) + fail ("ERROR: notification does not work"); + } + + public void testTransitionListenerNotification() + { + fsm.setState(MIDDLE); + if (!lstnr2.isNotified()) + fail ("ERROR: notification does not work"); + } + + public void testPreEnterListenerNotification() + { + fsm.setState(MIDDLE); + if (!lstnr3.isNotified()) + fail ("ERROR: notification does not work"); + } + + public void testPreTransitionListenerNotification() + { + fsm.setState(MIDDLE); + if (!lstnr4.isNotified()) + fail ("ERROR: notification does not work"); + } + +} diff --git a/public/transactions/src/test/java/com/atomikos/finitestates/TestListener.java b/public/transactions/src/test/java/com/atomikos/finitestates/TestListener.java new file mode 100644 index 000000000..16982a921 --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/finitestates/TestListener.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.finitestates; + +/** + * + * + *A test listener for FSMImp testing. + */ + +public class TestListener implements FSMEnterListener, + FSMTransitionListener +{ + private boolean notified_=false; + +// public TestListener() +// { +// +// } + + public boolean isNotified() + { + return notified_; + } + + public void resetNotified() + { + notified_=false; + } + + public void entered (FSMEnterEvent e) + { + notified_=true; + } + + public void preEnter (FSMEnterEvent e) + { + notified_=true; + } + + public void beforeTransition (FSMTransitionEvent e) + { + notified_=true; + } + + public void transitionPerformed (FSMTransitionEvent e) + { + notified_=true; + } + + +} diff --git a/public/transactions/src/test/java/com/atomikos/icatch/config/ConfigurationTestJUnit.java b/public/transactions/src/test/java/com/atomikos/icatch/config/ConfigurationTestJUnit.java new file mode 100644 index 000000000..e302498ac --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/icatch/config/ConfigurationTestJUnit.java @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.config; + + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.atomikos.icatch.provider.Assembler; +import com.atomikos.icatch.provider.ConfigProperties; + +public class ConfigurationTestJUnit { + + private static final String CUSTOM_PROPERTY_NAME = "com.atomikos.icatch.bla"; + private static final String CUSTOM_PROPERTY_VALUE = "blabla"; + + private ConfigProperties props; + + @Before + public void setUp() throws Exception { + System.setProperty("com.atomikos.icatch.jta.to.override.by.system", "system.properties.override"); + System.setProperty("com.atomikos.icatch.custom.to.override.by.system", "system.properties.override"); + props = Configuration.getConfigProperties(); + } + + @After + public void tearDown() throws Exception { + System.clearProperty("com.atomikos.icatch.jta.to.override.by.system"); + System.clearProperty("com.atomikos.icatch.file"); + System.clearProperty("com.atomikos.icatch.custom.to.override.by.system"); + Configuration.resetConfigProperties(); + } + + @Test + public void testFindAssemblerInClasspath() { + Assembler assembler = Configuration.getAssembler(); + Assert.assertNotNull(assembler); + } + + @Test + public void testDefaultValueForMaxActives() { + Assert.assertNotNull(props.getProperty("com.atomikos.icatch.max_actives")); + } + + @Test + public void testSpecificPropertyFromPropertiesFileInClasspath() { + Assert.assertNotNull(props.getProperty("com.atomikos.icatch.bla")); + } + + @Test + public void testJtaPropertiesFileInClasspathOverridesDefaults() { + Assert.assertEquals("jta.properties.override", props.getProperty("com.atomikos.icatch.default.to.override.by.jta")); + } + + @Test + public void testTransactionsPropertiesFileInClasspathOverridesDefaults() { + Assert.assertEquals("transactions.properties.override", props.getProperty("com.atomikos.icatch.default.to.override.by.transactions")); + } + + @Test + public void testJtaPropertiesFileOverridesTransactionsPropertiesFile() { + Assert.assertEquals("jta.properties.override", props.getProperty("com.atomikos.icatch.transactions.to.override.by.jta")); + } + + @Test + public void testSystemPropertyOverridesJtaPropertiesFile() { + Assert.assertEquals("system.properties.override", props.getProperty("com.atomikos.icatch.jta.to.override.by.system")); + } + + @Test + public void testCustomPropertiesFileOverridesJtaPropertiesFile() { + Configuration.resetConfigProperties(); + String customFileNamePath ="custom.properties"; + System.setProperty("com.atomikos.icatch.file", customFileNamePath); + ConfigProperties props = Configuration.getConfigProperties(); + Assert.assertEquals("custom.properties.override", props.getProperty("com.atomikos.icatch.jta.to.override.by.custom")); + } + + @Test + public void testSystemPropertyOverridesCustomPropertiesFile() { + Assert.assertEquals("system.properties.override", props.getProperty("com.atomikos.icatch.custom.to.override.by.system")); + } + + @Test + public void testProgrammaticallySetPropertiesAreTakenIntoAccount() { + setCustomProperty(); + ConfigProperties props = Configuration.getConfigProperties(); + Assert.assertEquals(CUSTOM_PROPERTY_VALUE, props.getProperty(CUSTOM_PROPERTY_NAME)); + } + + @Test + public void testSystemPropertyOverridesProgrammaticallySetProperty() { + setCustomProperty(); + final String systemOverride = overrideCustomPropertyAsSystemProperty(); + Assert.assertEquals(systemOverride, props.getProperty(CUSTOM_PROPERTY_NAME)); + } + + private String overrideCustomPropertyAsSystemProperty() { + String ret = CUSTOM_PROPERTY_VALUE + "xxx"; + System.setProperty(CUSTOM_PROPERTY_NAME, ret); + return ret; + } + + private void setCustomProperty() { + props.setProperty(CUSTOM_PROPERTY_NAME, CUSTOM_PROPERTY_VALUE); + } + @After + public void cleanup(){ + System.clearProperty(CUSTOM_PROPERTY_NAME); + System.clearProperty("com.atomikos.icatch.file"); + } + + @Test + public void testPlaceHolderSubstitution() { + Assert.assertEquals("system.properties.override", props.getProperty("com.atomikos.icatch.jta.to.substitute")); + } + + +} diff --git a/public/transactions/src/test/java/com/atomikos/icatch/imp/AbstractJUnitMaxActivesTest.java b/public/transactions/src/test/java/com/atomikos/icatch/imp/AbstractJUnitMaxActivesTest.java new file mode 100644 index 000000000..514154524 --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/icatch/imp/AbstractJUnitMaxActivesTest.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.CompositeTransaction; +import com.atomikos.icatch.CompositeTransactionManager; +import com.atomikos.icatch.config.Configuration; + +import junit.framework.TestCase; + +/** + * + * + * + * + * + */ +public abstract class AbstractJUnitMaxActivesTest extends TestCase +{ + + + + public AbstractJUnitMaxActivesTest ( String name ) + { + super ( name ); + } + + protected abstract void startTS ( int maxActivesValue ); + + protected abstract void stopTS(); + + protected final void setUp() + { + + startTS ( 1 ); + } + + protected final void tearDown() + { + stopTS(); + } + + public void testMaxActives() + throws Exception + { + + CompositeTransactionManager ctm = Configuration.getCompositeTransactionManager(); + CompositeTransaction ct1 = ctm.createCompositeTransaction( 1000 ); + + //new tx should fail + try { + ctm.suspend(); + ctm.createCompositeTransaction ( 200); + throw new Exception ( "Max actives is not respected?"); + } + catch ( IllegalStateException ok ) {} + + ct1.rollback(); + + //now create should work + try { + ct1 = ctm.createCompositeTransaction ( 100 ); + } + catch ( Exception e ) { + throw new Exception ( "Max actives not reached and create fails???"); + } + ct1.rollback(); + } + +} diff --git a/public/transactions/src/test/java/com/atomikos/icatch/imp/ConditionalWaiterTestJUnit.java b/public/transactions/src/test/java/com/atomikos/icatch/imp/ConditionalWaiterTestJUnit.java new file mode 100644 index 000000000..25878dd98 --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/icatch/imp/ConditionalWaiterTestJUnit.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +public class ConditionalWaiterTestJUnit { + + private ConditionalWaiter waiter; + + @Before + public void setUp() { + waiter = new ConditionalWaiter(1000); + } + + @Test + public void testTimeout() { + boolean timeout = waiter.waitWhile(() -> {return true;}); + assertTrue(timeout); + } + + @Test + public void testNoTimeout() { + boolean timeout = waiter.waitWhile(() -> {return false;}); + assertFalse(timeout); + } +} diff --git a/public/transactions/src/test/java/com/atomikos/icatch/imp/RollbackOnlyParticipantTestJUnit.java b/public/transactions/src/test/java/com/atomikos/icatch/imp/RollbackOnlyParticipantTestJUnit.java new file mode 100644 index 000000000..ad775fc6a --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/icatch/imp/RollbackOnlyParticipantTestJUnit.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.HeurCommitException; +import com.atomikos.icatch.HeurHazardException; +import com.atomikos.icatch.HeurMixedException; +import com.atomikos.icatch.HeurRollbackException; +import com.atomikos.icatch.RollbackException; +import com.atomikos.icatch.SysException; + +import junit.framework.TestCase; + +public class RollbackOnlyParticipantTestJUnit extends TestCase { + + private RollbackOnlyParticipant p; + + + protected void setUp() throws Exception { + super.setUp(); + p = new RollbackOnlyParticipant(); + } + + public void testURI() + { + assertNull ( p.getURI() ); + } + + public void testCascadeList() + { + p.setCascadeList(null); + } + + public void testGlobalSiblingCount() + { + p.setGlobalSiblingCount(0); + } + + public void testPrepare() throws SysException, HeurHazardException, HeurMixedException + { + try { + p.prepare(); + fail ( "Prepare works?" ); + } + catch ( RollbackException ok ) {} + } + + public void testCommit() throws SysException, HeurRollbackException, HeurHazardException, HeurMixedException + { + try { + p.commit ( true ); + fail ( "Commit works?" ); + } + catch ( RollbackException ok ) {} + } + + public void testRollback() throws SysException, HeurCommitException, HeurMixedException, HeurHazardException + { + p.rollback(); + } + + public void testForget() + { + p.forget(); + } + +} diff --git a/public/transactions/src/test/java/com/atomikos/icatch/imp/SysExceptionTestJUnit.java b/public/transactions/src/test/java/com/atomikos/icatch/imp/SysExceptionTestJUnit.java new file mode 100644 index 000000000..4c924d2a6 --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/icatch/imp/SysExceptionTestJUnit.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.icatch.imp; + +import com.atomikos.icatch.SysException; + +import junit.framework.TestCase; + +public class SysExceptionTestJUnit extends TestCase +{ + + public void testSysExceptionWithoutNestedErrors() + { + SysException se = new SysException ( "" ); + StackTraceElement[] st = se.getStackTrace(); + assertNotNull ( st ); + assertFalse ( st.length == 0 ); + } + + public void testSysExceptionWithNestedException() + { + Exception exception = new Exception ( ); + exception.fillInStackTrace(); + SysException se = new SysException ( null , exception ); + StackTraceElement[] st = se.getStackTrace(); + assertNotNull ( st ); + assertFalse ( st.length == 0 ); + } + + public void testSysExceptionWithNestedSysException() + { + Exception exception = new SysException ( "test" ); + exception.fillInStackTrace(); + SysException se = new SysException (null , exception ); + StackTraceElement[] st = se.getStackTrace(); + assertNotNull ( st ); + assertFalse ( st.length == 0 ); + } + +} diff --git a/public/transactions/src/test/java/com/atomikos/persistence/imp/LogFileLockTestJUnit.java b/public/transactions/src/test/java/com/atomikos/persistence/imp/LogFileLockTestJUnit.java new file mode 100644 index 000000000..434d2b07b --- /dev/null +++ b/public/transactions/src/test/java/com/atomikos/persistence/imp/LogFileLockTestJUnit.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.persistence.imp; + +import java.io.File; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.atomikos.recovery.LogException; + + +public class LogFileLockTestJUnit { + + private LogFileLock lock; + + @Before + public void setUp() throws Exception { + lock = new LogFileLock("." + File.separatorChar, "LogFileLockTest"); + } + + @After + public void tearDown() throws Exception { + if (lock != null) lock.releaseLock(); + } + + @Test + public void testLockWorksForFirstAcquisition() throws LogException { + lock.acquireLock(); + } + + @Test(expected=LogException.class) + public void testLockFailsForSecondAcquisition() throws LogException { + lock.acquireLock(); + lock.acquireLock(); + } + + @Test + public void testLockWorksAfterAcquisitionAndRelease() throws LogException { + lock.acquireLock(); + lock.releaseLock(); + lock.acquireLock(); + } + +} diff --git a/public/transactions/src/test/resources/custom.properties b/public/transactions/src/test/resources/custom.properties new file mode 100644 index 000000000..97fb32e18 --- /dev/null +++ b/public/transactions/src/test/resources/custom.properties @@ -0,0 +1,10 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +com.atomikos.icatch.jta.to.override.by.custom=custom.properties.override +com.atomikos.icatch.custom.to.override.by.system=default \ No newline at end of file diff --git a/public/transactions/src/test/resources/jta.properties b/public/transactions/src/test/resources/jta.properties new file mode 100644 index 000000000..91c6b6fab --- /dev/null +++ b/public/transactions/src/test/resources/jta.properties @@ -0,0 +1,14 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +com.atomikos.icatch.bla=bla +com.atomikos.icatch.default.to.override.by.jta=jta.properties.override +com.atomikos.icatch.transactions.to.override.by.jta=jta.properties.override +com.atomikos.icatch.jta.to.override.by.system=default +com.atomikos.icatch.jta.to.override.by.custom=default +com.atomikos.icatch.jta.to.substitute=${com.atomikos.icatch.jta.to.override.by.system} \ No newline at end of file diff --git a/public/transactions/src/test/resources/transactions-defaults.properties b/public/transactions/src/test/resources/transactions-defaults.properties new file mode 100644 index 000000000..603f9e3a7 --- /dev/null +++ b/public/transactions/src/test/resources/transactions-defaults.properties @@ -0,0 +1,33 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +com.atomikos.icatch.enable_logging=true +com.atomikos.icatch.force_shutdown_on_vm_exit=false +com.atomikos.icatch.checkpoint_interval=500 +com.atomikos.icatch.serial_jta_transactions=true +com.atomikos.icatch.default_jta_timeout=10000 +com.atomikos.icatch.max_timeout=300000 +com.atomikos.icatch.log_base_dir=./ +com.atomikos.icatch.threaded_2pc=false +com.atomikos.icatch.max_actives=50 +com.atomikos.icatch.log_base_name=tmlog +java.naming.factory.initial=com.sun.jndi.rmi.registry.RegistryContextFactory +com.atomikos.icatch.client_demarcation=false +java.naming.provider.url=rmi://localhost:1099 +com.atomikos.icatch.rmi_export_class=none +com.atomikos.icatch.trust_client_tm=false +com.atomikos.icatch.forget_orphaned_log_entries_delay=1800000 +com.atomikos.icatch.recovery_delay=${com.atomikos.icatch.default_jta_timeout} +com.atomikos.icatch.oltp_max_retries=5 +com.atomikos.icatch.oltp_retry_interval=10000 +com.atomikos.icatch.allow_subtransactions=true +com.atomikos.icatch.log_lock_acquisition_max_attempts=3 +com.atomikos.icatch.log_lock_acquisition_retry_delay=1000 + +com.atomikos.icatch.default.to.override.by.jta=default +com.atomikos.icatch.default.to.override.by.transactions=default \ No newline at end of file diff --git a/public/transactions/src/test/resources/transactions.properties b/public/transactions/src/test/resources/transactions.properties new file mode 100644 index 000000000..ef71d1a9c --- /dev/null +++ b/public/transactions/src/test/resources/transactions.properties @@ -0,0 +1,10 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +com.atomikos.icatch.default.to.override.by.transactions=transactions.properties.override +com.atomikos.icatch.transactions.to.override.by.jta=default \ No newline at end of file diff --git a/public/util/pom.xml b/public/util/pom.xml new file mode 100644 index 000000000..9adf1260b --- /dev/null +++ b/public/util/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.atomikos + ate + 6.0.1-SNAPSHOT + + + atomikos-util + + Atomikos Util + + + com.atomikos + transactions-api + 6.0.1-SNAPSHOT + + + + org.slf4j + slf4j-api + 1.4.0 + true + + + log4j + log4j + 1.2.17 + true + + + org.apache.logging.log4j + log4j-core + 2.2 + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + 1 + false + + **/*TestJUnit.java + + + ${project.build.testOutputDirectory} + + + + + + + diff --git a/public/util/src/main/java/com/atomikos/beans/PropertyException.java b/public/util/src/main/java/com/atomikos/beans/PropertyException.java new file mode 100644 index 000000000..5b73f5e35 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/beans/PropertyException.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.beans; + + /** + * + * + *An exception indicating a failure to set or get a bean property. + */ + +public class PropertyException +extends Exception +{ + private Throwable nested_; + //the nested exception + + + public PropertyException ( String msg ) { + super ( msg ); + } + /** + *Creates a new instance with a nested exception. + *@param nested The nested exception. + */ + + public PropertyException ( Throwable nested ) + { + this ( null , nested ); + } + + /** + *Creates a new instance with a message and nested exception. + *@param msg The message. + *@param nested The nested exception. + */ + + public PropertyException ( String msg , Throwable nested ) + { + super ( msg ); + nested_ = nested; + } + + /** + *Get the nested exception. + *@return Exception The nested exception. + */ + + public Throwable getNestedException() + { + return nested_; + } + + public void printStackTrace() + { + if ( nested_ != null ) nested_.printStackTrace(); + super.printStackTrace(); + } +} diff --git a/public/util/src/main/java/com/atomikos/beans/PropertyUtils.java b/public/util/src/main/java/com/atomikos/beans/PropertyUtils.java new file mode 100644 index 000000000..29faa69ca --- /dev/null +++ b/public/util/src/main/java/com/atomikos/beans/PropertyUtils.java @@ -0,0 +1,329 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.beans; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Smart reflection helper without the need for GUI classes (works on headless servers too). + *

Extracted from the BTM project and re-licensed under Apache 2.0 license. + * © Bitronix 2005, 2006, 2007

+ * + * @author lorban + */ +@SuppressWarnings({"rawtypes","unchecked"}) +public class PropertyUtils +{ + + /** + * Gets all implemented interfaces of a class. + */ + + public static Set> getAllImplementedInterfaces ( Class clazz ) + { + Set ret = null; + + if ( clazz.getSuperclass() != null ) { + //if superclass exists: first add the superclass interfaces!!! + ret = getAllImplementedInterfaces ( clazz.getSuperclass() ); + } + else { + //no superclass: start with empty set + ret = new HashSet(); + } + + + //add the interfaces in this class + Class[] interfaces = clazz.getInterfaces(); + ret.addAll(Arrays.asList(interfaces)); + + return ret; + } + + /** + * Sets a direct or indirect property (dotted property: prop1.prop2.prop3) on the target object. This method tries + * to be smart in the way that intermediate properties currently set to null are set if it is possible to create + * and set an object. Conversions from propertyValue to the proper destination type are performed when possible. + * @param target the target object on which to set the property. + * @param propertyName the name of the property to set. + * @param propertyValue the value of the property to set. + * @throws PropertyException if an error happened while trying to set the property. + */ + public static void setProperty ( Object target, String propertyName, Object propertyValue ) throws PropertyException + { + String[] propertyNames = propertyName.split("\\."); + Object currentTarget = target; + for (int i = 0; i < propertyNames.length -1; i++) { + String name = propertyNames[i]; + Object result = callGetter(currentTarget, name); + if (result == null) { + Class propertyType = getPropertyType(target, name); + try { + result = propertyType.newInstance(); + } catch (InstantiationException ex) { + throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex); + } catch (IllegalAccessException ex) { + throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex); + } + callSetter(currentTarget, name, result); + } + currentTarget = result; + } + + String lastPropertyName = propertyNames[propertyNames.length - 1]; + if (currentTarget instanceof Properties) { + Properties p = (Properties) currentTarget; + p.setProperty(lastPropertyName, propertyValue.toString()); + } + else { + setDirectProperty(currentTarget, lastPropertyName, propertyValue); + } + } + + + /** + * Sets a map of properties on the target object. + * @param target the target object on which to set the properties. + * @param properties a map of String/Object pairs. + * @throws PropertyException if an error happened while trying to set a property. + */ + public static void setProperties ( Object target, Map properties ) throws PropertyException + { + Iterator it = properties.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + String name = (String) entry.getKey(); + Object value = entry.getValue(); + setProperty(target, name, value); + } + } + + /** + * Sets a direct property on the target object. Conversions from propertyValue to the proper destination type + * are performed whenever possible. + * @param target the target object on which to set the property. + * @param propertyName the name of the property to set. + * @param propertyValue the value of the property to set. + * @throws PropertyException if an error happened while trying to set the property. + */ + private static void setDirectProperty ( Object target, String propertyName, Object propertyValue ) throws PropertyException + { + Method setter = getSetter(target, propertyName); + Class parameterType = setter.getParameterTypes()[0]; + try { + if (propertyValue != null) { + Object transformedPropertyValue = convert(propertyValue, parameterType); + setter.invoke(target, new Object[] {transformedPropertyValue}); + } + else { + setter.invoke(target, new Object[] {null}); + } + } catch (IllegalAccessException ex) { + throw new PropertyException("property '" + propertyName + "' is not accessible", ex); + } catch (InvocationTargetException ex) { + throw new PropertyException("property '" + propertyName + "' access threw an exception", ex); + } + } + + private static Object convert ( Object value, Class destinationClass ) + throws PropertyException + { + if (value.getClass() == destinationClass) + return value; + + if (isPrimitiveType(value.getClass())) + return value; + + if (value.getClass() == String.class && isPrimitiveType(destinationClass)) { + return convertStringToPrimitive((String) value, destinationClass); + } + + if(Set.class.isAssignableFrom(destinationClass)){ + if(value.getClass() == String.class) { + return convertStringToSet((String)value); + } + } + + if(destinationClass.isAssignableFrom(value.getClass())){ + return value; + } + + throw new PropertyException("cannot convert values of type '" + value.getClass().getName() + "' into type '" + destinationClass + "'"); + } + + private static Set convertStringToSet(String propertyValue) { + String[] elements=propertyValue.split(","); + Set result= new HashSet(); + for (String element: elements) { + result.add(element); + } + + return result; + } + + private static Object convertStringToPrimitive(String value, Class destinationClass) { + if ((destinationClass == int.class || destinationClass == Integer.class)) { + return new Integer(value); + } + + if ((destinationClass == boolean.class || destinationClass == Boolean.class)) { + return Boolean.valueOf(value); + } + + if ((destinationClass == short.class || destinationClass == Short.class)) { + return new Short(value); + } + + if ((destinationClass == byte.class || destinationClass == Byte.class)) { + return new Byte(value); + } + + if ((destinationClass == long.class || destinationClass == Long.class)) { + return new Long(value); + } + + if ((destinationClass == float.class || destinationClass == Float.class)) { + return new Float(value); + } + + if ((destinationClass == double.class || destinationClass == Double.class)) { + return new Double(value); + } + + if ((destinationClass == char.class || destinationClass == Character.class)) { + return value.charAt(0); + } + + return null; + } + + private static boolean isPrimitiveType(Class clazz) { + return clazz == int.class || clazz == Integer.class || + clazz == boolean.class || clazz == Boolean.class || + clazz == byte.class || clazz == Byte.class || + clazz == short.class || clazz == Short.class || + clazz == long.class || clazz == Long.class || + clazz == float.class || clazz == Float.class || + clazz == double.class || clazz == Double.class || + clazz == char.class || clazz == Character.class; + } + + private static void callSetter ( Object target, String propertyName, Object parameter) throws PropertyException + { + Method setter = getSetter(target, propertyName); + try { + setter.invoke(target, new Object[] {parameter}); + } catch (IllegalAccessException ex) { + throw new PropertyException("property '" + propertyName + "' is not accessible", ex); + } catch (InvocationTargetException ex) { + throw new PropertyException("property '" + propertyName + "' access threw an exception", ex); + } + } + + private static Object callGetter ( Object target, String propertyName ) throws PropertyException + { + Method getter = getGetter(target, propertyName); + try { + return getter.invoke(target, (Object[]) null); + } catch (IllegalAccessException ex) { + throw new PropertyException("property '" + propertyName + "' is not accessible", ex); + } catch (InvocationTargetException ex) { + throw new PropertyException("property '" + propertyName + "' access threw an exception", ex); + } + } + + private static Method getSetter ( Object target, String propertyName ) throws PropertyException + { + if (propertyName == null || "".equals(propertyName)) + throw new PropertyException("encountered invalid null or empty property name"); + String setterName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method[] methods = target.getClass().getMethods(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (method.getName().equals(setterName) && method.getReturnType().equals(void.class) && method.getParameterTypes().length == 1) { + return method; + } + } + throw new PropertyException("no writeable property '" + propertyName + "' in class '" + target.getClass().getName() + "'"); + } + + private static Method getGetter ( Object target, String propertyName ) throws PropertyException + { + String getterName = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method[] methods = target.getClass().getMethods(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (method.getName().equals(getterName) && !method.getReturnType().equals(void.class) && method.getParameterTypes().length == 0) { + return method; + } + } + throw new PropertyException("no readable property '" + propertyName + "' in class '" + target.getClass().getName() + "'"); + } + + private static boolean containsSetterForGetter ( Class clazz, Method getterMethod ) throws PropertyException + { + String getterMethodName = getterMethod.getName(); + String setterMethodName; + + if (getterMethodName.startsWith("get")) + setterMethodName = "set" + getterMethodName.substring(3); + else if (getterMethodName.startsWith("is")) + setterMethodName = "set" + getterMethodName.substring(2); + else + throw new PropertyException("method '" + getterMethodName + "' is not a getter, no setter can be found"); + + Method[] methods = clazz.getMethods(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (method.getName().equals(setterMethodName)) + return true; + } + return false; + } + + private static Class getPropertyType ( Object target, String propertyName ) throws PropertyException + { + String getterName = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Method[] methods = target.getClass().getMethods(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (method.getName().equals(getterName) && !method.getReturnType().equals(void.class) && method.getParameterTypes().length == 0) { + return method.getReturnType(); + } + } + throw new PropertyException("no property '" + propertyName + "' in class '" + target.getClass().getName() + "'"); + } + + public static String toString(Properties xaProperties) { + + StringBuffer ret = new StringBuffer(); + if ( xaProperties != null ) { + Set it = xaProperties.stringPropertyNames(); + ret.append ( "[" ); + boolean first = true; + for (String name : it) { + if ( ! first ) ret.append ( "," ); + String value = xaProperties.getProperty( name); + ret.append ( name ); ret.append ( "=" ); ret.append ( value ); + first = false; + } + ret.append ( "]" ); + } + return ret.toString(); + + } + +} diff --git a/public/util/src/main/java/com/atomikos/logging/JULLogger.java b/public/util/src/main/java/com/atomikos/logging/JULLogger.java new file mode 100644 index 000000000..1917b07ee --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/JULLogger.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import java.util.logging.Level; + +class JULLogger implements Logger { + + private final java.util.logging.Logger julLogger; + + public JULLogger(Class clazz) { + julLogger = java.util.logging.Logger.getLogger(clazz.getName()); + + } + + public void logWarning(String message) { + julLogger.log(Level.WARNING, message); + } + + public void logInfo(String message) { + julLogger.log(Level.INFO, message); + + } + + public void logDebug(String message) { + julLogger.log(Level.FINE, message); + + } + + public void logTrace(String message) { + julLogger.log(Level.FINEST, message); + } + + public void logWarning(String message, Throwable error) { + julLogger.log(Level.WARNING, message, error); + } + + public void logDebug(String message, Throwable error) { + julLogger.log(Level.FINE, message, error); + + } + + public void logTrace(String message, Throwable error) { + julLogger.log(Level.FINEST, message, error); + } + + public boolean isTraceEnabled() { + return julLogger.isLoggable(Level.FINEST); + } + + public boolean isDebugEnabled() { + return julLogger.isLoggable(Level.FINE); + } + + public void logError(String message) { + julLogger.log(Level.SEVERE, message); + } + + public void logError(String message, Throwable error) { + julLogger.log(Level.SEVERE, message, error); + } + + public boolean isErrorEnabled() { + return julLogger.isLoggable(Level.SEVERE); + } + + @Override + public void logInfo(String message, Throwable error) { + julLogger.log(Level.INFO, message, error); + } + + @Override + public boolean isInfoEnabled() { + return julLogger.isLoggable(Level.INFO); + } + + @Override + public void logFatal(String message) { + julLogger.log(Level.SEVERE, message); + } + + @Override + public void logFatal(String message, Throwable error) { + julLogger.log(Level.SEVERE, message, error); + } + +} diff --git a/public/util/src/main/java/com/atomikos/logging/JULLoggerFactoryDelegate.java b/public/util/src/main/java/com/atomikos/logging/JULLoggerFactoryDelegate.java new file mode 100644 index 000000000..381466103 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/JULLoggerFactoryDelegate.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +class JULLoggerFactoryDelegate implements LoggerFactoryDelegate { + + public Logger createLogger(Class clazz) { + + return new JULLogger(clazz); + } + + @Override + public String toString() { + + return "Java Util Logging"; + } +} diff --git a/public/util/src/main/java/com/atomikos/logging/Log4JLogger.java b/public/util/src/main/java/com/atomikos/logging/Log4JLogger.java new file mode 100644 index 000000000..fdeca20c3 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Log4JLogger.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import org.apache.log4j.Level; + +class Log4JLogger implements Logger { + + private final org.apache.log4j.Logger log4jLogger; + + public Log4JLogger(Class clazz) { + log4jLogger = org.apache.log4j.Logger.getLogger(clazz); + } + + public void logWarning(String message) { + log4jLogger.warn(message); + } + + @Override + public void logInfo(String message) { + log4jLogger.info(message); + } + + public void logDebug(String message) { + log4jLogger.debug(message); + } + + public void logTrace(String message) { + log4jLogger.trace(message); + } + + public void logWarning(String message, Throwable error) { + log4jLogger.warn(message, error); + + } + + public void logDebug(String message, Throwable error) { + log4jLogger.debug(message, error); + } + + public void logTrace(String message, Throwable error) { + log4jLogger.trace(message, error); + } + + public boolean isTraceEnabled() { + + return log4jLogger.isTraceEnabled(); + } + + public boolean isDebugEnabled() { + + return log4jLogger.isDebugEnabled(); + } + + public void logError(String message) { + log4jLogger.error(message); + } + + public void logError(String message, Throwable error) { + log4jLogger.error(message, error); + } + + public boolean isErrorEnabled() { + return log4jLogger.isEnabledFor(Level.ERROR); + } + + @Override + public void logInfo(String message, Throwable error) { + log4jLogger.info(message, error); + + } + + @Override + public boolean isInfoEnabled() { + return log4jLogger.isInfoEnabled(); + } + + @Override + public void logFatal(String message) { + log4jLogger.fatal(message); + } + + @Override + public void logFatal(String message, Throwable error) { + log4jLogger.fatal(message, error); + } + +} diff --git a/public/util/src/main/java/com/atomikos/logging/Log4JLoggerFactoryDelegate.java b/public/util/src/main/java/com/atomikos/logging/Log4JLoggerFactoryDelegate.java new file mode 100644 index 000000000..c38c34057 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Log4JLoggerFactoryDelegate.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +class Log4JLoggerFactoryDelegate implements LoggerFactoryDelegate { + + public Logger createLogger(Class clazz) { + + return new Log4JLogger(clazz); + } + + @Override + public String toString() { + + return "Log4j"; + } +} diff --git a/public/util/src/main/java/com/atomikos/logging/Log4j2Logger.java b/public/util/src/main/java/com/atomikos/logging/Log4j2Logger.java new file mode 100644 index 000000000..f0ad4caac --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Log4j2Logger.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import org.apache.logging.log4j.LogManager; + +public class Log4j2Logger implements Logger { + + private org.apache.logging.log4j.Logger logger; + + public Log4j2Logger(Class clazz) { + logger = LogManager.getLogger(clazz); + } + @Override + public void logError(String message) { + logger.error(message); + } + + @Override + public void logWarning(String message) { + logger.warn(message); + } + + @Override + public void logInfo(String message) { + logger.info(message); + } + + @Override + public void logDebug(String message) { + logger.debug(message); + } + + @Override + public void logTrace(String message) { + logger.trace(message); + } + + @Override + public void logError(String message, Throwable error) { + logger.error(message, error); + } + + @Override + public void logWarning(String message, Throwable error) { + logger.warn(message, error); + + } + + @Override + public void logDebug(String message, Throwable error) { + logger.debug(message, error); + } + + @Override + public void logTrace(String message, Throwable error) { + logger.trace(message, error); + + } + + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + @Override + public void logInfo(String message, Throwable error) { + logger.info(message, error); + } + + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + @Override + public void logFatal(String message) { + logger.fatal(message); + } + @Override + public void logFatal(String message, Throwable error) { + logger.fatal(message, error); + } + +} diff --git a/public/util/src/main/java/com/atomikos/logging/Log4j2LoggerFactoryDelegate.java b/public/util/src/main/java/com/atomikos/logging/Log4j2LoggerFactoryDelegate.java new file mode 100644 index 000000000..f2e47647d --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Log4j2LoggerFactoryDelegate.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +public class Log4j2LoggerFactoryDelegate implements LoggerFactoryDelegate { + + @Override + public Logger createLogger(Class clazz) { + + return new Log4j2Logger(clazz); + } + + @Override + public String toString() { + + return "Log4j2"; + } +} diff --git a/public/util/src/main/java/com/atomikos/logging/Logger.java b/public/util/src/main/java/com/atomikos/logging/Logger.java new file mode 100644 index 000000000..4e053604b --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Logger.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +public interface Logger { + + void logFatal(String message); + + void logError(String message); + + void logWarning(String message); + + void logInfo(String message); + + void logDebug(String message); + + void logTrace(String message); + + void logFatal(String message, Throwable error); + + void logError(String message, Throwable error); + + void logWarning(String message, Throwable error); + + void logInfo(String message, Throwable error); + + void logDebug(String message, Throwable error); + + void logTrace(String message, Throwable error); + + boolean isTraceEnabled(); + + boolean isDebugEnabled(); + + boolean isErrorEnabled(); + + boolean isInfoEnabled(); + +} \ No newline at end of file diff --git a/public/util/src/main/java/com/atomikos/logging/LoggerFactory.java b/public/util/src/main/java/com/atomikos/logging/LoggerFactory.java new file mode 100644 index 000000000..5c14be423 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/LoggerFactory.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +public final class LoggerFactory { + + private LoggerFactory() { + + } + + static LoggerFactoryDelegate loggerFactoryDelegate; + + public static final Logger createLogger(Class clazz) { + return loggerFactoryDelegate.createLogger(clazz); + } + + static void setLoggerFactoryDelegate(LoggerFactoryDelegate loggerFactoryDelegate) { + LoggerFactory.loggerFactoryDelegate = loggerFactoryDelegate; + } + + static { + String cname = null; + //let's try with + try { + Class.forName("org.slf4j.impl.StaticLoggerBinder"); + cname = "com.atomikos.logging.Slf4JLoggerFactoryDelegate"; + } catch (Throwable ex) { + System.out.println("No org.slf4j.impl.StaticLoggerBinder found in ClassPath, trying with log4j2..."); + } + + if(cname==null){ + try { + Class.forName("org.apache.logging.log4j.Logger"); + cname = "com.atomikos.logging.Log4j2LoggerFactoryDelegate"; + } catch (Throwable ex) { + //don't print stackTrace - cf bug 118228 + System.out.println("No org.apache.logging.log4j.Logger found found in ClassPath, trying with log4j..."); + } + } + + if(cname==null){ + try { + Class.forName("org.apache.log4j.Logger"); + cname = "com.atomikos.logging.Log4JLoggerFactoryDelegate"; + } catch (Throwable ex) { + System.out.println("No org.apache.log4j.Logger found found in ClassPath, falling back default..."); + } + } + + try { + if (cname != null) { + Class loggerClass = (Class) Class.forName(cname.trim(), true, LoggerFactory.class.getClassLoader()); + loggerFactoryDelegate = (LoggerFactoryDelegate) loggerClass.newInstance(); + } else { + fallbackToDefault(); + } + } catch (Throwable ex) { + // ignore - if we get here, some issue prevented the logger class + // from being loaded. + // maybe a ClassNotFound or NoClassDefFound or similar. Just use + // j.u.l + fallbackToDefault(); + } + Logger logger = createLogger(LoggerFactory.class); + logger.logDebug("Using " + loggerFactoryDelegate + " for logging."); + } + + private static void fallbackToDefault() { + setLoggerFactoryDelegate(new JULLoggerFactoryDelegate()); + } +} diff --git a/public/util/src/main/java/com/atomikos/logging/LoggerFactoryDelegate.java b/public/util/src/main/java/com/atomikos/logging/LoggerFactoryDelegate.java new file mode 100644 index 000000000..93023a15a --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/LoggerFactoryDelegate.java @@ -0,0 +1,15 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +interface LoggerFactoryDelegate { + + Logger createLogger(Class clazz); + +} diff --git a/public/util/src/main/java/com/atomikos/logging/Slf4JLoggerFactoryDelegate.java b/public/util/src/main/java/com/atomikos/logging/Slf4JLoggerFactoryDelegate.java new file mode 100644 index 000000000..95507a7a1 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Slf4JLoggerFactoryDelegate.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +class Slf4JLoggerFactoryDelegate implements LoggerFactoryDelegate { + + public Logger createLogger(Class clazz) { + + return new Slf4jLogger(clazz); + } + + @Override + public String toString() { + + return "Slf4J"; + } +} diff --git a/public/util/src/main/java/com/atomikos/logging/Slf4jLogger.java b/public/util/src/main/java/com/atomikos/logging/Slf4jLogger.java new file mode 100644 index 000000000..b7b7349b9 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/Slf4jLogger.java @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +class Slf4jLogger implements Logger { + + private final org.slf4j.Logger slf4j; + private final Marker FATAL = MarkerFactory.getMarker("FATAL"); + + public Slf4jLogger(Class clazz) { + slf4j = org.slf4j.LoggerFactory.getLogger(clazz); + } + + public void logWarning(String message) { + slf4j.warn(message); + } + + public void logInfo(String message) { + slf4j.info(message); + } + + public void logDebug(String message) { + slf4j.debug(message); + } + + public void logTrace(String message) { + slf4j.trace(message); + } + + public void logWarning(String message, Throwable error) { + slf4j.warn(message, error); + + } + + public void logDebug(String message, Throwable error) { + slf4j.debug(message, error); + } + + public void logTrace(String message, Throwable error) { + slf4j.trace(message, error); + + } + + public boolean isTraceEnabled() { + return slf4j.isTraceEnabled(); + } + + public boolean isDebugEnabled() { + return slf4j.isDebugEnabled(); + } + + public void logError(String message) { + slf4j.error(message); + } + + public void logError(String message, Throwable error) { + slf4j.error(message, error); + } + + public boolean isErrorEnabled() { + return slf4j.isErrorEnabled(); + } + + @Override + public void logInfo(String message, Throwable error) { + slf4j.info(message, error); + } + + @Override + public boolean isInfoEnabled() { + return slf4j.isInfoEnabled(); + } + + @Override + public void logFatal(String message) { + slf4j.error(FATAL, message); + } + + @Override + public void logFatal(String message, Throwable error) { + slf4j.error(FATAL, message, error); + } + +} diff --git a/public/util/src/main/java/com/atomikos/logging/StackTrace.java b/public/util/src/main/java/com/atomikos/logging/StackTrace.java new file mode 100644 index 000000000..d1b882ad1 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/logging/StackTrace.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +public class StackTrace { + + private static final String TAB = "\t"; + static final String EMPTY_STRING = ""; + public static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + public static String toString(StackTraceElement[] stackTrace) { + if (stackTrace != null) { + StringBuffer stackTraces = new StringBuffer(); + String lineSeparator = EMPTY_STRING; + for (StackTraceElement ste : stackTrace) { + stackTraces.append(lineSeparator); + lineSeparator = LINE_SEPARATOR; + stackTraces.append(TAB); + stackTraces.append(ste); + } + return stackTraces.toString(); + } + return EMPTY_STRING; + } + +} \ No newline at end of file diff --git a/public/util/src/main/java/com/atomikos/publish/EventPublisher.java b/public/util/src/main/java/com/atomikos/publish/EventPublisher.java new file mode 100644 index 000000000..065efd8d6 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/publish/EventPublisher.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.publish; + +import java.util.HashSet; +import java.util.Set; + +import com.atomikos.icatch.event.Event; +import com.atomikos.icatch.event.EventListener; +import com.atomikos.icatch.event.transaction.ParticipantHeuristicEvent; +import com.atomikos.icatch.event.transaction.TransactionHeuristicEvent; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public enum EventPublisher { + INSTANCE; + + private static Logger LOGGER = LoggerFactory.createLogger(EventPublisher.class); + + private Set listeners = new HashSet<>(); + + private boolean alreadyWarned = false; + + private EventPublisher(){} + + public void publish(Event event) { + if (event != null) { + notifyAllListeners(event); + } + } + + private void notifyAllListeners(Event event) { + warnIfNoListeners(event); + for (EventListener listener : listeners) { + try { + listener.eventOccurred(event); + } catch (Exception e) { + LOGGER.logError("Error notifying listener " + listener, e); + } + } + } + + private void warnIfNoListeners(Event event) { + if (listeners.isEmpty()) { + if (!alreadyWarned) { + LOGGER.logWarning("No event listeners are configured - you may want to consider https://www.atomikos.com/Main/ExtremeTransactions for detailed monitoring functionality..."); + } + if (logEvent(event)) { + LOGGER.logWarning(event.toString()); + } + alreadyWarned = true; + } + } + + private boolean logEvent(Event event) { + return !alreadyWarned || event instanceof ParticipantHeuristicEvent || event instanceof TransactionHeuristicEvent; + } + + /** + * For internal use only - listeners should register via the ServiceLoader mechanism. + * + * @param listener + */ + public void registerEventListener(EventListener listener) { + LOGGER.logInfo("Registering EventListener: " + listener); + listeners.add(listener); + } + +} \ No newline at end of file diff --git a/public/util/src/main/java/com/atomikos/thread/InterruptedExceptionHelper.java b/public/util/src/main/java/com/atomikos/thread/InterruptedExceptionHelper.java new file mode 100644 index 000000000..f2fbb4e9d --- /dev/null +++ b/public/util/src/main/java/com/atomikos/thread/InterruptedExceptionHelper.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.thread; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public class InterruptedExceptionHelper +{ + private static final Logger LOGGER = LoggerFactory.createLogger(InterruptedExceptionHelper.class); + + public static void handleInterruptedException ( InterruptedException e ) + { + LOGGER.logWarning ( "Thread interrupted " , e ); + // interrupt again - cf http://www.javaspecialists.co.za/archive/Issue056.html + Thread.currentThread().interrupt(); + } +} diff --git a/public/util/src/main/java/com/atomikos/thread/TaskManager.java b/public/util/src/main/java/com/atomikos/thread/TaskManager.java new file mode 100644 index 000000000..028d6dc04 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/thread/TaskManager.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.thread; + +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +/** + * Scheduling logic for tasks/threads. + */ + +public enum TaskManager { + SINGLETON; + + private static final Logger LOGGER = LoggerFactory.createLogger(TaskManager.class); + + private ThreadPoolExecutor executor; + + + private void init() { + SynchronousQueue synchronousQueue = new SynchronousQueue(); + executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, new Long(60L), + TimeUnit.SECONDS, synchronousQueue, new AtomikosThreadFactory()); + + } + + /** + * Notification of shutdown to close all pooled threads. + * + */ + public synchronized void shutdown() { + if (executor != null) { + executor.shutdown(); + executor = null; + } + } + + /** + * Schedules a task for execution by a thread. + * + * @param task + */ + public void executeTask(Runnable task) { + if (executor == null) { + // happens on restart of TS within same VM + init(); + } + executor.execute(task); + } + + private static class AtomikosThreadFactory implements + java.util.concurrent.ThreadFactory { + + private volatile AtomicInteger count = new AtomicInteger(0); + private final ThreadGroup group; + + private AtomikosThreadFactory() { + SecurityManager sm = System.getSecurityManager(); + group = (sm != null ? sm.getThreadGroup() : Thread.currentThread() + .getThreadGroup()); + } + + @Override + public Thread newThread(Runnable r) { + String realName = "Atomikos:" + count.incrementAndGet(); + if (LOGGER.isTraceEnabled()) + LOGGER.logTrace("ThreadFactory: creating new thread: "+ realName); + Thread thread = new Thread(group, r, realName); + thread.setContextClassLoader(getClass().getClassLoader()); //cf case 185557: avoid thread leak in Tomcat + thread.setDaemon(true); + return thread; + } + + } + +} diff --git a/public/util/src/main/java/com/atomikos/timing/AlarmTimer.java b/public/util/src/main/java/com/atomikos/timing/AlarmTimer.java new file mode 100644 index 000000000..99cb4fed6 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/timing/AlarmTimer.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.timing; + + +/** + * A common interface for timers. + * + * @author Lars J. Nilsson + */ +public interface AlarmTimer extends Runnable { + + public long getTimeout(); + + public boolean isActive(); + + public void stopTimer(); + + public void addAlarmTimerListener(AlarmTimerListener lstnr); + + public void removeAlarmTimerListener(AlarmTimerListener lstnr); + +} diff --git a/public/util/src/main/java/com/atomikos/timing/AlarmTimerListener.java b/public/util/src/main/java/com/atomikos/timing/AlarmTimerListener.java new file mode 100644 index 000000000..068795615 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/timing/AlarmTimerListener.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + +package com.atomikos.timing; + + +/** + * + * + *A listener for timer events. + */ + +public interface AlarmTimerListener +{ + /** + *Notify the instance of an alarm coming from a timer. + * + *@param timer The timer raising the alarm. + */ + + public void alarm(AlarmTimer timer); +} diff --git a/public/util/src/main/java/com/atomikos/timing/PooledAlarmTimer.java b/public/util/src/main/java/com/atomikos/timing/PooledAlarmTimer.java new file mode 100644 index 000000000..686318f41 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/timing/PooledAlarmTimer.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.timing; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.atomikos.thread.InterruptedExceptionHelper; + +/** + * An alarm timer for use in a pool of threads. + * + */ +public final class PooledAlarmTimer extends Thread implements AlarmTimer { + + private final List listeners; + private final long timeout; + + private final Object runMonitor = new Object(); + private boolean runFlag = true; + + public PooledAlarmTimer(long timeout) { + listeners = new ArrayList(); + this.timeout = timeout; + } + + public void addAlarmTimerListener(AlarmTimerListener lstnr) { + synchronized(listeners) { + listeners.add(lstnr); + } + } + + public void removeAlarmTimerListener(AlarmTimerListener lstnr) { + synchronized(listeners) { + listeners.remove(lstnr); + } + } + + public long getTimeout() { + return timeout; + } + + public boolean isActive() { + synchronized(runMonitor) { + return runFlag; + } + } + + public void stopTimer() { + synchronized(runMonitor) { + runFlag = false; + runMonitor.notify(); + } + try { + this.join(); // cf case 182106 + } catch (InterruptedException e) { + InterruptedExceptionHelper.handleInterruptedException(e); + } + } + + public void run() { + /* + * A short comment here, it is possible for Object.wait to + * return before the timeout silently, thus we need to double check + * the actual elapsed time before calling it quits. /LJN + */ + while(isActive()) { + try { + + doWait(timeout); + } catch(InterruptedException e) { + // Question: At this point, the thread pool is most + // likely shutting down, but I'm not entirely sure, does it + // mean we should or shouldn't notify waiters? If we shouldn't + // we should return from this method to kill the thread + + // return; + } + + if ( isActive() ) + notifyListeners(); + } + } + + // --- PRIVATE METHODS --- // + + /* + * Notify all listeners + */ + private void notifyListeners() { + List tempList = new ArrayList(listeners); + for (Iterator it = tempList.iterator(); it.hasNext(); ) { + AlarmTimerListener list = it.next(); + list.alarm(this); + } + } + + /* + * Wait on run monitor... + */ + private void doWait(long millis) throws InterruptedException { + synchronized(runMonitor) { + + runMonitor.wait(millis); + } + } +} diff --git a/public/util/src/main/java/com/atomikos/util/Assert.java b/public/util/src/main/java/com/atomikos/util/Assert.java new file mode 100644 index 000000000..42cc00ed0 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/Assert.java @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +public abstract class Assert { + + + public static void notNull(String message, Object o) { + if (o == null) { + throw new IllegalArgumentException(message); + } + } +} diff --git a/public/util/src/main/java/com/atomikos/util/Atomikos.java b/public/util/src/main/java/com/atomikos/util/Atomikos.java new file mode 100644 index 000000000..dc3394ccf --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/Atomikos.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.InputStream; +import java.util.Properties; + +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + +public final class Atomikos { + final private static Logger LOGGER = LoggerFactory.createLogger(Atomikos.class); + + private Atomikos() { + //Nobody can load me !!! + } + public final static String VERSION = loadVersion(); + + private static String loadVersion() { + final Properties properties = new Properties(); + try { + InputStream inStream = Atomikos.class.getClassLoader().getResourceAsStream("META-INF/maven/com.atomikos/atomikos-util/pom.properties"); + properties.load(inStream); + } catch (Exception e) { + LOGGER.logWarning("Unable to load version.properties using Util.class.getClassLoader().getResourceAsStream(...)", e); + return "UNKNOWN"; + } + + return properties.getProperty("version"); + } + + public static boolean isEvaluationVersion() { + return VERSION.endsWith(".EVAL"); + } + + +} diff --git a/public/util/src/main/java/com/atomikos/util/ClassLoadingHelper.java b/public/util/src/main/java/com/atomikos/util/ClassLoadingHelper.java new file mode 100644 index 000000000..14522b66e --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/ClassLoadingHelper.java @@ -0,0 +1,201 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +/** + * + */ +package com.atomikos.util; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.URL; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; + +/** + * A helper class for class loading. + * + * + */ + +public class ClassLoadingHelper { + + + private static Set javaLangObjectMethodNames = new HashSet<>(); + static { + try { + BeanInfo infos = Introspector.getBeanInfo(java.lang.Object.class); + MethodDescriptor[] methods= infos.getMethodDescriptors(); + for (MethodDescriptor methodDescriptor : methods) { + javaLangObjectMethodNames.add(methodDescriptor.getName()); + } + } catch (IntrospectionException e) { + //ignore, return false + } + } + + public static boolean existsInJavaObjectClass(Method method) { + return javaLangObjectMethodNames.contains(method.getName()); + } + private static ClassLoader lastGoodClassLoader; + + /** + * Creates a new dynamic proxy instance for the given delegate. + * + * @param classLoadersToTry + * The class loaders to try, in the specified list order. + * @param interfaces + * The interfaces to add to the returned proxy. + * @param delegate + * The underlying object that will receive the calls on the + * proxy. + * @return The proxy. + * + * @exception IllegalArgumentException + * If any of the interfaces involved could not be loaded. + */ + + private static RequiredInterfaceType newProxyInstance(Deque classLoadersToTry, Class[] interfaces, InvocationHandler delegate) throws IllegalArgumentException { + RequiredInterfaceType ret = null; + + ClassLoader cl = classLoadersToTry.pop(); + + try { + ret= (RequiredInterfaceType) Proxy.newProxyInstance(cl, interfaces, delegate); + lastGoodClassLoader = cl; + } catch (IllegalArgumentException someClassNotFound) { + if (!classLoadersToTry.isEmpty()) { + // try with remaining class loaders + ret = newProxyInstance(classLoadersToTry, interfaces, delegate); + } else { + // rethrow to caller + throw someClassNotFound; + } + } + + return ret; + } + private static boolean fallbackToMinimumSetOfInterfaces = false; + + private static RequiredInterfaceType newProxyInstanceStartingWithLastGoodClassLoader(Deque classLoadersToTry, Class[] interfaces, InvocationHandler delegate) throws IllegalArgumentException { + RequiredInterfaceType ret = null; + if (lastGoodClassLoader != null) { //see case 151842 + try { + ret = (RequiredInterfaceType) Proxy.newProxyInstance(lastGoodClassLoader, interfaces, delegate); + } catch (IllegalArgumentException someClassNotFound) { + //happens if lastGoodClassLoader is no longer valid + } + } + if (ret == null) { //try the whole list + ret = newProxyInstance(classLoadersToTry, interfaces, delegate); + } + + + return ret; + } + /** + * Creates a new dynamic proxy instance for the given delegate. + * + * @param classLoadersToTry + * The class loaders to try, in the specified list order. + * @param requiredInterfaceType + * The minimum interface required, if not all interface + * classes were found. + * @param interfaces + * The interfaces to add to the returned proxy. + * @param delegate + * The underlying object that will receive the calls on the + * proxy. + * @return The proxy. + * + * @exception IllegalArgumentException + * If any of the interfaces involved could not be loaded. + */ + + public static RequiredInterfaceType newProxyInstance(Deque classLoadersToTry, + Class requiredInterfaceType, Class[] interfaces, + InvocationHandler delegate) throws IllegalArgumentException { + RequiredInterfaceType ret = null; + if(fallbackToMinimumSetOfInterfaces) { + Class[] minimumSetOfInterfaces = {requiredInterfaceType}; + ret = newProxyInstanceStartingWithLastGoodClassLoader(classLoadersToTry, minimumSetOfInterfaces, delegate); + } else { + try { + ret = newProxyInstanceStartingWithLastGoodClassLoader(classLoadersToTry, interfaces, delegate); + } catch (IllegalArgumentException | IllegalAccessError someClassNotFound) { + fallbackToMinimumSetOfInterfaces = true; + Class[] minimumSetOfInterfaces = {requiredInterfaceType}; + ret = newProxyInstanceStartingWithLastGoodClassLoader(classLoadersToTry, minimumSetOfInterfaces, delegate); + } + } + + return ret; + } + + + /** + * Loads a class with the given name. + * + * @param className + * @return The class object + * @throws ClassNotFoundException + * If not found + */ + public static Class loadClass(String className) + throws ClassNotFoundException { + Class clazz = null; + try { + clazz = (Class)Thread.currentThread().getContextClassLoader() + .loadClass(className); + } catch (ClassNotFoundException nf) { + clazz = (Class)Class.forName(className); + } + return clazz; + } + + /** + * Attempts to load a given resource from the classpath. + * + * @param clazz + * The class to use as reference re classpath. + * @param resourceName + * The name of the resource + * @return The URL to the resource, or null if not found. + */ + public static URL loadResourceFromClasspath(Class clazz, String resourceName) { + URL ret = null; + // first try from package scope + ret = clazz.getResource(resourceName); + if (ret == null) { + // not found in package -> try from absolute path + ret = clazz.getResource("/" + resourceName); + } + return ret; + } + + public static Object newInstance(String className) { + try { + Class clazz = ClassLoadingHelper.loadClass(className); + return clazz.newInstance(); + } catch (ClassNotFoundException e) { + //don't print stackTrace - cf bug 118228 + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/public/util/src/main/java/com/atomikos/util/DateHelper.java b/public/util/src/main/java/com/atomikos/util/DateHelper.java new file mode 100644 index 000000000..1a9d06a62 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/DateHelper.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class DateHelper { + + private static ThreadLocal threadSafeSimpleDateFormat = new ThreadLocal() { + protected SimpleDateFormat initialValue() { + + return new SimpleDateFormat("yyyy.MM.dd HH:mm:ss:SSS"); + + }; + + }; + + public static String format(Date date) { + return threadSafeSimpleDateFormat.get().format(date); + } +} diff --git a/public/util/src/main/java/com/atomikos/util/DynamicProxySupport.java b/public/util/src/main/java/com/atomikos/util/DynamicProxySupport.java new file mode 100644 index 000000000..8272d0a6d --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/DynamicProxySupport.java @@ -0,0 +1,223 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.atomikos.beans.PropertyUtils; +import com.atomikos.logging.Logger; +import com.atomikos.logging.LoggerFactory; + + /** + * Abstract reusable logic for easy dynamic proxy creation. + * + * Subclasses can have 'overriding' methods annotated with @Proxied + * to implement custom logic for a given interface. + * + * + * + * IMPORTANT: subclasses and proxied methods must be PUBLIC in scope for this to work! + * In addition, subclasses should override toString with descriptive information so logging is clear. + * + */ + +public abstract class DynamicProxySupport implements InvocationHandler { + + private static final Logger LOGGER = LoggerFactory.createLogger(DynamicProxySupport.class); + + public static boolean isProxyInstanceOfClass(Class clazz, Object o) { + return clazz.isAssignableFrom(Proxy.getInvocationHandler(o).getClass()); + } + + protected boolean closed = false; + protected final RequiredInterfaceType delegate; + protected final Map proxiedMethods = new HashMap(); + + protected DynamicProxySupport(RequiredInterfaceType delegate) { + this.delegate = delegate; + fillProxiedMethodsCache(); + } + + private void fillProxiedMethodsCache() { + + Class dynamicProxyClass = this.getClass(); + Method[] methods = dynamicProxyClass.getMethods(); + if (methods == null) { + throw new IllegalStateException(dynamicProxyClass.getSimpleName() +": at least one @Proxied method is expected but none was found."); + } + boolean proxiedMethodFound = false; + for (Method m : methods) { + if (m.isAnnotationPresent(Proxied.class)) { + proxiedMethodFound = true; + proxiedMethods.put(createSignature(m), m); + + } + } + if (!proxiedMethodFound) { + throw new IllegalStateException(dynamicProxyClass.getSimpleName() +": at least one @Proxied method is expected but none was found."); + } + + + } + + private String createSignature(Method m) { + StringBuilder ret = new StringBuilder(32); + ret.append(m.getName()); + for (Class c : m.getParameterTypes()) { + ret.append(c.getName()); + } + return ret.toString(); + } + + private String formatCallDetails(Method method, Object... args) { + StringBuffer ret = new StringBuffer(); + ret.append(method.getName()); + if (args!=null && args.length>0) { + ret.append("("); + for (int i = 0; i < args.length; i++) { + ret.append(args[i]); + if (i < args.length-1) ret.append(","); + } + ret.append(")"); + } + return ret.toString(); + } + + + private static List methodsAllowedAfterClose = Arrays.asList("close", "isClosed"); + + + private boolean methodAllowedAfterClose(Method method) { + return methodsAllowedAfterClose.contains(method.getName()) || ClassLoadingHelper.existsInJavaObjectClass(method); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object ret = null; + if( closed && !methodAllowedAfterClose(method) ) { + throwInvocationAfterClose(method.getName()); + return null; + } + + try { + Method proxiedMethod = findProxiedMethodFor(method); + if (proxiedMethod != null) { + ret = callProxiedMethod(proxiedMethod, args); + } else { + ret = callNativeMethod(method, args); + } + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause != null) { + // If available: throw the underlying vendor exception to preserve the context. + handleInvocationException(cause); + } else { + // Strangely enough the javadoc says 'cause' can be null: + // https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/InvocationTargetException.html + // although that does not seem to make sense because + // InvocationTargetException is intended to wrap + // occurrences of underlying exceptions? + // Whatever, let's just handle this exotic case to be sure... + handleInvocationException(e); + } + } + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace ( this + ": " + method.getName() + " returning " + ret ); + } + return ret; + } + + /** + * Down-call to handle exceptions after an invocation. + * Implementations should decide on whether to mark the proxy as erroneous and + * whether or not to re-throw. + * + * @param e + * @throws Throwable + */ + protected abstract void handleInvocationException(Throwable e) throws Throwable; + + protected abstract void throwInvocationAfterClose(String method) throws Exception; + + + private Object callProxiedMethod(Method proxiedMethod, Object... args) + throws IllegalAccessException, InvocationTargetException { + if (LOGGER.isDebugEnabled()) { + LOGGER.logDebug(this + ": calling proxied " + formatCallDetails(proxiedMethod, args)); + } + return proxiedMethod.invoke(this, args); + } + + + /** + * Delegates the call to the native method in the delegate. This method can safely be reused in subclasses when needed. + * + * @param method + * @param args + * @return + * @throws Throwable + */ + protected Object callNativeMethod(Method method, Object... args) throws Throwable { + if (LOGGER.isTraceEnabled()) { + LOGGER.logTrace(this + ": calling native " + formatCallDetails(method, args)); + } + return method.invoke(delegate, args); + } + + private Method findProxiedMethodFor(Method method) { + + return proxiedMethods.get(createSignature(method)); + } + + + public RequiredInterfaceType createDynamicProxy() { + + return ClassLoadingHelper.newProxyInstance(getClassLoadersToTry(), getRequiredInterfaceType(), getInterfaceClasses(), this); + } + + protected Deque getClassLoadersToTry() { + Deque classLoaders = new ArrayDeque(); + addIfNotNull(classLoaders, Thread.currentThread().getContextClassLoader()); + addIfNotNull(classLoaders, delegate.getClass().getClassLoader()); + addIfNotNull(classLoaders, DynamicProxySupport.class.getClassLoader() ); + return classLoaders; + + } + + protected void addIfNotNull(Deque classLoaders, ClassLoader cl) { + if (cl != null) { //cf case 182578 + classLoaders.add ( cl ); + } + } + + protected abstract Class getRequiredInterfaceType(); + + + public void markClosed() { + LOGGER.logTrace(this +": marking connection proxy as closed..."); + this.closed = true; + } + + protected Class[] getInterfaceClasses() { + + Set> interfaces = PropertyUtils.getAllImplementedInterfaces(delegate.getClass()); + return interfaces.toArray(new Class[0]); + } + +} diff --git a/public/util/src/main/java/com/atomikos/util/IntraVmObjectFactory.java b/public/util/src/main/java/com/atomikos/util/IntraVmObjectFactory.java new file mode 100644 index 000000000..bb4b96d3b --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/IntraVmObjectFactory.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.Serializable; +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import javax.naming.spi.ObjectFactory; + + + +/** + * An intra-VM object factory. Objects registered in the + * IntraVmObjectRegistry can be restored from a reference + * with the help of this factory. + * + * + */ + +public class IntraVmObjectFactory implements ObjectFactory +{ + + private static final String NAME_REF_ADDRESS_TYPE = "uniqueResourceName"; + + private static SerializableObjectFactory serializableObjectFactory = new SerializableObjectFactory(); + + //synchronized static to avoid race conditions between concurrent retrievals + @SuppressWarnings("rawtypes") + private static synchronized Object retrieveObjectInstance ( + Object obj, Name name, Context nameCtx, Hashtable environment) + throws Exception + { + Object ret = null; + + + if ( obj instanceof Reference ) { + String resourceName = null; + Reference ref = ( Reference ) obj; + StringRefAddr nameAsRefAddr = ( StringRefAddr ) ref.get ( NAME_REF_ADDRESS_TYPE ); + resourceName = (String) nameAsRefAddr.getContent(); + try { + ret = IntraVmObjectRegistry.getResource ( resourceName ); + } catch ( NameNotFoundException notYetInited ) { + //not registered in this VM -> init and return the deserialized instance + Object resource = serializableObjectFactory.getObjectInstance(obj, name, nameCtx, environment); + IntraVmObjectRegistry.addResource ( resourceName, resource ); + ret = resource; + } + } + + return ret; + } + + public static synchronized Reference createReference ( Serializable object , String name ) throws NamingException + { + Reference ret = null; + if ( object == null ) throw new IllegalArgumentException ( "invalid resource: null" ); + if ( name == null ) throw new IllegalArgumentException ( "name should not be null" ); + + //make sure that lookup works - add the bean to the registry if needed + try { + Object existing = IntraVmObjectRegistry.getResource ( name ); + if ( existing != object ) { + //another instance with the same name already there + String msg = "Another resource already exists with name " + name + " - pick a different name"; + throw new NamingException ( msg ); + } + } catch ( NameNotFoundException notThere ) { + // make sure this bean is registered for JNDI lookups to find the same instance + // otherwise, concurrent lookups would create race conditions during init + // and the thread that creates a bean might not be able to use it (unfair?) + IntraVmObjectRegistry.addResource ( name , object ); + } + ret = SerializableObjectFactory.createReference ( object , IntraVmObjectFactory.class.getName() ); + //also add the unique resource name for helping during retrieval + ret.add ( new StringRefAddr ( NAME_REF_ADDRESS_TYPE , name ) ); + return ret; + } + + + @SuppressWarnings("rawtypes") + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception + { + return retrieveObjectInstance(obj, name, nameCtx, environment); + } + +} diff --git a/public/util/src/main/java/com/atomikos/util/IntraVmObjectRegistry.java b/public/util/src/main/java/com/atomikos/util/IntraVmObjectRegistry.java new file mode 100644 index 000000000..ba9546cb4 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/IntraVmObjectRegistry.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.naming.NameAlreadyBoundException; +import javax.naming.NameNotFoundException; + +/** + * An intra-VM object registry for reconstructing Objects from references. + * + */ +public class IntraVmObjectRegistry { + + private final static Map resourcesMap = new HashMap(); + + public synchronized static void addResource(String resourceName, Object resource) throws NameAlreadyBoundException + { + if (resourcesMap.containsKey(resourceName)) + throw new NameAlreadyBoundException("resource with name '" + resourceName + "' already registered"); + + resourcesMap.put(resourceName, resource); + } + + public synchronized static Object getResource(String resourceName) throws NameNotFoundException + { + if (!resourcesMap.containsKey(resourceName)) + throw new NameNotFoundException("no resource with name '" + resourceName + "' has been registered yet"); + + return resourcesMap.get(resourceName); + } + + public synchronized static void removeResource(String resourceName) throws NameNotFoundException + { + if (!resourcesMap.containsKey(resourceName)) + throw new NameNotFoundException("no resource with name '" + resourceName + "' has been registered yet"); + + resourcesMap.remove(resourceName); + } + + public synchronized static List findAllResourcesOfType(Class clazz) { + List ret = new ArrayList(); + for (Object r : resourcesMap.values()) { + if (clazz.isInstance(r)) { + ret.add(r); + } + } + return ret; + } + +} diff --git a/public/util/src/main/java/com/atomikos/util/Proxied.java b/public/util/src/main/java/com/atomikos/util/Proxied.java new file mode 100644 index 000000000..d6dd1622f --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/Proxied.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + /** + * Annotation for marking a specific method as being a dynamic proxy implementation. + */ + +@Retention(value=RetentionPolicy.RUNTIME) +@Target(value=ElementType.METHOD) +public @interface Proxied { + +} diff --git a/public/util/src/main/java/com/atomikos/util/SerializableObjectFactory.java b/public/util/src/main/java/com/atomikos/util/SerializableObjectFactory.java new file mode 100644 index 000000000..ac8a9be7d --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/SerializableObjectFactory.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.Serializable; +import java.util.Hashtable; + +import javax.naming.BinaryRefAddr; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +/** + * + * A default object factory for serializable objects that + * need to be bound in JNDI. + */ + +public class SerializableObjectFactory implements ObjectFactory +{ + + static Reference createReference ( Serializable object , String factoryClassName ) + throws NamingException + { + Reference ret = null; + BinaryRefAddr handle = null; + ByteArrayOutputStream bout; + try + { + bout = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream ( bout ); + out.writeObject ( object ); + out.close(); + } + catch (IOException e) + { + throw new NamingException ( e.getMessage() ); + } + + handle = new BinaryRefAddr ( "com.atomikos.serializable" , bout.toByteArray() ); + ret = new Reference ( object.getClass().getName() , handle , + factoryClassName , null ); + return ret; + } + + /** + * Create a reference for the given (Serializable) object. + * + * @param object The object to create a reference for. + * @return Reference The reference that can be bound + * in JNDI and used along with this factory class to + * reconstruct the original object. + * @exception NamingException On failure. + */ + + public static Reference createReference ( Serializable object ) + throws NamingException + { + return createReference ( object , SerializableObjectFactory.class.getName() ); + } + + /** + * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context, java.util.Hashtable) + */ + + @SuppressWarnings("rawtypes") + public Object getObjectInstance( + Object obj, Name name, + Context nameCtx, + Hashtable environment ) + throws Exception + { + Object ret = null; + if ( obj instanceof Reference ) { + Reference ref = ( Reference ) obj; + RefAddr ra = ref.get ( "com.atomikos.serializable" ); + if ( ra != null ) { + byte[] bytes = ( byte[] ) ra.getContent(); + ByteArrayInputStream bin = new ByteArrayInputStream ( bytes ); + ObjectInputStream in = new ObjectInputStream ( bin ) { + protected Class resolveClass ( ObjectStreamClass desc ) + throws IOException, ClassNotFoundException + { + try + { + //try default class loading + return super.resolveClass(desc); + } + catch ( ClassNotFoundException ex ) + { + //try the context class loader - happens in OSGi? + return Thread.currentThread().getContextClassLoader() + .loadClass(desc.getName()); + } + } + }; + ret = in.readObject(); + in.close(); + } + } + + return ret; + } + +} diff --git a/public/util/src/main/java/com/atomikos/util/UniqueIdMgr.java b/public/util/src/main/java/com/atomikos/util/UniqueIdMgr.java new file mode 100644 index 000000000..3baa75df0 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/UniqueIdMgr.java @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + + +package com.atomikos.util; + + + +/** + * + * + *For managing a set of unique IDs on behalf of a given server + * + */ + +public class UniqueIdMgr +{ + + + + private final static int MAX_COUNTER_WITHIN_SAME_MILLIS = 32000; + private final static int MAX_LENGTH_OF_NUMERIC_SUFFIX = String.valueOf(Long.MAX_VALUE).length() + String.valueOf(MAX_COUNTER_WITHIN_SAME_MILLIS).length(); + + + + private final String commonPartOfId; //name of server + private int lastcounter; + + + /** + *Generate a new instance for a given server. + *Assumption: there are never two servers with the same name! + * + */ + + public UniqueIdMgr ( String server ) { + super(); + commonPartOfId=getCommonPartOfId(server); + lastcounter = 0; + } + + + //FIX FOR BUG 10104 + private String getCountWithLeadingZeroes (int number) + { + String ret = Long.toString ( number ); + int max = Long.toString(MAX_COUNTER_WITHIN_SAME_MILLIS).length(); + int len = ret.length(); + StringBuffer zeroes = new StringBuffer(); + + while ( len < max ) { + zeroes.append ( "0" ); + len++; + } + ret = zeroes.append ( ret ).toString(); + return ret; + } + + + /** + *The main way of obtaining a new UniqueId. + * + */ + + public String get() + { + StringBuffer buffer = new StringBuffer(); + String id = buffer.append(commonPartOfId). + append(System.currentTimeMillis()). + append(getCountWithLeadingZeroes ( incrementAndGet() )). + toString(); + return id; + } + + + private synchronized int incrementAndGet() { + lastcounter++; + if (lastcounter == MAX_COUNTER_WITHIN_SAME_MILLIS) lastcounter = 0; + return lastcounter; + } + + private static String getCommonPartOfId(String server) { + StringBuffer ret = new StringBuffer(64); + ret.append(server); + return ret.toString(); + } + + public int getMaxIdLengthInBytes() { + // see case 73086 + return commonPartOfId.getBytes().length + MAX_LENGTH_OF_NUMERIC_SUFFIX; + } + + +} + + diff --git a/public/util/src/main/java/com/atomikos/util/VersionedFile.java b/public/util/src/main/java/com/atomikos/util/VersionedFile.java new file mode 100644 index 000000000..0846339c7 --- /dev/null +++ b/public/util/src/main/java/com/atomikos/util/VersionedFile.java @@ -0,0 +1,255 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; + + + /** + * A file with underlying version capabilities to ensure safe overwriting. + * + * Unlike regular files, this type of file is safe w.r.t. (over)writing + * a previous version: a backup version of the original content is kept + * until the client application explicitly states that a consistent + * new version has been written. + * + */ + +public class VersionedFile +{ + + private static final String FILE_SEPARATOR = String.valueOf(File.separatorChar); + private String baseDir; + private String suffix; + private String baseName; + + //state attributes below + + private long version; + private FileInputStream inputStream; + + private RandomAccessFile randomAccessFile; + + + /** + * Creates a new instance based on the given name parameters. + * The actual complete name(s) of the physical file(s) will be based on a version number + * inserted in between, to identify versions. + * + * @param baseDir The base folder. + * @param baseName The base name for of the file path/name. + * @param suffix The suffix to append to the complete file name. + */ + public VersionedFile ( String baseDir , String baseName , String suffix ) + { + + if(!baseDir.endsWith(FILE_SEPARATOR)) { + baseDir += FILE_SEPARATOR; + } + this.baseDir = baseDir; + this.suffix = suffix; + this.baseName = baseName; + resetVersion(); + } + + private void resetVersion() + { + this.version = extractLastValidVersionNumberFromFileNames(); + } + + private long extractLastValidVersionNumberFromFileNames() { + long version = -1; + File cd = new File ( getBaseDir() ); + String[] names = cd.list ( new FilenameFilter () { + public boolean accept ( File dir , String name ) + { + return (name.startsWith ( getBaseName() ) && name + .endsWith ( getSuffix() )); + } + } ); + if ( names!= null ) { + for ( int i = 0; i < names.length; i++ ) { + long sfx = extractVersion ( names[i] ); + if ( version < 0 || sfx < version ) + version = sfx; + } + } + + return version; + } + + private long extractVersion ( String name ) + { + long ret = 0; + int lastpos = name.lastIndexOf ( '.' ); + int startpos = getBaseName().length (); + String suffix = name.substring ( startpos, lastpos ); + try { + + ret = Long.valueOf( suffix ); + } catch ( NumberFormatException e ) { + IllegalArgumentException err = new IllegalArgumentException ( "Error extracting version from file: " + name+" in " + getBaseDir() ); + err.initCause ( e ); + throw err; + } + return ret; + } + + private String getBackupVersionFileName() + { + return getBaseUrl() + (version - 1) + getSuffix(); + } + + public String getCurrentVersionFileName() + { + return getBaseUrl() + version + getSuffix(); + } + + public String getBaseUrl() + { + return baseDir + baseName; + } + + public String getBaseDir() + { + return this.baseDir; + } + + public String getBaseName() + { + return this.baseName; + } + + public String getSuffix() + { + return this.suffix; + } + + /** + * Opens the last valid version for reading. + * + * @return A stream to read the last valid contents + * of the file: either the backup version (if present) + * or the current (and only) version if no backup is found. + * + * @throws IllegalStateException If a newer version was opened for writing. + * @throws FileNotFoundException If no last version was found. + */ + public FileInputStream openLastValidVersionForReading() + throws IllegalStateException, FileNotFoundException + { + if ( randomAccessFile != null ) throw new IllegalStateException ( "Already started writing." ); + inputStream = new FileInputStream ( getCurrentVersionFileName() ); + return inputStream; + } + + /** + * Opens a new version for writing to. Note that + * this new version is tentative and cannot be read + * by {@link #openLastValidVersionForReading()} until + * {@link #discardBackupVersion()} is called. + * + * @return A stream for writing to. + * @throws IllegalStateException If called more than once + * without a close in between. + * @throws IOException If the file cannot be opened for writing. + */ + public FileOutputStream openNewVersionForWriting() throws IOException + { + openNewVersionForNioWriting(); + return new FileOutputStream(randomAccessFile.getFD()); + } + + /** + * Opens a new version for writing to. Note that + * this new version is tentative and cannot be read + * by {@link #openLastValidVersionForReading()} until + * {@link #discardBackupVersion()} is called. + * + * @return A file for writing to. + * @throws IOException + * + * @throws IllegalStateException If called more than once + * without a close in between. + * @throws FileNotFoundException If the file cannot be opened for writing. + * @throws IOException + */ + public FileChannel openNewVersionForNioWriting() throws FileNotFoundException + { + if ( randomAccessFile != null ) throw new IllegalStateException ( "Already writing a new version." ); + version++; + randomAccessFile = new RandomAccessFile(getCurrentVersionFileName(), "rw"); + return randomAccessFile.getChannel(); + } + /** + * Discards the backup version (if any). + * After calling this method, the newer version + * produced after calling {@link #openNewVersionForWriting()} + * becomes valid for reading next time when + * {@link #openLastValidVersionForReading()} is called. + * + * Note: it is the caller's responsibility to make sure that + * all new data has been flushed to disk before calling this method! + * + * @throws IllegalStateException If {@link #openNewVersionForWriting()} has not been called yet. + * @throws IOException If the previous version exists but could no be deleted. + */ + public void discardBackupVersion() throws IllegalStateException, IOException + { + if ( randomAccessFile == null ) throw new IllegalStateException ( "No new version yet!" ); + String fileName = getBackupVersionFileName(); + + File temp = new File ( fileName ); + if ( temp.exists() && !temp.delete() ) throw new IOException ( "Failed to delete backup version: " + fileName ); + + } + + /** + * Closes any open resources and resets the file for reading again. + * @throws IOException If the output stream could not be closed. + */ + + public void close() throws IOException + { + resetVersion(); + if ( inputStream != null ) { + try { + inputStream.close(); + } catch (IOException e) { + //don't care and won't happen: closing an input stream + //does nothing says the JDK javadoc! + } finally { + inputStream = null; + } + } + if ( randomAccessFile != null ) { + try { + if ( randomAccessFile.getFD().valid() ) randomAccessFile.close(); + } finally { + randomAccessFile = null; + } + } + } + + public long getSize() + { + long res = -1; + File f = new File ( getCurrentVersionFileName() ); + res = f.length(); + return res; + } + +} diff --git a/public/util/src/main/reference/Slf4jLicense.pdf b/public/util/src/main/reference/Slf4jLicense.pdf new file mode 100644 index 000000000..2ef5b944e Binary files /dev/null and b/public/util/src/main/reference/Slf4jLicense.pdf differ diff --git a/public/util/src/test/java/com/atomikos/logging/AbstractLoggerFactoryTest.java b/public/util/src/test/java/com/atomikos/logging/AbstractLoggerFactoryTest.java new file mode 100644 index 000000000..1c781a047 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/logging/AbstractLoggerFactoryTest.java @@ -0,0 +1,132 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import junit.framework.TestCase; + +public abstract class AbstractLoggerFactoryTest extends TestCase { + + protected static final String MESSAGE = "warning"; + + protected static final Throwable ERROR = new Exception(); + + protected Logger logger; + + public void testLogTrace() { + configureLoggingFrameworkWithTrace(); + logger.logTrace(MESSAGE); + assertLoggedAsTrace(); + } + + protected abstract void configureLoggingFrameworkWithTrace() ; + + protected abstract void assertLoggedAsTrace(); + + public void testLogDebug() { + configureLoggingFrameworkWithDebug(); + logger.logDebug(MESSAGE); + assertLoggedAsDebug(); + } + + protected abstract void assertLoggedAsDebug(); + + public void testLogTraceWithException() { + configureLoggingFrameworkWithTrace(); + logger.logTrace(MESSAGE,ERROR); + assertLoggedAsTraceWithException(); + } + + protected abstract void assertLoggedAsTraceWithException() ; + + public void testLogDebugWithException() { + configureLoggingFrameworkWithDebug(); + logger.logDebug(MESSAGE,ERROR); + assertLoggedAsDebugWithException(); + } + + protected abstract void assertLoggedAsDebugWithException(); + + public void testLoggerCreated() { + assertNotNull(logger); + } + + public void testLogInfo() { + logger.logInfo(MESSAGE); + assertLoggedAsInfo(); + } + + protected abstract void assertLoggedAsInfo(); + + public void testLogInfoWithException() { + logger.logInfo(MESSAGE,ERROR); + assertLoggedAsInfoWithException(); + } + + protected abstract void assertLoggedAsInfoWithException(); + + public void testLogWarning() { + logger.logWarning(MESSAGE); + assertLoggedAsWarning(); + } + + protected abstract void assertLoggedAsWarning(); + + public void testLogWarningWithException() { + logger.logWarning(MESSAGE,ERROR); + assertLoggedAsWarningWithException(); + } + + protected abstract void assertLoggedAsWarningWithException(); + + public void testLogError() { + logger.logError(MESSAGE); + assertLoggedAsError(); + } + + protected abstract void assertLoggedAsError(); + + public void testLogErrorWithException() { + logger.logError(MESSAGE,ERROR); + assertLoggedAsErrorWithException(); + } + + protected abstract void assertLoggedAsErrorWithException(); + + public void testIsTraceEnabled() { + assertFalse(logger.isTraceEnabled()); + configureLoggingFrameworkWithTrace(); + assertTrue(logger.isTraceEnabled()); + } + public void testIsDebugEnabled() { + assertFalse(logger.isDebugEnabled()); + configureLoggingFrameworkWithDebug(); + assertTrue(logger.isDebugEnabled()); + } + + public void testIsInfoEnabled() { + assertFalse(logger.isInfoEnabled()); + configureLoggingFrameworkWithInfo(); + assertTrue(logger.isInfoEnabled()); + } + + public void testIsErrorEnabled() { + configureLoggingFrameworkWithNone(); + assertFalse(logger.isErrorEnabled()); + configureLoggingFrameworkWithError(); + assertTrue(logger.isErrorEnabled()); + } + + protected abstract void configureLoggingFrameworkWithNone(); + + protected abstract void configureLoggingFrameworkWithError(); + + protected abstract void configureLoggingFrameworkWithInfo(); + + protected abstract void configureLoggingFrameworkWithDebug(); +} \ No newline at end of file diff --git a/public/util/src/test/java/com/atomikos/logging/JULLoggerFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/logging/JULLoggerFactoryTestJUnit.java new file mode 100644 index 000000000..046731f0c --- /dev/null +++ b/public/util/src/test/java/com/atomikos/logging/JULLoggerFactoryTestJUnit.java @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class JULLoggerFactoryTestJUnit extends AbstractLoggerFactoryTest { + + // can't mock the "logger" Itself but Logger call a "publish" method on a + // mocked handler + private Handler handler = Mockito.mock(Handler.class); + + @Override + public void setUp() { + LoggerFactory.setLoggerFactoryDelegate(new JULLoggerFactoryDelegate()); + logger = LoggerFactory.createLogger(getClass()); + getUnderlyingLogger().addHandler(handler); + getUnderlyingLogger().setLevel(Level.INFO); + } + + public void testAssertSlf4jLoggerCreated() { + assertTrue(logger instanceof JULLogger); + } + + @Override + protected void assertLoggedAsDebug() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.FINE, false); + } + + @Override + protected void assertLoggedAsDebugWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.FINE, true); + } + + @Override + protected void assertLoggedAsInfo() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.INFO, false); + } + + @Override + protected void assertLoggedAsInfoWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.INFO, true); + } + + @Override + protected void assertLoggedAsWarning() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.WARNING, false); + } + + @Override + protected void assertLoggedAsWarningWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.WARNING, true); + + } + + private void assertLogRecord(LogRecord logRecord, Level expectedLevel, + boolean withThrowable) { + assertEquals(MESSAGE, logRecord.getMessage()); + assertEquals(expectedLevel, logRecord.getLevel()); + if (withThrowable) { + assertNotNull(logRecord.getThrown()); + } else { + assertNull(logRecord.getThrown()); + } + } + + public void testIsInfoEnabled() { + getUnderlyingLogger().setLevel(Level.WARNING); + super.testIsInfoEnabled(); + } + + @Override + protected void configureLoggingFrameworkWithInfo() { + getUnderlyingLogger().setLevel(Level.INFO); + } + + @Override + protected void configureLoggingFrameworkWithDebug() { + getUnderlyingLogger().setLevel(Level.FINE); + } + + private java.util.logging.Logger getUnderlyingLogger() { + return java.util.logging.Logger.getLogger(getClass().getName()); + } + + @Override + protected void assertLoggedAsError() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.SEVERE, false); + } + + @Override + protected void assertLoggedAsErrorWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.SEVERE, true); + } + + @Override + protected void configureLoggingFrameworkWithError() { + getUnderlyingLogger().setLevel(Level.SEVERE); + } + + @Override + protected void configureLoggingFrameworkWithNone() { + getUnderlyingLogger().setLevel(Level.OFF); + } + + @Override + protected void configureLoggingFrameworkWithTrace() { + getUnderlyingLogger().setLevel(Level.FINEST); + } + + @Override + protected void assertLoggedAsTrace() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.FINEST, false); + } + + @Override + protected void assertLoggedAsTraceWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogRecord.class); + Mockito.verify(handler).publish(arg.capture()); + LogRecord logRecord = arg.getValue(); + assertLogRecord(logRecord, Level.FINEST, true); + + } + +} diff --git a/public/util/src/test/java/com/atomikos/logging/Log4JLoggerFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/logging/Log4JLoggerFactoryTestJUnit.java new file mode 100644 index 000000000..9d400cea2 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/logging/Log4JLoggerFactoryTestJUnit.java @@ -0,0 +1,173 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import org.apache.log4j.Appender; +import org.apache.log4j.Level; +import org.apache.log4j.spi.LoggingEvent; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class Log4JLoggerFactoryTestJUnit extends AbstractLoggerFactoryTest { + + // can't mock the "logger" Itself but Logger call a "publish" method on a + // mocked handler + private Appender mockedAppender = Mockito.mock(Appender.class); + + public void setUp() { + LoggerFactory.setLoggerFactoryDelegate(new Log4JLoggerFactoryDelegate()); + logger = LoggerFactory.createLogger(getClass()); + + getUnderlyingLogger().addAppender(mockedAppender); + getUnderlyingLogger().setLevel(Level.INFO); + } + + public void testAssertSlf4jLoggerCreated() { + assertTrue(logger instanceof Log4JLogger); + } + + public void testLogDebug() { + configureLoggingFrameworkWithDebug(); + logger.logDebug(MESSAGE); + assertLoggedAsDebug(); + } + + @Override + protected void assertLoggedAsDebug() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.DEBUG, false); + } + + @Override + protected void assertLoggedAsDebugWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.DEBUG, true); + } + + private void assertLoggingEvent(LoggingEvent loggingEvent, + Level expectedLevel, boolean withThrowable) { + assertEquals(MESSAGE, loggingEvent.getMessage()); + assertEquals(expectedLevel, loggingEvent.getLevel()); + if (withThrowable) { + assertNotNull(loggingEvent.getThrowableInformation()); + } else { + assertNull(loggingEvent.getThrowableInformation()); + } + + } + + @Override + protected void assertLoggedAsInfo() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.INFO, false); + } + + @Override + protected void assertLoggedAsInfoWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.INFO, true); + + } + + @Override + protected void assertLoggedAsWarning() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.WARN, false); + } + + @Override + protected void assertLoggedAsWarningWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.WARN, true); + } + + public void testIsInfoEnabled() { + getUnderlyingLogger().setLevel(Level.WARN); + super.testIsInfoEnabled(); + } + + @Override + protected void configureLoggingFrameworkWithInfo() { + getUnderlyingLogger().setLevel(Level.INFO); + + } + + @Override + protected void configureLoggingFrameworkWithDebug() { + getUnderlyingLogger().setLevel(Level.DEBUG); + + } + + private org.apache.log4j.Logger getUnderlyingLogger() { + return org.apache.log4j.Logger.getLogger(getClass()); + } + + @Override + protected void assertLoggedAsError() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.ERROR, false); + } + + @Override + protected void assertLoggedAsErrorWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.ERROR, true); + } + + @Override + protected void configureLoggingFrameworkWithNone() { + getUnderlyingLogger().setLevel(Level.OFF); + } + + @Override + protected void configureLoggingFrameworkWithError() { + getUnderlyingLogger().setLevel(Level.ERROR); + } + + @Override + protected void configureLoggingFrameworkWithTrace() { + getUnderlyingLogger().setLevel(Level.TRACE); + + } + + @Override + protected void assertLoggedAsTrace() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.TRACE, false); + + } + + @Override + protected void assertLoggedAsTraceWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LoggingEvent.class); + Mockito.verify(mockedAppender).doAppend(arg.capture()); + LoggingEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.TRACE, true); + + } + +} diff --git a/public/util/src/test/java/com/atomikos/logging/Log4j2LoggerFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/logging/Log4j2LoggerFactoryTestJUnit.java new file mode 100644 index 000000000..b46a6e781 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/logging/Log4j2LoggerFactoryTestJUnit.java @@ -0,0 +1,179 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class Log4j2LoggerFactoryTestJUnit extends AbstractLoggerFactoryTest { + + private org.apache.logging.log4j.core.Appender mockedAppender = Mockito + .mock(org.apache.logging.log4j.core.Appender.class); + + public void setUp() { + LoggerFactory + .setLoggerFactoryDelegate(new Log4j2LoggerFactoryDelegate()); + logger = LoggerFactory.createLogger(getClass()); + + Mockito.when(mockedAppender.getName()).thenReturn("mock"); + Mockito.when(mockedAppender.isStarted()).thenReturn(true); + + getUnderlyingLogger().addAppender(mockedAppender); + + getUnderlyingLogger().setLevel(Level.INFO); + } + + public void testAssertLog4j2LoggerCreated() { + assertTrue(logger instanceof Log4j2Logger); + } + + public void testLogDebug() { + configureLoggingFrameworkWithDebug(); + logger.logDebug(MESSAGE); + assertLoggedAsDebug(); + } + + @Override + protected void assertLoggedAsDebug() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + // Mockito.verify(mockedAppender).doAppend(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.DEBUG, false); + } + + @Override + protected void assertLoggedAsDebugWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.DEBUG, true); + } + + private void assertLoggingEvent(LogEvent loggingEvent, Level expectedLevel, + boolean withThrowable) { + assertEquals(MESSAGE, loggingEvent.getMessage().getFormattedMessage()); + assertEquals(expectedLevel, loggingEvent.getLevel()); + if (withThrowable) { + assertNotNull(loggingEvent.getThrown()); + } else { + assertNull(loggingEvent.getThrown()); + } + + } + + @Override + protected void assertLoggedAsInfo() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.INFO, false); + } + + @Override + protected void assertLoggedAsInfoWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.INFO, true); + + } + + @Override + protected void assertLoggedAsWarning() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.WARN, false); + } + + @Override + protected void assertLoggedAsWarningWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.WARN, true); + } + + public void testIsInfoEnabled() { + getUnderlyingLogger().setLevel(Level.WARN); + super.testIsInfoEnabled(); + } + + @Override + protected void configureLoggingFrameworkWithInfo() { + getUnderlyingLogger().setLevel(Level.INFO); + + } + + @Override + protected void configureLoggingFrameworkWithDebug() { + getUnderlyingLogger().setLevel(Level.DEBUG); + + } + + private org.apache.logging.log4j.core.Logger getUnderlyingLogger() { + return (org.apache.logging.log4j.core.Logger) LogManager + .getLogger(getClass()); + } + + @Override + protected void assertLoggedAsError() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.ERROR, false); + } + + @Override + protected void assertLoggedAsErrorWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.ERROR, true); + } + + @Override + protected void configureLoggingFrameworkWithNone() { + getUnderlyingLogger().setLevel(Level.OFF); + } + + @Override + protected void configureLoggingFrameworkWithError() { + getUnderlyingLogger().setLevel(Level.ERROR); + } + + @Override + protected void configureLoggingFrameworkWithTrace() { + getUnderlyingLogger().setLevel(Level.TRACE); + + } + + @Override + protected void assertLoggedAsTrace() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.TRACE, false); + + } + + @Override + protected void assertLoggedAsTraceWithException() { + ArgumentCaptor arg = ArgumentCaptor.forClass(LogEvent.class); + Mockito.verify(mockedAppender).append(arg.capture()); + LogEvent loggingEvent = arg.getValue(); + assertLoggingEvent(loggingEvent, Level.TRACE, true); + + } + +} diff --git a/public/util/src/test/java/com/atomikos/logging/LoggerFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/logging/LoggerFactoryTestJUnit.java new file mode 100644 index 000000000..e5e07e191 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/logging/LoggerFactoryTestJUnit.java @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import junit.framework.TestCase; + +public class LoggerFactoryTestJUnit extends TestCase { + + public void testCreateLogger() { + System.out.println(LoggerFactory.loggerFactoryDelegate); + + assertEquals(com.atomikos.logging.Slf4JLoggerFactoryDelegate.class, LoggerFactory.loggerFactoryDelegate.getClass()); + } + +} diff --git a/public/util/src/test/java/com/atomikos/logging/Slf4jLoggerFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/logging/Slf4jLoggerFactoryTestJUnit.java new file mode 100644 index 000000000..4ab76b83a --- /dev/null +++ b/public/util/src/test/java/com/atomikos/logging/Slf4jLoggerFactoryTestJUnit.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.logging; + +import org.mockito.Mockito; +import org.slf4j.impl.StaticLoggerBinder; + + +public class Slf4jLoggerFactoryTestJUnit extends AbstractLoggerFactoryTest { + + + + public void setUp() { + LoggerFactory.setLoggerFactoryDelegate(new Slf4JLoggerFactoryDelegate()); + logger = LoggerFactory.createLogger(getClass()); + } + + protected void assertLoggedAsDebug() { + Mockito.verify(StaticLoggerBinder.mockito).debug(MESSAGE); + } + + protected void assertLoggedAsTrace() { + Mockito.verify(StaticLoggerBinder.mockito).trace(MESSAGE); + } + protected void assertLoggedAsDebugWithException() { + Mockito.verify(StaticLoggerBinder.mockito).debug(MESSAGE,ERROR); + + } + @Override + protected void assertLoggedAsTraceWithException() { + Mockito.verify(StaticLoggerBinder.mockito).trace(MESSAGE,ERROR); + } + + protected void assertLoggedAsInfo() { + Mockito.verify(StaticLoggerBinder.mockito).info(MESSAGE); + } + + protected void assertLoggedAsInfoWithException() { + Mockito.verify(StaticLoggerBinder.mockito).info(MESSAGE,ERROR); + + } + + protected void assertLoggedAsWarning() { + Mockito.verify(StaticLoggerBinder.mockito).warn(MESSAGE); + } + + protected void assertLoggedAsWarningWithException() { + Mockito.verify(StaticLoggerBinder.mockito).warn(MESSAGE,ERROR); + } + + public void testAssertSlf4jLoggerCreated() { + assertTrue(logger instanceof Slf4jLogger); + } + + @Override + protected void configureLoggingFrameworkWithDebug() { + Mockito.when(StaticLoggerBinder.mockito.isDebugEnabled()).thenReturn(true); + } + + @Override + protected void configureLoggingFrameworkWithTrace() { + Mockito.when(StaticLoggerBinder.mockito.isTraceEnabled()).thenReturn(true); + } + + @Override + protected void configureLoggingFrameworkWithInfo() { + Mockito.when(StaticLoggerBinder.mockito.isInfoEnabled()).thenReturn(true); + } + + @Override + protected void assertLoggedAsError() { + Mockito.verify(StaticLoggerBinder.mockito).error(MESSAGE); + } + + @Override + protected void assertLoggedAsErrorWithException() { + Mockito.verify(StaticLoggerBinder.mockito).error(MESSAGE,ERROR); + } + + @Override + protected void configureLoggingFrameworkWithNone() { + Mockito.when(StaticLoggerBinder.mockito.isErrorEnabled()).thenReturn(false); + } + + @Override + protected void configureLoggingFrameworkWithError() { + Mockito.when(StaticLoggerBinder.mockito.isErrorEnabled()).thenReturn(true); + } + + + + +} diff --git a/public/util/src/test/java/com/atomikos/publish/EventPublisherTestJUnit.java b/public/util/src/test/java/com/atomikos/publish/EventPublisherTestJUnit.java new file mode 100644 index 000000000..d7bbc4ed2 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/publish/EventPublisherTestJUnit.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.publish; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.atomikos.icatch.event.Event; +import com.atomikos.icatch.event.EventListener; +import com.atomikos.icatch.event.transaction.ParticipantHeuristicEvent; + +public class EventPublisherTestJUnit { + + private EventListener mock; + private Event event; + + @Before + public void setUp() throws Exception { + event = new ParticipantHeuristicEvent("id", null, null); + mock = Mockito.mock(EventListener.class); + EventPublisher.INSTANCE.registerEventListener(mock); + } + + @Test + public void testPublishNullEventDoesNotThrow() { + EventPublisher.INSTANCE.publish(null); + } + + @Test + public void testPublishNullEventDoesNotNotifyListeners() { + EventPublisher.INSTANCE.publish(null); + Mockito.verify(mock,Mockito.times(0)).eventOccurred((Event) Mockito.any()); + } + + @Test + public void testPublishNotifiesListener() { + EventPublisher.INSTANCE.publish(event); + Mockito.verify(mock,Mockito.times(1)).eventOccurred((Event) Mockito.any()); + } + + + @Test + public void testPublishNotifiesListenerWithSameEvent() { + EventPublisher.INSTANCE.publish(event); + Mockito.verify(mock,Mockito.times(1)).eventOccurred(event); + } + + +} diff --git a/public/util/src/test/java/com/atomikos/timing/PooledAlarmTimerTestJUnit.java b/public/util/src/test/java/com/atomikos/timing/PooledAlarmTimerTestJUnit.java new file mode 100644 index 000000000..f9fb4af90 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/timing/PooledAlarmTimerTestJUnit.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.timing; + +import junit.framework.TestCase; + +public class PooledAlarmTimerTestJUnit extends TestCase { + + private int count1; + private int count2; + + + + protected void setUp() throws Exception { + count1 = 0; + count2 = 0; + } +// +// public void testSingleAlarmListener() throws Exception { +// PooledAlarmTimer timer = new PooledAlarmTimer(100); +// timer.addAlarmTimerListener(new AlarmTimerListener() { +// public void alarm(AlarmTimer timer) { +// count1 ++; +// } +// }); +// +// Thread thread = new Thread(timer); +// thread.start(); +// +// Thread.sleep(1010); +// assertEquals(10, count1); +// +// Thread.sleep(1010); +// assertEquals(20, count1); +// +// timer.stop(); +// Thread.sleep(500); +// assertFalse(thread.isAlive()); +// +// Thread.sleep(1010); +// assertEquals(20, count1); +// } + + public void testMultiAlarmListener() throws Exception { + PooledAlarmTimer timer = new PooledAlarmTimer(100); + + timer.addAlarmTimerListener(new AlarmTimerListener() { + public void alarm(AlarmTimer timer) { + count1 ++; + } + }); + timer.addAlarmTimerListener(new AlarmTimerListener() { + public void alarm(AlarmTimer timer) { + count2 ++; + if (count2 >= 10) + timer.removeAlarmTimerListener(this); + } + }); + + Thread thread = new Thread(timer); + thread.start(); + + Thread.sleep(1050); + assertEquals(10, count1); + assertEquals(10, count2); + + Thread.sleep(1050); + assertEquals(20, count1); + assertEquals(10, count2); + + timer.stopTimer(); + Thread.sleep(500); + assertFalse(thread.isAlive()); + + timer.addAlarmTimerListener(new AlarmTimerListener() { + public void alarm(AlarmTimer timer) { + count2 ++; + } + }); + + Thread.sleep(1050); + assertEquals(20, count1); + assertEquals(10, count2); + } + +} diff --git a/public/util/src/test/java/com/atomikos/util/DynamicProxySupportTestJUnit.java b/public/util/src/test/java/com/atomikos/util/DynamicProxySupportTestJUnit.java new file mode 100644 index 000000000..9b6a64822 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/DynamicProxySupportTestJUnit.java @@ -0,0 +1,129 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class DynamicProxySupportTestJUnit { + + private TestInterface dynamicProxy; + private TestInterface delegate; + private ProxiedClass proxy; + + @Before + public void setUp() { + delegate = Mockito.mock(TestInterface.class); + proxy = new ProxiedClass(delegate); + dynamicProxy = proxy.createDynamicProxy(); + } + + + @Test + public void testProxiedMethod() { + dynamicProxy.methodToProxy(); + Mockito.verifyNoInteractions(delegate); + } + + @Test + public void testOverloadedProxiedMethod() { + dynamicProxy.methodToProxy(10); + Mockito.verify(delegate, Mockito.times(1)).methodToProxy(Mockito.anyInt()); + } + + @Test + public void testNativeMethod() { + dynamicProxy.nativeMethod(); + Mockito.verify(delegate, Mockito.times(1)).nativeMethod(); + } + + @Test(expected = IllegalStateException.class) + public void testThrowInvocationAfterClose() throws Exception { + dynamicProxy.close(); + dynamicProxy.methodToProxy(); + } + + @Test + public void testProxiedMethodOverriddenInSubclass() { + proxy = new SubclassOfProxiedClass(delegate); + dynamicProxy = proxy.createDynamicProxy(); + dynamicProxy.methodToProxy(); + Mockito.verify(delegate, Mockito.times(1)).methodToProxy(); + } + + public static class ProxiedClass extends DynamicProxySupport { + + ProxiedClass(TestInterface delegate) { + super(delegate); + } + + + @Proxied + public void methodToProxy() { + // do NOT delegate - for test purposes + } + + @Proxied + public void methodToProxy(int parameter) { + delegate.methodToProxy(parameter); + } + + @Proxied + public void close() { + markClosed(); + } + + public void nativeMethod() { + delegate.nativeMethod(); + } + + @Override + protected void throwInvocationAfterClose(String method) throws Exception { + throw new IllegalStateException("method "+method+" not allowed after close"); + } + + + @Override + protected void handleInvocationException(Throwable e) throws Throwable { + throw e; + } + + + @Override + protected Class getRequiredInterfaceType() { + return TestInterface.class; + } + + + } + + public static class SubclassOfProxiedClass extends ProxiedClass { + + SubclassOfProxiedClass(TestInterface delegate) { + super(delegate); + } + + @Proxied + @Override + public void methodToProxy() { + delegate.methodToProxy(); + } + + } + + interface TestInterface { + + void methodToProxy(); + void nativeMethod(); + void methodToProxy(int parameter); + void close(); + } + +} diff --git a/public/util/src/test/java/com/atomikos/util/FutureTestJUnit.java b/public/util/src/test/java/com/atomikos/util/FutureTestJUnit.java new file mode 100644 index 000000000..9bac197a5 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/FutureTestJUnit.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.Before; +import org.junit.Test; + + /** + * Learning tests for Futures. Kept for later reference. + * + */ + +public class FutureTestJUnit { + + private ExecutorService executor = Executors.newFixedThreadPool(1); + + private Future future; + + @Before + public void setUp() { + future = executor.submit(() -> {return 0;}); + } + + @Test(expected=CancellationException.class) + public void testCancelBeforeGet() throws Exception { + future.cancel(true); + future.get(); + } + + @Test + public void testCancelDoesNotAffectNextSubmit() throws Exception { + future.cancel(true); + future = executor.submit(() -> {return 0;}); + future.get(); + } + + @Test(expected=TimeoutException.class) + public void testTimeout() throws Exception { + future = executor.submit(() -> {Thread.currentThread().sleep(100);return 0;}); + future.get(10, TimeUnit.MILLISECONDS); + } + + @Test + public void testCancelAfterTimeoutIsAllowed() throws Exception { + try { + testTimeout(); + } catch (TimeoutException e) {} + future.cancel(true); + } + + @Test + public void testCancelTwiceIsAllowed() throws Exception { + future.cancel(true); + future.cancel(true); + } + + @Test + public void testIsNotCancelledByDefault() throws Exception { + assertFalse(future.isCancelled()); + } + + @Test + public void testIsCancelledAfterCancel() { + future.cancel(true); + assertTrue(future.isCancelled()); + } + + @Test + public void testIsDoneAfterCancel() { + future.cancel(true); + assertTrue(future.isDone()); + } + + @Test + public void testIsDoneAfterGet() throws Exception { + future.get(); + assertTrue(future.isDone()); + } + + @Test + public void testIsNotDoneByDefault() { + assertFalse(future.isDone()); + } + + @Test + public void testIsNotDoneAfterTimeout() throws Exception { + try { + testTimeout(); + } catch (TimeoutException e) {} + assertFalse(future.isDone()); + } + } diff --git a/public/util/src/test/java/com/atomikos/util/IntraVmObjectFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/util/IntraVmObjectFactoryTestJUnit.java new file mode 100644 index 000000000..3f7c224f5 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/IntraVmObjectFactoryTestJUnit.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import javax.naming.Reference; + +import junit.framework.TestCase; + +public class IntraVmObjectFactoryTestJUnit extends TestCase { + + IntraVmObjectFactory fact; + + protected void setUp() throws Exception { + super.setUp(); + fact = new IntraVmObjectFactory(); + } + + public void testReturnsNullForNonReferenceArgument() throws Exception { + Object ref = new Object(); + assertNull ( fact.getObjectInstance( ref , null , null , null ) ); + } + + public void testReturnsNullForNullArgument() throws Exception { + Object ref = null; + assertNull ( fact.getObjectInstance( ref , null , null , null ) ); + } + + public void testReturnsObjectIfBoundInRegistry() throws Exception { + TestSerializableLocalResource object = new TestSerializableLocalResource(); + //following removed: creating a reference now also adds in the registry + //IntraVmObjectRegistry.addResource( object.getUniqueResourceName() , object ); + Reference ref = IntraVmObjectFactory.createReference( object , object.getUniqueResourceName() ); + Object lookup = fact.getObjectInstance( ref , null , null , null ); + assertNotNull ( lookup ); + assertSame ( object , lookup ); + } + + public void testReferenceContainsRightObjectFactory() throws Exception + { + //test for bug 23617 + TestSerializableLocalResource object = new TestSerializableLocalResource(); + Reference ref = IntraVmObjectFactory.createReference( object , object.getUniqueResourceName() ); + assertEquals ( IntraVmObjectFactory.class.getName() , ref.getFactoryClassName() ); + } + + //relevant for lookup in remote JNDI server + public void testInitIfNotAlreadyBoundInRegistry() throws Exception { + TestSerializableLocalResource object = new TestSerializableLocalResource(); + Reference ref = IntraVmObjectFactory.createReference( object , getName() ); + //simulate remote VM by removing the registration created by createReference + IntraVmObjectRegistry.removeResource( getName() ); + TestSerializableLocalResource result = ( TestSerializableLocalResource ) fact.getObjectInstance( ref , null , null , null ); + //no existing instance registered -> init should have happened + assertTrue ( result.wasInitCalled() ); + } + @SuppressWarnings("serial") + static class TestSerializableLocalResource implements Serializable { + + private boolean initCalled; + private long id; + + TestSerializableLocalResource() { + this.id = System.currentTimeMillis(); + + } + + public String getUniqueResourceName() { + return "" + id; + } + + private void readObject ( ObjectInputStream in ) throws IOException , ClassNotFoundException + { + in.defaultReadObject(); + initCalled = true; + } + + + public boolean wasInitCalled() { + return initCalled; + } + + public boolean equals ( Object o ) { + boolean ret = false; + if ( o instanceof TestSerializableLocalResource ) { + TestSerializableLocalResource other = ( TestSerializableLocalResource ) o; + ret = id == other.id; + } + return ret; + } + + }; + +} diff --git a/public/util/src/test/java/com/atomikos/util/IntraVmObjectRegistryTestJUnit.java b/public/util/src/test/java/com/atomikos/util/IntraVmObjectRegistryTestJUnit.java new file mode 100644 index 000000000..66ec0f9c3 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/IntraVmObjectRegistryTestJUnit.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.util.List; + +import javax.naming.NameAlreadyBoundException; +import javax.naming.NameNotFoundException; + +import junit.framework.TestCase; + +public class IntraVmObjectRegistryTestJUnit extends TestCase { + + private Integer object; + private String name; + + + protected void setUp() throws Exception { + super.setUp(); + name = getName(); + object = new Integer(1); + } + + public void testGetResourceWithoutPriorAdd() throws Exception + { + try { + IntraVmObjectRegistry.getResource ( name ); + fail ( "get works without prior add?" ); + } + catch ( NameNotFoundException ok ) {} + } + + public void testGetResourceWithPriorAdd() throws Exception + { + IntraVmObjectRegistry.addResource(name, object ); + Integer res = ( Integer ) IntraVmObjectRegistry.getResource(name); + if ( res == null ) fail ( "get fails after add?" ); + } + + public void testRemoveResourceWithoutPriorAdd() throws Exception + { + try { + IntraVmObjectRegistry.removeResource ( name ); + fail ( "remove works without prior add?" ); + } + catch ( NameNotFoundException ok ) {} + } + + public void testRemoveResourceWithPriorAdd() throws Exception + { + IntraVmObjectRegistry.addResource(name, object ); + IntraVmObjectRegistry.removeResource(name); + try { + IntraVmObjectRegistry.getResource ( name ); + fail ( "get works after remove?" ); + } + catch ( NameNotFoundException ok ) {} + } + + public void testAddSecondObjectWithSameName() throws Exception + { + IntraVmObjectRegistry.addResource(name, object); + try { + IntraVmObjectRegistry.addResource(name, object); + fail ( "second add with same name works" ); + + } + catch ( NameAlreadyBoundException ok ) {} + } + + public void testFindAllResourcesOfGivenType() throws Exception { + IntraVmObjectRegistry.addResource(name, object); + IntraVmObjectRegistry.addResource("bla", new String()); + List it = IntraVmObjectRegistry.findAllResourcesOfType(String.class); + assertEquals(1, it.size()); + } +} diff --git a/public/util/src/test/java/com/atomikos/util/ProductVersionTestJUnit.java b/public/util/src/test/java/com/atomikos/util/ProductVersionTestJUnit.java new file mode 100644 index 000000000..51bb14475 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/ProductVersionTestJUnit.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import static com.atomikos.util.Atomikos.VERSION; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ProductVersionTestJUnit { + + @Test + public void checkAtomikosVersion() { + assertEquals("X.Y.Z", VERSION); + } + +} diff --git a/public/util/src/test/java/com/atomikos/util/SerializableObjectFactoryTestJUnit.java b/public/util/src/test/java/com/atomikos/util/SerializableObjectFactoryTestJUnit.java new file mode 100644 index 000000000..df003db30 --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/SerializableObjectFactoryTestJUnit.java @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.Serializable; + +import javax.naming.Reference; + +import junit.framework.TestCase; + +public class SerializableObjectFactoryTestJUnit extends TestCase { + private static final String NAME = "name"; + + private static final int VALUE = 12; + + private static final int TRANSIENT_VALUE = 3; + + private TestBean bean; + + public SerializableObjectFactoryTestJUnit(String name) { + super(name); + } + + protected void setUp() { + + bean = new TestBean(); + bean.setName(NAME); + bean.setValue(VALUE); + bean.setTransientValue(TRANSIENT_VALUE); + } + + public void testBasic() throws Exception { + Reference ref = SerializableObjectFactory.createReference(bean); + SerializableObjectFactory fact = new SerializableObjectFactory(); + + bean = (TestBean) fact.getObjectInstance(ref, null, null, null); + if (!bean.getName().equals(NAME)) + fail("getName failure"); + if (bean.getValue() != VALUE) + fail("getValue failure"); + if (bean.getTransientValue() == TRANSIENT_VALUE) + fail("getTransientValue failure"); + } + @SuppressWarnings("serial") + static class TestBean implements Serializable { + private String name; + + private int value; + + private transient int transientValue; + + TestBean() { + name = ""; + value = 0; + transientValue = 0; + } + + /** + * @return + */ + public String getName() { + return name; + } + + /** + * @return + */ + public int getTransientValue() { + return transientValue; + } + + /** + * @return + */ + public int getValue() { + return value; + } + + /** + * @param string + */ + public void setName(String string) { + name = string; + } + + /** + * @param i + */ + public void setTransientValue(int i) { + transientValue = i; + } + + /** + * @param i + */ + public void setValue(int i) { + value = i; + } + + } +} diff --git a/public/util/src/test/java/com/atomikos/util/UniqueIdMgrTestJUnit.java b/public/util/src/test/java/com/atomikos/util/UniqueIdMgrTestJUnit.java new file mode 100644 index 000000000..a5b62df3d --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/UniqueIdMgrTestJUnit.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import junit.framework.TestCase; + +public class UniqueIdMgrTestJUnit extends TestCase { + + private UniqueIdMgr idmgr; + + protected void setUp() throws Exception { + super.setUp(); + idmgr = new UniqueIdMgr ( "./testserver" ); + } + + + public void testGetReturnsUniqueId() { + assertFalse(idmgr.get().equals(idmgr.get())); + } + +} diff --git a/public/util/src/test/java/com/atomikos/util/VersionedFileTestJUnit.java b/public/util/src/test/java/com/atomikos/util/VersionedFileTestJUnit.java new file mode 100644 index 000000000..ae8fed8ec --- /dev/null +++ b/public/util/src/test/java/com/atomikos/util/VersionedFileTestJUnit.java @@ -0,0 +1,231 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package com.atomikos.util; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import junit.framework.TestCase; + +public class VersionedFileTestJUnit extends TestCase { + + private static final String SUFFIX = ".dat"; + private static final String BASEDIR = "." + File.separatorChar; + + private VersionedFile file; + + protected void setUp() throws Exception + { + super.setUp(); + file = new VersionedFile ( BASEDIR , getBaseName() , SUFFIX ); + } + + protected void tearDown() throws Exception + { + super.tearDown(); + } + + private String getBaseName() + { + return getName(); + } + + private String getBaseUrl() + { + return BASEDIR + getBaseName(); + } + + public void testGetBaseUrl() + { + assertEquals ( getBaseUrl() , file.getBaseUrl() ); + } + + public void testAppendFileSeparator() { + file = new VersionedFile ( "." , getBaseName() , SUFFIX ); + testGetBaseUrl(); + } + public void testGetBaseName() + { + assertEquals ( getBaseName() , file.getBaseName() ); + } + + + public void testReadingNonExistingLogBaseDirShouldFailWithMeaningFulException() throws Exception { + String nonExistingBaseDir = "nonExistingBaseDir"; + try { + file = new VersionedFile ( nonExistingBaseDir , getBaseName() , SUFFIX ); + file.openLastValidVersionForReading(); + } catch (Exception e) { + String message = e.getMessage(); + assertTrue(message.contains(nonExistingBaseDir)); + } + } + public void testWritingNonExistingLogBaseDirShouldFailWithMeaningFulException() throws Exception { + String nonExistingBaseDir = "nonExistingBaseDir"; + try { + file = new VersionedFile ( nonExistingBaseDir , getBaseName() , SUFFIX ); + file.openNewVersionForNioWriting(); + } catch (Exception e) { + String message = e.getMessage(); + assertTrue(message.contains(nonExistingBaseDir)); + } + } + public void testGetSuffix() + { + assertEquals ( SUFFIX , file.getSuffix() ); + } + + public void testGetBaseDir() + { + assertEquals ( BASEDIR , file.getBaseDir() ); + } + + public void testBasicUsage() throws IOException + { + int value = 0; + //first recover the file's content + InputStream is; + DataInputStream dis; + try { + is = file.openLastValidVersionForReading(); + dis = new DataInputStream ( is ); + if ( dis.available() > 0 ) value = dis.readInt(); + dis.close(); + } catch ( FileNotFoundException noPreviousVersionExists ) { + + } + + int previousValue = value; + + //increment the value read + value++; + + //now, start writing and save the incremented value + //if anything fails here then the file is guaranteed to have the + //original (recovered) value still available on disk as a previous version + OutputStream os = file.openNewVersionForWriting(); + DataOutputStream dos = new DataOutputStream ( os ); + dos.writeInt ( value ); + dos.close(); + + //now we are sure that sufficient information has been written: + //discard any backup version - which makes the newer version current + file.discardBackupVersion(); + file.close(); + + //we must now be able to read the newer version + is = file.openLastValidVersionForReading(); + dis = new DataInputStream ( is ); + if ( dis.available() > 0 ) value = dis.readInt(); + dis.close(); + assertEquals ( previousValue + 1 , value ); + file.close(); + } + + + + public void testCallingOpenNewVersionForWritingTwiceThrowsException() throws IOException + { + file.openNewVersionForWriting(); + try { + file.openNewVersionForWriting(); + fail ( "Exception expected" ); + } catch ( IllegalStateException ok ) { + + } + } + + public void testDiscardBackupVersionFailsIfNoNewVersion() throws IOException + { + try { + file.discardBackupVersion(); + fail ( "Exception expected" ); + } catch ( IllegalStateException ok ) {} + } + + public void testCallingOpenLastValidVersionForReadingFailsIfWriting() throws IOException + { + file.openNewVersionForWriting(); + try { + file.openLastValidVersionForReading(); + fail ( "Exception expected" ); + } catch ( IllegalStateException ok ) { + + } + } + + public void testNotCallingDiscardBackupVersionReturnsOldDataOnReadingAgain() throws IOException + { + int value = 0; + //first recover the file's content + InputStream is; + DataInputStream dis; + try { + is = file.openLastValidVersionForReading(); + dis = new DataInputStream ( is ); + if ( dis.available() > 0 ) value = dis.readInt(); + dis.close(); + } catch ( FileNotFoundException noPreviousVersionExists ) { + + } + + int previousValue = value; + + //increment the value read + value++; + + //now, start writing and save the incremented value + //if anything fails here then the file is guaranteed to have the + //original (recovered) value still available on disk as a previous version + OutputStream os = file.openNewVersionForWriting(); + DataOutputStream dos = new DataOutputStream ( os ); + dos.writeInt ( value ); + dos.close(); + + //now we are sure that sufficient information has been written: + //discard any backup version - which makes the newer version current + file.discardBackupVersion(); + file.close(); + + //we must now be able to read the newer version + is = file.openLastValidVersionForReading(); + dis = new DataInputStream ( is ); + if ( dis.available() > 0 ) value = dis.readInt(); + dis.close(); + assertEquals ( previousValue + 1 , value ); + file.close(); + + //increment the value read again + previousValue = value; + value++; + + os = file.openNewVersionForWriting(); + dos = new DataOutputStream ( os ); + dos.writeInt ( value ); + dos.close(); + //close and don't discard the backup version + file.close(); + + //we must now be able to read the old version + //since the backup version was never discarded + //even though a new version was written + is = file.openLastValidVersionForReading(); + dis = new DataInputStream ( is ); + if ( dis.available() > 0 ) value = dis.readInt(); + dis.close(); + assertEquals ( previousValue , value ); + file.close(); + } + +} diff --git a/public/util/src/test/java/org/slf4j/impl/StaticLoggerBinder.java b/public/util/src/test/java/org/slf4j/impl/StaticLoggerBinder.java new file mode 100644 index 000000000..6c4c55554 --- /dev/null +++ b/public/util/src/test/java/org/slf4j/impl/StaticLoggerBinder.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package org.slf4j.impl; + +import org.mockito.Mockito; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.spi.LoggerFactoryBinder; + +public class StaticLoggerBinder implements LoggerFactoryBinder { + + public static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); + + public static Logger mockito; + + public ILoggerFactory getLoggerFactory() { + return new ILoggerFactory() { + + public Logger getLogger(String clazz) { + mockito = Mockito.mock(Logger.class); + return mockito; + } + + }; + } + + public String getLoggerFactoryClassStr() { + return null; + } + +} diff --git a/public/util/src/test/java/org/slf4j/impl/StaticMarkerBinder.java b/public/util/src/test/java/org/slf4j/impl/StaticMarkerBinder.java new file mode 100644 index 000000000..2483d6532 --- /dev/null +++ b/public/util/src/test/java/org/slf4j/impl/StaticMarkerBinder.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2000-2024 Atomikos + * + * LICENSE CONDITIONS + * + * See http://www.atomikos.com/Main/WhichLicenseApplies for details. + */ + +package org.slf4j.impl; + +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.spi.MarkerFactoryBinder; + +public class StaticMarkerBinder implements MarkerFactoryBinder { + + /** + * The unique instance of this class. + */ + public static final StaticMarkerBinder SINGLETON = new StaticMarkerBinder(); + + final IMarkerFactory markerFactory = new BasicMarkerFactory(); + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public String getMarkerFactoryClassStr() { + return BasicMarkerFactory.class.getName(); + } + +} diff --git a/public/util/src/test/resources/META-INF/maven/com.atomikos/atomikos-util/pom.properties b/public/util/src/test/resources/META-INF/maven/com.atomikos/atomikos-util/pom.properties new file mode 100644 index 000000000..20a582ca2 --- /dev/null +++ b/public/util/src/test/resources/META-INF/maven/com.atomikos/atomikos-util/pom.properties @@ -0,0 +1,9 @@ +# +# Copyright (C) 2000-2024 Atomikos +# +# LICENSE CONDITIONS +# +# See http://www.atomikos.com/Main/WhichLicenseApplies for details. +# + +version=X.Y.Z