Full Code of gruelbox/transaction-outbox for AI

master 6e1814a539d1 cached
198 files
614.5 KB
145.0k tokens
982 symbols
1 requests
Download .txt
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

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-core/badge.svg)](#stable-releases)
[![Javadocs](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-core.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core)
[![GitHub Release Date](https://img.shields.io/github/release-date/gruelbox/transaction-outbox)](https://github.com/gruelbox/transaction-outbox/releases/latest)
[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot&sort=semver)](#development-snapshots)
[![GitHub last commit](https://img.shields.io/github/last-commit/gruelbox/transaction-outbox)](https://github.com/gruelbox/transaction-outbox/commits/master)
[![CD](https://github.com/gruelbox/transaction-outbox/workflows/Continous%20Delivery/badge.svg)](https://github.com/gruelbox/transaction-outbox/actions)
[![CodeFactor](https://www.codefactor.io/repository/github/gruelbox/transaction-outbox/badge)](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 < ? "
Download .txt
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
Download .txt
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[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-"
  },
  {
    "path": "pom.xml",
    "chars": 20221,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--suppress MavenModelInspection, MavenModelInspection, MavenModelInspection -->"
  },
  {
    "path": "settings.xml",
    "chars": 975,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\" xmlns:xsi=\"http://www.w3"
  },
  {
    "path": "transactionoutbox-acceptance/pom.xml",
    "chars": 2615,
    "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-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestComplexConfigurationExample.java",
    "chars": 6522,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.sql.Connection;"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestH2.java",
    "chars": 6604,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport s"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2019.java",
    "chars": 1196,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2022.java",
    "chars": 1196,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql5.java",
    "chars": 1163,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql8.java",
    "chars": 1163,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java",
    "chars": 1271,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java",
    "chars": 1271,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres11.java",
    "chars": 1145,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres12.java",
    "chars": 1145,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres13.java",
    "chars": 1145,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres14.java",
    "chars": 1145,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres15.java",
    "chars": 1145,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres16.java",
    "chars": 1145,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.t"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestRequestSerialization.java",
    "chars": 3079,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport c"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestStubbing.java",
    "chars": 4886,
    "preview": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static o"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorH2.java",
    "chars": 1121,
    "preview": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nim"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMSSqlServer2019.java",
    "chars": 1568,
    "preview": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nim"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql5.java",
    "chars": 1619,
    "preview": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nim"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql8.java",
    "chars": 1530,
    "preview": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nim"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorOracle18.java",
    "chars": 1465,
    "preview": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nim"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorPostgres16.java",
    "chars": 1516,
    "preview": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nim"
  },
  {
    "path": "transactionoutbox-core/pom.xml",
    "chars": 2312,
    "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-core/src/main/java/com/gruelbox/transactionoutbox/AlreadyScheduledException.java",
    "chars": 434,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Duration;\n\n/**\n * Thrown when we attempt to schedule an invoca"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ConnectionProvider.java",
    "chars": 656,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\n\n/**\n * Source for JDBC connections to be provided "
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DataSourceConnectionProvider.java",
    "chars": 729,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.sql.Connection;\nim"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultDialect.java",
    "chars": 7737,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.State"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultInvocationSerializer.java",
    "chars": 24604,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport com.google.gson.*;\nimport com.google.gson.stream.JsonReader;\nimport com."
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultMigrationManager.java",
    "chars": 5520,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\n\nimport java.io"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultPersistor.java",
    "chars": 16979,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.io.*;\nimpor"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Dialect.java",
    "chars": 11449,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.State"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DriverConnectionProvider.java",
    "chars": 1815,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\n\nimport jav"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ExecutorSubmitter.java",
    "chars": 3662,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.util.concurrent.Ar"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/FailedDeserializingInvocation.java",
    "chars": 874,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.io.IOException;\nimport java.lang.reflect.InvocationTargetException;"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/FunctionInstantiator.java",
    "chars": 435,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameInstantiato"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Instantiator.java",
    "chars": 2615,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.util.function.Function;\n\n/**\n * Provides callbacks for the creation"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Invocation.java",
    "chars": 5551,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport com.google.gson.annotations.SerializedName;\nimport java.lang.reflect.Inv"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/InvocationSerializer.java",
    "chars": 1791,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.Writer;\n\n/**\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Migration.java",
    "chars": 312,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport lombok.Value;\n\n/** A database migration script entry. See {@link Dialect"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/MissingOptionalDependencyException.java",
    "chars": 428,
    "preview": "package com.gruelbox.transactionoutbox;\n\npublic class MissingOptionalDependencyException extends RuntimeException {\n\n  p"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/NoTransactionActiveException.java",
    "chars": 385,
    "preview": "package com.gruelbox.transactionoutbox;\n\n/** Thrown if an active transaction is required by a method and no transaction "
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/OptimisticLockException.java",
    "chars": 191,
    "preview": "package com.gruelbox.transactionoutbox;\n\n/** Thrown when we attempt to update a record which has been modified by anothe"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ParameterContextTransactionManager.java",
    "chars": 5244,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\n\n/**\n * A transaction"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Persistor.java",
    "chars": 6537,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.List;\n\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ReflectionInstantiator.java",
    "chars": 986,
    "preview": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameInstantiato"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/RuntimeTypeAdapterFactory.java",
    "chars": 9185,
    "preview": "/*\n * Copyright (C) 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[![Guice on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/tra"
  },
  {
    "path": "transactionoutbox-guice/pom.xml",
    "chars": 2399,
    "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-guice/src/main/java/com/gruelbox/transactionoutbox/guice/GuiceInstantiator.java",
    "chars": 523,
    "preview": "package com.gruelbox.transactionoutbox.guice;\n\nimport com.google.inject.Injector;\nimport com.gruelbox.transactionoutbox."
  },
  {
    "path": "transactionoutbox-guice/src/test/java/com/gruelbox/transactionoutbox/guice/acceptance/TestGuiceBinding.java",
    "chars": 3718,
    "preview": "package com.gruelbox.transactionoutbox.guice.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nim"
  },
  {
    "path": "transactionoutbox-guice/src/test/java/com/gruelbox/transactionoutbox/guice/acceptance/TestGuiceInstantiator.java",
    "chars": 852,
    "preview": "package com.gruelbox.transactionoutbox.guice.acceptance;\n\nimport com.google.inject.Guice;\nimport com.google.inject.Injec"
  },
  {
    "path": "transactionoutbox-guice/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-jackson/README.md",
    "chars": 2955,
    "preview": "# transaction-outbox-jackson\n\n[![Jackson on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox"
  },
  {
    "path": "transactionoutbox-jackson/pom.xml",
    "chars": 3006,
    "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-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/CustomInvocationDeserializer.java",
    "chars": 3290,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jack"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/CustomInvocationSerializer.java",
    "chars": 1468,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.j"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/JacksonInvocationSerializer.java",
    "chars": 2790,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.gruelbox"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/TransactionOutboxEntryDeserializer.java",
    "chars": 2557,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jack"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/TransactionOutboxJacksonModule.java",
    "chars": 1859,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/MonetaryAmount.java",
    "chars": 1662,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport ja"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/SerializationStressTestInput.java",
    "chars": 673,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.*;\n\n@Getter\n@"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/TestJacksonInvocationSerializer.java",
    "chars": 7265,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport co"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/TestTransactionOutboxEntrySerialization.java",
    "chars": 3159,
    "preview": "package com.gruelbox.transactionoutbox.jackson;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport co"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/acceptance/TestJacksonSerializer.java",
    "chars": 3357,
    "preview": "package com.gruelbox.transactionoutbox.jackson.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;"
  },
  {
    "path": "transactionoutbox-jackson/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-jooq/README.md",
    "chars": 5412,
    "preview": "# transaction-outbox-jooq\n\n[![jOOQ on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/trans"
  },
  {
    "path": "transactionoutbox-jooq/pom.xml",
    "chars": 2910,
    "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-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/DefaultJooqTransactionManager.java",
    "chars": 1456,
    "preview": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.ParameterContextTransactionManager;\n"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/JooqTransactionListener.java",
    "chars": 2265,
    "preview": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.spi.SimpleTransaction;\nimport lombok"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/JooqTransactionManager.java",
    "chars": 3920,
    "preview": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.ParameterContextTransactionManager;\n"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/ThreadLocalJooqTransactionManager.java",
    "chars": 1407,
    "preview": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.ThrowingTransactionalSupplier;\nimpor"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/AbstractJooqAcceptanceTest.java",
    "chars": 688,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimpor"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/AbstractJooqAcceptanceThreadLocalTest.java",
    "chars": 9139,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/JooqTestUtils.java",
    "chars": 1815,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimp"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalH2.java",
    "chars": 176,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\nclass TestJooqThreadL"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMSSqlServer2019.java",
    "chars": 1299,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport java.time"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMySql5.java",
    "chars": 1311,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport java.time"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMySql8.java",
    "chars": 1305,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.time.Durat"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalPostgres16.java",
    "chars": 1233,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport java.time"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext.java",
    "chars": 13191,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static com.gruelbox.transactionoutbox.jooq.acceptance.Jo"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext.java",
    "chars": 16026,
    "preview": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;"
  },
  {
    "path": "transactionoutbox-jooq/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-quarkus/README.md",
    "chars": 2522,
    "preview": "# transactionoutbox-quarkus\n\n\nExtension for [transaction-outbox-core](../README.md) 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[![Spring on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/t"
  },
  {
    "path": "transactionoutbox-spring/pom.xml",
    "chars": 3946,
    "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-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringInstantiator.java",
    "chars": 1536,
    "preview": "package com.gruelbox.transactionoutbox.spring;\n\nimport com.gruelbox.transactionoutbox.Instantiator;\nimport java.util.Arr"
  },
  {
    "path": "transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionManager.java",
    "chars": 5972,
    "preview": "package com.gruelbox.transactionoutbox.spring;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport s"
  },
  {
    "path": "transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java",
    "chars": 400,
    "preview": "package com.gruelbox.transactionoutbox.spring;\n\nimport org.springframework.context.annotation.Configuration;\nimport org."
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/SpringTransactionManagerTest.java",
    "chars": 4851,
    "preview": "package com.gruelbox.transactionoutbox.spring;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport stat"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/EventuallyConsistentController.java",
    "chars": 3541,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport static org.springframework.http.HttpS"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/ExternalsConfiguration.java",
    "chars": 340,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport com.gruelbox.transactionoutbox.spring"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/MultipleDataSourcesTest.java",
    "chars": 9814,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport static java.util.concurrent.TimeUnit."
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxBackgroundProcessor.java",
    "chars": 1139,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport com.gruelbox.transactionoutbox.Transa"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxProperties.java",
    "chars": 488,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport java.time.Duration;\nimport lombok.Dat"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxSpringMultipleDatasourcesDemoApplication.java",
    "chars": 3431,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport com.fasterxml.jackson.databind.Object"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/Computer.java",
    "chars": 500,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport jakarta.persistence.Column;\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputerExternalQueueService.java",
    "chars": 737,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport java.util.HashSet;\nimport ja"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputerRepository.java",
    "chars": 285,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport org.springframework.data.rep"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputersDbConfiguration.java",
    "chars": 2459,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport jakarta.persistence.EntityMa"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/Employee.java",
    "chars": 447,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport jakarta.persistence.Column;\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeeExternalQueueService.java",
    "chars": 737,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport java.util.HashSet;\nimport ja"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeeRepository.java",
    "chars": 285,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport org.springframework.data.rep"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeesDbConfiguration.java",
    "chars": 2593,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport jakarta.persistence.EntityMa"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/Customer.java",
    "chars": 418,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport jakarta.persistence.Column;\nimport jakarta.persist"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/CustomerRepository.java",
    "chars": 256,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport org.springframework.data.repository.CrudRepository"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/EventuallyConsistentController.java",
    "chars": 1411,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport static org.springframework.http.HttpStatus.NOT_FOU"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/EventuallyConsistentControllerTest.java",
    "chars": 4830,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimpo"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/ExternalQueueService.java",
    "chars": 693,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport j"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/ExternalsConfiguration.java",
    "chars": 430,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.gruelbox.transactionoutbox.spring.SpringInstan"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxBackgroundProcessor.java",
    "chars": 987,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxProperties.java",
    "chars": 475,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport java.time.Duration;\nimport lombok.Data;\nimport org"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxSpringDemoApplication.java",
    "chars": 2057,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimpor"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/Utils.java",
    "chars": 964,
    "preview": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.gruelbox.transactionoutbox.ThrowingRunnable;\ni"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/MyRemoteService.java",
    "chars": 169,
    "preview": "package com.gruelbox.transactionoutbox.spring.it;\n\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/SpringTransactionManagerIT.java",
    "chars": 1642,
    "preview": "package com.gruelbox.transactionoutbox.spring.it;\n\nimport com.gruelbox.transactionoutbox.AlreadyScheduledException;\nimpo"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/TestApplication.java",
    "chars": 1396,
    "preview": "package com.gruelbox.transactionoutbox.spring.it;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.tr"
  },
  {
    "path": "transactionoutbox-spring/src/test/resources/META-INF/persistence.xml",
    "chars": 1103,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<persistence xmlns=\"http://xmlns.jcp.org/xml/ns/persistence\" xmlns:xsi=\"http://ww"
  },
  {
    "path": "transactionoutbox-spring/src/test/resources/application.properties",
    "chars": 124,
    "preview": "server.port=8081\noutbox.repeatEvery=PT1S\noutbox.attemptFrequency=PT0.5S\noutbox.blockAfterAttempts=100\noutbox.useJackson="
  },
  {
    "path": "transactionoutbox-spring/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": "transactionoutbox-testing/pom.xml",
    "chars": 2415,
    "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-testing/src/main/java/com/gruelbox/transactionoutbox/TestingMode.java",
    "chars": 306,
    "preview": "package com.gruelbox.transactionoutbox;\n\npublic class TestingMode {\n\n  public static void enable() {\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.

Copied to clipboard!