Showing preview only (688K chars total). Download the full file or copy to clipboard to get everything.
Repository: gruelbox/transaction-outbox
Branch: master
Commit: 6e1814a539d1
Files: 198
Total size: 614.5 KB
Directory structure:
gitextract_2k8z5qvm/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── cd_build.yml
│ ├── pull_request.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── pom.xml
├── settings.xml
├── transactionoutbox-acceptance/
│ ├── pom.xml
│ └── src/
│ └── test/
│ └── java/
│ └── com/
│ └── gruelbox/
│ └── transactionoutbox/
│ └── acceptance/
│ ├── TestComplexConfigurationExample.java
│ ├── TestH2.java
│ ├── TestMSSqlServer2019.java
│ ├── TestMSSqlServer2022.java
│ ├── TestMySql5.java
│ ├── TestMySql8.java
│ ├── TestOracle18.java
│ ├── TestOracle21.java
│ ├── TestPostgres11.java
│ ├── TestPostgres12.java
│ ├── TestPostgres13.java
│ ├── TestPostgres14.java
│ ├── TestPostgres15.java
│ ├── TestPostgres16.java
│ ├── TestRequestSerialization.java
│ ├── TestStubbing.java
│ └── persistor/
│ ├── TestDefaultPersistorH2.java
│ ├── TestDefaultPersistorMSSqlServer2019.java
│ ├── TestDefaultPersistorMySql5.java
│ ├── TestDefaultPersistorMySql8.java
│ ├── TestDefaultPersistorOracle18.java
│ └── TestDefaultPersistorPostgres16.java
├── transactionoutbox-core/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ ├── AlreadyScheduledException.java
│ │ ├── ConnectionProvider.java
│ │ ├── DataSourceConnectionProvider.java
│ │ ├── DefaultDialect.java
│ │ ├── DefaultInvocationSerializer.java
│ │ ├── DefaultMigrationManager.java
│ │ ├── DefaultPersistor.java
│ │ ├── Dialect.java
│ │ ├── DriverConnectionProvider.java
│ │ ├── ExecutorSubmitter.java
│ │ ├── FailedDeserializingInvocation.java
│ │ ├── FunctionInstantiator.java
│ │ ├── Instantiator.java
│ │ ├── Invocation.java
│ │ ├── InvocationSerializer.java
│ │ ├── Migration.java
│ │ ├── MissingOptionalDependencyException.java
│ │ ├── NoTransactionActiveException.java
│ │ ├── OptimisticLockException.java
│ │ ├── ParameterContextTransactionManager.java
│ │ ├── Persistor.java
│ │ ├── ReflectionInstantiator.java
│ │ ├── RuntimeTypeAdapterFactory.java
│ │ ├── SQLAction.java
│ │ ├── SimpleTransactionManager.java
│ │ ├── StubParameterContextTransactionManager.java
│ │ ├── StubPersistor.java
│ │ ├── StubThreadLocalTransactionManager.java
│ │ ├── Submitter.java
│ │ ├── ThreadLocalContextTransactionManager.java
│ │ ├── ThrowingRunnable.java
│ │ ├── ThrowingTransactionalSupplier.java
│ │ ├── ThrowingTransactionalWork.java
│ │ ├── Transaction.java
│ │ ├── TransactionContextPlaceholder.java
│ │ ├── TransactionManager.java
│ │ ├── TransactionOutbox.java
│ │ ├── TransactionOutboxEntry.java
│ │ ├── TransactionOutboxImpl.java
│ │ ├── TransactionOutboxListener.java
│ │ ├── TransactionalInvocation.java
│ │ ├── TransactionalSupplier.java
│ │ ├── TransactionalWork.java
│ │ ├── UncheckedException.java
│ │ ├── Validatable.java
│ │ ├── Validator.java
│ │ └── spi/
│ │ ├── AbstractFullyQualifiedNameInstantiator.java
│ │ ├── AbstractThreadLocalTransactionManager.java
│ │ ├── ProxyFactory.java
│ │ ├── SimpleTransaction.java
│ │ └── Utils.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ ├── AbstractTestDefaultInvocationSerializer.java
│ │ ├── TestDefaultInvocationSerializer.java
│ │ ├── TestDefaultMigrationManager.java
│ │ ├── TestDefaultPersistorConfiguration.java
│ │ ├── TestProxyGeneration.java
│ │ └── TestValidator.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-guice/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── guice/
│ │ └── GuiceInstantiator.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── guice/
│ │ └── acceptance/
│ │ ├── TestGuiceBinding.java
│ │ └── TestGuiceInstantiator.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-jackson/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jackson/
│ │ ├── CustomInvocationDeserializer.java
│ │ ├── CustomInvocationSerializer.java
│ │ ├── JacksonInvocationSerializer.java
│ │ ├── TransactionOutboxEntryDeserializer.java
│ │ └── TransactionOutboxJacksonModule.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jackson/
│ │ ├── MonetaryAmount.java
│ │ ├── SerializationStressTestInput.java
│ │ ├── TestJacksonInvocationSerializer.java
│ │ ├── TestTransactionOutboxEntrySerialization.java
│ │ └── acceptance/
│ │ └── TestJacksonSerializer.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-jooq/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jooq/
│ │ ├── DefaultJooqTransactionManager.java
│ │ ├── JooqTransactionListener.java
│ │ ├── JooqTransactionManager.java
│ │ └── ThreadLocalJooqTransactionManager.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jooq/
│ │ └── acceptance/
│ │ ├── AbstractJooqAcceptanceTest.java
│ │ ├── AbstractJooqAcceptanceThreadLocalTest.java
│ │ ├── JooqTestUtils.java
│ │ ├── TestJooqThreadLocalH2.java
│ │ ├── TestJooqThreadLocalMSSqlServer2019.java
│ │ ├── TestJooqThreadLocalMySql5.java
│ │ ├── TestJooqThreadLocalMySql8.java
│ │ ├── TestJooqThreadLocalPostgres16.java
│ │ ├── TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext.java
│ │ └── TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-quarkus/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── quarkus/
│ │ ├── CdiInstantiator.java
│ │ └── QuarkusTransactionManager.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── quarkus/
│ │ └── acceptance/
│ │ ├── ApplicationConfig.java
│ │ ├── BusinessService.java
│ │ ├── BusinessServiceTest.java
│ │ ├── DaoImpl.java
│ │ └── RemoteCallService.java
│ └── resources/
│ ├── application.properties
│ └── db/
│ └── create.sql
├── transactionoutbox-spring/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── spring/
│ │ ├── SpringInstantiator.java
│ │ ├── SpringTransactionManager.java
│ │ └── SpringTransactionOutboxConfiguration.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── spring/
│ │ ├── SpringTransactionManagerTest.java
│ │ ├── example/
│ │ │ ├── multipledatasources/
│ │ │ │ ├── EventuallyConsistentController.java
│ │ │ │ ├── ExternalsConfiguration.java
│ │ │ │ ├── MultipleDataSourcesTest.java
│ │ │ │ ├── TransactionOutboxBackgroundProcessor.java
│ │ │ │ ├── TransactionOutboxProperties.java
│ │ │ │ ├── TransactionOutboxSpringMultipleDatasourcesDemoApplication.java
│ │ │ │ ├── computer/
│ │ │ │ │ ├── Computer.java
│ │ │ │ │ ├── ComputerExternalQueueService.java
│ │ │ │ │ ├── ComputerRepository.java
│ │ │ │ │ └── ComputersDbConfiguration.java
│ │ │ │ └── employee/
│ │ │ │ ├── Employee.java
│ │ │ │ ├── EmployeeExternalQueueService.java
│ │ │ │ ├── EmployeeRepository.java
│ │ │ │ └── EmployeesDbConfiguration.java
│ │ │ └── simple/
│ │ │ ├── Customer.java
│ │ │ ├── CustomerRepository.java
│ │ │ ├── EventuallyConsistentController.java
│ │ │ ├── EventuallyConsistentControllerTest.java
│ │ │ ├── ExternalQueueService.java
│ │ │ ├── ExternalsConfiguration.java
│ │ │ ├── TransactionOutboxBackgroundProcessor.java
│ │ │ ├── TransactionOutboxProperties.java
│ │ │ ├── TransactionOutboxSpringDemoApplication.java
│ │ │ └── Utils.java
│ │ └── it/
│ │ ├── MyRemoteService.java
│ │ ├── SpringTransactionManagerIT.java
│ │ └── TestApplication.java
│ └── resources/
│ ├── META-INF/
│ │ └── persistence.xml
│ ├── application.properties
│ └── logback-test.xml
├── transactionoutbox-testing/
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ ├── TestingMode.java
│ │ └── testing/
│ │ ├── AbstractAcceptanceTest.java
│ │ ├── AbstractPersistorTest.java
│ │ ├── BaseTest.java
│ │ ├── ClassProcessor.java
│ │ ├── InterfaceProcessor.java
│ │ ├── LatchListener.java
│ │ ├── OrderedEntryListener.java
│ │ ├── ProcessedEntryListener.java
│ │ └── TestUtils.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-virtthreads/
│ ├── pom.xml
│ └── src/
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── virtthreads/
│ │ ├── AbstractVirtualThreadsTest.java
│ │ ├── TestVirtualThreadsH2.java
│ │ ├── TestVirtualThreadsH2Jooq.java
│ │ ├── TestVirtualThreadsMySql5.java
│ │ ├── TestVirtualThreadsMySql8.java
│ │ ├── TestVirtualThreadsOracle21.java
│ │ └── TestVirtualThreadsPostgres16.java
│ └── resources/
│ └── logback-test.xml
└── ~/
├── settings.xml
└── toolchains.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: maven
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
================================================
FILE: .github/workflows/cd_build.yml
================================================
name: Continous Delivery
on:
push:
branches: [master]
jobs:
build:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-package: jdk
java-version: 25
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
cache: 'maven'
- name: Build, publish to GPR and tag
run: |
if [ "$GITHUB_REPOSITORY" == "gruelbox/transaction-outbox" ]; then
revision="7.0.$GITHUB_RUN_NUMBER"
echo "Building $revision at $GITHUB_SHA"
mvn -Pdelombok,only-nodb-tests -B deploy -s $GITHUB_WORKSPACE/settings.xml -Drevision="$revision" -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
echo "Tagging $revision"
git tag $revision
git push origin $revision
else
mvn -Pdelombok,only-nodb-tests -B package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
fi
env:
GITHUB_TOKEN: ${{ github.token }}
================================================
FILE: .github/workflows/pull_request.yml
================================================
name: Pull request
on:
pull_request:
branches: [ master ]
jobs:
build:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-package: jdk
java-version: 25
cache: maven
- name: Build
run: mvn -Pdelombok -B fmt:check package test-compile -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
- uses: actions/upload-artifact@v4
with:
name: build
path: "**/*"
compression-level: 9
test:
needs: build
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
strategy:
matrix:
jdk: [ 17,21,25 ]
db: [ nodb,mysql5,mysql8,postgres,oracle18,oracle21,mssqlserver ]
fail-fast: false
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: .
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-package: jdk
java-version: ${{ matrix.jdk }}
cache: maven
- name: test
run: mvn -Pnoformat,only-${{ matrix.db }}-tests -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
================================================
FILE: .github/workflows/release.yml
================================================
name: Publish to Central
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-package: jdk
java-version: 25
settings-path: "~" # location for the settings.xml file
cache: 'maven'
- name: Build and publish
run: |
set -e
revision=${GITHUB_REF##*/}
echo "Publishing version $revision to Central"
echo ${{ secrets.GPG_SECRETKEYS }} | base64 --decode | $GPG_EXECUTABLE --import --no-tty --batch --yes
echo ${{ secrets.GPG_OWNERTRUST }} | base64 --decode | $GPG_EXECUTABLE --import-ownertrust --no-tty --batch --yes
sed -i "s_\(<revision>\)[^<]*_\1${revision}_g" pom.xml
mvn -Prelease,delombok -B deploy -s $GITHUB_WORKSPACE/settings.xml -Drevision=$revision -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
env:
GITHUB_TOKEN: ${{ github.token }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
GPG_EXECUTABLE: gpg
GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Update READMEs
run: |
set -e
revision=${GITHUB_REF##*/}
echo "Updating READMEs"
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-guice/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-jackson/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-jooq/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-spring/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-quarkus/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-core:\)[^']*_\1${revision}_g" README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-guice:\)[^']*_\1${revision}_g" transactionoutbox-guice/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-jackson:\)[^']*_\1${revision}_g" transactionoutbox-jackson/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-jooq:\)[^']*_\1${revision}_g" transactionoutbox-jooq/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-spring:\)[^']*_\1${revision}_g" transactionoutbox-spring/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-quarkus:\)[^']*_\1${revision}_g" transactionoutbox-quarkus/README.md
- name: Create version update pull request
uses: gruelbox/create-pull-request@master
with:
commit-message: "Update versions in READMEs [skip ci]"
title: Update versions in READMEs
body: Updates the versions in the README files following the release
branch: update-readme-version
base: master
author: GitHub <noreply@github.com>
================================================
FILE: .gitignore
================================================
**/.classpath
**/.project
**/.settings
**/.factorypath
/.idea
/*.iml
**/target/**
*.iml
**/.flattened-pom.xml
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# transaction-outbox
[](#stable-releases)
[](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core)
[](https://github.com/gruelbox/transaction-outbox/releases/latest)
[](#development-snapshots)
[](https://github.com/gruelbox/transaction-outbox/commits/master)
[](https://github.com/gruelbox/transaction-outbox/actions)
[](https://www.codefactor.io/repository/github/gruelbox/transaction-outbox)
A flexible implementation of the [Transaction Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html) for Java. `TransactionOutbox` has a clean, extensible API, very few dependencies and plays nicely with a variety of database platforms, transaction management approaches and application frameworks. Every aspect is highly configurable or overridable. It features out-of-the-box support for **Spring DI**, **Spring Txn**, **Guice**, **MySQL 5 & 8**, **PostgreSQL 11-16**, **Oracle 18 & 21**, **MS SQL Server 2017** and **H2**.
## Contents
1. [Why do I need it?](#why-do-i-need-it)
1. [Installation](#installation)
1. [Requirements](#requirements)
1. [Stable releases](#stable-releases)
1. [Development snapshots](#development-snapshots)
1. [Basic Configuration](#basic-configuration)
1. [No existing transaction manager or dependency injection](#no-existing-transaction-manager-or-dependency-injection)
1. [Spring](#spring)
1. [Guice](#guice)
1. [jOOQ](#jooq)
1. [Set up the background worker](#set-up-the-background-worker)
1. [Managing the "dead letter queue"](#managing-the-dead-letter-queue)
1. [Advanced](#advanced)
1. [Topics and FIFO ordering](#topics-and-fifo-ordering)
1. [The nested outbox pattern](#the-nested-outbox-pattern)
1. [Idempotency protection](#idempotency-protection)
1. [Delayed/scheduled processing](#delayedscheduled-processing)
1. [Flexible serialization](#flexible-serialization-beta)
1. [Clustering](#clustering)
1. [OpenTelemetry](#opentelemetry)
1. [Encryption](#encryption)
1. [Configuration reference](#configuration-reference)
1. [Stubbing in tests](#stubbing-in-tests)
## Why do I need it?
[This article](https://microservices.io/patterns/data/transactional-outbox.html) explains the concept in an abstract manner, but let's say we have a microservice that handles point-of-sale and need to implement a REST endpoint to record a sale. We end up with this:
### Attempt 1
```java
@POST
@Path("/sales")
@Transactional
public SaleId createWidget(Sale sale) {
var saleId = saleRepository.save(sale);
messageQueue.postMessage(StockReductionEvent.of(sale.item(), sale.amount()));
messageQueue.postMessage(IncomeEvent.of(sale.value()));
return saleId;
}
```
The `SaleRepository` handles recording the sale in the customer's account, the `StockReductionEvent` goes off to our _warehouse_ service, and the `IncomeEvent` goes to our financial records service (let's ignore the potential flaws in the domain modelling for now).
There's a big problem here: the `@Transactional` annotation is a lie (no, [really](https://lmgtfy.com/?q=dont+use+distributed+transactions)). It only really wraps the `SaleRepository` call, but not the two event postings. This means that we could end up sending the two events and fail to actually commit the sale. Our system is now inconsistent.
### Attempt 2 - Use Idempotency
We could make whole method [idempotent](http://restcookbook.com/HTTP%20Methods/idempotency/) and re-write it to work a bit more like this:
```java
@PUT
@Path("/sales/{id}")
public void createWidget(@PathParam("id") SaleId saleId, Sale sale) {
saleRepository.saveInNewTransaction(saleId, sale);
messageQueue.postMessage(StockReductionEvent.of(saleId, sale.item(), sale.amount()));
messageQueue.postMessage(IncomeEvent.of(saleId, sale.value()));
}
```
This is better. As long as the caller keeps calling the method until they get a success, we can keep re-saving and re-sending the messages without any risk of duplicating work. This works regardless of the order of the calls (and in any case, there may be good reasons of referential integrity to fix the order).
The problem is that _they might stop trying_, and if they do, we could end up with only part of this transaction completed. If this is a public API, we can't force clients to use it correctly.
We also still have another problem: external calls are inherently more vulnerable to downtime and performance degredation. We could find our service rendered unresponsive or failing if they are unavailable. Ideally, we would like to "buffer" these external calls within our service safely until our downstream dependencies are available.
### Attempt 3 - Transaction Outbox
Idempotency is a good thing, so let's stick with the `PUT`. Here is the same example, using Transaction Outbox:
```java
@PUT
@Path("/sales/{id}")
@Transactional
public void createWidget(@PathParam("id") SaleId saleId, Sale sale) {
saleRepository.save(saleId, sale);
MessageQueue proxy = transactionOutbox.schedule(MessageQueue.class);
proxy.postMessage(StockReductionEvent.of(saleId, sale.item(), sale.amount()));
proxy.postMessage(IncomeEvent.of(saleId, sale.value()));
}
```
Here's what happens:
- When you create an instance of [`TransactionOutbox`](https://www.javadoc.io/static/com.gruelbox/transactionoutbox-core/0.1.57/com/gruelbox/transactionoutbox/TransactionOutbox.html) (see [Basic Configuration](#basic-configuration)), it will, by default, automatically create two database tables, `TXNO_OUTBOX` and `TXNO_VERSION`, and then keep these synchronized with schema changes as new versions are released. _Note: this is the default behaviour on a SQL database, but is completely overridable if you are using a different type of data store or don't want a third party library managing your database schema. See [Configuration reference](#configuration-reference)_.
- [`TransactionOutbox`](https://www.javadoc.io/static/com.gruelbox/transactionoutbox-core/0.1.57/com/gruelbox/transactionoutbox/TransactionOutbox.html) creates a proxy of `MessageQueue`. Any method calls on the proxy are serialized and written to the `TXNO_OUTBOX` table (by default) _in the same transaction_ as the `SaleRepository` call. The call returns immediately rather than actually invoking the real method.
- If the transaction rolls back, so do the serialized requests.
- Immediately after the transaction is successfully committed, another thread will attempt to make the _real_ call to `MessageQueue` asynchronously.
- If that call fails, or the application dies before the call is attempted, a [background "mop-up" thread](#set-up-the-background-worker) will re-attempt the call a configurable number of times, with configurable time between each, before [blocking](#managing-the-dead-letter-queue) the request and firing and event for it to be investigated (similar to a [dead letter queue](https://en.wikipedia.org/wiki/Dead_letter_queue)).
- Blocked requests can be easily [unblocked](#managing-the-dead-letter-queue) again once the underlying issue is resolved.
Our service is now resilient and explicitly eventually consistent, as long as all three elements (`SaleRepository` and the downstream event handlers) are idempotent, since those messages will be attempted repeatedly until confirmed successful, which means they could occur multiple times.
If you find yourself wondering _why bother with the queues now_? You're quite right. As we now have outgoing buffers, we already have most of the benefits of middleware (at least for some use cases). We could replace the calls to a message queue with direct queues to the other services' load balancers and switch to a peer-to-peer architecture, if we so choose.
> Note that for the above example to work, `StockReductionEvent` and `IncomeEvent` need to be included for serialization. See [Configuration reference](#configuration-reference).
## Installation
### Requirements
- At least **Java 11**. Downgrading to requiring Java 8 is [under consideration](https://github.com/gruelbox/transaction-outbox/issues/29).
- Currently, **MySQL**, **PostgreSQL**, **Oracle**, **MS SQL Server** or **H2** databases (pull requests to support other traditional RDMBS would be trivial. Beyond that, a SQL database is not strictly necessary for the pattern to work, merely a data store with the concept of a transaction spanning multiple mutation operations).
- Database access via **JDBC** (In principle, JDBC should not be required - alternatives such as R2DBC are under investigation - but the API is currently tied to it)
- Native transactions (not JTA or similar).
- (Optional) Proxying non-interfaces requires [ByteBuddy](https://bytebuddy.net/#/) and for proxying classes without default constructors [Objenesis](http://objenesis.org/) to be added as a dependency
### Stable releases
The latest stable release is available from Maven Central. Stable releases are [sort-of semantically versioned](https://semver.org/). That is, they follow semver in every respect other than that the version numbers are not monotically increasing. The project uses continuous delivery and selects individual stable releases to promote to Central, so Central releases will always be spaced apart numerically. The important thing, though, is that dependencies should be safe to upgrade as long as the major version number has not increased.
#### Maven
```xml
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-core</artifactId>
<version>7.0.707</version>
</dependency>
```
#### Gradle
```groovy
implementation 'com.gruelbox:transactionoutbox-core:7.0.707'
```
### Development snapshots
Maven Central is updated regularly. However, if you want to stay at the bleeding edge, you can use continuously-delivered releases from [Github Package Repository](https://github.com/gruelbox/transaction-outbox/packages). These can be used from your production builds since they will never be deleted (unlike `SNAPSHOT`s).
#### Maven
```xml
<repositories>
<repository>
<id>github-transaction-outbox</id>
<name>Gruelbox Github Repository</name>
<url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>
</repository>
</repositories>
```
You will need to authenticate with Github to use Github Package Repository. Create a personal access token in [your GitHub settings](https://github.com/settings/tokens). It only needs **read:package** permissions. Then add something like the following in your Maven `settings.xml`:
```xml
<servers>
<server>
<id>github-transaction-outbox</id>
<username>${env.GITHUB_USERNAME}</username>
<password>${env.GITHUB_TOKEN}</password>
</server>
</servers>
```
The above example uses environment variables, allowing you to keep the credentials out of source control, but you can hard-code them if you know what you're doing.
#### Gradle
```groovy
repositories {
maven {
name = "github-transaction-outbox"
url = uri("https://maven.pkg.github.com/gruelbox/transaction-outboxY")
credentials {
username = $githubUserName
password = $githubToken
}
}
}
```
## Basic Configuration
An application needs a single, shared instance of [`TransactionOutbox`](https://www.javadoc.io/static/com.gruelbox/transactionoutbox-core/0.1.57/com/gruelbox/transactionoutbox/TransactionOutbox.html), which is configured using a builder on construction. This takes some time to get right, particularly if you already have a transaction management solution in your application.
### No existing transaction manager or dependency injection
If you have no existing transaction management, connection pooling or dependency injection, here's a quick way to get started:
```java
// Use an in-memory H2 database
TransactionManager transactionManager = TransactionManager.fromConnectionDetails(
"org.h2.Driver", "jdbc:h2:mem:test;MV_STORE=TRUE", "test", "test"));
// Create the outbox
TransactionOutbox outbox = TransactionOutbox.builder()
.transactionManager(transactionManager)
.persistor(Persistor.forDialect(Dialect.H2))
.build();
// Start a transaction
transactionManager.inTransaction(tx -> {
// Save some stuff
tx.connection().createStatement().execute("INSERT INTO...");
// Create an outbox request
outbox.schedule(MyClass.class).myMethod("Foo", "Bar"));
});
```
Alternatively, you could create the [`TransactionManager`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionManager.html) from a [`DataSource`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionManager.html), allowing you to use a connection pooling `DataSource` such as Hikari:
```java
TransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);
```
In this default configuration, `MyClass` must have a default constructor so the "real" implementation can be constructed at the point the method is actually invoked (which might be on another day on another instance of the application). However, you can avoid this requirement by providing an [`Instantiator`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/Instantiator.html) on every instance of your application that knows how to create the objects:
```java
TransactionOutbox outbox = TransactionOutbox.builder()
.instantiator(Instantiator.using(clazz -> createInstanceOf(clazz)))
.build();
```
### Spring
See [transaction-outbox-spring](transactionoutbox-spring/README.md), which integrates Spring's DI and/or transaction management with `TransactionOutbox`.
### Guice
See [transaction-outbox-guice](transactionoutbox-guice/README.md), which integrates Guice DI `TransactionOutbox`.
### jOOQ
See [transaction-outbox-jooq](transactionoutbox-jooq/README.md), which integrates jOOQ transaction management with `TransactionOutbox`.
### Oracle
Oracle database compatibility requires to configure Oracle jdbc driver using following VM argument : -Doracle.jdbc.javaNetNio=false
## Set up the background worker
At the moment, if any work fails first time, it won't be retried. All we need to add is a background thread that repeatedly calls [`TransactionOutbox.flush()`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionOutbox.html) to pick up and reprocess stale work.
How you do this is up to you; it very much depends on how background processing works in your application (a reactive solution will be very different to one based on Guava `Service`, for example). However, here is a simple example:
```java
Thread backgroundThread = new Thread(() -> {
while (!Thread.interrupted()) {
try {
// Keep flushing work until there's nothing left to flush
while (outbox.flush()) {}
} catch (Exception e) {
log.error("Error flushing transaction outbox. Pausing", e);
}
try {
// When we run out of work, pause for a minute before checking again
Thread.sleep(60_000);
} catch (InterruptedException e) {
break;
}
}
});
// Startup
backgroundThread.start();
// Shut down
backgroundThread.interrupt();
backgroundThread.join();
```
`flush()` is designed to handle concurrent use on databases that support `SKIP LOCKED`, such as Postgres and MySQL 8+. Feel free to run this as often as you like (within reason, e.g. once a minute) on every instance of your application. This can have the benefit of spreading work across multiple instances when the work backlog is extremely high, but is not as effective as a proper [clustering](#clustering) approach.
However, multiple concurrent calls to `flush()` can cause lock timeout errors on databases without `SKIP LOCKED` support, such as MySQL 5.7. This is harmless, but will cause a lot of log noise, so you may prefer to run on a single instance at a time to avoid this.
## Managing the "dead letter queue"
Work might be retried too many times and enter a blocked state. You should set up an alert to allow you to manage this when it occurs, resolve the issue and unblock the work, since the work not being complete will usually be a sign that your system is out of sync in some way.
```java
TransactionOutbox.builder()
...
.listener(new TransactionOutboxListener() {
@Override
public void blocked(TransactionOutboxEntry entry, Throwable cause) {
// Spring example
applicationEventPublisher.publishEvent(new TransactionOutboxBlockedEvent(entry.getId(), cause);
}
})
.build();
```
To mark the work for reprocessing, just use [`TransactionOutbox.unblock()`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionOutbox.html). Its failure count will be marked back down to zero and it will get reprocessed on the next call to `flush()`:
```java
transactionOutboxEntry.unblock(entryId);
```
Or if using a `TransactionManager` that relies on explicit context (such as a non-thread local [`JooqTransactionManager`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-jooq/latest/com/gruelbox/transactionoutbox/JooqTransactionManager.html)):
```java
transactionOutboxEntry.unblock(entryId, context);
```
A good approach here is to use the [`TransactionOutboxListener`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionOutboxListener.html) callback to post an [interactive Slack message](https://api.slack.com/legacy/interactive-messages) - this can operate as both the alert and the "button" allowing a support engineer to submit the work for reprocessing.
## Advanced
### Topics and FIFO ordering
For some applications, the order in which tasks are processed is important, such as when:
- using the outbox to write to a FIFO queue, Kafka or AWS Kinesis topic; or
- data replication, e.g. when feeding a data warehouse or distributed cache.
In these scenarios, the default behaviour is unsuitable. Tasks are usually processed in a highly parallel fashion.
Even if the volume of tasks is low, if a task fails and is retried later, it can easily end up processing after
some later task even if that later task was processed hours or even days after the failing one.
To avoid problems associated with tasks being processed out-of-order, you can order the processing of your tasks
within a named "topic":
```java
outbox.with().ordered("topic1").schedule(Service.class).process("red");
outbox.with().ordered("topic2").schedule(Service.class).process("green");
outbox.with().ordered("topic1").schedule(Service.class).process("blue");
outbox.with().ordered("topic2").schedule(Service.class).process("yellow");
```
No matter what happens:
- `red` will always need to be processed (successfully) before `blue`;
- `green` will always need to be processed (successfully) before `yellow`; but
- `red` and `blue` can run in any sequence with respect to `green` and `yellow`.
This functionality was specifically designed to allow outboxed writing to Kafka topics. For maximum throughput
when writing to Kafka, it is advised that you form your outbox topic name by combining the Kafka topic and partition,
since that is the boundary where ordering is required.
There are a number of things to consider before using this feature:
- Tasks are not processed immediately when submitting, as normal, and are processed by
background flushing only. This means there will be an increased delay between the source transaction being
committed and the task being processed, depending on how your application calls `TransactionOutbox.flush()`.
- If a task fails, no further requests will be processed _in that topic_ until
a subsequent retry allows the failing task to succeed, to preserve ordered
processing. This means it is possible for topics to become entirely frozen in the event
that a task fails repeatedly. For this reason, it is essential to use a
`TransactionOutboxListener` to watch for failing tasks and investigate quickly. Note
that other topics will be unaffected.
- `TransactionOutboxBuilder.blockAfterAttempts` is ignored for all tasks that use this
option.
- A single topic can only be processed in single-threaded fashion, but separate topics can be processed in
parallel. If your tasks use a small number of topics, scalability will be affected since the degree of
parallelism will be reduced.
### The nested-outbox pattern
In practice, it can be extremely hard to guarantee that an entire unit of work is idempotent and thus suitable for retry. For example, the request might be to "update a customer record" with a new address, but this might record the change to an audit history table with a fresh UUID, the current date and time and so on, which in turn triggers external changes outside the transaction. The parent customer update request may be idempotent, but the downstream effects may not be.
To tackle this, `TransactionOutbox` supports a use case where outbox requests spawn further outbox requests, along with a layer of additional [idempotency protection](#idempotency-protection) for particularly diffcult cases. The nested pattern works as follows:
- Modify the customer record: `outbox.schedule(CustomerService.class).update(newDetails)`
- The `update` method spawns a new outbox request to process the downstream effect: `outbox.schedule(AuditService.class).audit("CUSTOMER_UPDATED", UUID.randomUUID(), Instant.now(), newDetails.customerId())`
Now, if any part of the top-level request throws, nothing occurs. If the top level request succeeds, an idempotent request to create the audit record will retry safely.
### Idempotency protection
A common use case for `TransactionOutbox` is to receive an incoming request (such as a message from a message queue), acknowledge it immediately and process it asynchronously, for example:
```java
public class FooEventHandler implements SQSEventHandler<ThingHappenedEvent> {
@Inject private TransactionOutbox outbox;
public void handle(ThingHappenedEvent event) {
outbox.schedule(FooService.class).handleEvent(event.getThingId());
}
}
```
However, incoming transports, whether they be message queues or APIs, usually need to rely on idempotency in message handlers (for the same reason that outgoing requests from outbox also rely on idempotency). This means the above code could get called twice.
As long as `FooService.handleEvent()` is idempotent itself, this is harmless, but we can't always assume this. The incoming message might be a broadcast, with no knowledge of the behaviour of handlers and therefore no way of pre-generating any new record ids the handler might need and passing them in the message.
To protect against this, `TransactionOutbox` can automatically detect duplicate requests and reject them with `AlreadyScheduledException`. Records of requests are retained up to a configurable threshold (see below).
To use this, use the call pattern:
```java
outbox.with()
.uniqueRequestId("context-clientid")
.schedule(Service.class)
.process("Foo");
```
Where `context-clientid` is a globally-unique identifier derived from the incoming request. Such ids are usually available from queue middleware as message ids, or if not you can require as part of the incoming API (possibly with a tenant prefix to ensure global uniqueness across tenants).
### Delayed/scheduled processing ###
To delay execution of a task, use:
```java
outbox.with()
.delayForAtLeast(Duration.of(5, MINUTES))
.schedule(Service.class)
.process("Foo");
```
There are some caveats around how accurate timing is. See the JavaDoc on the `delayForAtLeast` method for more information.
This is particularly useful when combined with the [nested outbox pattern](#the-nested-outbox-pattern) for creating polling/repeated or recursive tasks to throttle prcessing.
### Flexible serialization (beta)
Most people will use the default persistor, `DefaultPersistor`, to persist tasks to a relational database. This uses `DefaultInvocationSerializer` by default, which in turn uses [GSON](https://github.com/google/gson) to serialize as JSON. `DefaultInvocationSerializer` is extremely limited by design, with a small list of allowed classes in method arguments.
You can extend the list of support types by calling `serializableTypes` in its builder, but it will always be restricted to this global list. This is by design, to avoid building a [deserialization of untrusted data](https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data) vulnerability into the library.
Furthermore, there is no support for the case where run-time and compile-time types differ, such as in polymorphic collections. The following will always fail with `DefaultInvocationSerializer`:
```java
outbox.schedule(Service.class).processList(List.of(1, "2", 3L));
```
However, if you completely trust your serialized data (for example, your developers don't have write access to your production database, and the access credentials are well guarded) then you may prefer to have 100% flexibility, with no need to declare the types used and the ability to use any combination of run-time and compile-time types.
See [transaction-outbox-jackson](transactionoutbox-jackson/README.md), which uses a specially-configured Jackson `ObjectMapper` to achieve this.
### Clustering
The default mechanism for _running_ tasks (either immediately, or when they are picked up by background processing) is via a `java.concurrent.Executor`, which effectively does the following:
```java
executor.execute(() -> outbox.processNow(transactionOutboxEntry));
```
This offloads processing to a background thread _on the application instance_ on which the task was picked up. Under high load, this can mean thousands of tasks being picked up from the database queue and submitted at the same time on the same application instance, even if there are 20 instances of the application, effectively limiting the total rate of processing to what a single instance can handle.
If you want to instead push the work for processing by _any_ of your application instances, thus spreading the work around a cluster, there are multiple approaches, just some of which are listed below:
* An HTTP endpoint on a load-balanced DNS with service discovery (such as a container orchestrator e.g. Kubernetes or Nomad)
* A shared queue (AWS SQS, ActiveMQ, ZeroMQ)
* A lower-level clustering/messaging toolkit such as [JGroups](http://www.jgroups.org/).
All of these can be implemented as follows:
When defining the `TransactionOutbox`, replace `ExecutorSubmitter` with something which serializes a `TransactionOutboxEntry` and ships it to the remote queue/address. Here's what configuration might look for a `RestApiSubmitter` which ships the request to a load-balanced endpoint hosted on Nomad/Consul:
```java
TransactionOutbox outbox = TransactionOutbox.builder().submitter(restApiSubmitter)
```
It is strongly advised that you use a local executor in-line, to ensure that if there are communications issues with your endpoint or queue, it doesn't fail the calling thread. Here is an example using [Feign](https://github.com/OpenFeign/feign):
```java
@Slf4j
class RestApiSubmitter implements Submitter {
private final FeignResource feignResource;
private final ExecutorService localExecutor;
private final Provider<TransactionOutbox> outbox;
@Inject
RestApiExecutor(String endpointUrl, ExecutorService localExecutor, ObjectMapper objectMapper, Provider<TransactionOutbox> outbox) {
this.feignResource = Feign.builder()
.decoder(new JacksonDecoder(objectMapper))
.target(GitHub.class, "https://api.github.com");;
this.localExecutor = localExecutor;
this.outbox = outbox;
}
@Override
public void submit(TransactionOutboxEntry entry, Consumer<TransactionOutboxEntry> leIgnore) {
try {
localExecutor.execute(() -> processRemotely(entry));
log.info("Queued {} to be sent for remote processing", entry.description());
} catch (RejectedExecutionException e) {
log.info("Will queue {} for processing when local executor is available", entry.description());
} catch (Exception e) {
log.warn("Failed to queue {} for execution at {}. It will be re-attempted later.", entry.description(), url, e);
}
}
private void processRemotely(TransactionOutboxEntry entry) {
try {
feignResource.process(entry);
log.info("Submitted {} for remote processing at {}", entry.description(), url);
} catch (Exception e) {
log.warn(
"Failed to submit {} for remote processing at {}. It will be re-attempted later.",
entry.description(),
url,
e
);
}
}
public interface FeignResource {
@RequestLine("POST /outbox/process")
void process(TransactionOutboxEntry entry);
}
}
```
Then listen on your communication mechanism for incoming serialized `TransactionOutboxEntry`s, and push them to a normal local `ExecutorSubmitter`. Here's what a JAX-RS example might look like:
```java
@POST
@Path("/outbox/process")
void processOutboxEntry(String entry) {
TransactionOutboxEntry entry = somethingWhichCanSerializeTransactionOutboxEntries.deserialize(entry);
Submitter submitter = ExecutorSubmitter.builder().executor(localExecutor).logLevelWorkQueueSaturation(Level.INFO).build();
submitter.submit(entry, outbox.get()::processNow);
}
```
This whole approach is complicated a little by the inherent difficulty in serializing and deserializing a `TransactionOutboxEntry`, which is extremely polymorphic in nature. A reference approach is provided by [transaction-outbox-jackson](transactionoutbox-jackson/README.md), which provides the features necessary to make a Jackson `ObjectMapper` able to handle the work. With that on the classpath you can use an `ObjectMapper` as follows:
```java
// Add support for TransactionOutboxEntry to your normal application ObjectMapper
yourNormalSharedObjectMapper.registerModule(new TransactionOutboxJacksonModule());
// (Optional) support deep polymorphic requests - for this we need to copy the object
// mapper so it doesn't break the way the rest of your application works
ObjectMapper objectMapper = yourNormalSharedObjectMapper.copy();
objectMapper.setDefaultTyping(TransactionOutboxJacksonModule.typeResolver());
// Serialize
String message = objectMapper.writeValueAsString(entry);
// Deserialize
TransactionOutboxEntry entry = objectMapper.readValue(message, TransactionOutboxEntry.class);
```
Armed with the above, happy clustering!
### OpenTelemetry
A common request is to propagate [OTEL](https://opentelemetry.io/) traces from the calling code to an outboxed task. This can be achieved using `TransactionOutboxListener` to record the details of the parent `Span` as session variables, then restore them on invocation. Example:
```java
static class OtelListener implements TransactionOutboxListener {
/** Serialises the current context into {@link Invocation#getSession()}. */
@Override
public Map<String, String> extractSession() {
var result = new HashMap<String, String>();
SpanContext spanContext = Span.current().getSpanContext();
if (!spanContext.isValid()) {
return null;
}
result.put("traceId", spanContext.getTraceId());
result.put("spanId", spanContext.getSpanId());
log.info("Extracted: {}", result);
return result;
}
/**
* Deserialises {@link Invocation#getSession()} and sets it as the current context so that any
* new span started by the method we invoke will treat it as the parent span
*/
@Override
public void wrapInvocationAndInit(Invocator invocator) {
Invocation inv = invocator.getInvocation();
var spanBuilder =
otel.getTracer("transaction-outbox")
.spanBuilder(String.format("%s.%s", inv.getClassName(), inv.getMethodName()))
.setNoParent();
for (var i = 0; i < inv.getArgs().length; i++) {
spanBuilder.setAttribute("arg" + i, Utils.stringify(inv.getArgs()[i]));
}
if (inv.getSession() != null) {
var traceId = inv.getSession().get("traceId");
var spanId = inv.getSession().get("spanId");
if (traceId != null && spanId != null) {
spanBuilder.addLink(
SpanContext.createFromRemoteParent(
traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()));
}
}
var span = spanBuilder.startSpan();
try (Scope scope = span.makeCurrent()) {
invocator.runUnchecked();
} finally {
span.end();
}
}
}
```
Check out `AbstractAcceptanceTest.OtelListener` for an example of this in use.
`extractSession` can be used for other things too, such as authentication state, HTTP session variables or anything else that might be in static context at the time of a call and you want to be inherited by the task.
### Encryption
Be warned that all data written to disk is written unencrypted by default (including MDC, session variables, method arguments...). If you may be writing sensitive data, particularly authentication-related data, you are advised to create your own `InvocationSerializer` by decorating an existing implementation, and use an efficient stream encryptor to write the data in encrupted form.
## Configuration reference
This example shows a number of other configuration options in action:
```java
TransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);
TransactionOutbox outbox = TransactionOutbox.builder()
// The most complex part to set up for most will be synchronizing with your existing transaction
// management. Pre-rolled implementations are available for jOOQ and Spring (see above for more information)
// and you can use those examples to synchronize with anything else by defining your own TransactionManager.
// Or, if you have no formal transaction management at the moment, why not start, using transaction-outbox's
// built-in one?
.transactionManager(transactionManager)
// Modify how requests are persisted to the database. For more complex modifications, you may wish to subclass
// DefaultPersistor, or create a completely new Persistor implementation.
.persistor(DefaultPersistor.builder()
// Selecting the right SQL dialect ensures that features such as SKIP LOCKED are used correctly.
.dialect(Dialect.POSTGRESQL_9)
// Override the table name (defaults to "TXNO_OUTBOX")
.tableName("transactionOutbox")
// Shorten the time we will wait for write locks (defaults to 2)
.writeLockTimeoutSeconds(1)
// Disable automatic creation and migration of the outbox table, forcing the application to manage
// migrations itself
.migrate(false)
// Allow the SaleType enum and Money class to be used in arguments (see example below)
.serializer(DefaultInvocationSerializer.builder()
.serializableTypes(Set.of(SaleType.class, Money.class))
.build())
.build())
// GuiceInstantiator and SpringInstantiator are great if you are using Guice or Spring DI, but what if you
// have your own service locator? Wire it in here. Fully-custom Instantiator implementations are easy to
// implement.
.instantiator(Instantiator.using(myServiceLocator::createInstance))
// Change the log level used when work cannot be submitted to a saturated queue to INFO level (the default
// is WARN, which you should probably consider a production incident). You can also change the Executor used
// for submitting work to a shared thread pool used by the rest of your application. Fully-custom Submitter
// implementations are also easy to implement, for example to cluster work.
.submitter(ExecutorSubmitter.builder()
.executor(ForkJoinPool.commonPool())
.logLevelWorkQueueSaturation(Level.INFO)
.build())
// Lower the log level when a task fails temporarily from the default WARN.
.logLevelTemporaryFailure(Level.INFO)
// 10 attempts at a task before blocking it.
.blockAfterAttempts(10)
// When calling flush(), select 0.5m records at a time.
.flushBatchSize(500_000)
// Flush once every 15 minutes only
.attemptFrequency(Duration.ofMinutes(15))
// Include Slf4j's Mapped Diagnostic Context in tasks. This means that anything in the MDC when schedule()
// is called will be recreated in the task when it runs. Very useful for tracking things like user ids and
// request ids across invocations.
.serializeMdc(true)
// Sets how long we should keep records of requests with a unique request id so duplicate requests
// can be rejected. Defaults to 7 days.
.retentionThreshold(Duration.ofDays(1))
// We can intercept and modify numerous events. The most common use is to catch blocked tasks
// and raise alerts for these to be investigated. A Slack interactive message is particularly effective here
// since it can be wired up to call unblock() automatically.
.listener(new TransactionOutboxListener() {
@Override
public void success(TransactionOutboxEntry entry) {
eventPublisher.publish(new OutboxTaskProcessedEvent(entry.getId()));
}
@Override
public void blocked(TransactionOutboxEntry entry, Throwable cause) {
eventPublisher.publish(new BlockedOutboxTaskEvent(entry.getId()));
}
@Override
public Map<String, String> extractSession() {
return Map.of();
}
@Override
public void wrapInvocationAndInit(Invocator invocator) {
invocator.runUnchecked();
}
@Override
public void wrapInvocation(Invocator invocator) throws Exception {
invocator.run();
}
})
.build();
// Usage example, using the in-built transaction manager
MDC.put("SESSIONKEY", "Foo");
try {
transactionManager.inTransaction(tx -> {
writeSomeChanges(tx.connection());
outbox.schedule(getClass())
.performRemoteCall(SaleType.SALE, Money.of(10, Currency.getInstance("USD")));
});
} finally {
MDC.clear();
}
```
## Stubbing in tests
`TransactionOutbox` should not be directly stubbed (e.g. using Mockito); the contract is too complex to stub out.
Instead, [stubs](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/StubThreadLocalTransactionManager.html) [exist](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/StubPersistor.html) for the various arguments to the builder, allowing you to build a `TransactionOutbox` with minimal external dependencies which can be called and verified in tests.
```java
// GIVEN
SomeService mockService = Mockito.mock(SomeService.class);
// Also see StubParameterContextTransactionManager
TransactionManager transactionManager = new StubThreadLocalTransactionManager();
TransactionOutbox outbox = TransactionOutbox.builder()
.instantiator(Instantiator.using(clazz -> mockService)) // Return our mock
.persistor(StubPersistor.builder().build()) // Doesn't save anything
.submitter(Submitter.withExecutor(MoreExecutors.directExecutor())) // Execute all work in-line
.clockProvider(() ->
Clock.fixed(LocalDateTime.of(2020, 3, 1, 12, 0)
.toInstant(ZoneOffset.UTC), ZoneOffset.UTC)) // Fix the clock (not necessary here)
.transactionManager(transactionManager)
.build();
// WHEN
transactionManager.inTransaction(tx ->
outbox.schedule(SomeService.class).doAThing(1));
// THEN
Mockito.verify(mockService).doAThing(1);
```
Depending on the type of test, you may wish to use a real `Persistor` such as `DefaultPersistor` (if there's a real database available) or a real, multi-threaded `Submitter`. That's up to you.
================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--suppress MavenModelInspection, MavenModelInspection, MavenModelInspection -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-parent</artifactId>
<packaging>pom</packaging>
<version>${revision}</version>
<name>Transaction Outbox Parent</name>
<description>A safe implementation of the transactional outbox pattern for Java.</description>
<inceptionYear>2020</inceptionYear>
<url>https://github.com/gruelbox/transaction-outbox</url>
<organization>
<name>Graham Crockford</name>
<url>https://gruelbox.com</url>
</organization>
<properties>
<!-- Core -->
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<file.encoding>UTF-8</file.encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- For delomboking -->
<src.dir>src/main/java</src.dir>
<test.dir>src/test/java</test.dir>
<skip.format>false</skip.format>
<!-- Dependency versions -->
<logback.version>1.5.32</logback.version>
<lombok.version>1.18.42</lombok.version>
<revision>7.0.707</revision>
<junit.jupiter.version>6.0.3</junit.jupiter.version>
<testcontainers.version>1.21.4</testcontainers.version>
<maven.source.plugin.version>3.4.0</maven.source.plugin.version>
<maven.gpg.plugin.version>3.2.8</maven.gpg.plugin.version>
<surefire.version>3.5.4</surefire.version>
</properties>
<modules>
<module>transactionoutbox-core</module>
<module>transactionoutbox-jackson</module>
<module>transactionoutbox-guice</module>
<module>transactionoutbox-testing</module>
<module>transactionoutbox-acceptance</module>
<module>transactionoutbox-quarkus</module>
<module>transactionoutbox-spring</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.18.5</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>3.5</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>7.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-xe</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>23.26.1.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mssqlserver</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>13.3.1.jre11-preview</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<version>1.58.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<fork>true</fork>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<argLine>-Doracle.jdbc.javaNetNio=false -XX:+EnableDynamicAgentLoading</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.7.3</version>
<configuration>
<flattenMode>oss</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify.fmt</groupId>
<artifactId>fmt-maven-plugin</artifactId>
<version>2.29</version>
<configuration>
<verbose>true</verbose>
<filesNamePattern>.*\.java</filesNamePattern>
<skip>false</skip>
<skipSortingImports>false</skipSortingImports>
<style>google</style>
</configuration>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>format</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>au.com.acegi</groupId>
<artifactId>xml-format-maven-plugin</artifactId>
<version>4.1.0</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>java-21-modules</id>
<activation>
<jdk>[21,)</jdk>
</activation>
<modules>
<module>transactionoutbox-jooq</module>
</modules>
</profile>
<profile>
<id>java-25-modules</id>
<activation>
<jdk>[25,)</jdk>
</activation>
<modules>
<module>transactionoutbox-virtthreads</module>
</modules>
</profile>
<profile>
<id>release</id>
<properties>
<gpg.executable>gpg2</gpg.executable>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven.source.plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.10.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>published</waitUntil>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven.gpg.plugin.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<keyname>${gpg.keyname}</keyname>
<passphraseServerId>${gpg.keyname}</passphraseServerId>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</profile>
<profile>
<id>delombok</id>
<properties>
<src.dir>target/generated-sources/delombok</src.dir>
<skip.format>true</skip.format>
</properties>
<pluginRepositories>
<pluginRepository>
<id>projectlombok.org</id>
<url>https://projectlombok.org/edge-releases</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>delombok</id>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
<configuration>
<addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>src/main/java</sourceDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<configuration>
<failOnError>true</failOnError>
<quiet>true</quiet>
<defaultVersion>${project.version}</defaultVersion>
<sourcepath>target/generated-sources/delombok</sourcepath>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-nodb-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*Oracle*.java</exclude>
<exclude>**/*Postgres*.java</exclude>
<exclude>**/*MySql5*.java</exclude>
<exclude>**/*MySql8*.java</exclude>
<exclude>**/*MSSqlServer*.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-oracle18-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Oracle18*.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-oracle21-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Oracle21*.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-postgres-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Postgres*.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-mysql5-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*MySql5*.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-mysql8-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*MySql8*.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>only-mssqlserver-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*MSSqlServer*.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>noformat</id>
<build>
<plugins>
<plugin>
<groupId>com.spotify.fmt</groupId>
<artifactId>fmt-maven-plugin</artifactId>
<executions>
<execution>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<executions>
<execution>
<id>flatten</id>
<phase>none</phase>
</execution>
<execution>
<id>flatten.clean</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<developers>
<developer>
<name>Graham Crockford</name>
<email>graham@gruelbox.com</email>
<organization>Gruelbox</organization>
<organizationUrl>https://gruelbox.com</organizationUrl>
</developer>
</developers>
<issueManagement>
<system>GitHub Issues</system>
<url>https://github.com/gruelbox/transaction-outbox/issues</url>
</issueManagement>
<scm>
<connection>scm:git:https://github.com/gruelbox/transaction-outbox.git</connection>
<developerConnection>scm:git:git@github.com:gruelbox/transaction-outbox.git</developerConnection>
<url>https://github.com/gruelbox/transaction-outbox</url>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>github</id>
<name>GitHub OWNER Apache Maven Packages</name>
<url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>
</repository>
</distributionManagement>
</project>
================================================
FILE: settings.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>github</id>
<username>${env.GITHUB_ACTOR}</username>
<password>${env.GITHUB_TOKEN}</password>
</server>
<server>
<id>central</id>
<username>${env.SONATYPE_USERNAME}</username>
<password>${env.SONATYPE_PASSWORD}</password>
</server>
</servers>
<profiles>
<profile>
<id>ossrh</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<gpg.keyname>${env.GPG_KEYNAME}</gpg.keyname>
<gpg.executable>${env.GPG_EXECUTABLE}</gpg.executable>
<gpg.passphrase>${env.GPG_PASSPHRASE}</gpg.passphrase>
</properties>
</profile>
</profiles>
</settings>
================================================
FILE: transactionoutbox-acceptance/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transactionoutbox-parent</artifactId>
<groupId>com.gruelbox</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<name>Transaction Outbox Acceptance Tests</name>
<packaging>jar</packaging>
<artifactId>transactionoutbox-acceptance</artifactId>
<description>A safe implementation of the transactional outbox pattern for Java (core library)</description>
<dependencies>
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-core</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<!-- Compile time -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-testing</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-xe</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mssqlserver</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
</dependencies>
</project>
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestComplexConfigurationExample.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.*;
import java.sql.Connection;
import java.time.Duration;
import java.util.Currency;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import javax.sql.DataSource;
import lombok.Value;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.MDC;
import org.slf4j.event.Level;
/**
* Just syntax-checks the example given in the README to give a warning if the example needs to
* change.
*/
class TestComplexConfigurationExample {
@Test
@Disabled
void test() {
DataSource dataSource = Mockito.mock(DataSource.class);
ServiceLocator myServiceLocator = Mockito.mock(ServiceLocator.class);
EventPublisher eventPublisher = Mockito.mock(EventPublisher.class);
TransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);
TransactionOutbox outbox =
TransactionOutbox.builder()
// The most complex part to set up for most will be synchronizing with your existing
// transaction
// management. Pre-rolled implementations are available for jOOQ and Spring (see above
// for more information)
// and you can use those examples to synchronize with anything else by defining your own
// TransactionManager.
// Or, if you have no formal transaction management at the moment, why not start, using
// transaction-outbox's
// built-in one?
.transactionManager(transactionManager)
// Modify how requests are persisted to the database.
.persistor(
DefaultPersistor.builder()
// Selecting the right SQL dialect ensures that features such as SKIP LOCKED are
// used correctly.
.dialect(Dialect.POSTGRESQL_9)
// Override the table name (defaults to "TXNO_OUTBOX")
.tableName("transactionOutbox")
// Shorten the time we will wait for write locks (defaults to 2)
.writeLockTimeoutSeconds(1)
// Disable automatic creation and migration of the outbox table, forcing the
// application to manage
// migrations itself
.migrate(false)
// Allow the SaleType enum and Money class to be used in arguments (see example
// below)
.serializer(
DefaultInvocationSerializer.builder()
.serializableTypes(Set.of(SaleType.class, Money.class))
.build())
.build())
.instantiator(Instantiator.using(myServiceLocator::createInstance))
// Change the log level used when work cannot be submitted to a saturated queue to INFO
// level (the default
// is WARN, which you should probably consider a production incident). You can also
// change the Executor used
// for submitting work to a shared thread pool used by the rest of your application.
// Fully-custom Submitter
// implementations are also easy to implement.
.submitter(
ExecutorSubmitter.builder()
.executor(ForkJoinPool.commonPool())
.logLevelWorkQueueSaturation(Level.INFO)
.build())
// Lower the log level when a task fails temporarily from the default WARN.
.logLevelTemporaryFailure(Level.INFO)
// 10 attempts at a task before it is blocked (and would require intervention)
.blockAfterAttempts(10)
// When calling flush(), select 0.5m records at a time.
.flushBatchSize(500_000)
// Flush once every 15 minutes only
.attemptFrequency(Duration.ofMinutes(15))
// Include Slf4j's Mapped Diagnostic Context in tasks. This means that anything in the
// MDC when schedule()
// is called will be recreated in the task when it runs. Very useful for tracking things
// like user ids and
// request ids across invocations.
.serializeMdc(true)
// We can intercept task successes, single failures and blocked tasks. The most common
// use is
// to catch blocked tasks.
// and raise alerts for these to be investigated. A Slack interactive message is
// particularly effective here
// since it can be wired up to call unblock() automatically.
.listener(
new TransactionOutboxListener() {
@Override
public void success(TransactionOutboxEntry entry) {
eventPublisher.publish(new OutboxTaskProcessedEvent(entry.getId()));
}
@Override
public void blocked(TransactionOutboxEntry entry, Throwable cause) {
eventPublisher.publish(new BlockedOutboxTaskEvent(entry.getId()));
}
})
.build();
// Usage example, using the in-built transaction manager
MDC.put("SESSIONKEY", "Foo");
try {
transactionManager.inTransaction(
tx -> {
writeSomeChanges(tx.connection());
outbox
.schedule(getClass())
.performRemoteCall(SaleType.SALE, Money.of(10, Currency.getInstance("USD")));
});
} finally {
MDC.clear();
}
}
void performRemoteCall(
@SuppressWarnings({"unused", "SameParameterValue"}) SaleType saleType,
@SuppressWarnings("unused") Money amount) {}
private void writeSomeChanges(@SuppressWarnings("unused") Connection connection) {}
private interface ServiceLocator {
<T> T createInstance(Class<T> clazz);
}
private interface EventPublisher {
void publish(Object o);
}
@Value
private static class BlockedOutboxTaskEvent {
String id;
}
@Value
private static class OutboxTaskProcessedEvent {
String id;
}
@SuppressWarnings("unused")
private enum SaleType {
SALE,
REFUND
}
private interface Money {
static Money of(
@SuppressWarnings("unused") int amount, @SuppressWarnings("unused") Currency currency) {
return null;
}
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestH2.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.gruelbox.transactionoutbox.*;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import com.gruelbox.transactionoutbox.testing.InterfaceProcessor;
import com.gruelbox.transactionoutbox.testing.LatchListener;
import com.gruelbox.transactionoutbox.testing.OrderedEntryListener;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.slf4j.MDC;
@SuppressWarnings("WeakerAccess")
class TestH2 extends AbstractAcceptanceTest {
static final ThreadLocal<Boolean> inWrappedInvocation = ThreadLocal.withInitial(() -> false);
@Test
final void delayedExecutionImmediateSubmission() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
TransactionManager transactionManager = txManager();
TransactionOutbox outbox =
TransactionOutbox.builder()
.transactionManager(transactionManager)
.instantiator(Instantiator.using(clazz -> (InterfaceProcessor) (foo, bar) -> {}))
.listener(new OrderedEntryListener(latch, new CountDownLatch(1)))
.persistor(Persistor.forDialect(connectionDetails().dialect()))
.attemptFrequency(Duration.ofSeconds(60))
.build();
outbox.initialize();
clearOutbox();
var start = Instant.now();
transactionManager.inTransaction(
() ->
outbox
.with()
.delayForAtLeast(Duration.ofSeconds(1))
.schedule(InterfaceProcessor.class)
.process(1, "bar"));
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(start.plus(Duration.ofSeconds(1)).isBefore(Instant.now()));
}
@Test
final void delayedExecutionFlushOnly() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
TransactionManager transactionManager = txManager();
TransactionOutbox outbox =
TransactionOutbox.builder()
.transactionManager(transactionManager)
.instantiator(Instantiator.using(clazz -> (InterfaceProcessor) (foo, bar) -> {}))
.listener(new OrderedEntryListener(latch, new CountDownLatch(1)))
.persistor(Persistor.forDialect(connectionDetails().dialect()))
.attemptFrequency(Duration.ofSeconds(1))
.build();
outbox.initialize();
clearOutbox();
transactionManager.inTransaction(
() ->
outbox
.with()
.delayForAtLeast(Duration.ofSeconds(2))
.schedule(InterfaceProcessor.class)
.process(1, "bar"));
assertFalse(latch.await(3, TimeUnit.SECONDS));
withRunningFlusher(outbox, () -> assertTrue(latch.await(3, TimeUnit.SECONDS)));
}
@Test
final void wrapInvocations() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
TransactionManager transactionManager = txManager();
TransactionOutbox outbox =
TransactionOutbox.builder()
.transactionManager(transactionManager)
.instantiator(
Instantiator.using(
clazz ->
(InterfaceProcessor)
(foo, bar) -> {
if (!inWrappedInvocation.get()) {
throw new IllegalStateException("Not in a wrapped invocation");
}
}))
.listener(
new LatchListener(latch)
.andThen(
new TransactionOutboxListener() {
@Override
public void wrapInvocation(Invocator invocator)
throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException {
inWrappedInvocation.set(true);
try {
invocator.run();
} finally {
inWrappedInvocation.remove();
}
}
}))
.persistor(Persistor.forDialect(connectionDetails().dialect()))
.build();
outbox.initialize();
clearOutbox();
transactionManager.inTransaction(
() -> outbox.schedule(InterfaceProcessor.class).process(1, "bar"));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
final void wrapInvocationsWithMDC() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
TransactionManager transactionManager = txManager();
TransactionOutbox outbox =
TransactionOutbox.builder()
.transactionManager(transactionManager)
.instantiator(
Instantiator.using(
clazz ->
(InterfaceProcessor)
(foo, bar) -> {
if (!Boolean.parseBoolean(MDC.get("BAR"))) {
throw new IllegalStateException("Not in a wrapped invocation");
}
}))
.listener(
new LatchListener(latch)
.andThen(
new TransactionOutboxListener() {
@Override
public void wrapInvocation(Invocator invocator)
throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException {
MDC.put("BAR", "true");
try {
invocator.run();
} finally {
MDC.remove("BAR");
}
}
}))
.persistor(Persistor.forDialect(connectionDetails().dialect()))
.build();
outbox.initialize();
clearOutbox();
transactionManager.inTransaction(
() -> outbox.schedule(InterfaceProcessor.class).process(1, "bar"));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2019.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MSSQLServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestMSSqlServer2019 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-latest")
.acceptLicense()
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.MS_SQL_SERVER)
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2022.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MSSQLServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestMSSqlServer2022 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2022-latest")
.acceptLicense()
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.MS_SQL_SERVER)
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql5.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import java.util.Map;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestMySql5 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MySQLContainer<>("mysql:5")
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true)
.withTmpFs(Map.of("/var/lib/mysql", "rw"));
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.MY_SQL_5)
.driverClassName("com.mysql.cj.jdbc.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql8.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import java.util.Map;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestMySql8 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MySQLContainer<>("mysql:8")
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true)
.withTmpFs(Map.of("/var/lib/mysql", "rw"));
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.MY_SQL_8)
.driverClassName("com.mysql.cj.jdbc.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestOracle18 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings("rawtypes")
private static final JdbcDatabaseContainer container =
new OracleContainer("gvenzl/oracle-xe:18-slim-faststart")
.withStartupTimeout(Duration.ofHours(1));
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.ORACLE)
.driverClassName("oracle.jdbc.OracleDriver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
@Override
protected String createTestTable() {
return "CREATE TABLE TEST_TABLE (topic VARCHAR(50), ix NUMBER, foo INTEGER, CONSTRAINT TEST_TABLE_sequencing_pk PRIMARY KEY (topic, ix))";
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestOracle21 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings("rawtypes")
private static final JdbcDatabaseContainer container =
new OracleContainer("gvenzl/oracle-xe:21-slim-faststart")
.withStartupTimeout(Duration.ofHours(1));
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.ORACLE)
.driverClassName("oracle.jdbc.OracleDriver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
@Override
protected String createTestTable() {
return "CREATE TABLE TEST_TABLE (topic VARCHAR(50), ix NUMBER, foo INTEGER, CONSTRAINT TEST_TABLE_sequencing_pk PRIMARY KEY (topic, ix))";
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres11.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestPostgres11 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:11")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.POSTGRESQL_9)
.driverClassName("org.postgresql.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres12.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestPostgres12 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:12")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.POSTGRESQL_9)
.driverClassName("org.postgresql.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres13.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestPostgres13 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:13")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.POSTGRESQL_9)
.driverClassName("org.postgresql.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres14.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestPostgres14 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:14")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.POSTGRESQL_9)
.driverClassName("org.postgresql.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres15.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestPostgres15 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:15")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.POSTGRESQL_9)
.driverClassName("org.postgresql.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres16.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SuppressWarnings("WeakerAccess")
@Testcontainers
class TestPostgres16 extends AbstractAcceptanceTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:16")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
@Override
protected ConnectionDetails connectionDetails() {
return ConnectionDetails.builder()
.dialect(Dialect.POSTGRESQL_9)
.driverClassName("org.postgresql.Driver")
.url(container.getJdbcUrl())
.user(container.getUsername())
.password(container.getPassword())
.build();
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestRequestSerialization.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.gruelbox.transactionoutbox.*;
import com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;
import com.gruelbox.transactionoutbox.testing.LatchListener;
import com.gruelbox.transactionoutbox.testing.TestUtils;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import org.junit.jupiter.api.Test;
public class TestRequestSerialization {
/**
* Ensures that we are serializing and deserializing any request before processing it. Otherwise
* work could get processed locally successfully but fail when retried since the serialized
* version of the request is not equivalent to the original.
*/
@Test
final void workAlwaysSerialized() throws Exception {
TransactionManager transactionManager = simpleTxnManager();
CountDownLatch latch = new CountDownLatch(1);
TransactionOutbox outbox =
TransactionOutbox.builder()
.transactionManager(transactionManager)
.persistor(
DefaultPersistor.builder()
.dialect(connectionDetails().dialect())
.serializer(
DefaultInvocationSerializer.builder()
.serializableTypes(Set.of(Arg.class))
.build())
.build())
.listener(new LatchListener(latch))
.build();
clearOutbox();
Arg arg = new Arg();
arg.hiddenData = "HIDDEN";
arg.serializedData = "SERIALIZED";
transactionManager.inTransaction(() -> outbox.schedule(ComplexProcessor.class).process(arg));
assertTrue(latch.await(15, TimeUnit.SECONDS));
}
protected AbstractAcceptanceTest.ConnectionDetails connectionDetails() {
return AbstractAcceptanceTest.ConnectionDetails.builder()
.dialect(Dialect.H2)
.driverClassName("org.h2.Driver")
.url(
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE")
.user("test")
.password("test")
.build();
}
private TransactionManager simpleTxnManager() {
return TransactionManager.fromConnectionDetails(
connectionDetails().driverClassName(),
connectionDetails().url(),
connectionDetails().user(),
connectionDetails().password());
}
private void clearOutbox() {
TestUtils.runSql(simpleTxnManager(), "DELETE FROM TXNO_OUTBOX");
}
static class ComplexProcessor {
public void process(Arg arg) {
if (arg.hiddenData != null) {
throw new IllegalStateException(
"Running with state that could not possibly have been serialized");
}
if (!"SERIALIZED".equals(arg.serializedData)) {
throw new IllegalStateException("No serialized state");
}
}
}
@Getter
@Setter
static class Arg {
transient String hiddenData;
String serializedData;
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestStubbing.java
================================================
package com.gruelbox.transactionoutbox.acceptance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import com.gruelbox.transactionoutbox.*;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.Value;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/** Checks that stubbing {@link TransactionOutbox} works cleanly. */
class TestStubbing {
@Test
void testStubbingWithThreadLocalContext() {
StubThreadLocalTransactionManager transactionManager = new StubThreadLocalTransactionManager();
TransactionOutbox outbox = createOutbox(transactionManager);
Interface.invocations.clear();
transactionManager.inTransaction(
() -> {
outbox
.schedule(Interface.class)
.doThing(1, "2", new BigDecimal[] {BigDecimal.ONE, BigDecimal.TEN});
outbox.schedule(Interface.class).doThing(2, "3", new BigDecimal[] {});
outbox.schedule(Interface.class).doThing(3, null, null);
});
transactionManager.inTransaction(() -> outbox.schedule(Interface.class).doThing(4, null, null));
Object expected1 = List.of(1, "2", List.of(BigDecimal.ONE, BigDecimal.TEN));
Object expected2 = List.of(2, "3", List.of());
List<Object> expected3 = new ArrayList<>();
expected3.add(3);
expected3.add(null);
expected3.add(null);
List<Object> expected4 = new ArrayList<>();
expected4.add(4);
expected4.add(null);
expected4.add(null);
assertThat(Interface.invocations, contains(expected1, expected2, expected3, expected4));
}
@Test
void testStubbingWithExplicitContextInvalidContext() {
StubParameterContextTransactionManager<Context> transactionManager =
new StubParameterContextTransactionManager<>(Context.class, () -> new Context(1L));
TransactionOutbox outbox = createOutbox(transactionManager);
Assertions.assertThrows(
IllegalArgumentException.class,
() ->
transactionManager.inTransaction(
tx -> outbox.schedule(Interface.class).doThing(1, new Context(2L))));
}
@Test
void testStubbingWithExplicitContextPassingTransaction() {
StubParameterContextTransactionManager<Context> transactionManager =
new StubParameterContextTransactionManager<>(Context.class, () -> new Context(1L));
TransactionOutbox outbox = createOutbox(transactionManager);
Interface.invocations.clear();
transactionManager.inTransaction(tx -> outbox.schedule(Interface.class).doThing(1, tx));
assertThat(Interface.invocations, hasSize(1));
assertThat(Interface.invocations.get(0).get(0), equalTo(1));
assertThat(Interface.invocations.get(0).get(1), isA(Transaction.class));
}
@Test
void testStubbingWithExplicitContextPassingContext() {
StubParameterContextTransactionManager<Context> transactionManager =
new StubParameterContextTransactionManager<>(Context.class, () -> new Context(1L));
TransactionOutbox outbox = createOutbox(transactionManager);
Interface.invocations.clear();
transactionManager.inTransaction(
tx -> outbox.schedule(Interface.class).doThing(1, (Context) tx.context()));
assertThat(Interface.invocations, hasSize(1));
assertThat(Interface.invocations.get(0).get(0), equalTo(1));
assertThat(Interface.invocations.get(0).get(1), isA(Context.class));
}
private TransactionOutbox createOutbox(TransactionManager transactionManager) {
return TransactionOutbox.builder()
.instantiator(Instantiator.usingReflection())
.persistor(StubPersistor.builder().build())
.submitter(Submitter.withExecutor(Runnable::run))
.transactionManager(transactionManager)
.clockProvider(
() ->
Clock.fixed(
LocalDateTime.of(2020, 3, 1, 12, 0).toInstant(ZoneOffset.UTC),
ZoneOffset.UTC)) // Fix the clock
.build();
}
static class Interface {
static List<List<Object>> invocations = new ArrayList<>();
void doThing(int arg1, String arg2, BigDecimal[] arg3) {
ArrayList<Object> args = new ArrayList<>();
args.add(arg1);
args.add(arg2);
args.add(arg3 == null ? null : Arrays.asList(arg3));
invocations.add(args);
}
void doThing(@SuppressWarnings("SameParameterValue") int arg1, Transaction transaction) {
assertThat(transaction, notNullValue());
invocations.add(List.of(arg1, transaction));
}
void doThing(@SuppressWarnings("SameParameterValue") int arg1, Context context) {
assertThat(context, notNullValue());
invocations.add(List.of(arg1, context));
}
}
@Value
static class Context {
long id;
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorH2.java
================================================
package com.gruelbox.transactionoutbox.acceptance.persistor;
import com.gruelbox.transactionoutbox.DefaultPersistor;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.TransactionManager;
import com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class TestDefaultPersistorH2 extends AbstractPersistorTest {
private final DefaultPersistor persistor = DefaultPersistor.builder().dialect(Dialect.H2).build();
private final TransactionManager txManager =
TransactionManager.fromConnectionDetails(
"org.h2.Driver",
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=2000;LOB_TIMEOUT=2000;MV_STORE=TRUE",
"test",
"test");
@Override
protected DefaultPersistor persistor() {
return persistor;
}
@Override
protected TransactionManager txManager() {
return txManager;
}
@Override
protected Dialect dialect() {
return Dialect.H2;
}
@Override
public void testSkipLocked() throws Exception {
// Not supported.
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMSSqlServer2019.java
================================================
package com.gruelbox.transactionoutbox.acceptance.persistor;
import com.gruelbox.transactionoutbox.DefaultPersistor;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.TransactionManager;
import com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MSSQLServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class TestDefaultPersistorMSSqlServer2019 extends AbstractPersistorTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-latest")
.acceptLicense()
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true);
private final DefaultPersistor persistor =
DefaultPersistor.builder().dialect(Dialect.MS_SQL_SERVER).build();
private final TransactionManager txManager =
TransactionManager.fromConnectionDetails(
"com.microsoft.sqlserver.jdbc.SQLServerDriver",
container.getJdbcUrl(),
container.getUsername(),
container.getPassword());
@Override
protected DefaultPersistor persistor() {
return persistor;
}
@Override
protected TransactionManager txManager() {
return txManager;
}
@Override
protected Dialect dialect() {
return Dialect.MS_SQL_SERVER;
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql5.java
================================================
package com.gruelbox.transactionoutbox.acceptance.persistor;
import com.gruelbox.transactionoutbox.DefaultPersistor;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.TransactionManager;
import com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;
import java.time.Duration;
import java.util.Map;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class TestDefaultPersistorMySql5 extends AbstractPersistorTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MySQLContainer<>("mysql:5")
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true)
.withTmpFs(Map.of("/var/lib/mysql", "rw"));
private final DefaultPersistor persistor =
DefaultPersistor.builder().dialect(Dialect.MY_SQL_5).build();
private final TransactionManager txManager =
TransactionManager.fromConnectionDetails(
"com.mysql.cj.jdbc.Driver",
container.getJdbcUrl(),
container.getUsername(),
container.getPassword());
@Override
protected DefaultPersistor persistor() {
return persistor;
}
@Override
protected TransactionManager txManager() {
return txManager;
}
@Override
protected Dialect dialect() {
return Dialect.MY_SQL_5;
}
@Override
public void testSkipLocked() throws Exception {
// Not supported.
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql8.java
================================================
package com.gruelbox.transactionoutbox.acceptance.persistor;
import com.gruelbox.transactionoutbox.DefaultPersistor;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.TransactionManager;
import com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;
import java.time.Duration;
import java.util.Map;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class TestDefaultPersistorMySql8 extends AbstractPersistorTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
new MySQLContainer<>("mysql:8")
.withStartupTimeout(Duration.ofMinutes(5))
.withReuse(true)
.withTmpFs(Map.of("/var/lib/mysql", "rw"));
private final DefaultPersistor persistor =
DefaultPersistor.builder().dialect(Dialect.MY_SQL_8).build();
private final TransactionManager txManager =
TransactionManager.fromConnectionDetails(
"com.mysql.cj.jdbc.Driver",
container.getJdbcUrl(),
container.getUsername(),
container.getPassword());
@Override
protected DefaultPersistor persistor() {
return persistor;
}
@Override
protected TransactionManager txManager() {
return txManager;
}
@Override
protected Dialect dialect() {
return Dialect.MY_SQL_8;
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorOracle18.java
================================================
package com.gruelbox.transactionoutbox.acceptance.persistor;
import com.gruelbox.transactionoutbox.DefaultPersistor;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.TransactionManager;
import com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class TestDefaultPersistorOracle18 extends AbstractPersistorTest {
@Container
@SuppressWarnings("rawtypes")
private static final JdbcDatabaseContainer container =
new OracleContainer("gvenzl/oracle-xe:18-slim-faststart")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
private final DefaultPersistor persistor =
DefaultPersistor.builder().dialect(Dialect.ORACLE).build();
private final TransactionManager txManager =
TransactionManager.fromConnectionDetails(
"oracle.jdbc.OracleDriver",
container.getJdbcUrl(),
container.getUsername(),
container.getPassword());
@Override
protected DefaultPersistor persistor() {
return persistor;
}
@Override
protected TransactionManager txManager() {
return txManager;
}
@Override
protected Dialect dialect() {
return Dialect.ORACLE;
}
}
================================================
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorPostgres16.java
================================================
package com.gruelbox.transactionoutbox.acceptance.persistor;
import com.gruelbox.transactionoutbox.DefaultPersistor;
import com.gruelbox.transactionoutbox.Dialect;
import com.gruelbox.transactionoutbox.TransactionManager;
import com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;
import java.time.Duration;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class TestDefaultPersistorPostgres16 extends AbstractPersistorTest {
@Container
@SuppressWarnings({"rawtypes", "resource"})
private static final JdbcDatabaseContainer container =
(JdbcDatabaseContainer)
new PostgreSQLContainer("postgres:16")
.withStartupTimeout(Duration.ofHours(1))
.withReuse(true);
private final DefaultPersistor persistor =
DefaultPersistor.builder().dialect(Dialect.POSTGRESQL_9).build();
private final TransactionManager txManager =
TransactionManager.fromConnectionDetails(
"org.postgresql.Driver",
container.getJdbcUrl(),
container.getUsername(),
container.getPassword());
@Override
protected DefaultPersistor persistor() {
return persistor;
}
@Override
protected TransactionManager txManager() {
return txManager;
}
@Override
protected Dialect dialect() {
return Dialect.POSTGRESQL_9;
}
}
================================================
FILE: transactionoutbox-core/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transactionoutbox-parent</artifactId>
<groupId>com.gruelbox</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<name>Transaction Outbox Core</name>
<packaging>jar</packaging>
<artifactId>transactionoutbox-core</artifactId>
<description>A safe implementation of the transactional outbox pattern for Java (core library)</description>
<dependencies>
<!-- Run time -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Compile time -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
</project>
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/AlreadyScheduledException.java
================================================
package com.gruelbox.transactionoutbox;
import java.time.Duration;
/**
* Thrown when we attempt to schedule an invocation with a unique client id which has already been
* used within {@link TransactionOutbox.TransactionOutboxBuilder#retentionThreshold(Duration)}.
*/
public class AlreadyScheduledException extends RuntimeException {
AlreadyScheduledException(String message, Throwable cause) {
super(message, cause);
}
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ConnectionProvider.java
================================================
package com.gruelbox.transactionoutbox;
import java.sql.Connection;
/**
* Source for JDBC connections to be provided to a {@link TransactionManager}. It is not required
* for a {@link TransactionManager} to use {@link ConnectionProvider}, and when integrating with
* existing applications with transaction management, it is indeed unlikely to do so.
*/
@SuppressWarnings("WeakerAccess")
public interface ConnectionProvider {
/**
* Requests a new connection, or an available connection from a pool. The caller is responsible
* for calling {@link Connection#close()}.
*
* @return The connection.
*/
Connection obtainConnection();
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DataSourceConnectionProvider.java
================================================
package com.gruelbox.transactionoutbox;
import com.gruelbox.transactionoutbox.spi.Utils;
import java.sql.Connection;
import javax.sql.DataSource;
import lombok.Builder;
/**
* A {@link ConnectionProvider} which requests connections from a {@link DataSource}. This is
* suitable for applications using connection pools or container-provided JDBC.
*
* <p>Usage:
*
* <pre>ConnectionProvider provider = DataSourceConnectionProvider.builder()
* .dataSource(ds)
* .build()</pre>
*/
@Builder
final class DataSourceConnectionProvider implements ConnectionProvider {
private final DataSource dataSource;
@Override
public Connection obtainConnection() {
return Utils.uncheckedly(dataSource::getConnection);
}
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultDialect.java
================================================
package com.gruelbox.transactionoutbox;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
class DefaultDialect implements Dialect {
static Builder builder(String name) {
return new Builder(name);
}
@Getter private final String name;
@Getter private final String deleteExpired;
@Getter private final String delete;
@Getter private final String selectBatch;
@Getter private final String lock;
@Getter private final String checkSql;
@Getter private final String fetchNextInAllTopics;
@Getter private final String fetchNextInSelectedTopics;
@Getter private final String fetchCurrentVersion;
@Getter private final String fetchNextSequence;
private final Collection<Migration> migrations;
@Override
public String booleanValue(boolean criteriaValue) {
return criteriaValue ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
}
@Override
public void createVersionTableIfNotExists(Connection connection) throws SQLException {
try (Statement s = connection.createStatement()) {
s.execute(
"CREATE TABLE IF NOT EXISTS TXNO_VERSION (id INT DEFAULT 0, version INT, PRIMARY KEY (id))");
}
}
@Override
public String toString() {
return name;
}
@Override
public Stream<Migration> getMigrations() {
return migrations.stream();
}
@Setter
@Accessors(fluent = true)
static final class Builder {
private final String name;
private String delete = "DELETE FROM {{table}} WHERE id = ? and version = ?";
private String deleteExpired =
"DELETE FROM {{table}} WHERE nextAttemptTime < ? AND processed = true AND blocked = false"
+ " LIMIT {{batchSize}}";
private String selectBatch =
"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? "
+ "AND blocked = false AND processed = false AND topic = '*' LIMIT {{batchSize}}";
private String lock =
"SELECT id, invocation FROM {{table}} WHERE id = ? AND version = ? FOR UPDATE";
private String checkSql = "SELECT 1";
private Map<Integer, Migration> migrations;
private Function<Boolean, String> booleanValueFrom;
private SQLAction createVersionTableBy;
private String fetchNextInAllTopics =
"SELECT {{allFields}} FROM {{table}} a"
+ " WHERE processed = false AND topic <> '*' AND nextAttemptTime < ?"
+ " AND seq = ("
+ "SELECT MIN(seq) FROM {{table}} b WHERE b.topic=a.topic AND b.processed = false"
+ ") LIMIT {{batchSize}}";
private String fetchNextInSelectedTopics =
"SELECT {{allFields}} FROM {{table}} a"
+ " WHERE processed = false AND topic IN ({{topicNames}}) AND nextAttemptTime < ?"
+ " AND seq = ("
+ "SELECT MIN(seq) FROM {{table}} b WHERE b.topic=a.topic AND b.processed = false"
+ ") LIMIT {{batchSize}}";
private String fetchCurrentVersion = "SELECT version FROM TXNO_VERSION FOR UPDATE";
private String fetchNextSequence = "SELECT seq FROM TXNO_SEQUENCE WHERE topic = ? FOR UPDATE";
Builder(String name) {
this.name = name;
this.migrations = new TreeMap<>();
migrations.put(
1,
new Migration(
1,
"Create outbox table",
"CREATE TABLE TXNO_OUTBOX (\n"
+ " id VARCHAR(36) PRIMARY KEY,\n"
+ " invocation TEXT,\n"
+ " nextAttemptTime TIMESTAMP(6),\n"
+ " attempts INT,\n"
+ " blacklisted BOOLEAN,\n"
+ " version INT\n"
+ ")"));
migrations.put(
2,
new Migration(
2,
"Add unique request id",
"ALTER TABLE TXNO_OUTBOX ADD COLUMN uniqueRequestId VARCHAR(100) NULL UNIQUE"));
migrations.put(
3,
new Migration(
3, "Add processed flag", "ALTER TABLE TXNO_OUTBOX ADD COLUMN processed BOOLEAN"));
migrations.put(
4,
new Migration(
4,
"Add flush index",
"CREATE INDEX IX_TXNO_OUTBOX_1 ON TXNO_OUTBOX (processed, blacklisted, nextAttemptTime)"));
migrations.put(
5,
new Migration(
5,
"Increase size of uniqueRequestId",
"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN uniqueRequestId VARCHAR(250)"));
migrations.put(
6,
new Migration(
6,
"Rename column blacklisted to blocked",
"ALTER TABLE TXNO_OUTBOX CHANGE COLUMN blacklisted blocked VARCHAR(250)"));
migrations.put(
7,
new Migration(
7,
"Add lastAttemptTime column to outbox",
"ALTER TABLE TXNO_OUTBOX ADD COLUMN lastAttemptTime TIMESTAMP(6) NULL AFTER invocation"));
migrations.put(
8,
new Migration(
8,
"Update length of invocation column on outbox for MySQL dialects only.",
"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN invocation MEDIUMTEXT"));
migrations.put(
9,
new Migration(
9,
"Add topic",
"ALTER TABLE TXNO_OUTBOX ADD COLUMN topic VARCHAR(250) NOT NULL DEFAULT '*'"));
migrations.put(
10,
new Migration(10, "Add sequence", "ALTER TABLE TXNO_OUTBOX ADD COLUMN seq BIGINT NULL"));
migrations.put(
11,
new Migration(
11,
"Add sequence table",
"CREATE TABLE TXNO_SEQUENCE (topic VARCHAR(250) NOT NULL, seq BIGINT NOT NULL, PRIMARY KEY (topic, seq))"));
migrations.put(
12,
new Migration(
12,
"Add flush index to support ordering",
"CREATE INDEX IX_TXNO_OUTBOX_2 ON TXNO_OUTBOX (topic, processed, seq)"));
migrations.put(13, new Migration(13, "Enforce UTF8 collation for outbox messages", null));
}
Builder setMigration(Migration migration) {
this.migrations.put(migration.getVersion(), migration);
return this;
}
Builder changeMigration(int version, String sql) {
return setMigration(this.migrations.get(version).withSql(sql));
}
Builder disableMigration(@SuppressWarnings("SameParameterValue") int version) {
return setMigration(this.migrations.get(version).withSql(null));
}
Dialect build() {
return new DefaultDialect(
name,
deleteExpired,
delete,
selectBatch,
lock,
checkSql,
fetchNextInAllTopics,
fetchNextInSelectedTopics,
fetchCurrentVersion,
fetchNextSequence,
migrations.values()) {
@Override
public String booleanValue(boolean criteriaValue) {
if (booleanValueFrom != null) {
return booleanValueFrom.apply(criteriaValue);
}
return super.booleanValue(criteriaValue);
}
@Override
public void createVersionTableIfNotExists(Connection connection) throws SQLException {
if (createVersionTableBy != null) {
createVersionTableBy.doAction(connection);
} else {
super.createVersionTableIfNotExists(connection);
}
}
};
}
}
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultInvocationSerializer.java
================================================
package com.gruelbox.transactionoutbox;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
/**
* A locked-down serializer which supports a limited list of primitives and simple JDK value types.
* Only the following are supported:
*
* <ul>
* <li>{@link Invocation} itself
* <li>Primitive types such as {@code int} or {@code double} or the boxed equivalents
* <li>{@link String}
* <li>{@link java.util.Date}
* <li>{@link java.util.UUID}
* <li>The {@code java.time} classes:
* <ul>
* <li>{@link java.time.DayOfWeek}
* <li>{@link java.time.Duration}
* <li>{@link java.time.Instant}
* <li>{@link java.time.LocalDate}
* <li>{@link java.time.LocalDateTime}
* <li>{@link java.time.ZonedDateTime}
* <li>{@link java.time.Month}
* <li>{@link java.time.MonthDay}
* <li>{@link java.time.Period}
* <li>{@link java.time.Year}
* <li>{@link java.time.YearMonth}
* <li>{@link java.time.ZoneOffset}
* <li>{@link java.time.DayOfWeek}
* <li>{@link java.time.temporal.ChronoUnit}
* </ul>
* <li>Arrays specifically typed to one of the above types
* <li>Any types specifically passed in, which must be GSON compatible.
* </ul>
*/
@Slf4j
public final class DefaultInvocationSerializer implements InvocationSerializer {
private final Gson gson;
@Builder
DefaultInvocationSerializer(Set<Class<?>> serializableTypes, Integer version) {
this.gson =
new GsonBuilder()
.registerTypeAdapter(
Invocation.class,
new InvocationJsonSerializer(
serializableTypes == null ? Set.of() : serializableTypes,
version == null ? 2 : version))
.registerTypeAdapter(Date.class, new UtcDateTypeAdapter())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter())
.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter())
.registerTypeAdapter(Instant.class, new InstantTypeAdapter())
.registerTypeAdapter(Duration.class, new DurationTypeAdapter())
.registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter())
.registerTypeAdapter(MonthDay.class, new MonthDayTypeAdapter())
.registerTypeAdapter(Period.class, new PeriodTypeAdapter())
.registerTypeAdapter(Year.class, new YearTypeAdapter())
.registerTypeAdapter(YearMonth.class, new YearMonthAdapter())
.excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
.create();
}
@Override
public void serializeInvocation(Invocation invocation, Writer writer) {
try {
gson.toJson(invocation, writer);
} catch (Exception e) {
throw new IllegalArgumentException("Cannot serialize " + invocation, e);
}
}
@Override
public Invocation deserializeInvocation(Reader reader) throws IOException {
try {
return gson.fromJson(reader, Invocation.class);
} catch (JsonIOException | JsonSyntaxException exception) {
throw new IOException(exception);
}
}
private static final class InvocationJsonSerializer
implements JsonSerializer<Invocation>, JsonDeserializer<Invocation> {
private final int version;
private final Map<Class<?>, String> classToName = new HashMap<>();
private final Map<String, Class<?>> nameToClass = new HashMap<>();
InvocationJsonSerializer(Set<Class<?>> serializableClasses, int version) {
this.version = version;
addClassPair(byte.class, "byte");
addClassPair(short.class, "short");
addClassPair(int.class, "int");
addClassPair(long.class, "long");
addClassPair(float.class, "float");
addClassPair(double.class, "double");
addClassPair(boolean.class, "boolean");
addClassPair(char.class, "char");
addClass(Byte.class);
addClass(Short.class);
addClass(Integer.class);
addClass(Long.class);
addClass(Float.class);
addClass(Double.class);
addClass(Boolean.class);
addClass(Character.class);
addClass(BigDecimal.class);
addClass(String.class);
addClass(Date.class);
addClass(UUID.class);
addClass(DayOfWeek.class);
addClass(Duration.class);
addClass(Instant.class);
addClass(LocalDate.class);
addClass(LocalDateTime.class);
addClass(ZonedDateTime.class);
addClass(Month.class);
addClass(MonthDay.class);
addClass(Period.class);
addClass(Year.class);
addClass(YearMonth.class);
addClass(ZoneOffset.class);
addClass(DayOfWeek.class);
addClass(ChronoUnit.class);
addClass(Transaction.class);
addClassPair(TransactionContextPlaceholder.class, "TransactionContext");
serializableClasses.forEach(clazz -> addClassPair(clazz, clazz.getName()));
}
private void addClass(Class<?> clazz) {
addClassPair(clazz, clazz.getSimpleName());
}
private void addClassPair(Class<?> clazz, String name) {
classToName.put(clazz, name);
nameToClass.put(name, clazz);
String arrayClassName = toArrayClassName(clazz);
Class<?> arrayClass = toClass(clazz.getClassLoader(), arrayClassName);
classToName.put(arrayClass, arrayClassName);
nameToClass.put(arrayClassName, arrayClass);
}
private String toArrayClassName(Class<?> clazz) {
if (clazz.isArray()) {
return "[" + clazz.getName();
} else if (clazz == boolean.class) {
return "[Z";
} else if (clazz == byte.class) {
return "[B";
} else if (clazz == char.class) {
return "[C";
} else if (clazz == double.class) {
return "[D";
} else if (clazz == float.class) {
return "[F";
} else if (clazz == int.class) {
return "[I";
} else if (clazz == long.class) {
return "[J";
} else if (clazz == short.class) {
return "[S";
} else {
return "[L" + clazz.getName() + ";";
}
}
private Class<?> toClass(ClassLoader classLoader, String name) {
try {
return classLoader != null ? Class.forName(name, false, classLoader) : Class.forName(name);
} catch (ClassNotFoundException e) {
throw new RuntimeException(
"Cannot determine array type for "
+ name
+ " using "
+ (classLoader == null ? "root classloader" : "base classloader"),
e);
}
}
@Override
public JsonElement serialize(Invocation src, Type typeOfSrc, JsonSerializationContext context) {
if (version == 1) {
log.warn("Serializing as deprecated version {}", version);
return serializeV1(src, typeOfSrc, context);
}
JsonObject obj = new JsonObject();
obj.addProperty("c", src.getClassName());
obj.addProperty("m", src.getMethodName());
JsonArray params = new JsonArray();
JsonArray args = new JsonArray();
int i = 0;
for (Class<?> parameterType : src.getParameterTypes()) {
params.add(nameForClass(parameterType));
Object arg = src.getArgs()[i];
if (arg == null) {
JsonObject jsonObject = new JsonObject();
jsonObject.add("t", null);
jsonObject.add("v", null);
args.add(jsonObject);
} else {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("t", nameForClass(arg.getClass()));
jsonObject.add("v", context.serialize(arg));
args.add(jsonObject);
}
i++;
}
obj.add("p", params);
obj.add("a", args);
obj.add("x", context.serialize(src.getMdc()));
obj.add("s", context.serialize(src.getSession()));
return obj;
}
JsonElement serializeV1(Invocation src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("c", src.getClassName());
obj.addProperty("m", src.getMethodName());
JsonArray params = new JsonArray();
int i = 0;
for (Class<?> parameterType : src.getParameterTypes()) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("t", nameForClass(parameterType));
jsonObject.add("v", context.serialize(src.getArgs()[i]));
params.add(jsonObject);
i++;
}
obj.add("p", params);
obj.add("x", context.serialize(src.getMdc()));
return obj;
}
@Override
public Invocation deserialize(
JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String className = jsonObject.get("c").getAsString();
String methodName = jsonObject.get("m").getAsString();
JsonArray jsonParams = jsonObject.get("p").getAsJsonArray();
Class<?>[] params = new Class<?>[jsonParams.size()];
for (int i = 0; i < jsonParams.size(); i++) {
JsonElement param = jsonParams.get(i);
if (param.isJsonObject()) {
// For backwards compatibility
params[i] = classForName(param.getAsJsonObject().get("t").getAsString());
} else {
params[i] = classForName(param.getAsString());
}
}
JsonElement argsElement = jsonObject.get("a");
if (argsElement == null) {
// For backwards compatibility
argsElement = jsonObject.get("p");
}
JsonArray jsonArgs = argsElement.getAsJsonArray();
Object[] args = new Object[jsonArgs.size()];
for (int i = 0; i < jsonArgs.size(); i++) {
JsonElement arg = jsonArgs.get(i);
JsonElement argType = arg.getAsJsonObject().get("t");
if (argType != null) {
JsonElement argValue = arg.getAsJsonObject().get("v");
Class<?> argClass = classForName(argType.getAsString());
try {
args[i] = context.deserialize(argValue, argClass);
} catch (Exception e) {
throw new RuntimeException(
"Failed to deserialize arg [" + argValue + "] of type [" + argType + "]", e);
}
}
}
Map<String, String> mdc = context.deserialize(jsonObject.get("x"), Map.class);
Map<String, String> session = context.deserialize(jsonObject.get("s"), Map.class);
return new Invocation(className, methodName, params, args, mdc, session);
}
private Class<?> classForName(String name) {
var clazz = nameToClass.get(name);
if (clazz == null) {
throw new IllegalArgumentException("Cannot deserialize class - not found: " + name);
}
return clazz;
}
private String nameForClass(Class<?> clazz) {
var name = classToName.get(clazz);
if (name == null) {
throw new IllegalArgumentException(
"Cannot serialize class - not found: " + clazz.getName());
}
return name;
}
}
static final class ZonedDateTimeTypeAdapter extends TypeAdapter<ZonedDateTime> {
@Override
public void write(final JsonWriter out, final ZonedDateTime value) throws IOException {
out.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
}
@Override
public ZonedDateTime read(final JsonReader in) throws IOException {
return ZonedDateTime.parse(in.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);
}
}
static final class LocalDateTimeTypeAdapter extends TypeAdapter<LocalDateTime> {
@Override
public void write(JsonWriter out, LocalDateTime value) throws IOException {
out.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
@Override
public LocalDateTime read(JsonReader in) throws IOException {
return LocalDateTime.parse(in.nextString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}
static final class InstantTypeAdapter extends TypeAdapter<Instant> {
@Override
public void write(JsonWriter out, Instant value) throws IOException {
out.value(DateTimeFormatter.ISO_INSTANT.format(value));
}
@Override
public Instant read(JsonReader in) throws IOException {
return DateTimeFormatter.ISO_INSTANT.parse(in.nextString(), Instant::from);
}
}
static final class DurationTypeAdapter extends TypeAdapter<Duration> {
@Override
public void write(JsonWriter out, Duration value) throws IOException {
out.value(value.get(ChronoUnit.SECONDS));
}
@Override
public Duration read(JsonReader in) throws IOException {
return Duration.of(in.nextLong(), ChronoUnit.SECONDS);
}
}
static final class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {
@Override
public void write(JsonWriter out, LocalDate value) throws IOException {
out.value(DateTimeFormatter.ISO_LOCAL_DATE.format(value));
}
@Override
public LocalDate read(JsonReader in) throws IOException {
return DateTimeFormatter.ISO_LOCAL_DATE.parse(in.nextString(), LocalDate::from);
}
}
static final class MonthDayTypeAdapter extends TypeAdapter<MonthDay> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d/M");
@Override
public void write(JsonWriter out, MonthDay value) throws IOException {
out.value(value.format(formatter));
}
@Override
public MonthDay read(JsonReader in) throws IOException {
return MonthDay.parse(in.nextString(), formatter);
}
}
static final class PeriodTypeAdapter extends TypeAdapter<Period> {
@Override
public void write(JsonWriter out, Period value) throws IOException {
out.value(value.toString());
}
@Override
public Period read(JsonReader in) throws IOException {
return Period.parse(in.nextString());
}
}
static final class YearTypeAdapter extends TypeAdapter<Year> {
@Override
public void write(JsonWriter out, Year value) throws IOException {
out.value(value.getValue());
}
@Override
public Year read(JsonReader in) throws IOException {
return Year.of(in.nextInt());
}
}
static final class YearMonthAdapter extends TypeAdapter<YearMonth> {
@Override
public void write(JsonWriter out, YearMonth value) throws IOException {
out.value(value.toString());
}
@Override
public YearMonth read(JsonReader in) throws IOException {
return YearMonth.parse(in.nextString());
}
}
static final class UtcDateTypeAdapter extends TypeAdapter<Date> {
private final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
@Override
public void write(JsonWriter out, Date date) throws IOException {
if (date == null) {
out.nullValue();
} else {
String value = format(date, true, UTC_TIME_ZONE);
out.value(value);
}
}
@Override
public Date read(JsonReader in) throws IOException {
try {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
String date = in.nextString();
// Instead of using iso8601Format.parse(value), we use Jackson's date parsing
// This is because Android doesn't support XXX because it is JDK 1.6
return parse(date, new ParsePosition(0));
} catch (ParseException e) {
throw new JsonParseException(e);
}
}
// Date parsing code from Jackson databind ISO8601Utils.java
// https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
private static final String GMT_ID = "GMT";
/**
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
*
* @param date the date to format
* @param millis true to include millis precision otherwise false
* @param tz timezone to use for the formatting (GMT will produce 'Z')
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
*/
private static String format(Date date, boolean millis, TimeZone tz) {
Calendar calendar = new GregorianCalendar(tz, Locale.US);
calendar.setTime(date);
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
int capacity = "yyyy-MM-ddThh:mm:ss".length();
capacity += millis ? ".sss".length() : 0;
capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length();
StringBuilder formatted = new StringBuilder(capacity);
padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
formatted.append('-');
padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
formatted.append('-');
padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
formatted.append('T');
padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
formatted.append(':');
padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
formatted.append(':');
padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
if (millis) {
formatted.append('.');
padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
}
int offset = tz.getOffset(calendar.getTimeInMillis());
if (offset != 0) {
int hours = Math.abs((offset / (60 * 1000)) / 60);
int minutes = Math.abs((offset / (60 * 1000)) % 60);
formatted.append(offset < 0 ? '-' : '+');
padInt(formatted, hours, "hh".length());
formatted.append(':');
padInt(formatted, minutes, "mm".length());
} else {
formatted.append('Z');
}
return formatted.toString();
}
/**
* Zero pad a number to a specified length
*
* @param buffer buffer to use for padding
* @param value the integer value to pad if necessary.
* @param length the length of the string we should zero pad
*/
private static void padInt(StringBuilder buffer, int value, int length) {
String strValue = Integer.toString(value);
buffer.append("0".repeat(Math.max(0, length - strValue.length())));
buffer.append(strValue);
}
/**
* Parse a date from ISO-8601 formatted string. It expects a format
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
*
* @param date ISO string to parse in the appropriate format.
* @param pos The position to start parsing from, updated to where parsing stopped.
* @return the parsed date
* @throws ParseException if the date is not in the appropriate format
*/
private static Date parse(String date, ParsePosition pos) throws ParseException {
Exception fail;
try {
int offset = pos.getIndex();
// extract year
int year = parseInt(date, offset, offset += 4);
if (checkOffset(date, offset, '-')) {
offset += 1;
}
// extract month
int month = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, '-')) {
offset += 1;
}
// extract day
int day = parseInt(date, offset, offset += 2);
// default time value
int hour = 0;
int minutes = 0;
int seconds = 0;
int milliseconds =
0; // always use 0 otherwise returned date will include millis of current time
if (checkOffset(date, offset, 'T')) {
// extract hours, minutes, seconds and milliseconds
hour = parseInt(date, offset += 1, offset += 2);
if (checkOffset(date, offset, ':')) {
offset += 1;
}
minutes = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, ':')) {
offset += 1;
}
// second and milliseconds can be optional
if (date.length() > offset) {
char c = date.charAt(offset);
if (c != 'Z' && c != '+' && c != '-') {
seconds = parseInt(date, offset, offset += 2);
// milliseconds can be optional in the format
if (checkOffset(date, offset, '.')) {
milliseconds = parseInt(date, offset += 1, offset += 3);
}
}
}
}
// extract timezone
String timezoneId;
if (date.length() <= offset) {
throw new IllegalArgumentException("No time zone indicator");
}
char timezoneIndicator = date.charAt(offset);
if (timezoneIndicator == '+' || timezoneIndicator == '-') {
String timezoneOffset = date.substring(offset);
timezoneId = GMT_ID + timezoneOffset;
offset += timezoneOffset.length();
} else if (timezoneIndicator == 'Z') {
timezoneId = GMT_ID;
offset += 1;
} else {
throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator);
}
TimeZone timezone = TimeZone.getTimeZone(timezoneId);
if (!timezone.getID().equals(timezoneId)) {
throw new IndexOutOfBoundsException();
}
Calendar calendar = new GregorianCalendar(timezone);
calendar.setLenient(false);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minutes);
calendar.set(Calendar.SECOND, seconds);
calendar.set(Calendar.MILLISECOND, milliseconds);
pos.setIndex(offset);
return calendar.getTime();
// If we get a ParseException it'll already have the right message/offset.
// Other exception types can convert here.
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
fail = e;
}
String input = (date == null) ? null : ("'" + date + "'");
throw new ParseException(
"Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
}
/**
* Check if the expected character exist at the given offset in the value.
*
* @param value the string to check at the specified offset
* @param offset the offset to look for the expected character
* @param expected the expected character
* @return true if the expected character exist at the given offset
*/
private static boolean checkOffset(String value, int offset, char expected) {
return (offset < value.length()) && (value.charAt(offset) == expected);
}
/**
* Parse an integer located between 2 given offsets in a string
*
* @param value the string to parse
* @param beginIndex the start index for the integer in the string
* @param endIndex the end index for the integer in the string
* @return the int
* @throws NumberFormatException if the value is not a number
*/
private static int parseInt(String value, int beginIndex, int endIndex)
throws NumberFormatException {
if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
throw new NumberFormatException(value);
}
// use same logic as in Integer.parseInt() but less generic we're not supporting negative
// values
int i = beginIndex;
int result = 0;
int digit;
if (i < endIndex) {
digit = Character.digit(value.charAt(i++), 10);
if (digit < 0) {
throw new NumberFormatException("Invalid number: " + value);
}
result = -digit;
}
while (i < endIndex) {
digit = Character.digit(value.charAt(i++), 10);
if (digit < 0) {
throw new NumberFormatException("Invalid number: " + value);
}
result *= 10;
result -= digit;
}
return -result;
}
}
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultMigrationManager.java
================================================
package com.gruelbox.transactionoutbox;
import static com.gruelbox.transactionoutbox.spi.Utils.uncheck;
import java.io.PrintWriter;
import java.io.Writer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
/**
* Simple database migration manager. Inspired by Flyway, Liquibase, Morf etc, just trimmed down for
* minimum dependencies.
*/
@Slf4j
class DefaultMigrationManager {
private static final Executor basicExecutor =
runnable -> {
new Thread(runnable).start();
};
private static CountDownLatch waitLatch;
private static CountDownLatch readyLatch;
static void withLatch(CountDownLatch readyLatch, Consumer<CountDownLatch> runnable) {
waitLatch = new CountDownLatch(1);
DefaultMigrationManager.readyLatch = readyLatch;
try {
runnable.accept(waitLatch);
} finally {
waitLatch = null;
DefaultMigrationManager.readyLatch = null;
}
}
static void migrate(TransactionManager transactionManager, Dialect dialect) {
transactionManager.inTransaction(
transaction -> {
try {
int currentVersion = currentVersion(transaction.connection(), dialect);
dialect
.getMigrations()
.filter(migration -> migration.getVersion() > currentVersion)
.forEach(
migration ->
uncheck(
() -> runSql(transactionManager, transaction.connection(), migration)));
} catch (Exception e) {
throw new RuntimeException("Migrations failed", e);
}
});
}
static void writeSchema(Writer writer, Dialect dialect) {
PrintWriter printWriter = new PrintWriter(writer);
dialect
.getMigrations()
.forEach(
migration -> {
printWriter.print("-- ");
printWriter.print(migration.getVersion());
printWriter.print(": ");
printWriter.println(migration.getName());
if (migration.getSql() == null || migration.getSql().isEmpty()) {
printWriter.println("-- Nothing for " + dialect);
} else {
printWriter.println(migration.getSql());
}
printWriter.println();
});
printWriter.flush();
}
private static void runSql(TransactionManager txm, Connection connection, Migration migration)
throws SQLException {
log.info("Running migration {}: {}", migration.getVersion(), migration.getName());
if (migration.getSql() != null && !migration.getSql().isEmpty()) {
CompletableFuture.runAsync(
() -> {
try {
txm.inTransactionThrows(
tx -> {
try (var s = tx.connection().prepareStatement(migration.getSql())) {
s.execute();
}
});
} catch (SQLException e) {
throw new RuntimeException(e);
}
},
basicExecutor)
.join();
}
try (var s = connection.prepareStatement("UPDATE TXNO_VERSION SET version = ?")) {
s.setInt(1, migration.getVersion());
if (s.executeUpdate() != 1) {
throw new IllegalStateException("Version table should already exist");
}
}
}
private static int currentVersion(Connection connection, Dialect dialect) throws SQLException {
dialect.createVersionTableIfNotExists(connection);
int version = fetchCurrentVersion(connection, dialect);
if (version >= 0) {
return version;
}
try {
log.info("No version record found. Attempting to create");
if (waitLatch != null) {
log.info("Stopping at latch");
readyLatch.countDown();
if (!waitLatch.await(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("Latch not released in 10 seconds");
}
log.info("Latch released");
}
try (var s = connection.prepareStatement("INSERT INTO TXNO_VERSION (version) VALUES (0)")) {
s.executeUpdate();
}
log.info("Created version record.");
return fetchCurrentVersion(connection, dialect);
} catch (Exception e) {
log.info(
"Error attempting to create ({} - {}). May have been beaten to it, attempting second fetch",
e.getClass().getSimpleName(),
e.getMessage());
version = fetchCurrentVersion(connection, dialect);
if (version >= 0) {
return version;
}
throw new IllegalStateException("Unable to read or create version record", e);
}
}
private static int fetchCurrentVersion(Connection connection, Dialect dialect)
throws SQLException {
try (PreparedStatement s = connection.prepareStatement(dialect.getFetchCurrentVersion());
ResultSet rs = s.executeQuery()) {
if (rs.next()) {
var version = rs.getInt(1);
log.info("Current version is {}, obtained lock", version);
if (rs.next()) {
throw new IllegalStateException("More than one version record");
}
return version;
}
return -1;
}
}
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultPersistor.java
================================================
package com.gruelbox.transactionoutbox;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.*;
import java.sql.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.experimental.SuperBuilder;
import lombok.extern.slf4j.Slf4j;
/**
* The default {@link Persistor} for {@link TransactionOutbox}.
*
* <p>Saves requests to a relational database table, by default called {@code TXNO_OUTBOX}. This can
* optionally be automatically created and upgraded by {@link DefaultPersistor}, although this
* behaviour can be disabled if you wish.
*
* <p>More significant changes can be achieved by subclassing, which is explicitly supported. If, on
* the other hand, you want to use a completely non-relational underlying data store or do something
* equally esoteric, you may prefer to implement {@link Persistor} from the ground up.
*/
@Slf4j
@SuperBuilder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class DefaultPersistor implements Persistor, Validatable {
private static final String ALL_FIELDS =
"id, uniqueRequestId, invocation, topic, seq, lastAttemptTime, nextAttemptTime, attempts, blocked, processed, version";
/**
* @param writeLockTimeoutSeconds How many seconds to wait before timing out on obtaining a write
* lock. There's no point making this long; it's always better to just back off as quickly as
* possible and try another record. Generally these lock timeouts only kick in if the database
* does not support skip locking.
*/
@SuppressWarnings("JavaDoc")
@Builder.Default
private final int writeLockTimeoutSeconds = 2;
/**
* @param dialect The database dialect to use. Required.
*/
@SuppressWarnings("JavaDoc")
private final Dialect dialect;
/**
* @param tableName The database table name. The default is {@code TXNO_OUTBOX}.
*/
@SuppressWarnings("JavaDoc")
@Builder.Default
private final String tableName = "TXNO_OUTBOX";
/**
* @param migrate Set to false to disable automatic database migrations. This may be preferred if
* the default migration behaviour interferes with your existing toolset, and you prefer to
* manage the migrations explicitly (e.g. using FlyWay or Liquibase), or you do not give the
* application DDL permissions at runtime. You may use {@link #writeSchema(Writer)} to access
* the migrations.
*/
@SuppressWarnings("JavaDoc")
@Builder.Default
private final boolean migrate = true;
/**
* @param serializer The serializer to use for {@link Invocation}s. See {@link
* InvocationSerializer} for more information. Defaults to {@link
* InvocationSerializer#createDefaultJsonSerializer()} with no custom serializable classes.
*/
@SuppressWarnings("JavaDoc")
@Builder.Default
private final InvocationSerializer serializer =
InvocationSerializer.createDefaultJsonSerializer();
@Override
public void validate(Validator validator) {
validator.notNull("dialect", dialect);
validator.notNull("tableName", tableName);
}
@Override
public void migrate(TransactionManager transactionManager) {
if (migrate) {
DefaultMigrationManager.migrate(transactionManager, dialect);
}
}
/**
* Provides access to the database schema so that you may optionally use your existing toolset to
* manage migrations.
*
* @param writer The writer to which the migrations are written.
*/
public void writeSchema(Writer writer) {
DefaultMigrationManager.writeSchema(writer, dialect);
}
@Override
public void save(Transaction tx, TransactionOutboxEntry entry)
throws SQLException, AlreadyScheduledException {
var insertSql =
"INSERT INTO "
+ tableName
+ " ("
+ ALL_FIELDS
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
var writer = new StringWriter();
serializer.serializeInvocation(entry.getInvocation(), writer);
if (entry.getTopic() != null) {
setNextSequence(tx, entry);
log.info("Assigned sequence number {} to topic {}", entry.getSequence(), entry.getTopic());
}
PreparedStatement stmt = tx.prepareBatchStatement(insertSql);
setupInsert(entry, writer, stmt);
if (entry.getUniqueRequestId() == null) {
stmt.addBatch();
log.debug("Inserted {} in batch", entry.description());
} else {
try {
stmt.executeUpdate();
log.debug("Inserted {} immediately", entry.description());
} catch (Exception e) {
if (indexViolation(e)) {
throw new AlreadyScheduledException(
"Request " + entry.description() + " already exists", e);
}
throw e;
}
}
}
@Override
public Invocation serializeAndDeserialize(Invocation invocation) {
try (var baos = new ByteArrayOutputStream()) {
try (var writer = new OutputStreamWriter(baos, UTF_8)) {
serializer.serializeInvocation(invocation, writer);
}
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
try (Reader reader = new InputStreamReader(bais, UTF_8)) {
return serializer.deserializeInvocation(reader);
}
} catch (IOException e) {
throw new UncheckedException(e);
}
}
private void setNextSequence(Transaction tx, TransactionOutboxEntry entry) throws SQLException {
//noinspection resource
var seqSelect = tx.prepareBatchStatement(dialect.getFetchNextSequence());
seqSelect.setString(1, entry.getTopic());
try (ResultSet rs = seqSelect.executeQuery()) {
if (rs.next()) {
entry.setSequence(rs.getLong(1) + 1L);
//noinspection resource
var seqUpdate =
tx.prepareBatchStatement("UPDATE TXNO_SEQUENCE SET seq = ? WHERE topic = ?");
seqUpdate.setLong(1, entry.getSequence());
seqUpdate.setString(2, entry.getTopic());
seqUpdate.executeUpdate();
} else {
try {
entry.setSequence(1L);
//noinspection resource
var seqInsert =
tx.prepareBatchStatement("INSERT INTO TXNO_SEQUENCE (topic, seq) VALUES (?, ?)");
seqInsert.setString(1, entry.getTopic());
seqInsert.setLong(2, entry.getSequence());
seqInsert.executeUpdate();
} catch (Exception e) {
if (indexViolation(e)) {
setNextSequence(tx, entry);
} else {
throw e;
}
}
}
}
}
private boolean indexViolation(Exception e) {
return (e instanceof SQLIntegrityConstraintViolationException)
|| (e.getClass().getName().equals("org.postgresql.util.PSQLException")
&& e.getMessage().contains("constraint"))
|| (e.getClass().getName().equals("com.microsoft.sqlserver.jdbc.SQLServerException")
&& e.getMessage().contains("duplicate key"));
}
private void setupInsert(
TransactionOutboxEntry entry, StringWriter writer, PreparedStatement stmt)
throws SQLException {
stmt.setString(1, entry.getId());
stmt.setString(2, entry.getUniqueRequestId());
stmt.setString(3, writer.toString());
stmt.setString(4, entry.getTopic() == null ? "*" : entry.getTopic());
if (entry.getSequence() == null) {
stmt.setObject(5, null);
} else {
stmt.setLong(5, entry.getSequence());
}
stmt.setTimestamp(
6, entry.getLastAttemptTime() == null ? null : Timestamp.from(entry.getLastAttemptTime()));
stmt.setTimestamp(7, Timestamp.from(entry.getNextAttemptTime()));
stmt.setInt(8, entry.getAttempts());
stmt.setBoolean(9, entry.isBlocked());
stmt.setBoolean(10, entry.isProcessed());
stmt.setInt(11, entry.getVersion());
}
@Override
public void delete(Transaction tx, TransactionOutboxEntry entry) throws Exception {
//noinspection resource
try (PreparedStatement stmt =
tx.connection().prepareStatement(dialect.getDelete().replace("{{table}}", tableName))) {
stmt.setString(1, entry.getId());
stmt.setInt(2, entry.getVersion());
if (stmt.executeUpdate() != 1) {
throw new OptimisticLockException();
}
log.debug("Deleted {}", entry.description());
}
}
@Override
public void update(Transaction tx, TransactionOutboxEntry entry) throws Exception {
//noinspection resource
try (PreparedStatement stmt =
tx.connection()
.prepareStatement(
// language=MySQL
"UPDATE "
+ tableName
+ " "
+ "SET lastAttemptTime = ?, nextAttemptTime = ?, attempts = ?, blocked = ?, processed = ?, version = ? "
+ "WHERE id = ? and version = ?")) {
stmt.setTimestamp(
1,
entry.getLastAttemptTime() == null ? null : Timestamp.from(entry.getLastAttemptTime()));
stmt.setTimestamp(2, Timestamp.from(entry.getNextAttemptTime()));
stmt.setInt(3, entry.getAttempts());
stmt.setBoolean(4, entry.isBlocked());
stmt.setBoolean(5, entry.isProcessed());
stmt.setInt(6, entry.getVersion() + 1);
stmt.setString(7, entry.getId());
stmt.setInt(8, entry.getVersion());
if (stmt.executeUpdate() != 1) {
throw new OptimisticLockException();
}
entry.setVersion(entry.getVersion() + 1);
log.debug("Updated {}", entry.description());
}
}
@Override
public boolean lock(Transaction tx, TransactionOutboxEntry entry) throws Exception {
//noinspection resource
try (PreparedStatement stmt =
tx.connection()
.prepareStatement(
dialect
.getLock()
.replace("{{table}}", tableName)
.replace("{{allFields}}", ALL_FIELDS))) {
stmt.setString(1, entry.getId());
stmt.setInt(2, entry.getVersion());
stmt.setQueryTimeout(writeLockTimeoutSeconds);
try {
try (ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
return false;
}
// Ensure that subsequent processing uses a deserialized invocation rather than
// the object from the caller, which might not serialize well and thus cause a
// difference between immediate and retry processing
try (Reader invocationStream = rs.getCharacterStream("invocation")) {
entry.setInvocation(serializer.deserializeInvocation(invocationStream));
}
return true;
}
} catch (SQLTimeoutException e) {
log.debug("Lock attempt timed out on {}", entry.description());
return false;
}
}
}
@Override
public boolean unblock(Transaction tx, String entryId) throws Exception {
//noinspection resource
try (PreparedStatement stmt =
tx.connection()
.prepareStatement(
"UPDATE "
+ tableName
+ " SET attempts = 0, blocked = "
+ dialect.booleanValue(false)
+ " "
+ "WHERE blocked = "
+ dialect.booleanValue(true)
+ " AND processed = "
+ dialect.booleanValue(false)
+ " AND id = ?")) {
stmt.setString(1, entryId);
stmt.setQueryTimeout(writeLockTimeoutSeconds);
return stmt.executeUpdate() != 0;
}
}
@Override
public List<TransactionOutboxEntry> selectBatch(Transaction tx, int batchSize, Instant now)
throws Exception {
//noinspection resource
try (PreparedStatement stmt =
tx.connection()
.prepareStatement(
dialect
.getSelectBatch()
.replace("{{table}}", tableName)
.replace("{{batchSize}}", Integer.toString(batchSize))
.replace("{{allFields}}", ALL_FIELDS))) {
stmt.setTimestamp(1, Timestamp.from(now));
var result = new ArrayList<TransactionOutboxEntry>(batchSize);
gatherResults(stmt, result);
return result;
}
}
@Override
public Collection<TransactionOutboxEntry> selectNextInTopics(
Transaction tx, int batchSize, Instant now) throws Exception {
var sql =
dialect
.getFetchNextInAllTopics()
.replace("{{table}}", tableName)
.replace("{{batchSize}}", Integer.toString(batchSize))
.replace("{{allFields}}", ALL_FIELDS);
//noinspection resource
try (PreparedStatement stmt = tx.connection().prepareStatement(sql)) {
stmt.setTimestamp(1, Timestamp.from(now));
var results = new ArrayList<TransactionOutboxEntry>();
gatherResults(stmt, results);
return results;
}
}
@Override
public Collection<TransactionOutboxEntry> selectNextInSelectedTopics(
Transaction tx, List<String> topicNames, int batchSize, Instant now) throws Exception {
var topicsInParameterList = topicNames.stream().map(it -> "?").collect(Collectors.joining(","));
var sql =
dialect
.getFetchNextInSelectedTopics()
.replace("{{table}}", tableName)
.replace("{{topicNames}}", topicsInParameterList)
.replace("{{batchSize}}", Integer.toString(batchSize))
.replace("{{allFields}}", ALL_FIELDS);
//noinspection resource
try (PreparedStatement stmt = tx.connection().prepareStatement(sql)) {
var counter = 1;
for (var topicName : topicNames) {
stmt.setString(counter, topicName);
counter++;
}
stmt.setTimestamp(counter, Timestamp.from(now));
var results = new ArrayList<TransactionOutboxEntry>();
gatherResults(stmt, results);
return results;
}
}
@Override
public int deleteProcessedAndExpired(Transaction tx, int batchSize, Instant now)
throws Exception {
//noinspection resource
try (PreparedStatement stmt =
tx.connection()
.prepareStatement(
dialect
.getDeleteExpired()
.replace("{{table}}", tableName)
.replace("{{batchSize}}", Integer.toString(batchSize)))) {
stmt.setTimestamp(1, Timestamp.from(now));
return stmt.executeUpdate();
}
}
private void gatherResults(PreparedStatement stmt, Collection<TransactionOutboxEntry> output)
throws SQLException, IOException {
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
output.add(map(rs));
}
log.debug("Found {} results", output.size());
}
}
private TransactionOutboxEntry map(ResultSet rs) throws SQLException, IOException {
String topic = rs.getString("topic");
Long sequence = rs.getLong("seq");
if (rs.wasNull()) {
sequence = null;
}
// Reading invocationStream *must* occur first because some drivers (ex. SQL Server)
// implement true streams that are not buffered in memory. Calling any other getter
// on ResultSet before invocationStream is read will cause Reader to be closed
// prematurely.
try (Reader invocationStream = rs.getCharacterStream("invocation")) {
Invocation invocation;
try {
invocation = serializer.deserializeInvocation(invocationStream);
} catch (IOException e) {
invocation = new FailedDeserializingInvocation(e);
}
TransactionOutboxEntry entry =
TransactionOutboxEntry.builder()
.invocation(invocation)
.id(rs.getString("id"))
.uniqueRequestId(rs.getString("uniqueRequestId"))
.topic("*".equals(topic) ? null : topic)
.sequence(sequence)
.lastAttemptTime(
rs.getTimestamp("lastAttemptTime") == null
? null
: rs.getTimestamp("lastAttemptTime").toInstant())
.nextAttemptTime(rs.getTimestamp("nextAttemptTime").toInstant())
.attempts(rs.getInt("attempts"))
.blocked(rs.getBoolean("blocked"))
.processed(rs.getBoolean("processed"))
.version(rs.getInt("version"))
.build();
log.debug("Found {}", entry);
return entry;
}
}
// For testing. Assumed low volume.
@Override
public void clear(Transaction tx) throws SQLException {
//noinspection resource
try (Statement stmt = tx.connection().createStatement()) {
stmt.execute("DELETE FROM " + tableName);
}
}
@Override
public boolean checkConnection(Transaction tx) throws SQLException {
//noinspection resource
try (Statement stmt = tx.connection().createStatement();
ResultSet rs = stmt.executeQuery(dialect.getCheckSql())) {
return rs.next() && (rs.getInt(1) == 1);
}
}
}
================================================
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Dialect.java
================================================
package com.gruelbox.transactionoutbox;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.stream.Stream;
/** The SQL dialects supported by {@link DefaultPersistor}. */
public interface Dialect {
String getDelete();
/**
* @return Format string for the SQL required to delete expired retained records.
*/
String getDeleteExpired();
String getSelectBatch();
String getLock();
String getCheckSql();
String getFetchNextInAllTopics();
String getFetchNextInSelectedTopics();
String getFetchCurrentVersion();
String getFetchNextSequence();
String booleanValue(boolean criteriaValue);
void createVersionTableIfNotExists(Connection connection) throws SQLException;
Stream<Migration> getMigrations();
Dialect MY_SQL_5 =
DefaultDialect.builder("MY_SQL_5")
.changeMigration(
13,
"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN invocation mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
.build();
Dialect MY_SQL_8 =
DefaultDialect.builder("MY_SQL_8")
.fetchNextInAllTopics(
"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn"
+ " FROM {{table}} WHERE processed = false AND topic <> '*')"
+ " SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}")
.fetchNextInSelectedTopics(
"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn"
+ " FROM {{table}} WHERE processed = false AND topic IN ({{topicNames}}))"
+ " SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}")
.deleteExpired(
"DELETE FROM {{table}} WHERE nextAttemptTime < ? AND processed = true AND blocked = false"
+ " LIMIT {{batchSize}}")
.selectBatch(
"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? "
+ "AND blocked = false AND processed = false AND topic = '*' LIMIT {{batchSize}} FOR UPDATE "
+ "SKIP LOCKED")
.lock(
"SELECT id, invocation FROM {{table}} WHERE id = ? AND version = ? FOR "
+ "UPDATE SKIP LOCKED")
.changeMigration(
13,
"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN invocation mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
.build();
Dialect POSTGRESQL_9 =
DefaultDialect.builder("POSTGRESQL_9")
.fetchNextInAllTopics(
"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn"
+ " FROM {{table}} WHERE processed = false AND topic <> '*')"
+ " SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}")
.fetchNextInSelectedTopics(
"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn"
+ " FROM {{table}} WHERE processed = false AND topic IN ({{topicNames}}))"
+ " SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}")
.deleteExpired(
"DELETE FROM {{table}} WHERE id IN "
+ "(SELECT id FROM {{table}} WHERE nextAttemptTime < ? AND processed = true AND blocked = false LIMIT {{batchSize}})")
.selectBatch(
"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? "
gitextract_2k8z5qvm/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── cd_build.yml
│ ├── pull_request.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── pom.xml
├── settings.xml
├── transactionoutbox-acceptance/
│ ├── pom.xml
│ └── src/
│ └── test/
│ └── java/
│ └── com/
│ └── gruelbox/
│ └── transactionoutbox/
│ └── acceptance/
│ ├── TestComplexConfigurationExample.java
│ ├── TestH2.java
│ ├── TestMSSqlServer2019.java
│ ├── TestMSSqlServer2022.java
│ ├── TestMySql5.java
│ ├── TestMySql8.java
│ ├── TestOracle18.java
│ ├── TestOracle21.java
│ ├── TestPostgres11.java
│ ├── TestPostgres12.java
│ ├── TestPostgres13.java
│ ├── TestPostgres14.java
│ ├── TestPostgres15.java
│ ├── TestPostgres16.java
│ ├── TestRequestSerialization.java
│ ├── TestStubbing.java
│ └── persistor/
│ ├── TestDefaultPersistorH2.java
│ ├── TestDefaultPersistorMSSqlServer2019.java
│ ├── TestDefaultPersistorMySql5.java
│ ├── TestDefaultPersistorMySql8.java
│ ├── TestDefaultPersistorOracle18.java
│ └── TestDefaultPersistorPostgres16.java
├── transactionoutbox-core/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ ├── AlreadyScheduledException.java
│ │ ├── ConnectionProvider.java
│ │ ├── DataSourceConnectionProvider.java
│ │ ├── DefaultDialect.java
│ │ ├── DefaultInvocationSerializer.java
│ │ ├── DefaultMigrationManager.java
│ │ ├── DefaultPersistor.java
│ │ ├── Dialect.java
│ │ ├── DriverConnectionProvider.java
│ │ ├── ExecutorSubmitter.java
│ │ ├── FailedDeserializingInvocation.java
│ │ ├── FunctionInstantiator.java
│ │ ├── Instantiator.java
│ │ ├── Invocation.java
│ │ ├── InvocationSerializer.java
│ │ ├── Migration.java
│ │ ├── MissingOptionalDependencyException.java
│ │ ├── NoTransactionActiveException.java
│ │ ├── OptimisticLockException.java
│ │ ├── ParameterContextTransactionManager.java
│ │ ├── Persistor.java
│ │ ├── ReflectionInstantiator.java
│ │ ├── RuntimeTypeAdapterFactory.java
│ │ ├── SQLAction.java
│ │ ├── SimpleTransactionManager.java
│ │ ├── StubParameterContextTransactionManager.java
│ │ ├── StubPersistor.java
│ │ ├── StubThreadLocalTransactionManager.java
│ │ ├── Submitter.java
│ │ ├── ThreadLocalContextTransactionManager.java
│ │ ├── ThrowingRunnable.java
│ │ ├── ThrowingTransactionalSupplier.java
│ │ ├── ThrowingTransactionalWork.java
│ │ ├── Transaction.java
│ │ ├── TransactionContextPlaceholder.java
│ │ ├── TransactionManager.java
│ │ ├── TransactionOutbox.java
│ │ ├── TransactionOutboxEntry.java
│ │ ├── TransactionOutboxImpl.java
│ │ ├── TransactionOutboxListener.java
│ │ ├── TransactionalInvocation.java
│ │ ├── TransactionalSupplier.java
│ │ ├── TransactionalWork.java
│ │ ├── UncheckedException.java
│ │ ├── Validatable.java
│ │ ├── Validator.java
│ │ └── spi/
│ │ ├── AbstractFullyQualifiedNameInstantiator.java
│ │ ├── AbstractThreadLocalTransactionManager.java
│ │ ├── ProxyFactory.java
│ │ ├── SimpleTransaction.java
│ │ └── Utils.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ ├── AbstractTestDefaultInvocationSerializer.java
│ │ ├── TestDefaultInvocationSerializer.java
│ │ ├── TestDefaultMigrationManager.java
│ │ ├── TestDefaultPersistorConfiguration.java
│ │ ├── TestProxyGeneration.java
│ │ └── TestValidator.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-guice/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── guice/
│ │ └── GuiceInstantiator.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── guice/
│ │ └── acceptance/
│ │ ├── TestGuiceBinding.java
│ │ └── TestGuiceInstantiator.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-jackson/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jackson/
│ │ ├── CustomInvocationDeserializer.java
│ │ ├── CustomInvocationSerializer.java
│ │ ├── JacksonInvocationSerializer.java
│ │ ├── TransactionOutboxEntryDeserializer.java
│ │ └── TransactionOutboxJacksonModule.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jackson/
│ │ ├── MonetaryAmount.java
│ │ ├── SerializationStressTestInput.java
│ │ ├── TestJacksonInvocationSerializer.java
│ │ ├── TestTransactionOutboxEntrySerialization.java
│ │ └── acceptance/
│ │ └── TestJacksonSerializer.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-jooq/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jooq/
│ │ ├── DefaultJooqTransactionManager.java
│ │ ├── JooqTransactionListener.java
│ │ ├── JooqTransactionManager.java
│ │ └── ThreadLocalJooqTransactionManager.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── jooq/
│ │ └── acceptance/
│ │ ├── AbstractJooqAcceptanceTest.java
│ │ ├── AbstractJooqAcceptanceThreadLocalTest.java
│ │ ├── JooqTestUtils.java
│ │ ├── TestJooqThreadLocalH2.java
│ │ ├── TestJooqThreadLocalMSSqlServer2019.java
│ │ ├── TestJooqThreadLocalMySql5.java
│ │ ├── TestJooqThreadLocalMySql8.java
│ │ ├── TestJooqThreadLocalPostgres16.java
│ │ ├── TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext.java
│ │ └── TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-quarkus/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── quarkus/
│ │ ├── CdiInstantiator.java
│ │ └── QuarkusTransactionManager.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── quarkus/
│ │ └── acceptance/
│ │ ├── ApplicationConfig.java
│ │ ├── BusinessService.java
│ │ ├── BusinessServiceTest.java
│ │ ├── DaoImpl.java
│ │ └── RemoteCallService.java
│ └── resources/
│ ├── application.properties
│ └── db/
│ └── create.sql
├── transactionoutbox-spring/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── spring/
│ │ ├── SpringInstantiator.java
│ │ ├── SpringTransactionManager.java
│ │ └── SpringTransactionOutboxConfiguration.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── spring/
│ │ ├── SpringTransactionManagerTest.java
│ │ ├── example/
│ │ │ ├── multipledatasources/
│ │ │ │ ├── EventuallyConsistentController.java
│ │ │ │ ├── ExternalsConfiguration.java
│ │ │ │ ├── MultipleDataSourcesTest.java
│ │ │ │ ├── TransactionOutboxBackgroundProcessor.java
│ │ │ │ ├── TransactionOutboxProperties.java
│ │ │ │ ├── TransactionOutboxSpringMultipleDatasourcesDemoApplication.java
│ │ │ │ ├── computer/
│ │ │ │ │ ├── Computer.java
│ │ │ │ │ ├── ComputerExternalQueueService.java
│ │ │ │ │ ├── ComputerRepository.java
│ │ │ │ │ └── ComputersDbConfiguration.java
│ │ │ │ └── employee/
│ │ │ │ ├── Employee.java
│ │ │ │ ├── EmployeeExternalQueueService.java
│ │ │ │ ├── EmployeeRepository.java
│ │ │ │ └── EmployeesDbConfiguration.java
│ │ │ └── simple/
│ │ │ ├── Customer.java
│ │ │ ├── CustomerRepository.java
│ │ │ ├── EventuallyConsistentController.java
│ │ │ ├── EventuallyConsistentControllerTest.java
│ │ │ ├── ExternalQueueService.java
│ │ │ ├── ExternalsConfiguration.java
│ │ │ ├── TransactionOutboxBackgroundProcessor.java
│ │ │ ├── TransactionOutboxProperties.java
│ │ │ ├── TransactionOutboxSpringDemoApplication.java
│ │ │ └── Utils.java
│ │ └── it/
│ │ ├── MyRemoteService.java
│ │ ├── SpringTransactionManagerIT.java
│ │ └── TestApplication.java
│ └── resources/
│ ├── META-INF/
│ │ └── persistence.xml
│ ├── application.properties
│ └── logback-test.xml
├── transactionoutbox-testing/
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ ├── TestingMode.java
│ │ └── testing/
│ │ ├── AbstractAcceptanceTest.java
│ │ ├── AbstractPersistorTest.java
│ │ ├── BaseTest.java
│ │ ├── ClassProcessor.java
│ │ ├── InterfaceProcessor.java
│ │ ├── LatchListener.java
│ │ ├── OrderedEntryListener.java
│ │ ├── ProcessedEntryListener.java
│ │ └── TestUtils.java
│ └── resources/
│ └── logback-test.xml
├── transactionoutbox-virtthreads/
│ ├── pom.xml
│ └── src/
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── gruelbox/
│ │ └── transactionoutbox/
│ │ └── virtthreads/
│ │ ├── AbstractVirtualThreadsTest.java
│ │ ├── TestVirtualThreadsH2.java
│ │ ├── TestVirtualThreadsH2Jooq.java
│ │ ├── TestVirtualThreadsMySql5.java
│ │ ├── TestVirtualThreadsMySql8.java
│ │ ├── TestVirtualThreadsOracle21.java
│ │ └── TestVirtualThreadsPostgres16.java
│ └── resources/
│ └── logback-test.xml
└── ~/
├── settings.xml
└── toolchains.xml
SYMBOL INDEX (982 symbols across 162 files)
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestComplexConfigurationExample.java
class TestComplexConfigurationExample (line 21) | class TestComplexConfigurationExample {
method test (line 23) | @Test
method performRemoteCall (line 129) | void performRemoteCall(
method writeSomeChanges (line 133) | private void writeSomeChanges(@SuppressWarnings("unused") Connection c...
type ServiceLocator (line 135) | private interface ServiceLocator {
method createInstance (line 136) | <T> T createInstance(Class<T> clazz);
type EventPublisher (line 139) | private interface EventPublisher {
method publish (line 140) | void publish(Object o);
class BlockedOutboxTaskEvent (line 143) | @Value
class OutboxTaskProcessedEvent (line 148) | @Value
type SaleType (line 153) | @SuppressWarnings("unused")
type Money (line 159) | private interface Money {
method of (line 160) | static Money of(
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestH2.java
class TestH2 (line 19) | @SuppressWarnings("WeakerAccess")
method delayedExecutionImmediateSubmission (line 24) | @Test
method delayedExecutionFlushOnly (line 53) | @Test
method wrapInvocations (line 82) | @Test
method wrapInvocationsWithMDC (line 127) | @Test
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2019.java
class TestMSSqlServer2019 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2022.java
class TestMSSqlServer2022 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql5.java
class TestMySql5 (line 12) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 24) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql8.java
class TestMySql8 (line 12) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 24) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java
class TestOracle18 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 21) | @Override
method createTestTable (line 32) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java
class TestOracle21 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 21) | @Override
method createTestTable (line 32) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres11.java
class TestPostgres11 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres12.java
class TestPostgres12 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres13.java
class TestPostgres13 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres14.java
class TestPostgres14 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres15.java
class TestPostgres15 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres16.java
class TestPostgres16 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestRequestSerialization.java
class TestRequestSerialization (line 16) | public class TestRequestSerialization {
method workAlwaysSerialized (line 23) | @Test
method connectionDetails (line 51) | protected AbstractAcceptanceTest.ConnectionDetails connectionDetails() {
method simpleTxnManager (line 62) | private TransactionManager simpleTxnManager() {
method clearOutbox (line 70) | private void clearOutbox() {
class ComplexProcessor (line 74) | static class ComplexProcessor {
method process (line 76) | public void process(Arg arg) {
class Arg (line 87) | @Getter
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestStubbing.java
class TestStubbing (line 19) | class TestStubbing {
method testStubbingWithThreadLocalContext (line 21) | @Test
method testStubbingWithExplicitContextInvalidContext (line 51) | @Test
method testStubbingWithExplicitContextPassingTransaction (line 64) | @Test
method testStubbingWithExplicitContextPassingContext (line 79) | @Test
method createOutbox (line 95) | private TransactionOutbox createOutbox(TransactionManager transactionM...
class Interface (line 109) | static class Interface {
method doThing (line 113) | void doThing(int arg1, String arg2, BigDecimal[] arg3) {
method doThing (line 121) | void doThing(@SuppressWarnings("SameParameterValue") int arg1, Trans...
method doThing (line 126) | void doThing(@SuppressWarnings("SameParameterValue") int arg1, Conte...
class Context (line 132) | @Value
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorH2.java
class TestDefaultPersistorH2 (line 9) | @Testcontainers
method persistor (line 20) | @Override
method txManager (line 25) | @Override
method dialect (line 30) | @Override
method testSkipLocked (line 35) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMSSqlServer2019.java
class TestDefaultPersistorMSSqlServer2019 (line 13) | @Testcontainers
method persistor (line 33) | @Override
method txManager (line 38) | @Override
method dialect (line 43) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql5.java
class TestDefaultPersistorMySql5 (line 14) | @Testcontainers
method persistor (line 34) | @Override
method txManager (line 39) | @Override
method dialect (line 44) | @Override
method testSkipLocked (line 49) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql8.java
class TestDefaultPersistorMySql8 (line 14) | @Testcontainers
method persistor (line 34) | @Override
method txManager (line 39) | @Override
method dialect (line 44) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorOracle18.java
class TestDefaultPersistorOracle18 (line 13) | @Testcontainers
method persistor (line 33) | @Override
method txManager (line 38) | @Override
method dialect (line 43) | @Override
FILE: transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorPostgres16.java
class TestDefaultPersistorPostgres16 (line 13) | @Testcontainers
method persistor (line 33) | @Override
method txManager (line 38) | @Override
method dialect (line 43) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/AlreadyScheduledException.java
class AlreadyScheduledException (line 9) | public class AlreadyScheduledException extends RuntimeException {
method AlreadyScheduledException (line 10) | AlreadyScheduledException(String message, Throwable cause) {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ConnectionProvider.java
type ConnectionProvider (line 10) | @SuppressWarnings("WeakerAccess")
method obtainConnection (line 19) | Connection obtainConnection();
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DataSourceConnectionProvider.java
class DataSourceConnectionProvider (line 18) | @Builder
method obtainConnection (line 23) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultDialect.java
class DefaultDialect (line 17) | @AllArgsConstructor(access = AccessLevel.PRIVATE)
method builder (line 20) | static Builder builder(String name) {
method booleanValue (line 36) | @Override
method createVersionTableIfNotExists (line 41) | @Override
method toString (line 49) | @Override
method getMigrations (line 54) | @Override
class Builder (line 59) | @Setter
method Builder (line 91) | Builder(String name) {
method setMigration (line 171) | Builder setMigration(Migration migration) {
method changeMigration (line 176) | Builder changeMigration(int version, String sql) {
method disableMigration (line 180) | Builder disableMigration(@SuppressWarnings("SameParameterValue") int...
method build (line 184) | Dialect build() {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultInvocationSerializer.java
class DefaultInvocationSerializer (line 61) | @Slf4j
method DefaultInvocationSerializer (line 66) | @Builder
method serializeInvocation (line 89) | @Override
method deserializeInvocation (line 98) | @Override
class InvocationJsonSerializer (line 107) | private static final class InvocationJsonSerializer
method InvocationJsonSerializer (line 114) | InvocationJsonSerializer(Set<Class<?>> serializableClasses, int vers...
method addClass (line 160) | private void addClass(Class<?> clazz) {
method addClassPair (line 164) | private void addClassPair(Class<?> clazz, String name) {
method toArrayClassName (line 173) | private String toArrayClassName(Class<?> clazz) {
method toClass (line 197) | private Class<?> toClass(ClassLoader classLoader, String name) {
method serialize (line 210) | @Override
method serializeV1 (line 245) | JsonElement serializeV1(Invocation src, Type typeOfSrc, JsonSerializ...
method deserialize (line 263) | @Override
method classForName (line 311) | private Class<?> classForName(String name) {
method nameForClass (line 319) | private String nameForClass(Class<?> clazz) {
class ZonedDateTimeTypeAdapter (line 329) | static final class ZonedDateTimeTypeAdapter extends TypeAdapter<ZonedD...
method write (line 331) | @Override
method read (line 336) | @Override
class LocalDateTimeTypeAdapter (line 342) | static final class LocalDateTimeTypeAdapter extends TypeAdapter<LocalD...
method write (line 344) | @Override
method read (line 349) | @Override
class InstantTypeAdapter (line 355) | static final class InstantTypeAdapter extends TypeAdapter<Instant> {
method write (line 357) | @Override
method read (line 362) | @Override
class DurationTypeAdapter (line 368) | static final class DurationTypeAdapter extends TypeAdapter<Duration> {
method write (line 370) | @Override
method read (line 375) | @Override
class LocalDateTypeAdapter (line 381) | static final class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {
method write (line 383) | @Override
method read (line 388) | @Override
class MonthDayTypeAdapter (line 394) | static final class MonthDayTypeAdapter extends TypeAdapter<MonthDay> {
method write (line 398) | @Override
method read (line 403) | @Override
class PeriodTypeAdapter (line 409) | static final class PeriodTypeAdapter extends TypeAdapter<Period> {
method write (line 411) | @Override
method read (line 416) | @Override
class YearTypeAdapter (line 422) | static final class YearTypeAdapter extends TypeAdapter<Year> {
method write (line 424) | @Override
method read (line 429) | @Override
class YearMonthAdapter (line 435) | static final class YearMonthAdapter extends TypeAdapter<YearMonth> {
method write (line 437) | @Override
method read (line 442) | @Override
class UtcDateTypeAdapter (line 448) | static final class UtcDateTypeAdapter extends TypeAdapter<Date> {
method write (line 451) | @Override
method read (line 461) | @Override
method format (line 489) | private static String format(Date date, boolean millis, TimeZone tz) {
method padInt (line 537) | private static void padInt(StringBuilder buffer, int value, int leng...
method parse (line 552) | private static Date parse(String date, ParsePosition pos) throws Par...
method checkOffset (line 654) | private static boolean checkOffset(String value, int offset, char ex...
method parseInt (line 667) | private static int parseInt(String value, int beginIndex, int endIndex)
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultMigrationManager.java
class DefaultMigrationManager (line 22) | @Slf4j
method withLatch (line 33) | static void withLatch(CountDownLatch readyLatch, Consumer<CountDownLat...
method migrate (line 44) | static void migrate(TransactionManager transactionManager, Dialect dia...
method writeSchema (line 62) | static void writeSchema(Writer writer, Dialect dialect) {
method runSql (line 82) | private static void runSql(TransactionManager txm, Connection connecti...
method currentVersion (line 112) | private static int currentVersion(Connection connection, Dialect diale...
method fetchCurrentVersion (line 146) | private static int fetchCurrentVersion(Connection connection, Dialect ...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultPersistor.java
class DefaultPersistor (line 29) | @Slf4j
method validate (line 81) | @Override
method migrate (line 87) | @Override
method writeSchema (line 100) | public void writeSchema(Writer writer) {
method save (line 104) | @Override
method serializeAndDeserialize (line 138) | @Override
method setNextSequence (line 153) | private void setNextSequence(Transaction tx, TransactionOutboxEntry en...
method indexViolation (line 186) | private boolean indexViolation(Exception e) {
method setupInsert (line 194) | private void setupInsert(
method delete (line 215) | @Override
method update (line 229) | @Override
method lock (line 259) | @Override
method unblock (line 292) | @Override
method selectBatch (line 314) | @Override
method selectNextInTopics (line 333) | @Override
method selectNextInSelectedTopics (line 351) | @Override
method deleteProcessedAndExpired (line 377) | @Override
method gatherResults (line 393) | private void gatherResults(PreparedStatement stmt, Collection<Transact...
method map (line 403) | private TransactionOutboxEntry map(ResultSet rs) throws SQLException, ...
method clear (line 443) | @Override
method checkConnection (line 451) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Dialect.java
type Dialect (line 9) | public interface Dialect {
method getDelete (line 10) | String getDelete();
method getDeleteExpired (line 15) | String getDeleteExpired();
method getSelectBatch (line 17) | String getSelectBatch();
method getLock (line 19) | String getLock();
method getCheckSql (line 21) | String getCheckSql();
method getFetchNextInAllTopics (line 23) | String getFetchNextInAllTopics();
method getFetchNextInSelectedTopics (line 25) | String getFetchNextInSelectedTopics();
method getFetchCurrentVersion (line 27) | String getFetchCurrentVersion();
method getFetchNextSequence (line 29) | String getFetchNextSequence();
method booleanValue (line 31) | String booleanValue(boolean criteriaValue);
method createVersionTableIfNotExists (line 33) | void createVersionTableIfNotExists(Connection connection) throws SQLEx...
method getMigrations (line 35) | Stream<Migration> getMigrations();
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DriverConnectionProvider.java
class DriverConnectionProvider (line 25) | @SuperBuilder
method obtainConnection (line 36) | @Override
method validate (line 54) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ExecutorSubmitter.java
class ExecutorSubmitter (line 39) | @Slf4j
method submit (line 58) | @Override
method validate (line 77) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/FailedDeserializingInvocation.java
class FailedDeserializingInvocation (line 7) | public class FailedDeserializingInvocation extends Invocation {
method FailedDeserializingInvocation (line 14) | public FailedDeserializingInvocation(IOException exceptionDuringDeseri...
method invoke (line 19) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/FunctionInstantiator.java
class FunctionInstantiator (line 7) | @SuperBuilder
method createInstance (line 12) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Instantiator.java
type Instantiator (line 8) | public interface Instantiator {
method usingReflection (line 19) | static Instantiator usingReflection() {
method using (line 39) | static Instantiator using(Function<Class<?>, Object> fn) {
method getName (line 54) | String getName(Class<?> clazz);
method getInstance (line 67) | Object getInstance(String name);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Invocation.java
class Invocation (line 21) | @SuppressWarnings("WeakerAccess")
method Invocation (line 79) | public Invocation(String className, String methodName, Class<?>[] para...
method Invocation (line 93) | @Deprecated(forRemoval = true)
method Invocation (line 114) | public Invocation(
method withinMDC (line 129) | <T> T withinMDC(Callable<T> callable) throws Exception {
method invoke (line 147) | void invoke(Object instance, TransactionOutboxListener listener)
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/InvocationSerializer.java
type InvocationSerializer (line 18) | public interface InvocationSerializer {
method createDefaultJsonSerializer (line 26) | static InvocationSerializer createDefaultJsonSerializer() {
method serializeInvocation (line 36) | void serializeInvocation(Invocation invocation, Writer writer);
method deserializeInvocation (line 44) | Invocation deserializeInvocation(Reader reader) throws IOException;
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Migration.java
class Migration (line 6) | @Value
method withSql (line 12) | public Migration withSql(String sql) {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/MissingOptionalDependencyException.java
class MissingOptionalDependencyException (line 3) | public class MissingOptionalDependencyException extends RuntimeException {
method MissingOptionalDependencyException (line 5) | public MissingOptionalDependencyException(String groupId, String artif...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/NoTransactionActiveException.java
class NoTransactionActiveException (line 4) | @SuppressWarnings("WeakerAccess")
method NoTransactionActiveException (line 7) | public NoTransactionActiveException() {
method NoTransactionActiveException (line 11) | public NoTransactionActiveException(Throwable cause) {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/OptimisticLockException.java
class OptimisticLockException (line 4) | public class OptimisticLockException extends Exception {}
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ParameterContextTransactionManager.java
type ParameterContextTransactionManager (line 24) | public interface ParameterContextTransactionManager<T> extends Transacti...
method transactionFromContext (line 33) | Transaction transactionFromContext(T context);
method contextType (line 38) | Class<T> contextType();
method extractTransaction (line 50) | @SuppressWarnings("unchecked")
method injectTransaction (line 98) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Persistor.java
type Persistor (line 13) | public interface Persistor {
method forDialect (line 22) | static DefaultPersistor forDialect(Dialect dialect) {
method migrate (line 32) | void migrate(TransactionManager transactionManager);
method save (line 43) | void save(Transaction tx, TransactionOutboxEntry entry) throws Exception;
method serializeAndDeserialize (line 51) | default Invocation serializeAndDeserialize(Invocation invocation) {
method delete (line 67) | void delete(Transaction tx, TransactionOutboxEntry entry) throws Excep...
method update (line 80) | void update(Transaction tx, TransactionOutboxEntry entry) throws Excep...
method lock (line 91) | boolean lock(Transaction tx, TransactionOutboxEntry entry) throws Exce...
method unblock (line 102) | boolean unblock(Transaction tx, String entryId) throws Exception;
method selectBatch (line 116) | List<TransactionOutboxEntry> selectBatch(Transaction tx, int batchSize...
method selectNextInTopics (line 128) | Collection<TransactionOutboxEntry> selectNextInTopics(Transaction tx, ...
method selectNextInSelectedTopics (line 141) | Collection<TransactionOutboxEntry> selectNextInSelectedTopics(
method deleteProcessedAndExpired (line 153) | int deleteProcessedAndExpired(Transaction tx, int batchSize, Instant n...
method checkConnection (line 161) | boolean checkConnection(Transaction tx) throws Exception;
method clear (line 168) | void clear(Transaction tx) throws Exception;
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ReflectionInstantiator.java
class ReflectionInstantiator (line 14) | @Slf4j
method createInstance (line 18) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/RuntimeTypeAdapterFactory.java
class RuntimeTypeAdapterFactory (line 159) | @Slf4j
method RuntimeTypeAdapterFactory (line 167) | private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldN...
method of (line 180) | static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
method registerSubtype (line 190) | RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, ...
method create (line 202) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/SQLAction.java
type SQLAction (line 6) | @FunctionalInterface
method doAction (line 8) | void doAction(Connection connection) throws SQLException;
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/SimpleTransactionManager.java
class SimpleTransactionManager (line 15) | @SuperBuilder
method inTransactionReturnsThrows (line 22) | @Override
method processAndCommitOrRollback (line 33) | private <T, E extends Exception> T processAndCommitOrRollback(
method withTransaction (line 56) | private <T, E extends Exception> T withTransaction(ThrowingTransaction...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubParameterContextTransactionManager.java
class StubParameterContextTransactionManager (line 16) | @Slf4j
method StubParameterContextTransactionManager (line 28) | public StubParameterContextTransactionManager(Class<C> contextClass, S...
method transactionFromContext (line 33) | @Override
method contextType (line 38) | @Override
method inTransactionReturnsThrows (line 43) | @Override
method withTransaction (line 54) | private <T, E extends Exception> T withTransaction(ThrowingTransaction...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubPersistor.java
class StubPersistor (line 9) | @Builder
method StubPersistor (line 12) | StubPersistor() {}
method migrate (line 14) | @Override
method save (line 19) | @Override
method delete (line 24) | @Override
method update (line 29) | @Override
method lock (line 34) | @Override
method unblock (line 39) | @Override
method selectBatch (line 44) | @Override
method selectNextInTopics (line 49) | @Override
method selectNextInSelectedTopics (line 55) | @Override
method deleteProcessedAndExpired (line 61) | @Override
method clear (line 66) | @Override
method checkConnection (line 69) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubThreadLocalTransactionManager.java
class StubThreadLocalTransactionManager (line 14) | @Slf4j
method StubThreadLocalTransactionManager (line 18) | public StubThreadLocalTransactionManager() {
method inTransactionReturnsThrows (line 22) | @Override
method withTransaction (line 33) | private <T, E extends Exception> T withTransaction(ThrowingTransaction...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Submitter.java
type Submitter (line 11) | public interface Submitter {
method withExecutor (line 21) | static Submitter withExecutor(Executor executor) {
method withDefaultExecutor (line 32) | static Submitter withDefaultExecutor() {
method submit (line 74) | void submit(TransactionOutboxEntry entry, Consumer<TransactionOutboxEn...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThreadLocalContextTransactionManager.java
type ThreadLocalContextTransactionManager (line 22) | public interface ThreadLocalContextTransactionManager extends Transactio...
method requireTransaction (line 33) | default <E extends Exception> void requireTransaction(ThrowingTransact...
method requireTransactionReturns (line 51) | <T, E extends Exception> T requireTransactionReturns(ThrowingTransacti...
method extractTransaction (line 63) | @Override
method injectTransaction (line 81) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingRunnable.java
type ThrowingRunnable (line 4) | @FunctionalInterface
method run (line 7) | void run() throws Exception;
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingTransactionalSupplier.java
type ThrowingTransactionalSupplier (line 3) | @FunctionalInterface
method fromRunnable (line 6) | static <F extends Exception> ThrowingTransactionalSupplier<Void, F> fr...
method fromWork (line 14) | static <F extends Exception> ThrowingTransactionalSupplier<Void, F> fr...
method fromWork (line 22) | static ThrowingTransactionalSupplier<Void, RuntimeException> fromWork(...
method fromSupplier (line 29) | static <T> ThrowingTransactionalSupplier<T, RuntimeException> fromSupp...
method doWork (line 34) | T doWork(Transaction transaction) throws E;
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingTransactionalWork.java
type ThrowingTransactionalWork (line 3) | @FunctionalInterface
method doWork (line 6) | void doWork(Transaction transaction) throws E;
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Transaction.java
type Transaction (line 7) | public interface Transaction {
method connection (line 12) | Connection connection();
method context (line 20) | default <T> T context() {
method prepareBatchStatement (line 31) | PreparedStatement prepareBatchStatement(String sql);
method addPostCommitHook (line 39) | void addPostCommitHook(Runnable runnable);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionContextPlaceholder.java
type TransactionContextPlaceholder (line 7) | interface TransactionContextPlaceholder {}
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionManager.java
type TransactionManager (line 15) | public interface TransactionManager {
method fromDataSource (line 28) | static ThreadLocalContextTransactionManager fromDataSource(DataSource ...
method fromConnectionDetails (line 48) | static ThreadLocalContextTransactionManager fromConnectionDetails(
method inTransaction (line 68) | default void inTransaction(Runnable runnable) {
method inTransaction (line 79) | default void inTransaction(TransactionalWork work) {
method inTransactionReturns (line 92) | default <T> T inTransactionReturns(TransactionalSupplier<T> supplier) {
method inTransactionThrows (line 106) | @SuppressWarnings("SameReturnValue")
method inTransactionReturnsThrows (line 123) | <T, E extends Exception> T inTransactionReturnsThrows(ThrowingTransact...
method extractTransaction (line 136) | TransactionalInvocation extractTransaction(Method method, Object[] args);
method injectTransaction (line 146) | Invocation injectTransaction(Invocation invocation, Transaction transa...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java
type TransactionOutbox (line 19) | public interface TransactionOutbox {
method builder (line 24) | static TransactionOutboxBuilder builder() {
method initialize (line 33) | void initialize();
method schedule (line 61) | <T> T schedule(Class<T> clazz);
method with (line 69) | ParameterizedScheduleBuilder with();
method flush (line 78) | default boolean flush() {
method flush (line 105) | boolean flush(Executor executor);
method flushTopics (line 115) | default boolean flushTopics(Executor executor, String... topicNames) {
method flushTopics (line 127) | boolean flushTopics(Executor executor, List<String> topicNames);
method unblock (line 137) | boolean unblock(String entryId);
method unblock (line 150) | @SuppressWarnings("unused")
method processNow (line 159) | @SuppressWarnings("WeakerAccess")
class TransactionOutboxBuilder (line 163) | @ToString
method TransactionOutboxBuilder (line 180) | protected TransactionOutboxBuilder() {}
method transactionManager (line 188) | public TransactionOutboxBuilder transactionManager(TransactionManage...
method instantiator (line 199) | public TransactionOutboxBuilder instantiator(Instantiator instantiat...
method submitter (line 211) | public TransactionOutboxBuilder submitter(Submitter submitter) {
method attemptFrequency (line 222) | public TransactionOutboxBuilder attemptFrequency(Duration attemptFre...
method blockAfterAttempts (line 232) | public TransactionOutboxBuilder blockAfterAttempts(int blockAfterAtt...
method flushBatchSize (line 243) | public TransactionOutboxBuilder flushBatchSize(int flushBatchSize) {
method clockProvider (line 253) | public TransactionOutboxBuilder clockProvider(Supplier<Clock> clockP...
method listener (line 263) | public TransactionOutboxBuilder listener(TransactionOutboxListener l...
method persistor (line 276) | public TransactionOutboxBuilder persistor(Persistor persistor) {
method logLevelTemporaryFailure (line 287) | public TransactionOutboxBuilder logLevelTemporaryFailure(Level logLe...
method serializeMdc (line 298) | public TransactionOutboxBuilder serializeMdc(Boolean serializeMdc) {
method retentionThreshold (line 309) | public TransactionOutboxBuilder retentionThreshold(Duration retentio...
method initializeImmediately (line 320) | public TransactionOutboxBuilder initializeImmediately(boolean initia...
method build (line 330) | public abstract TransactionOutbox build();
type ParameterizedScheduleBuilder (line 333) | interface ParameterizedScheduleBuilder {
method uniqueRequestId (line 349) | ParameterizedScheduleBuilder uniqueRequestId(String uniqueRequestId);
method ordered (line 393) | ParameterizedScheduleBuilder ordered(String topic);
method delayForAtLeast (line 419) | ParameterizedScheduleBuilder delayForAtLeast(Duration duration);
method schedule (line 436) | <T> T schedule(Class<T> clazz);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxEntry.java
class TransactionOutboxEntry (line 15) | @SuperBuilder(toBuilder = true)
method description (line 122) | public String description() {
method validate (line 148) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java
class TransactionOutboxImpl (line 31) | @Slf4j
method validate (line 59) | @Override
method builder (line 74) | static TransactionOutboxBuilder builder() {
method initialize (line 78) | @Override
method schedule (line 90) | @Override
method with (line 95) | @Override
method doFlush (line 100) | private boolean doFlush(Function<Transaction, Collection<TransactionOu...
method flush (line 123) | @Override
method flushTopics (line 159) | @Override
method expireIdempotencyProtection (line 173) | private void expireIdempotencyProtection(Instant now) {
method unblock (line 200) | @Override
method unblock (line 218) | @Override
method schedule (line 242) | private <T> T schedule(
method submitNow (line 297) | private void submitNow(TransactionOutboxEntry entry) {
method processNow (line 301) | @Override
method processNowInternal (line 318) | private void processNowInternal(TransactionOutboxEntry entry) {
method invoke (line 363) | private void invoke(TransactionOutboxEntry entry, Transaction transact...
method newEntry (line 372) | private TransactionOutboxEntry newEntry(
method pushBack (line 402) | private void pushBack(Transaction transaction, TransactionOutboxEntry ...
method after (line 416) | private Instant after(Duration duration) {
method updateAttemptCount (line 420) | private void updateAttemptCount(TransactionOutboxEntry entry, Throwabl...
class TransactionOutboxBuilderImpl (line 452) | @ToString
method TransactionOutboxBuilderImpl (line 455) | TransactionOutboxBuilderImpl() {
method build (line 459) | public TransactionOutboxImpl build() {
class ParameterizedScheduleBuilderImpl (line 484) | @Accessors(fluent = true, chain = true)
method schedule (line 492) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxListener.java
type TransactionOutboxListener (line 8) | public interface TransactionOutboxListener {
method scheduled (line 23) | default void scheduled(TransactionOutboxEntry entry) {
method wrapInvocation (line 43) | default void wrapInvocation(Invocator invocator)
method wrapInvocationAndInit (line 60) | default void wrapInvocationAndInit(Invocator invocator) {
type Invocator (line 68) | interface Invocator {
method run (line 69) | void run() throws IllegalAccessException, IllegalArgumentException, ...
method runUnchecked (line 71) | default void runUnchecked() {
method getInvocation (line 83) | Invocation getInvocation();
method success (line 100) | default void success(TransactionOutboxEntry entry) {
method failure (line 114) | default void failure(TransactionOutboxEntry entry, Throwable cause) {
method blocked (line 126) | default void blocked(TransactionOutboxEntry entry, Throwable cause) {
method extractSession (line 137) | default Map<String, String> extractSession() {
method andThen (line 147) | default TransactionOutboxListener andThen(TransactionOutboxListener ot...
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalInvocation.java
class TransactionalInvocation (line 8) | @Value
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalSupplier.java
type TransactionalSupplier (line 3) | @FunctionalInterface
method fromWork (line 6) | static <U> TransactionalSupplier<U> fromWork(TransactionalWork work) {
method doWork (line 13) | T doWork(Transaction transaction);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalWork.java
type TransactionalWork (line 3) | @FunctionalInterface
method doWork (line 6) | void doWork(Transaction transaction);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/UncheckedException.java
class UncheckedException (line 4) | @SuppressWarnings("WeakerAccess")
method UncheckedException (line 7) | public UncheckedException(Throwable cause) {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Validatable.java
type Validatable (line 3) | interface Validatable {
method validate (line 4) | void validate(Validator validator);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Validator.java
class Validator (line 6) | class Validator {
method Validator (line 11) | Validator(Supplier<Clock> clockProvider) {
method Validator (line 16) | private Validator(String className, Validator validator) {
method validate (line 21) | public void validate(Validatable validatable) {
method valid (line 25) | public void valid(String propertyName, Object object) {
method notNull (line 34) | public void notNull(String propertyName, Object object) {
method isTrue (line 40) | public void isTrue(String propertyName, boolean condition, String mess...
method nullOrNotBlank (line 46) | public void nullOrNotBlank(String propertyName, String object) {
method notBlank (line 52) | public void notBlank(String propertyName, String object) {
method positiveOrZero (line 59) | public void positiveOrZero(String propertyName, int object) {
method min (line 63) | public void min(String propertyName, int object, int minimumValue) {
method error (line 69) | private void error(String propertyName, String message) {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractFullyQualifiedNameInstantiator.java
class AbstractFullyQualifiedNameInstantiator (line 15) | @Slf4j
method getName (line 20) | @Override
method getInstance (line 25) | @Override
method createInstance (line 31) | protected abstract Object createInstance(Class<?> clazz);
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractThreadLocalTransactionManager.java
class AbstractThreadLocalTransactionManager (line 12) | @Slf4j
method inTransaction (line 20) | @Override
method inTransaction (line 25) | @Override
method inTransactionReturns (line 30) | @Override
method inTransactionThrows (line 35) | @Override
method requireTransactionReturns (line 41) | @Override
method pushTransaction (line 47) | public final TX pushTransaction(TX transaction) {
method popTransaction (line 52) | public final TX popTransaction() {
method peekTransaction (line 60) | public Optional<TX> peekTransaction() {
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java
class ProxyFactory (line 20) | @Slf4j
method hasDefaultConstructor (line 26) | private static boolean hasDefaultConstructor(Class<?> clazz) {
method setupByteBuddyCache (line 35) | private TypeCache<Class<?>> setupByteBuddyCache() {
method setupObjenesis (line 45) | private ObjenesisStd setupObjenesis() {
method createProxy (line 55) | @SuppressWarnings({"unchecked", "cast"})
method constructProxy (line 70) | private <T> T constructProxy(
method buildByteBuddyProxyClass (line 92) | @SuppressWarnings({"unchecked", "cast"})
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/SimpleTransaction.java
class SimpleTransaction (line 18) | @Slf4j
method connection (line 27) | @Override
method addPostCommitHook (line 32) | @Override
method prepareBatchStatement (line 37) | @Override
method flushBatches (line 43) | public void flushBatches() {
method processHooks (line 52) | public void processHooks() {
method commit (line 59) | public void commit() {
method rollback (line 63) | public void rollback() throws SQLException {
method context (line 67) | @SuppressWarnings("unchecked")
method close (line 73) | @Override
FILE: transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/Utils.java
class Utils (line 15) | @Slf4j
method safelyRun (line 18) | @SuppressWarnings({"SameParameterValue", "WeakerAccess", "UnusedReturn...
method safelyClose (line 29) | @SuppressWarnings("unused")
method safelyClose (line 34) | public static void safelyClose(Iterable<? extends AutoCloseable> close...
method uncheck (line 42) | public static void uncheck(ThrowingRunnable runnable) {
method uncheckedly (line 50) | public static <T> T uncheckedly(Callable<T> runnable) {
method uncheckAndThrow (line 58) | public static <T> T uncheckAndThrow(Throwable e) {
method createLoggingProxy (line 68) | public static <T> T createLoggingProxy(ProxyFactory proxyFactory, Clas...
method firstNonNull (line 84) | public static <T> T firstNonNull(T one, Supplier<T> two) {
method logAtLevel (line 89) | public static void logAtLevel(Logger logger, Level level, String messa...
method stringify (line 112) | public static String stringify(Object o) {
FILE: transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java
class AbstractTestDefaultInvocationSerializer (line 17) | @SuppressWarnings("RedundantCast")
method AbstractTestDefaultInvocationSerializer (line 26) | protected AbstractTestDefaultInvocationSerializer(Integer version) {
method testNoArgs (line 34) | @Test
method testArrays (line 39) | @Test
method testPrimitives (line 61) | @Test
method testBoxedPrimitives (line 77) | @Test
method testJavaDateEnums (line 104) | @Test
method testJavaDateEnumsNulls (line 111) | @Test
method testJavaUtilDate (line 118) | @Test
method testJavaTimeClasses (line 125) | @Test
method testJavaTimeClassesNulls (line 152) | @Test
method testCustomEnum (line 169) | @Test
method testCustomEnumNulls (line 176) | @Test
method testCustomComplexClass (line 183) | @Test
method testMDCAndSession (line 192) | @Test
method testSession (line 206) | @Test
method testUUID (line 215) | @Test
method testUUIDNull (line 222) | @Test
method testDeserializationException (line 229) | @Test
method check (line 235) | void check(Invocation invocation) {
method serdeser (line 241) | Invocation serdeser(Invocation invocation) {
type ExampleCustomEnum (line 252) | enum ExampleCustomEnum {
class ExampleCustomClass (line 257) | @Getter
method ExampleCustomClass (line 263) | ExampleCustomClass(String arg1, String arg2) {
method toString (line 268) | @Override
method equals (line 273) | @Override
method hashCode (line 281) | @Override
FILE: transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java
class TestDefaultInvocationSerializer (line 6) | @Slf4j
class Version1 (line 9) | @Nested
method Version1 (line 11) | public Version1() {
method testSession (line 15) | @Override
method testMDCAndSession (line 20) | @Override
class Version2 (line 26) | @Nested
method Version2 (line 28) | public Version2() {
class NullVersion (line 33) | @Nested
method NullVersion (line 35) | public NullVersion() {
FILE: transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultMigrationManager.java
class TestDefaultMigrationManager (line 14) | @Slf4j
method beforeAll (line 19) | @BeforeAll
method afterAll (line 30) | @AfterAll
method parallelMigrations (line 35) | @Test
FILE: transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultPersistorConfiguration.java
class TestDefaultPersistorConfiguration (line 11) | class TestDefaultPersistorConfiguration {
method whenMigrateIsFalseDoNotMigrate (line 12) | @Test
method writeSchema (line 37) | @Test
method simpleTxnManager (line 61) | private TransactionManager simpleTxnManager() {
method runSql (line 69) | private void runSql(
FILE: transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestProxyGeneration.java
class TestProxyGeneration (line 10) | class TestProxyGeneration {
method setUp (line 14) | @BeforeEach
method testReflection (line 20) | @Test
method testByteBuddy (line 35) | @Test
method testObjensis (line 50) | @Test
type Interface (line 64) | interface Interface {
method doThing (line 65) | void doThing();
class Child (line 68) | static class Child {
method doThing (line 69) | void doThing() {
class Parent (line 74) | static class Parent {
method Parent (line 78) | Parent(Child child) {
method doThing (line 82) | void doThing() {
FILE: transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestValidator.java
class TestValidator (line 11) | class TestValidator {
method testEntryDateFuture (line 23) | @Test
FILE: transactionoutbox-guice/src/main/java/com/gruelbox/transactionoutbox/guice/GuiceInstantiator.java
class GuiceInstantiator (line 8) | @SuperBuilder
method createInstance (line 13) | @Override
FILE: transactionoutbox-guice/src/test/java/com/gruelbox/transactionoutbox/guice/acceptance/TestGuiceBinding.java
class TestGuiceBinding (line 32) | @Slf4j
method testProviderInjection (line 44) | @Test
class MyService (line 61) | static class MyService {
method process (line 64) | void process() {
class DemoModule (line 77) | static final class DemoModule extends AbstractModule {
method DemoModule (line 81) | DemoModule(AtomicBoolean processedWithRemote) {
method manager (line 85) | @Provides
method outbox (line 91) | @Provides
method remote (line 109) | @Provides
method local (line 116) | @Provides
FILE: transactionoutbox-guice/src/test/java/com/gruelbox/transactionoutbox/guice/acceptance/TestGuiceInstantiator.java
class TestGuiceInstantiator (line 11) | class TestGuiceInstantiator {
method testInjection (line 13) | @Test
class Child (line 21) | static final class Child {}
class Parent (line 23) | static final class Parent {
method Parent (line 27) | @Inject
FILE: transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/CustomInvocationDeserializer.java
class CustomInvocationDeserializer (line 17) | @Slf4j
method CustomInvocationDeserializer (line 27) | protected CustomInvocationDeserializer(Class<?> vc) {
method CustomInvocationDeserializer (line 31) | CustomInvocationDeserializer() {
method deserializeWithType (line 35) | @Override
method deserialize (line 42) | @Override
method replaceImmutableCollections (line 74) | private JsonNode replaceImmutableCollections(JsonNode arguments, JsonP...
FILE: transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/CustomInvocationSerializer.java
class CustomInvocationSerializer (line 10) | class CustomInvocationSerializer extends StdSerializer<Invocation> {
method CustomInvocationSerializer (line 12) | public CustomInvocationSerializer() {
method CustomInvocationSerializer (line 16) | protected CustomInvocationSerializer(Class<Invocation> t) {
method serializeWithType (line 20) | @Override
method serialize (line 27) | @Override
FILE: transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/JacksonInvocationSerializer.java
class JacksonInvocationSerializer (line 23) | public final class JacksonInvocationSerializer implements InvocationSeri...
method JacksonInvocationSerializer (line 27) | @Builder
method serializeInvocation (line 36) | @Override
method deserializeInvocation (line 45) | @Override
method checkForOldSerialization (line 62) | private boolean checkForOldSerialization(BufferedReader reader) throws...
FILE: transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/TransactionOutboxEntryDeserializer.java
class TransactionOutboxEntryDeserializer (line 15) | class TransactionOutboxEntryDeserializer extends JsonDeserializer<Transa...
method deserialize (line 17) | @Override
method mapNullableString (line 43) | private String mapNullableString(JsonNode node) {
method mapNullableInstant (line 50) | private Instant mapNullableInstant(JsonNode node, DeserializationConte...
method mapNullableStringMap (line 57) | private Map<String, String> mapNullableStringMap(JsonNode node, Deseri...
FILE: transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/TransactionOutboxJacksonModule.java
class TransactionOutboxJacksonModule (line 16) | public class TransactionOutboxJacksonModule extends Module {
method getModuleName (line 18) | @Override
method version (line 23) | @Override
method setupModule (line 28) | @Override
method typeResolver (line 41) | public static TypeResolverBuilder<?> typeResolver() {
FILE: transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/MonetaryAmount.java
class MonetaryAmount (line 11) | @AllArgsConstructor
method ofGbp (line 30) | public static MonetaryAmount ofGbp(final String amount) {
method equals (line 34) | @Override
method hashCode (line 45) | @Override
method toString (line 52) | @Override
FILE: transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/SerializationStressTestInput.java
class SerializationStressTestInput (line 7) | @Getter
FILE: transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/TestJacksonInvocationSerializer.java
class TestJacksonInvocationSerializer (line 22) | @SuppressWarnings("RedundantCast")
method beforeEach (line 30) | @BeforeEach
method check (line 48) | void check(Invocation invocation) {
method serdeser (line 54) | Invocation serdeser(Invocation invocation) {
method testNoArgs (line 64) | @Test
method testArrays (line 69) | @Test
method testPrimitives (line 91) | @Test
method testBoxedPrimitives (line 100) | @Test
method testJavaDateEnums (line 117) | @Test
method testJavaUtilDate (line 124) | @Test
method testJavaTimeClasses (line 131) | @Test
method deserializes_new_representation_correctly (line 156) | @Test
method deserializes_old_representation_correctly (line 173) | @Test
method serializes_new_representation_stress_test (line 190) | @Test
method serializes_new_representation_list (line 198) | @Test
method serializes_new_representation_set (line 205) | @Test
method serializes_new_representation_map (line 212) | @Test
FILE: transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/TestTransactionOutboxEntrySerialization.java
class TestTransactionOutboxEntrySerialization (line 17) | @Slf4j
method test (line 20) | @Test
method testWithSessionAndMdc (line 55) | @Test
FILE: transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/acceptance/TestJacksonSerializer.java
class TestJacksonSerializer (line 23) | @Slf4j
method persistor (line 29) | @Override
method process (line 45) | void process(List<Object> difficultDataStructure) {
method testPolymorphicDeserialization (line 50) | @Test
FILE: transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/DefaultJooqTransactionManager.java
class DefaultJooqTransactionManager (line 15) | @Slf4j
method DefaultJooqTransactionManager (line 21) | DefaultJooqTransactionManager(DSLContext dsl) {
method inTransactionReturnsThrows (line 25) | @Override
method transactionFromContext (line 31) | @Override
method contextType (line 41) | @Override
FILE: transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/JooqTransactionListener.java
class JooqTransactionListener (line 9) | @Slf4j
method JooqTransactionListener (line 16) | protected JooqTransactionListener() {}
method setJooqTransactionManager (line 18) | void setJooqTransactionManager(ThreadLocalJooqTransactionManager jooqT...
method beginStart (line 22) | @Override
method beginEnd (line 27) | @Override
method commitStart (line 41) | @Override
method commitEnd (line 51) | @Override
method rollbackStart (line 59) | @Override
method rollbackEnd (line 65) | @Override
method getTransaction (line 70) | private SimpleTransaction getTransaction(TransactionContext ctx) {
method safePopThreadLocals (line 74) | private void safePopThreadLocals() {
FILE: transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/JooqTransactionManager.java
type JooqTransactionManager (line 28) | public interface JooqTransactionManager extends TransactionManager {
method createListener (line 36) | static JooqTransactionListener createListener() {
method create (line 52) | static ThreadLocalContextTransactionManager create(
method create (line 92) | static ParameterContextTransactionManager<Configuration> create(DSLCon...
FILE: transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/ThreadLocalJooqTransactionManager.java
class ThreadLocalJooqTransactionManager (line 15) | @Slf4j
method ThreadLocalJooqTransactionManager (line 22) | ThreadLocalJooqTransactionManager(DSLContext parentDsl) {
method inTransactionReturnsThrows (line 26) | @Override
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/AbstractJooqAcceptanceTest.java
class AbstractJooqAcceptanceTest (line 8) | abstract class AbstractJooqAcceptanceTest extends AbstractAcceptanceTest {
method txManager (line 12) | @Override
method testNestedDirectInvocation (line 17) | @Test
method testNestedViaListener (line 20) | @Test
method testNestedWithInnerFailure (line 23) | @Test
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/AbstractJooqAcceptanceThreadLocalTest.java
class AbstractJooqAcceptanceThreadLocalTest (line 24) | @Slf4j
method txManager (line 28) | @Override
method jooqDialect (line 33) | protected SQLDialect jooqDialect() {
method beforeEach (line 37) | @BeforeEach
method testNestedDirectInvocation (line 52) | @Test
method testNestedViaListener (line 109) | @Test
method testNestedWithInnerFailure (line 167) | @Test
class Worker (line 226) | @SuppressWarnings("EmptyMethod")
method Worker (line 231) | Worker(ThreadLocalContextTransactionManager transactionManager) {
method process (line 235) | @SuppressWarnings("SameParameterValue")
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/JooqTestUtils.java
class JooqTestUtils (line 16) | @Slf4j
method createTestTable (line 22) | static void createTestTable(DSLContext dsl) {
method writeRecord (line 28) | static void writeRecord(Configuration configuration, int value) {
method writeRecord (line 33) | static void writeRecord(Transaction transaction, int value) {
method writeRecord (line 38) | static void writeRecord(ThreadLocalContextTransactionManager transacti...
method assertRecordExists (line 42) | static void assertRecordExists(DSLContext dsl, int value) {
method assertRecordNotExists (line 47) | static void assertRecordNotExists(
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalH2.java
class TestJooqThreadLocalH2 (line 5) | @Slf4j
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMSSqlServer2019.java
class TestJooqThreadLocalMSSqlServer2019 (line 12) | @Slf4j
method connectionDetails (line 24) | @Override
method jooqDialect (line 35) | @Override
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMySql5.java
class TestJooqThreadLocalMySql5 (line 13) | @Slf4j
method connectionDetails (line 26) | @Override
method jooqDialect (line 37) | @Override
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMySql8.java
class TestJooqThreadLocalMySql8 (line 13) | @Slf4j
method connectionDetails (line 26) | @Override
method jooqDialect (line 37) | @Override
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalPostgres16.java
class TestJooqThreadLocalPostgres16 (line 12) | @Slf4j
method connectionDetails (line 24) | @Override
method jooqDialect (line 35) | @Override
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext.java
class TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext (line 28) | @Slf4j
method beforeEach (line 35) | @BeforeEach
method afterEach (line 43) | @AfterEach
method pooledDataSource (line 49) | private HikariDataSource pooledDataSource() {
method createDsl (line 59) | private DSLContext createDsl() {
method createTransactionManager (line 65) | private TransactionManager createTransactionManager() {
method testSimplePassingTransaction (line 69) | @Test
method testSimplePassingContext (line 98) | @Test
method testNestedPassingContext (line 127) | @Test
method testNestedPassingTransaction (line 178) | @Test
method testNestedWithInnerFailure (line 236) | @Test
method testSessionVariables (line 294) | @Test
method clearOutbox (line 333) | private void clearOutbox(TransactionManager transactionManager) {
method withRunningFlusher (line 337) | private void withRunningFlusher(TransactionOutbox outbox, ThrowingRunn...
class Worker (line 358) | @SuppressWarnings("EmptyMethod")
method process (line 361) | @SuppressWarnings("SameParameterValue")
method checkSessionPresent (line 366) | void checkSessionPresent(String expected, Transaction transaction) {
method process (line 370) | void process(int i, Configuration configuration) {
FILE: transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext.java
class TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext (line 34) | @Slf4j
method beforeEach (line 45) | @BeforeEach
method afterEach (line 53) | @AfterEach
method pooledDataSource (line 59) | private HikariDataSource pooledDataSource() {
method createTransactionManager (line 69) | private ThreadLocalContextTransactionManager createTransactionManager() {
method testSimpleDirectInvocation (line 79) | @Test
method testSimpleViaListener (line 114) | @Test
method testNestedViaListener (line 149) | @Test
method testNestedWithInnerFailure (line 205) | @Test
method retryBehaviour (line 262) | @Test
method highVolumeUnreliable (line 291) | @Test
method testSessionVariables (line 344) | @Test
method clearOutbox (line 382) | private void clearOutbox(TransactionManager transactionManager) {
method withRunningFlusher (line 386) | private void withRunningFlusher(TransactionOutbox outbox, ThrowingRunn...
type InterfaceWorker (line 407) | interface InterfaceWorker {
method process (line 409) | void process(int i);
class Worker (line 412) | @SuppressWarnings("EmptyMethod")
method Worker (line 417) | Worker(ThreadLocalContextTransactionManager transactionManager) {
method process (line 421) | @SuppressWarnings("SameParameterValue")
method checkSessionPresent (line 426) | void checkSessionPresent(String expected) {
class FailingInstantiator (line 431) | private static class FailingInstantiator implements Instantiator {
method FailingInstantiator (line 435) | FailingInstantiator() {
method getName (line 439) | @Override
method getInstance (line 444) | @Override
FILE: transactionoutbox-quarkus/src/main/java/com/gruelbox/transactionoutbox/quarkus/CdiInstantiator.java
class CdiInstantiator (line 7) | @ApplicationScoped
method create (line 10) | @SuppressWarnings("unused")
method CdiInstantiator (line 15) | private CdiInstantiator() {}
method createInstance (line 17) | @Override
FILE: transactionoutbox-quarkus/src/main/java/com/gruelbox/transactionoutbox/quarkus/QuarkusTransactionManager.java
class QuarkusTransactionManager (line 23) | @ApplicationScoped
method QuarkusTransactionManager (line 32) | @Inject
method inTransaction (line 38) | @Override
method inTransaction (line 44) | @Override
method inTransactionReturnsThrows (line 50) | @Override
method requireTransactionReturns (line 57) | @Override
class CdiTransaction (line 67) | private final class CdiTransaction implements Transaction {
method connection (line 69) | public Connection connection() {
method prepareBatchStatement (line 77) | @Override
method addPostCommitHook (line 101) | @Override
type BatchCountingStatement (line 116) | private interface BatchCountingStatement extends PreparedStatement {
method getBatchCount (line 117) | int getBatchCount();
class BatchCountingStatementHandler (line 120) | private static final class BatchCountingStatementHandler implements In...
method BatchCountingStatementHandler (line 126) | private BatchCountingStatementHandler(PreparedStatement delegate) {
method countBatches (line 130) | static BatchCountingStatement countBatches(PreparedStatement delegat...
method invoke (line 138) | public Object invoke(Object proxy, Method method, Object[] args) thr...
FILE: transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/ApplicationConfig.java
class ApplicationConfig (line 11) | public class ApplicationConfig extends Application {
method getClasses (line 13) | @Override
method transactionOutbox (line 22) | @Produces
method block (line 40) | private void block(RemoteCallService testProxy) {
FILE: transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/BusinessService.java
class BusinessService (line 8) | @ApplicationScoped
method BusinessService (line 13) | @Inject
method writeSomeThingAndRemoteCall (line 19) | @Transactional
FILE: transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/BusinessServiceTest.java
class BusinessServiceTest (line 9) | @QuarkusTest
method purgeDatabase (line 17) | @BeforeEach
method writeOperationAndRemoteCallOK (line 24) | @Test
method writeOperationOkButRemoteCallErrorShouldBlockRemoteCall (line 36) | @Test
method transactionRollbackSoRemoteCallShouldNotBeMade (line 49) | @Test
FILE: transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/DaoImpl.java
class DaoImpl (line 10) | @ApplicationScoped
method writeSomethingIntoDatabase (line 14) | @SuppressWarnings("UnusedReturnValue")
method getFromDatabase (line 29) | public List<String> getFromDatabase() {
method purge (line 43) | @SuppressWarnings("UnusedReturnValue")
FILE: transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/RemoteCallService.java
class RemoteCallService (line 5) | @ApplicationScoped
method callRemote (line 11) | public void callRemote(boolean throwException) {
method isCalled (line 18) | public boolean isCalled() {
method setCalled (line 22) | public void setCalled(boolean called) {
method block (line 26) | public void block() {
method isBlocked (line 30) | public boolean isBlocked() {
method setBlocked (line 34) | public void setBlocked(boolean blocked) {
FILE: transactionoutbox-quarkus/src/test/resources/db/create.sql
type toto (line 1) | CREATE TABLE IF NOT EXISTS toto (toto VARCHAR(50) NOT NULL)
FILE: transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringInstantiator.java
class SpringInstantiator (line 14) | @Service
method SpringInstantiator (line 19) | @Autowired
method getName (line 24) | @Override
method getInstance (line 42) | @Override
FILE: transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionManager.java
class SpringTransactionManager (line 26) | @Slf4j
method SpringTransactionManager (line 34) | @Autowired
method inTransaction (line 41) | @Override
method inTransaction (line 46) | @Override
method inTransactionReturns (line 51) | @Override
method inTransactionThrows (line 57) | @Override
method inTransactionReturnsThrows (line 63) | @Override
method requireTransactionReturns (line 84) | @Override
class SpringTransaction (line 95) | private final class SpringTransaction implements Transaction {
method connection (line 97) | @Override
method prepareBatchStatement (line 102) | @Override
method addPostCommitHook (line 125) | @Override
type BatchCountingStatement (line 137) | private interface BatchCountingStatement extends PreparedStatement {
method getBatchCount (line 138) | int getBatchCount();
class BatchCountingStatementHandler (line 141) | private static final class BatchCountingStatementHandler implements In...
method BatchCountingStatementHandler (line 146) | private BatchCountingStatementHandler(PreparedStatement delegate) {
method countBatches (line 150) | static BatchCountingStatement countBatches(PreparedStatement delegat...
method invoke (line 158) | public Object invoke(Object proxy, Method method, Object[] args) thr...
FILE: transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java
class SpringTransactionOutboxConfiguration (line 9) | @Configuration
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/SpringTransactionManagerTest.java
class SpringTransactionManagerTest (line 23) | @ExtendWith(MockitoExtension.class)
method setUp (line 33) | @BeforeEach
class MyRuntimeException (line 40) | private static class MyRuntimeException extends RuntimeException {}
class MyCheckedException (line 42) | private static class MyCheckedException extends Exception {}
class MyUncheckedException (line 44) | private static class MyUncheckedException extends UncheckedException {
method MyUncheckedException (line 46) | public MyUncheckedException(Throwable cause) {
class MySpringTransactionException (line 51) | private static class MySpringTransactionException extends TransactionE...
method MySpringTransactionException (line 53) | public MySpringTransactionException(String msg, Throwable cause) {
class MySqlException (line 58) | private static class MySqlException extends SQLException {}
method shouldWorkInNewTransactionAndCommit (line 60) | @Test
method shouldRollbackOnFailure (line 75) | @Test
method shouldPreserveRuntimeException (line 97) | @Test
method shouldPreserveCheckedException (line 109) | @Test
method shouldPreserveUncheckedException (line 121) | @Test
method shouldPreserveSpringTransactionException (line 133) | @Test
method shouldPreserveSqlException (line 145) | @Test
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/EventuallyConsistentController.java
class EventuallyConsistentController (line 22) | @SuppressWarnings("unused")
method createComputer (line 34) | @SuppressWarnings("SameReturnValue")
method getComputer (line 55) | @GetMapping("/computer/{id}")
method createEmployee (line 62) | @SuppressWarnings("SameReturnValue")
method getEmployee (line 83) | @GetMapping("/employee/{id}")
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/ExternalsConfiguration.java
class ExternalsConfiguration (line 7) | @Configuration
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/MultipleDataSourcesTest.java
class MultipleDataSourcesTest (line 21) | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
method setUp (line 33) | @BeforeEach
method testCheckNormalEmployees (line 40) | @Test
method testCheckNormalComputers (line 108) | @Test
method testCheckOrderedEmployees (line 177) | @Test
method testCheckOrderedComputers (line 244) | @Test
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxBackgroundProcessor.java
class TransactionOutboxBackgroundProcessor (line 15) | @Component
method poll (line 22) | @Scheduled(fixedRateString = "${outbox.repeatEvery}")
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxProperties.java
class TransactionOutboxProperties (line 8) | @Configuration
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxSpringMultipleDatasourcesDemoApplication.java
class TransactionOutboxSpringMultipleDatasourcesDemoApplication (line 20) | @SpringBootApplication
method main (line 24) | public static void main(String[] args) {
method persistor (line 28) | @Bean
method computerTransactionOutbox (line 41) | @Bean
method employeeTransactionOutbox (line 57) | @Bean
method computerSpringTransactionManager (line 73) | @Bean
method employeeSpringTransactionManager (line 80) | @Bean
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/Computer.java
class Computer (line 10) | @Entity
type Type (line 16) | public enum Type {
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputerExternalQueueService.java
class ComputerExternalQueueService (line 10) | @Getter
method sendComputerCreatedEvent (line 17) | public void sendComputerCreatedEvent(Computer computer) {
method clear (line 24) | public void clear() {
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputerRepository.java
type ComputerRepository (line 6) | @Repository
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputersDbConfiguration.java
class ComputersDbConfiguration (line 17) | @Configuration
method computerDataSourceProperties (line 24) | @Bean
method computerDataSource (line 34) | @Bean
method computerJdbcTemplate (line 39) | @Bean
method computerEntityManager (line 44) | @Bean
method computerTransactionManager (line 58) | @Bean
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/Employee.java
class Employee (line 10) | @Entity
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeeExternalQueueService.java
class EmployeeExternalQueueService (line 10) | @Getter
method sendEmployeeCreatedEvent (line 17) | public void sendEmployeeCreatedEvent(Employee employee) {
method clear (line 24) | public void clear() {
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeeRepository.java
type EmployeeRepository (line 6) | @Repository
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeesDbConfiguration.java
class EmployeesDbConfiguration (line 18) | @Configuration
method employeeDataSourceProperties (line 25) | @Bean
method employeeDataSource (line 36) | @Bean
method employeeJdbcTemplate (line 41) | @Bean
method employeeEntityManager (line 46) | @Bean
method employeeTransactionManager (line 60) | @Bean
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/Customer.java
class Customer (line 10) | @Entity
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/CustomerRepository.java
type CustomerRepository (line 6) | @Repository
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/EventuallyConsistentController.java
class EventuallyConsistentController (line 11) | @SuppressWarnings("unused")
method createCustomer (line 18) | @SuppressWarnings("SameReturnValue")
method getCustomer (line 36) | @GetMapping("/customer/{id}")
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/EventuallyConsistentControllerTest.java
class EventuallyConsistentControllerTest (line 16) | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
method setUp (line 27) | @BeforeEach
method testCheckNormal (line 33) | @Test
method testCheckOrdered (line 102) | @Test
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/ExternalQueueService.java
class ExternalQueueService (line 10) | @Getter
method sendCustomerCreatedEvent (line 17) | void sendCustomerCreatedEvent(Customer customer) {
method clear (line 24) | public void clear() {
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/ExternalsConfiguration.java
class ExternalsConfiguration (line 8) | @Configuration
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxBackgroundProcessor.java
class TransactionOutboxBackgroundProcessor (line 14) | @Component
method poll (line 21) | @Scheduled(fixedRateString = "${outbox.repeatEvery}")
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxProperties.java
class TransactionOutboxProperties (line 8) | @Configuration
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxSpringDemoApplication.java
class TransactionOutboxSpringDemoApplication (line 17) | @SpringBootApplication
method main (line 21) | public static void main(String[] args) {
method persistor (line 25) | @Bean
method transactionOutbox (line 41) | @Bean
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/Utils.java
class Utils (line 8) | class Utils {
method safelyRun (line 12) | @SuppressWarnings({"SameParameterValue", "WeakerAccess", "UnusedReturn...
method safelyClose (line 23) | @SuppressWarnings("unused")
method safelyClose (line 28) | private static void safelyClose(Iterable<? extends AutoCloseable> clos...
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/MyRemoteService.java
class MyRemoteService (line 5) | @Service
method execute (line 8) | public void execute() {}
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/SpringTransactionManagerIT.java
class SpringTransactionManagerIT (line 12) | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
method shouldThrowAlreadyScheduledException (line 19) | @Test
FILE: transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/TestApplication.java
class TestApplication (line 15) | @SpringBootApplication
method main (line 19) | public static void main(String[] args) {
method persistor (line 23) | @Bean
method transactionOutbox (line 29) | @Bean
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/TestingMode.java
class TestingMode (line 3) | public class TestingMode {
method enable (line 5) | public static void enable() {
method disable (line 9) | public static void disable() {
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java
class AbstractAcceptanceTest (line 43) | @Slf4j
method beforeEachBase (line 53) | @BeforeEach
method afterEachBase (line 60) | @AfterEach
method testMDCPassedToTask (line 68) | @Test
method sequencing (line 98) | @Test
method simpleConnectionProviderCustomInstantiatorInterfaceClass (line 207) | @Test
method noAutomaticInitialization (line 265) | @Test
method duplicateRequests (line 292) | @Test
method dataSourceConnectionProviderReflectionInstantiatorConcreteClass (line 397) | @Test
method customTransactionManager (line 427) | @Test
method retryBehaviour (line 519) | @Test
method flushOnlyASpecifiedTopic (line 546) | @Test
method onSchedulingFailure_BubbleExceptionsUp (line 605) | @Test
method lastAttemptTime_updatesEveryTime (line 648) | @Test
method blockAndThenUnblockForRetry (line 703) | @Test
method highVolumeUnreliable (line 739) | @Test
method createTestTable (line 793) | protected String createTestTable() {
class FailingInstantiator (line 797) | private static class FailingInstantiator implements Instantiator {
method FailingInstantiator (line 801) | FailingInstantiator(AtomicInteger attempts) {
method getName (line 805) | @Override
method getInstance (line 810) | @Override
class RandomFailingInstantiator (line 826) | private static class RandomFailingInstantiator implements Instantiator {
method RandomFailingInstantiator (line 830) | RandomFailingInstantiator() {
method RandomFailingInstantiator (line 834) | RandomFailingInstantiator(InterfaceProcessor interfaceProcessor) {
method getName (line 838) | @Override
method getInstance (line 843) | @Override
method runWithParentOtelSpan (line 859) | @Test
method runWithoutParentOtelSpan (line 936) | @Test
class OtelListener (line 1000) | static class OtelListener implements TransactionOutboxListener {
method extractSession (line 1003) | @Override
method wrapInvocationAndInit (line 1020) | @Override
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractPersistorTest.java
class AbstractPersistorTest (line 21) | @Slf4j
method dialect (line 26) | protected abstract Dialect dialect();
method persistor (line 28) | protected abstract Persistor persistor();
method txManager (line 30) | protected abstract TransactionManager txManager();
method validateState (line 32) | protected void validateState() {}
method beforeEach (line 34) | @BeforeEach
method testInsertAndSelect (line 47) | @Test
method testInsertWithUniqueRequestIdFailureBubblesExceptionUp (line 57) | @Test
method testInsertDuplicate (line 68) | @Test
method testBatchLimitUnderThreshold (line 100) | @Test
method testBatchLimitMatchingThreshold (line 114) | @Test
method testBatchLimitOverThreshold (line 128) | @Test
method testBatchHorizon (line 142) | @Test
method testBlockedEntriesExcluded (line 156) | @Test
method testUnparseableEntriesExcluded (line 170) | @Test
class TransactionOutboxEntryMatcher (line 185) | static class TransactionOutboxEntryMatcher extends TypeSafeMatcher<Tra...
method TransactionOutboxEntryMatcher (line 188) | TransactionOutboxEntryMatcher(TransactionOutboxEntry entry) {
method matchesSafely (line 192) | @Override
method describeTo (line 203) | @Override
method matches (line 211) | TransactionOutboxEntryMatcher matches(TransactionOutboxEntry e) {
method testUpdate (line 215) | @Test
method testUpdateOptimisticLockFailure (line 240) | @Test
method testDelete (line 255) | @Test
method testDeleteOptimisticLockFailure (line 262) | @Test
method testLock (line 272) | @Test
method testLockOptimisticLockFailure (line 281) | @Test
method testSkipLocked (line 292) | @Test
method testLockPessimisticLockFailure (line 364) | @Test
method createEntry (line 411) | private TransactionOutboxEntry createEntry(String id, Instant nextAtte...
method createEntry (line 421) | private TransactionOutboxEntry createEntry(
method createEntry (line 436) | private TransactionOutboxEntry createEntry(
method createInvocation (line 447) | private Invocation createInvocation() {
method createUnparseableInvocation (line 455) | private Invocation createUnparseableInvocation() {
method expectTobeInterrupted (line 459) | private void expectTobeInterrupted() {
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/BaseTest.java
class BaseTest (line 19) | @Slf4j
method baseBeforeEach (line 25) | @BeforeEach
method baseAfterEach (line 37) | @AfterEach
method connectionDetails (line 45) | protected ConnectionDetails connectionDetails() {
method txManager (line 56) | protected TransactionManager txManager() {
method persistor (line 60) | protected Persistor persistor() {
method clearOutbox (line 64) | protected void clearOutbox() {
method withRunningFlusher (line 77) | protected void withRunningFlusher(TransactionOutbox outbox, ThrowingRu...
method withRunningFlusher (line 82) | protected void withRunningFlusher(
method withRunningFlusher (line 87) | protected void withRunningFlusher(
class ConnectionDetails (line 123) | @Value
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/ClassProcessor.java
class ClassProcessor (line 8) | public class ClassProcessor {
method process (line 14) | void process(String itemId) {
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/InterfaceProcessor.java
type InterfaceProcessor (line 3) | public interface InterfaceProcessor {
method process (line 4) | void process(int foo, String bar);
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/LatchListener.java
class LatchListener (line 8) | public final class LatchListener implements TransactionOutboxListener {
method LatchListener (line 14) | public LatchListener(CountDownLatch successLatch, CountDownLatch markF...
method LatchListener (line 19) | public LatchListener(CountDownLatch successLatch) {
method success (line 24) | @Override
method blocked (line 29) | @Override
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/OrderedEntryListener.java
class OrderedEntryListener (line 15) | @Slf4j
method OrderedEntryListener (line 26) | public OrderedEntryListener(CountDownLatch successLatch, CountDownLatc...
method scheduled (line 31) | @Override
method success (line 36) | @Override
method failure (line 46) | @Override
method blocked (line 51) | @Override
method getEvents (line 66) | public List<TransactionOutboxEntry> getEvents() {
method getSuccesses (line 70) | public List<TransactionOutboxEntry> getSuccesses() {
method from (line 74) | private TransactionOutboxEntry from(TransactionOutboxEntry entry) {
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/ProcessedEntryListener.java
class ProcessedEntryListener (line 14) | @Slf4j
method ProcessedEntryListener (line 23) | public ProcessedEntryListener(CountDownLatch successLatch) {
method success (line 27) | @Override
method failure (line 38) | @Override
method getSuccessfulEntries (line 51) | public List<TransactionOutboxEntry> getSuccessfulEntries() {
method getFailingEntries (line 55) | public List<TransactionOutboxEntry> getFailingEntries() {
method from (line 59) | private TransactionOutboxEntry from(TransactionOutboxEntry entry) {
FILE: transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/TestUtils.java
class TestUtils (line 6) | public class TestUtils {
method runSql (line 8) | @SuppressWarnings("SameParameterValue")
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/AbstractVirtualThreadsTest.java
class AbstractVirtualThreadsTest (line 30) | @Slf4j
method beforeEachAbstractVirtualThreadsTest (line 40) | @BeforeEach
method isTransientInitialization (line 79) | private boolean isTransientInitialization(RecordedEvent event) {
method afterEachAbstractVirtualThreadsTest (line 93) | @AfterEach
method didPin (line 104) | protected boolean didPin() {
method highVolumeVirtualThreads (line 119) | @Test
method warmupJdk (line 196) | private void warmupJdk(TransactionManager transactionManager, DefaultP...
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2.java
class TestVirtualThreadsH2 (line 13) | @Slf4j
method forceTriggerPinningViaUpcall (line 26) | @Test
method simulateThreadPin (line 34) | private void simulateThreadPin() throws NoSuchMethodException, Illegal...
method javaBlockCallback (line 86) | public static int javaBlockCallback(MemorySegment a, MemorySegment b) {
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2Jooq.java
class TestVirtualThreadsH2Jooq (line 13) | public class TestVirtualThreadsH2Jooq extends AbstractVirtualThreadsTest {
method txManager (line 17) | @Override
method beforeEach (line 22) | @BeforeEach
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql5.java
class TestVirtualThreadsMySql5 (line 12) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 24) | @Override
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql8.java
class TestVirtualThreadsMySql8 (line 12) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 24) | @Override
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsOracle21.java
class TestVirtualThreadsOracle21 (line 12) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 24) | @Override
FILE: transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsPostgres16.java
class TestVirtualThreadsPostgres16 (line 11) | @SuppressWarnings("WeakerAccess")
method connectionDetails (line 23) | @Override
Condensed preview — 198 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (675K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 127,
"preview": "version: 2\nupdates:\n- package-ecosystem: maven\n directory: \"/\"\n schedule:\n interval: daily\n open-pull-requests-lim"
},
{
"path": ".github/workflows/cd_build.yml",
"chars": 1361,
"preview": "name: Continous Delivery\n\non:\n push:\n branches: [master]\n\njobs:\n build:\n if: \"!contains(github.event.head_commit"
},
{
"path": ".github/workflows/pull_request.yml",
"chars": 1417,
"preview": "name: Pull request\n\non:\n pull_request:\n branches: [ master ]\n\njobs:\n build:\n if: \"!contains(github.event.head_co"
},
{
"path": ".github/workflows/release.yml",
"chars": 3238,
"preview": "name: Publish to Central\n\non:\n release:\n types: [created]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n "
},
{
"path": ".gitignore",
"chars": 110,
"preview": "**/.classpath\n**/.project\n**/.settings\n**/.factorypath\n/.idea\n/*.iml\n**/target/**\n*.iml\n**/.flattened-pom.xml\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3332,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 40739,
"preview": "# transaction-outbox\n\n[ 2011 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/SQLAction.java",
"chars": 205,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\n\n@FunctionalInterface"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/SimpleTransactionManager.java",
"chars": 2623,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractThreadLocalTransactionManager"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubParameterContextTransactionManager.java",
"chars": 2176,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.ProxyFactory;\nimport com.gruelbox.tra"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubPersistor.java",
"chars": 1613,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.List;\ni"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubThreadLocalTransactionManager.java",
"chars": 1393,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractThreadLocalTransactionManager"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Submitter.java",
"chars": 3648,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.Exe"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThreadLocalContextTransactionManager.java",
"chars": 3513,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.lang.reflect.Method;\n\n/**\n * A transaction manager which assumes th"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingRunnable.java",
"chars": 166,
"preview": "package com.gruelbox.transactionoutbox;\n\n/** A runnable... that throws. */\n@FunctionalInterface\npublic interface Throwin"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingTransactionalSupplier.java",
"chars": 926,
"preview": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface ThrowingTransactionalSupplier<T, E extend"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingTransactionalWork.java",
"chars": 180,
"preview": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface ThrowingTransactionalWork<E extends Excep"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Transaction.java",
"chars": 1298,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\n\n/** Access and "
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionContextPlaceholder.java",
"chars": 231,
"preview": "package com.gruelbox.transactionoutbox;\n\n/**\n * Marker for {@link Invocation} arguments holding transaction context. The"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionManager.java",
"chars": 6431,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport static c"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java",
"chars": 19243,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.Arrays;\nimp"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxEntry.java",
"chars": 4672,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static java.util.stream.Collectors.joining;\n\nimport com.gruelbox.transac"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java",
"chars": 18447,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.logAtLevel;\nimport stati"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxListener.java",
"chars": 8190,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.HashMap;\ni"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalInvocation.java",
"chars": 326,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport lombok.Value;\n\n/**\n * Describes a method invocation along with the trans"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalSupplier.java",
"chars": 310,
"preview": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface TransactionalSupplier<T> {\n\n static <U> "
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalWork.java",
"chars": 142,
"preview": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface TransactionalWork {\n\n void doWork(Transa"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/UncheckedException.java",
"chars": 306,
"preview": "package com.gruelbox.transactionoutbox;\n\n/** A wrapped {@link Exception} where unchecked exceptions are caught and propa"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Validatable.java",
"chars": 105,
"preview": "package com.gruelbox.transactionoutbox;\n\ninterface Validatable {\n void validate(Validator validator);\n}\n"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Validator.java",
"chars": 2046,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Clock;\nimport java.util.function.Supplier;\n\nclass Validator {\n"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractFullyQualifiedNameInstantiator.java",
"chars": 951,
"preview": "package com.gruelbox.transactionoutbox.spi;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\n\nimport"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractThreadLocalTransactionManager.java",
"chars": 1959,
"preview": "package com.gruelbox.transactionoutbox.spi;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.util.Deque;\nimport jav"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java",
"chars": 3954,
"preview": "package com.gruelbox.transactionoutbox.spi;\n\nimport com.gruelbox.transactionoutbox.MissingOptionalDependencyException;\ni"
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/SimpleTransaction.java",
"chars": 2098,
"preview": "package com.gruelbox.transactionoutbox.spi;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.safelyClose;\nimport "
},
{
"path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/Utils.java",
"chars": 3235,
"preview": "package com.gruelbox.transactionoutbox.spi;\n\nimport static java.util.stream.Collectors.joining;\n\nimport com.gruelbox.tra"
},
{
"path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java",
"chars": 7578,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.io.IO"
},
{
"path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java",
"chars": 683,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Nested;\n\n@Slf4j\n"
},
{
"path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultMigrationManager.java",
"chars": 2587,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.Dialect.H2;\nimport static org.juni"
},
{
"path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultPersistorConfiguration.java",
"chars": 2708,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest"
},
{
"path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestProxyGeneration.java",
"chars": 1760,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.gruelbox"
},
{
"path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestValidator.java",
"chars": 960,
"preview": "package com.gruelbox.transactionoutbox;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\n\nimport java"
},
{
"path": "transactionoutbox-core/src/test/resources/logback-test.xml",
"chars": 360,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppen"
},
{
"path": "transactionoutbox-guice/README.md",
"chars": 2999,
"preview": "# transaction-outbox-guice\n\n[ which integrates CDI's DI and Quarku"
},
{
"path": "transactionoutbox-quarkus/pom.xml",
"chars": 2463,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
},
{
"path": "transactionoutbox-quarkus/src/main/java/com/gruelbox/transactionoutbox/quarkus/CdiInstantiator.java",
"chars": 585,
"preview": "package com.gruelbox.transactionoutbox.quarkus;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameIns"
},
{
"path": "transactionoutbox-quarkus/src/main/java/com/gruelbox/transactionoutbox/quarkus/QuarkusTransactionManager.java",
"chars": 4671,
"preview": "package com.gruelbox.transactionoutbox.quarkus;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\n\nimport"
},
{
"path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/ApplicationConfig.java",
"chars": 1299,
"preview": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox"
},
{
"path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/BusinessService.java",
"chars": 750,
"preview": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimp"
},
{
"path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/BusinessServiceTest.java",
"chars": 1795,
"preview": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport io.quarkus.test.junit.QuarkusTest;\nimport jakarta.inj"
},
{
"path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/DaoImpl.java",
"chars": 1696,
"preview": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport "
},
{
"path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/RemoteCallService.java",
"chars": 698,
"preview": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport jakarta.enterprise.context.ApplicationScoped;\n\n@Appli"
},
{
"path": "transactionoutbox-quarkus/src/test/resources/application.properties",
"chars": 339,
"preview": "quarkus.application.name=transaction-outbox\nquarkus.http.port = 8082\nquarkus.datasource.db-kind = h2\nquarkus.datasource"
},
{
"path": "transactionoutbox-quarkus/src/test/resources/db/create.sql",
"chars": 61,
"preview": "CREATE TABLE IF NOT EXISTS toto (toto VARCHAR(50) NOT NULL);\n"
},
{
"path": "transactionoutbox-spring/README.md",
"chars": 3741,
"preview": "# transaction-outbox-spring\n\n[ {\n TransactionOutb"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java",
"chars": 38009,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static java"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractPersistorTest.java",
"chars": 17461,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport static java.time.temporal.ChronoUnit.MILLIS;\nimport static org.h"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/BaseTest.java",
"chars": 4154,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.zaxxer.hikari.Hikar"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/ClassProcessor.java",
"chars": 482,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/InterfaceProcessor.java",
"chars": 126,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\npublic interface InterfaceProcessor {\n void process(int foo, String ba"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/LatchListener.java",
"chars": 1026,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport co"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/OrderedEntryListener.java",
"chars": 2625,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport co"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/ProcessedEntryListener.java",
"chars": 2055,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport co"
},
{
"path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/TestUtils.java",
"chars": 589,
"preview": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport java.s"
},
{
"path": "transactionoutbox-testing/src/main/resources/logback-test.xml",
"chars": 332,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppen"
},
{
"path": "transactionoutbox-virtthreads/pom.xml",
"chars": 2959,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/AbstractVirtualThreadsTest.java",
"chars": 8391,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static java.util.stream.Collectors.joining;\nimport static or"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2.java",
"chars": 3624,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static org.junit.Assert.assertTrue;\n\nimport java.lang.foreig"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2Jooq.java",
"chars": 1367,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport com.gruelbox.transactionoutbox.ThreadLocalContextTransaction"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql5.java",
"chars": 1121,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.MY_SQL_5;\n\nimp"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql8.java",
"chars": 1121,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.MY_SQL_8;\n\nimp"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsOracle21.java",
"chars": 1154,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.ORACLE;\n\nimpor"
},
{
"path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsPostgres16.java",
"chars": 1107,
"preview": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.POSTGRESQL_9;\n"
},
{
"path": "transactionoutbox-virtthreads/src/test/resources/logback-test.xml",
"chars": 332,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppen"
},
{
"path": "~/settings.xml",
"chars": 403,
"preview": "<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:s"
},
{
"path": "~/toolchains.xml",
"chars": 542,
"preview": "<?xml version=\"1.0\"?>\n<toolchains xmlns=\"http://maven.apache.org/TOOLCHAINS/1.1.0\"\n xmlns:xsi=\"http://www.w3.org/2001/X"
}
]
About this extraction
This page contains the full source code of the gruelbox/transaction-outbox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 198 files (614.5 KB), approximately 145.0k tokens, and a symbol index with 982 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.