[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: maven\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/cd_build.yml",
    "content": "name: Continous Delivery\n\non:\n  push:\n    branches: [master]\n\njobs:\n  build:\n    if: \"!contains(github.event.head_commit.message, 'skip ci')\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up JDK 25\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'\n          java-package: jdk\n          java-version: 25\n          server-id: github # Value of the distributionManagement/repository/id field of the pom.xml\n          settings-path: ${{ github.workspace }} # location for the settings.xml file\n          cache: 'maven'\n      - name: Build, publish to GPR and tag\n        run: |\n          if [ \"$GITHUB_REPOSITORY\" == \"gruelbox/transaction-outbox\" ]; then\n            revision=\"7.0.$GITHUB_RUN_NUMBER\"\n            echo \"Building $revision at $GITHUB_SHA\"\n            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\n            echo \"Tagging $revision\"\n            git tag $revision\n            git push origin $revision\n          else\n            mvn -Pdelombok,only-nodb-tests -B package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn\n          fi\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n"
  },
  {
    "path": ".github/workflows/pull_request.yml",
    "content": "name: Pull request\n\non:\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    if: \"!contains(github.event.head_commit.message, 'skip ci')\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'\n          java-package: jdk\n          java-version: 25\n          cache: maven\n      - name: Build\n        run: mvn -Pdelombok -B fmt:check package test-compile -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn\n      - uses: actions/upload-artifact@v4\n        with:\n          name: build\n          path: \"**/*\"\n          compression-level: 9\n  test:\n    needs: build\n    if: \"!contains(github.event.head_commit.message, 'skip ci')\"\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        jdk: [ 17,21,25 ]\n        db: [ nodb,mysql5,mysql8,postgres,oracle18,oracle21,mssqlserver ]\n      fail-fast: false\n    steps:\n      - uses: actions/download-artifact@v4\n        with:\n          name: build\n          path: .\n      - uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'\n          java-package: jdk\n          java-version: ${{ matrix.jdk }}\n          cache: maven\n      - name: test\n        run: mvn -Pnoformat,only-${{ matrix.db }}-tests -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Publish to Central\n\non:\n  release:\n    types: [created]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up JDK 25\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu'\n          java-package: jdk\n          java-version: 25\n          settings-path: \"~\" # location for the settings.xml file\n          cache: 'maven'\n\n      - name: Build and publish\n        run: |\n          set -e\n          revision=${GITHUB_REF##*/}\n          echo \"Publishing version $revision to Central\"\n          echo ${{ secrets.GPG_SECRETKEYS }} | base64 --decode | $GPG_EXECUTABLE --import --no-tty --batch --yes\n          echo ${{ secrets.GPG_OWNERTRUST }} | base64 --decode | $GPG_EXECUTABLE --import-ownertrust --no-tty --batch --yes\n          sed -i \"s_\\(<revision>\\)[^<]*_\\1${revision}_g\" pom.xml\n          mvn -Prelease,delombok -B deploy -s $GITHUB_WORKSPACE/settings.xml -Drevision=$revision -DskipTests -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n          SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}\n          SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}\n          GPG_EXECUTABLE: gpg\n          GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }}\n          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n\n      - name: Update READMEs\n        run: |\n          set -e\n          revision=${GITHUB_REF##*/}\n          echo \"Updating READMEs\"\n          sed -i \"s_\\(<version>\\)[^<]*_\\1${revision}_g\" README.md\n          sed -i \"s_\\(<version>\\)[^<]*_\\1${revision}_g\" transactionoutbox-guice/README.md\n          sed -i \"s_\\(<version>\\)[^<]*_\\1${revision}_g\" transactionoutbox-jackson/README.md\n          sed -i \"s_\\(<version>\\)[^<]*_\\1${revision}_g\" transactionoutbox-jooq/README.md\n          sed -i \"s_\\(<version>\\)[^<]*_\\1${revision}_g\" transactionoutbox-spring/README.md\n          sed -i \"s_\\(<version>\\)[^<]*_\\1${revision}_g\" transactionoutbox-quarkus/README.md\n          sed -i \"s_\\(implementation 'com.gruelbox:transactionoutbox-core:\\)[^']*_\\1${revision}_g\" README.md\n          sed -i \"s_\\(implementation 'com.gruelbox:transactionoutbox-guice:\\)[^']*_\\1${revision}_g\" transactionoutbox-guice/README.md\n          sed -i \"s_\\(implementation 'com.gruelbox:transactionoutbox-jackson:\\)[^']*_\\1${revision}_g\" transactionoutbox-jackson/README.md\n          sed -i \"s_\\(implementation 'com.gruelbox:transactionoutbox-jooq:\\)[^']*_\\1${revision}_g\" transactionoutbox-jooq/README.md\n          sed -i \"s_\\(implementation 'com.gruelbox:transactionoutbox-spring:\\)[^']*_\\1${revision}_g\" transactionoutbox-spring/README.md\n          sed -i \"s_\\(implementation 'com.gruelbox:transactionoutbox-quarkus:\\)[^']*_\\1${revision}_g\" transactionoutbox-quarkus/README.md\n\n      - name: Create version update pull request\n        uses: gruelbox/create-pull-request@master\n        with:\n          commit-message: \"Update versions in READMEs [skip ci]\"\n          title: Update versions in READMEs\n          body: Updates the versions in the README files following the release\n          branch: update-readme-version\n          base: master\n          author: GitHub <noreply@github.com>\n\n"
  },
  {
    "path": ".gitignore",
    "content": "**/.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",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at . All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# transaction-outbox\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-core/badge.svg)](#stable-releases)\n[![Javadocs](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-core.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core)\n[![GitHub Release Date](https://img.shields.io/github/release-date/gruelbox/transaction-outbox)](https://github.com/gruelbox/transaction-outbox/releases/latest)\n[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot&sort=semver)](#development-snapshots)\n[![GitHub last commit](https://img.shields.io/github/last-commit/gruelbox/transaction-outbox)](https://github.com/gruelbox/transaction-outbox/commits/master)\n[![CD](https://github.com/gruelbox/transaction-outbox/workflows/Continous%20Delivery/badge.svg)](https://github.com/gruelbox/transaction-outbox/actions)\n[![CodeFactor](https://www.codefactor.io/repository/github/gruelbox/transaction-outbox/badge)](https://www.codefactor.io/repository/github/gruelbox/transaction-outbox)\n\nA 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**.\n\n## Contents\n\n1. [Why do I need it?](#why-do-i-need-it)\n1. [Installation](#installation)\n   1. [Requirements](#requirements)\n   1. [Stable releases](#stable-releases)\n   1. [Development snapshots](#development-snapshots)\n1. [Basic Configuration](#basic-configuration)\n   1. [No existing transaction manager or dependency injection](#no-existing-transaction-manager-or-dependency-injection)\n   1. [Spring](#spring)\n   1. [Guice](#guice)\n   1. [jOOQ](#jooq)\n1. [Set up the background worker](#set-up-the-background-worker)\n1. [Managing the \"dead letter queue\"](#managing-the-dead-letter-queue)\n1. [Advanced](#advanced)\n   1. [Topics and FIFO ordering](#topics-and-fifo-ordering)\n   1. [The nested outbox pattern](#the-nested-outbox-pattern)\n   1. [Idempotency protection](#idempotency-protection)\n   1. [Delayed/scheduled processing](#delayedscheduled-processing)\n   1. [Flexible serialization](#flexible-serialization-beta)\n   1. [Clustering](#clustering)\n   1. [OpenTelemetry](#opentelemetry)\n   1. [Encryption](#encryption)\n1. [Configuration reference](#configuration-reference)\n1. [Stubbing in tests](#stubbing-in-tests)\n\n## Why do I need it?\n\n[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:\n\n### Attempt 1\n\n```java\n@POST\n@Path(\"/sales\")\n@Transactional\npublic SaleId createWidget(Sale sale) {\n  var saleId = saleRepository.save(sale);\n  messageQueue.postMessage(StockReductionEvent.of(sale.item(), sale.amount()));\n  messageQueue.postMessage(IncomeEvent.of(sale.value()));\n  return saleId;\n}\n```\n\nThe `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).\n\nThere'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.\n\n### Attempt 2 - Use Idempotency\n\nWe could make whole method [idempotent](http://restcookbook.com/HTTP%20Methods/idempotency/) and re-write it to work a bit more like this:\n\n```java\n@PUT\n@Path(\"/sales/{id}\")\npublic void createWidget(@PathParam(\"id\") SaleId saleId, Sale sale) {\n  saleRepository.saveInNewTransaction(saleId, sale);\n  messageQueue.postMessage(StockReductionEvent.of(saleId, sale.item(), sale.amount()));\n  messageQueue.postMessage(IncomeEvent.of(saleId, sale.value()));\n}\n```\n\nThis 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).\n\nThe 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.\n\nWe 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.\n\n### Attempt 3 - Transaction Outbox\n\nIdempotency is a good thing, so let's stick with the `PUT`. Here is the same example, using Transaction Outbox:\n\n```java\n@PUT\n@Path(\"/sales/{id}\")\n@Transactional\npublic void createWidget(@PathParam(\"id\") SaleId saleId, Sale sale) {\n  saleRepository.save(saleId, sale);\n  MessageQueue proxy = transactionOutbox.schedule(MessageQueue.class);\n  proxy.postMessage(StockReductionEvent.of(saleId, sale.item(), sale.amount()));\n  proxy.postMessage(IncomeEvent.of(saleId, sale.value()));\n}\n```\n\nHere's what happens:\n\n- 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)_. \n- [`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.\n- If the transaction rolls back, so do the serialized requests.\n- Immediately after the transaction is successfully committed, another thread will attempt to make the _real_ call to `MessageQueue` asynchronously.\n- 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)).\n- Blocked requests can be easily [unblocked](#managing-the-dead-letter-queue) again once the underlying issue is resolved.\n\nOur 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.\n\nIf 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.\n\n> Note that for the above example to work, `StockReductionEvent` and `IncomeEvent` need to be included for serialization. See [Configuration reference](#configuration-reference).\n\n## Installation\n\n### Requirements\n- At least **Java 11**. Downgrading to requiring Java 8 is [under consideration](https://github.com/gruelbox/transaction-outbox/issues/29).\n- 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).\n- 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)\n- Native transactions (not JTA or similar).\n- (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\n\n### Stable releases\nThe 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. \n\n#### Maven\n\n```xml\n<dependency>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-core</artifactId>\n  <version>7.0.707</version>\n</dependency>\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-core:7.0.707'\n```\n\n### Development snapshots\n\nMaven 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).\n\n#### Maven\n\n```xml\n<repositories>\n  <repository>\n    <id>github-transaction-outbox</id>\n    <name>Gruelbox Github Repository</name>\n    <url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>\n  </repository>\n</repositories>\n```\n\nYou 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`:\n\n```xml\n<servers>\n    <server>\n        <id>github-transaction-outbox</id>\n        <username>${env.GITHUB_USERNAME}</username>\n        <password>${env.GITHUB_TOKEN}</password>\n    </server>\n</servers>\n```\n\nThe 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.\n\n#### Gradle\n\n```groovy\nrepositories {\n    maven {\n        name = \"github-transaction-outbox\"\n        url = uri(\"https://maven.pkg.github.com/gruelbox/transaction-outboxY\")\n        credentials {\n            username = $githubUserName\n            password = $githubToken\n        }\n    }\n}\n```\n\n## Basic Configuration\n\nAn 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.\n\n### No existing transaction manager or dependency injection\n\nIf you have no existing transaction management, connection pooling or dependency injection, here's a quick way to get started:\n\n```java\n// Use an in-memory H2 database\nTransactionManager transactionManager = TransactionManager.fromConnectionDetails(\n    \"org.h2.Driver\", \"jdbc:h2:mem:test;MV_STORE=TRUE\", \"test\", \"test\"));\n\n// Create the outbox\nTransactionOutbox outbox = TransactionOutbox.builder()\n  .transactionManager(transactionManager)\n  .persistor(Persistor.forDialect(Dialect.H2))\n  .build();\n\n// Start a transaction\ntransactionManager.inTransaction(tx -> {\n  // Save some stuff\n  tx.connection().createStatement().execute(\"INSERT INTO...\");\n  // Create an outbox request\n  outbox.schedule(MyClass.class).myMethod(\"Foo\", \"Bar\"));\n});\n```\n\nAlternatively, 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:\n\n```java\nTransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);\n```\n\nIn 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:\n\n```java\nTransactionOutbox outbox = TransactionOutbox.builder()\n  .instantiator(Instantiator.using(clazz -> createInstanceOf(clazz)))\n  .build();\n```\n\n### Spring\n\nSee [transaction-outbox-spring](transactionoutbox-spring/README.md), which integrates Spring's DI and/or transaction management with `TransactionOutbox`.\n\n### Guice\n\nSee [transaction-outbox-guice](transactionoutbox-guice/README.md), which integrates Guice DI `TransactionOutbox`.\n\n### jOOQ\n\nSee [transaction-outbox-jooq](transactionoutbox-jooq/README.md), which integrates jOOQ transaction management with `TransactionOutbox`.\n\n### Oracle\n\nOracle database compatibility requires to configure Oracle jdbc driver using following VM argument : -Doracle.jdbc.javaNetNio=false\n\n## Set up the background worker\n\nAt 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.\n\nHow 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:\n\n```java\nThread backgroundThread = new Thread(() -> {\n  while (!Thread.interrupted()) {\n    try {\n      // Keep flushing work until there's nothing left to flush\n      while (outbox.flush()) {}\n    } catch (Exception e) {\n      log.error(\"Error flushing transaction outbox. Pausing\", e);\n    }\n    try {\n       // When we run out of work, pause for a minute before checking again\n       Thread.sleep(60_000);\n    } catch (InterruptedException e) {\n       break;\n    }\n  }\n});\n\n// Startup\nbackgroundThread.start();\n\n// Shut down\nbackgroundThread.interrupt();\nbackgroundThread.join();\n```\n\n`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.\n\nHowever, 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.\n\n## Managing the \"dead letter queue\"\n\nWork 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.\n\n```java\nTransactionOutbox.builder()\n    ...\n    .listener(new TransactionOutboxListener() {\n        @Override\n        public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n           // Spring example\n           applicationEventPublisher.publishEvent(new TransactionOutboxBlockedEvent(entry.getId(), cause);\n        }\n    })\n    .build();\n```\n\nTo 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()`:\n\n```java\ntransactionOutboxEntry.unblock(entryId);\n```\n\nOr 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)):\n\n```java\ntransactionOutboxEntry.unblock(entryId, context);\n```\n\nA 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.\n\n## Advanced\n\n### Topics and FIFO ordering\n\nFor some applications, the order in which tasks are processed is important, such as when:\n\n - using the outbox to write to a FIFO queue, Kafka or AWS Kinesis topic; or\n - data replication, e.g. when feeding a data warehouse or distributed cache.\n\nIn these scenarios, the default behaviour is unsuitable. Tasks are usually processed in a highly parallel fashion.\nEven if the volume of tasks is low, if a task fails and is retried later, it can easily end up processing after\nsome later task even if that later task was processed hours or even days after the failing one.\n\nTo avoid problems associated with tasks being processed out-of-order, you can order the processing of your tasks\nwithin a named \"topic\":\n\n```java\noutbox.with().ordered(\"topic1\").schedule(Service.class).process(\"red\");\noutbox.with().ordered(\"topic2\").schedule(Service.class).process(\"green\");\noutbox.with().ordered(\"topic1\").schedule(Service.class).process(\"blue\");\noutbox.with().ordered(\"topic2\").schedule(Service.class).process(\"yellow\");\n```\n\nNo matter what happens:\n\n - `red` will always need to be processed (successfully) before `blue`;\n - `green` will always need to be processed (successfully) before `yellow`; but\n - `red` and `blue` can run in any sequence with respect to `green` and `yellow`.\n\nThis functionality was specifically designed to allow outboxed writing to Kafka topics. For maximum throughput\nwhen writing to Kafka, it is advised that you form your outbox topic name by combining the Kafka topic and partition,\nsince that is the boundary where ordering is required.\n\nThere are a number of things to consider before using this feature:\n\n - Tasks are not processed immediately when submitting, as normal, and are processed by \n   background flushing only. This means there will be an increased delay between the source transaction being\n   committed and the task being processed, depending on how your application calls `TransactionOutbox.flush()`.\n - If a task fails, no further requests will be processed _in that topic_ until\n   a subsequent retry allows the failing task to succeed, to preserve ordered\n   processing. This means it is possible for topics to become entirely frozen in the event\n   that a task fails repeatedly. For this reason, it is essential to use a \n   `TransactionOutboxListener` to watch for failing tasks and investigate quickly. Note\n   that other topics will be unaffected.\n - `TransactionOutboxBuilder.blockAfterAttempts` is ignored for all tasks that use this\n   option.\n - A single topic can only be processed in single-threaded fashion, but separate topics can be processed in\n   parallel. If your tasks use a small number of topics, scalability will be affected since the degree of \n   parallelism will be reduced.\n\n### The nested-outbox pattern\n\nIn 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.\n\nTo 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:\n\n- Modify the customer record: `outbox.schedule(CustomerService.class).update(newDetails)`\n- 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())`\n\nNow, 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.\n\n### Idempotency protection\n\nA 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:\n\n```java\npublic class FooEventHandler implements SQSEventHandler<ThingHappenedEvent> {\n\n  @Inject private TransactionOutbox outbox;\n\n  public void handle(ThingHappenedEvent event) {\n    outbox.schedule(FooService.class).handleEvent(event.getThingId());\n  }\n}\n```\n\nHowever, 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.\n\nAs 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.\n\nTo 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).\n\nTo use this, use the call pattern:\n\n```java\noutbox.with()\n  .uniqueRequestId(\"context-clientid\")\n  .schedule(Service.class)\n  .process(\"Foo\");\n```\n\nWhere `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).\n\n### Delayed/scheduled processing ###\n\nTo delay execution of a task, use:\n\n```java\noutbox.with()\n  .delayForAtLeast(Duration.of(5, MINUTES))\n  .schedule(Service.class)\n  .process(\"Foo\");\n```\n\nThere are some caveats around how accurate timing is. See the JavaDoc on the `delayForAtLeast` method for more information.\n\nThis is particularly useful when combined with the [nested outbox pattern](#the-nested-outbox-pattern) for creating polling/repeated or recursive tasks to throttle prcessing.\n\n### Flexible serialization (beta)\n\nMost 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. \nYou 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.\n\nFurthermore, 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`:\n```java\noutbox.schedule(Service.class).processList(List.of(1, \"2\", 3L));\n```\nHowever, 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.\n\nSee [transaction-outbox-jackson](transactionoutbox-jackson/README.md), which uses a specially-configured Jackson `ObjectMapper` to achieve this.\n\n### Clustering\n\nThe 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:\n```java\nexecutor.execute(() -> outbox.processNow(transactionOutboxEntry));\n```\nThis 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.\n\nIf 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:\n\n* An HTTP endpoint on a load-balanced DNS with service discovery (such as a container orchestrator e.g. Kubernetes or Nomad)\n* A shared queue (AWS SQS, ActiveMQ, ZeroMQ)\n* A lower-level clustering/messaging toolkit such as [JGroups](http://www.jgroups.org/).\n\nAll of these can be implemented as follows:\n\nWhen 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:\n```java\nTransactionOutbox outbox = TransactionOutbox.builder().submitter(restApiSubmitter)\n```\nIt 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):\n```java\n@Slf4j\nclass RestApiSubmitter implements Submitter {\n\n  private final FeignResource feignResource;\n  private final ExecutorService localExecutor;\n  private final Provider<TransactionOutbox> outbox;\n\n  @Inject\n  RestApiExecutor(String endpointUrl, ExecutorService localExecutor, ObjectMapper objectMapper, Provider<TransactionOutbox> outbox) {\n    this.feignResource = Feign.builder()\n      .decoder(new JacksonDecoder(objectMapper))\n      .target(GitHub.class, \"https://api.github.com\");;\n    this.localExecutor = localExecutor;\n    this.outbox = outbox;\n  }\n\n  @Override\n  public void submit(TransactionOutboxEntry entry, Consumer<TransactionOutboxEntry> leIgnore) {\n    try {\n      localExecutor.execute(() -> processRemotely(entry));\n      log.info(\"Queued {} to be sent for remote processing\", entry.description());\n    } catch (RejectedExecutionException e) {\n      log.info(\"Will queue {} for processing when local executor is available\", entry.description());\n    } catch (Exception e) {\n      log.warn(\"Failed to queue {} for execution at {}. It will be re-attempted later.\", entry.description(), url, e);\n    }\n  }\n\n  private void processRemotely(TransactionOutboxEntry entry) {\n    try {\n      feignResource.process(entry);\n      log.info(\"Submitted {} for remote processing at {}\", entry.description(), url);\n    } catch (Exception e) {\n      log.warn(\n        \"Failed to submit {} for remote processing at {}. It will be re-attempted later.\",\n        entry.description(),\n        url,\n        e\n      );\n    }\n  }\n  \n  public interface FeignResource {\n    @RequestLine(\"POST /outbox/process\")\n    void process(TransactionOutboxEntry entry);\n  }\n  \n}\n```\nThen 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:\n```java\n@POST\n@Path(\"/outbox/process\")\nvoid processOutboxEntry(String entry) {\n  TransactionOutboxEntry entry = somethingWhichCanSerializeTransactionOutboxEntries.deserialize(entry);\n  Submitter submitter = ExecutorSubmitter.builder().executor(localExecutor).logLevelWorkQueueSaturation(Level.INFO).build();\n  submitter.submit(entry, outbox.get()::processNow);\n}\n```\nThis 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:\n```java\n// Add support for TransactionOutboxEntry to your normal application ObjectMapper\nyourNormalSharedObjectMapper.registerModule(new TransactionOutboxJacksonModule());\n\n// (Optional) support deep polymorphic requests - for this we need to copy the object\n// mapper so it doesn't break the way the rest of your application works\nObjectMapper objectMapper = yourNormalSharedObjectMapper.copy();\nobjectMapper.setDefaultTyping(TransactionOutboxJacksonModule.typeResolver());\n\n// Serialize\nString message = objectMapper.writeValueAsString(entry);\n\n// Deserialize\nTransactionOutboxEntry entry = objectMapper.readValue(message, TransactionOutboxEntry.class);\n```\nArmed with the above, happy clustering!\n\n### OpenTelemetry\n\nA 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:\n```java\nstatic class OtelListener implements TransactionOutboxListener {\n\n   /** Serialises the current context into {@link Invocation#getSession()}. */\n   @Override\n   public Map<String, String> extractSession() {\n      var result = new HashMap<String, String>();\n      SpanContext spanContext = Span.current().getSpanContext();\n      if (!spanContext.isValid()) {\n         return null;\n      }\n      result.put(\"traceId\", spanContext.getTraceId());\n      result.put(\"spanId\", spanContext.getSpanId());\n      log.info(\"Extracted: {}\", result);\n      return result;\n   }\n\n   /**\n    * Deserialises {@link Invocation#getSession()} and sets it as the current context so that any\n    * new span started by the method we invoke will treat it as the parent span\n    */\n   @Override\n   public void wrapInvocationAndInit(Invocator invocator) {\n      Invocation inv = invocator.getInvocation();\n      var spanBuilder =\n              otel.getTracer(\"transaction-outbox\")\n                      .spanBuilder(String.format(\"%s.%s\", inv.getClassName(), inv.getMethodName()))\n                      .setNoParent();\n      for (var i = 0; i < inv.getArgs().length; i++) {\n         spanBuilder.setAttribute(\"arg\" + i, Utils.stringify(inv.getArgs()[i]));\n      }\n      if (inv.getSession() != null) {\n         var traceId = inv.getSession().get(\"traceId\");\n         var spanId = inv.getSession().get(\"spanId\");\n         if (traceId != null && spanId != null) {\n            spanBuilder.addLink(\n                    SpanContext.createFromRemoteParent(\n                            traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()));\n         }\n      }\n      var span = spanBuilder.startSpan();\n      try (Scope scope = span.makeCurrent()) {\n         invocator.runUnchecked();\n      } finally {\n         span.end();\n      }\n   }\n}\n```\nCheck out `AbstractAcceptanceTest.OtelListener` for an example of this in use.\n\n`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.\n\n### Encryption\nBe 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.\n\n## Configuration reference\n\nThis example shows a number of other configuration options in action:\n\n```java\nTransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);\n\nTransactionOutbox outbox = TransactionOutbox.builder()\n    // The most complex part to set up for most will be synchronizing with your existing transaction\n    // management. Pre-rolled implementations are available for jOOQ and Spring (see above for more information)\n    // and you can use those examples to synchronize with anything else by defining your own TransactionManager.\n    // Or, if you have no formal transaction management at the moment, why not start, using transaction-outbox's\n    // built-in one?\n    .transactionManager(transactionManager)\n    // Modify how requests are persisted to the database. For more complex modifications, you may wish to subclass\n    // DefaultPersistor, or create a completely new Persistor implementation.\n    .persistor(DefaultPersistor.builder()\n        // Selecting the right SQL dialect ensures that features such as SKIP LOCKED are used correctly.\n        .dialect(Dialect.POSTGRESQL_9)\n        // Override the table name (defaults to \"TXNO_OUTBOX\")\n        .tableName(\"transactionOutbox\")\n        // Shorten the time we will wait for write locks (defaults to 2)\n        .writeLockTimeoutSeconds(1)\n        // Disable automatic creation and migration of the outbox table, forcing the application to manage\n        // migrations itself\n        .migrate(false)\n        // Allow the SaleType enum and Money class to be used in arguments (see example below)\n        .serializer(DefaultInvocationSerializer.builder()\n            .serializableTypes(Set.of(SaleType.class, Money.class))\n            .build())\n        .build())\n    // GuiceInstantiator and SpringInstantiator are great if you are using Guice or Spring DI, but what if you\n    // have your own service locator? Wire it in here. Fully-custom Instantiator implementations are easy to\n    // implement.\n    .instantiator(Instantiator.using(myServiceLocator::createInstance))\n    // Change the log level used when work cannot be submitted to a saturated queue to INFO level (the default\n    // is WARN, which you should probably consider a production incident). You can also change the Executor used\n    // for submitting work to a shared thread pool used by the rest of your application. Fully-custom Submitter\n    // implementations are also easy to implement, for example to cluster work.\n    .submitter(ExecutorSubmitter.builder()\n        .executor(ForkJoinPool.commonPool())\n        .logLevelWorkQueueSaturation(Level.INFO)\n        .build())\n    // Lower the log level when a task fails temporarily from the default WARN.\n    .logLevelTemporaryFailure(Level.INFO)\n    // 10 attempts at a task before blocking it.\n    .blockAfterAttempts(10)\n    // When calling flush(), select 0.5m records at a time.\n    .flushBatchSize(500_000)\n    // Flush once every 15 minutes only\n    .attemptFrequency(Duration.ofMinutes(15))\n    // Include Slf4j's Mapped Diagnostic Context in tasks. This means that anything in the MDC when schedule()\n    // is called will be recreated in the task when it runs. Very useful for tracking things like user ids and\n    // request ids across invocations.\n    .serializeMdc(true)\n    // Sets how long we should keep records of requests with a unique request id so duplicate requests\n    // can be rejected. Defaults to 7 days.\n    .retentionThreshold(Duration.ofDays(1))\n    // We can intercept and modify numerous events. The most common use is to catch blocked tasks\n    // and raise alerts for these to be investigated. A Slack interactive message is particularly effective here\n    // since it can be wired up to call unblock() automatically.\n    .listener(new TransactionOutboxListener() {\n\n      @Override\n      public void success(TransactionOutboxEntry entry) {\n        eventPublisher.publish(new OutboxTaskProcessedEvent(entry.getId()));\n      }\n\n      @Override\n      public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n        eventPublisher.publish(new BlockedOutboxTaskEvent(entry.getId()));\n      }\n\n      @Override\n      public Map<String, String> extractSession() {\n        return Map.of();\n      }\n\n      @Override\n      public void wrapInvocationAndInit(Invocator invocator) {\n        invocator.runUnchecked();\n      }\n\n      @Override\n      public void wrapInvocation(Invocator invocator) throws Exception {\n        invocator.run();\n      }\n    })\n    .build();\n\n// Usage example, using the in-built transaction manager\nMDC.put(\"SESSIONKEY\", \"Foo\");\ntry {\n  transactionManager.inTransaction(tx -> {\n    writeSomeChanges(tx.connection());\n    outbox.schedule(getClass())\n        .performRemoteCall(SaleType.SALE, Money.of(10, Currency.getInstance(\"USD\")));\n  });\n} finally {\n  MDC.clear();\n}\n```\n\n## Stubbing in tests\n\n`TransactionOutbox` should not be directly stubbed (e.g. using Mockito); the contract is too complex to stub out.\n\nInstead, [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.\n\n```java\n// GIVEN\n\nSomeService mockService = Mockito.mock(SomeService.class);\n\n// Also see StubParameterContextTransactionManager\nTransactionManager transactionManager = new StubThreadLocalTransactionManager();\n\nTransactionOutbox outbox = TransactionOutbox.builder()\n    .instantiator(Instantiator.using(clazz -> mockService)) // Return our mock\n    .persistor(StubPersistor.builder().build()) // Doesn't save anything\n    .submitter(Submitter.withExecutor(MoreExecutors.directExecutor())) // Execute all work in-line\n    .clockProvider(() ->\n        Clock.fixed(LocalDateTime.of(2020, 3, 1, 12, 0)\n            .toInstant(ZoneOffset.UTC), ZoneOffset.UTC)) // Fix the clock (not necessary here)\n    .transactionManager(transactionManager)\n    .build();\n\n// WHEN\ntransactionManager.inTransaction(tx ->\n   outbox.schedule(SomeService.class).doAThing(1));\n\n// THEN\nMockito.verify(mockService).doAThing(1);\n```\n\nDepending 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.\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--suppress MavenModelInspection, MavenModelInspection, MavenModelInspection -->\n<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\">\n  <modelVersion>4.0.0</modelVersion>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-parent</artifactId>\n  <packaging>pom</packaging>\n  <version>${revision}</version>\n  <name>Transaction Outbox Parent</name>\n  <description>A safe implementation of the transactional outbox pattern for Java.</description>\n  <inceptionYear>2020</inceptionYear>\n  <url>https://github.com/gruelbox/transaction-outbox</url>\n  <organization>\n    <name>Graham Crockford</name>\n    <url>https://gruelbox.com</url>\n  </organization>\n  <properties>\n    <!-- Core -->\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <file.encoding>UTF-8</file.encoding>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n    <!-- For delomboking -->\n    <src.dir>src/main/java</src.dir>\n    <test.dir>src/test/java</test.dir>\n    <skip.format>false</skip.format>\n    <!-- Dependency versions -->\n    <logback.version>1.5.32</logback.version>\n    <lombok.version>1.18.42</lombok.version>\n    <revision>7.0.707</revision>\n    <junit.jupiter.version>6.0.3</junit.jupiter.version>\n    <testcontainers.version>1.21.4</testcontainers.version>\n    <maven.source.plugin.version>3.4.0</maven.source.plugin.version>\n    <maven.gpg.plugin.version>3.2.8</maven.gpg.plugin.version>\n    <surefire.version>3.5.4</surefire.version>\n  </properties>\n  <modules>\n    <module>transactionoutbox-core</module>\n    <module>transactionoutbox-jackson</module>\n    <module>transactionoutbox-guice</module>\n    <module>transactionoutbox-testing</module>\n    <module>transactionoutbox-acceptance</module>\n    <module>transactionoutbox-quarkus</module>\n    <module>transactionoutbox-spring</module>\n  </modules>\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>org.slf4j</groupId>\n        <artifactId>slf4j-api</artifactId>\n        <version>2.0.17</version>\n      </dependency>\n      <dependency>\n        <groupId>net.bytebuddy</groupId>\n        <artifactId>byte-buddy</artifactId>\n        <version>1.18.5</version>\n        <optional>true</optional>\n      </dependency>\n      <dependency>\n        <groupId>org.objenesis</groupId>\n        <artifactId>objenesis</artifactId>\n        <version>3.5</version>\n        <optional>true</optional>\n      </dependency>\n      <dependency>\n        <groupId>com.google.code.gson</groupId>\n        <artifactId>gson</artifactId>\n        <version>2.13.2</version>\n      </dependency>\n      <dependency>\n        <groupId>org.projectlombok</groupId>\n        <artifactId>lombok</artifactId>\n        <version>${lombok.version}</version>\n        <scope>provided</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.junit.jupiter</groupId>\n        <artifactId>junit-jupiter-engine</artifactId>\n        <version>${junit.jupiter.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.junit.jupiter</groupId>\n        <artifactId>junit-jupiter-api</artifactId>\n        <version>${junit.jupiter.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.junit.jupiter</groupId>\n        <artifactId>junit-jupiter-params</artifactId>\n        <version>${junit.jupiter.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.mockito</groupId>\n        <artifactId>mockito-all</artifactId>\n        <version>1.10.19</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.hamcrest</groupId>\n        <artifactId>hamcrest-core</artifactId>\n        <version>3.0</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>ch.qos.logback</groupId>\n        <artifactId>logback-classic</artifactId>\n        <version>${logback.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>ch.qos.logback</groupId>\n        <artifactId>logback-core</artifactId>\n        <version>${logback.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>com.h2database</groupId>\n        <artifactId>h2</artifactId>\n        <version>2.4.240</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>com.zaxxer</groupId>\n        <artifactId>HikariCP</artifactId>\n        <version>7.0.2</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.testcontainers</groupId>\n        <artifactId>testcontainers</artifactId>\n        <version>${testcontainers.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.testcontainers</groupId>\n        <artifactId>junit-jupiter</artifactId>\n        <version>${testcontainers.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.testcontainers</groupId>\n        <artifactId>postgresql</artifactId>\n        <version>${testcontainers.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.testcontainers</groupId>\n        <artifactId>mysql</artifactId>\n        <version>${testcontainers.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.postgresql</groupId>\n        <artifactId>postgresql</artifactId>\n        <version>42.7.10</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>com.mysql</groupId>\n        <artifactId>mysql-connector-j</artifactId>\n        <version>9.6.0</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.testcontainers</groupId>\n        <artifactId>oracle-xe</artifactId>\n        <version>${testcontainers.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>com.oracle.database.jdbc</groupId>\n        <artifactId>ojdbc11</artifactId>\n        <version>23.26.1.0.0</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>org.testcontainers</groupId>\n        <artifactId>mssqlserver</artifactId>\n        <version>${testcontainers.version}</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>com.microsoft.sqlserver</groupId>\n        <artifactId>mssql-jdbc</artifactId>\n        <version>13.3.1.jre11-preview</version>\n        <scope>test</scope>\n      </dependency>\n      <dependency>\n        <groupId>io.opentelemetry</groupId>\n        <artifactId>opentelemetry-sdk-testing</artifactId>\n        <version>1.58.0</version>\n        <scope>test</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n  <build>\n    <plugins>\n      <plugin>\n        <artifactId>maven-compiler-plugin</artifactId>\n        <version>3.15.0</version>\n        <configuration>\n          <source>${maven.compiler.source}</source>\n          <target>${maven.compiler.target}</target>\n          <fork>true</fork>\n          <annotationProcessorPaths>\n            <path>\n              <groupId>org.projectlombok</groupId>\n              <artifactId>lombok</artifactId>\n              <version>${lombok.version}</version>\n            </path>\n          </annotationProcessorPaths>\n          <compilerArgs>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>\n            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>\n          </compilerArgs>\n        </configuration>\n      </plugin>\n      <plugin>\n        <groupId>org.apache.maven.plugins</groupId>\n        <artifactId>maven-jar-plugin</artifactId>\n        <version>3.5.0</version>\n      </plugin>\n      <plugin>\n        <artifactId>maven-surefire-plugin</artifactId>\n        <version>${surefire.version}</version>\n        <configuration>\n          <argLine>-Doracle.jdbc.javaNetNio=false -XX:+EnableDynamicAgentLoading</argLine>\n        </configuration>\n      </plugin>\n      <plugin>\n        <groupId>org.codehaus.mojo</groupId>\n        <artifactId>flatten-maven-plugin</artifactId>\n        <version>1.7.3</version>\n        <configuration>\n          <flattenMode>oss</flattenMode>\n        </configuration>\n        <executions>\n          <execution>\n            <id>flatten</id>\n            <phase>process-resources</phase>\n            <goals>\n              <goal>flatten</goal>\n            </goals>\n          </execution>\n          <execution>\n            <id>flatten.clean</id>\n            <phase>clean</phase>\n            <goals>\n              <goal>clean</goal>\n            </goals>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <groupId>com.spotify.fmt</groupId>\n        <artifactId>fmt-maven-plugin</artifactId>\n        <version>2.29</version>\n        <configuration>\n          <verbose>true</verbose>\n          <filesNamePattern>.*\\.java</filesNamePattern>\n          <skip>false</skip>\n          <skipSortingImports>false</skipSortingImports>\n          <style>google</style>\n        </configuration>\n        <executions>\n          <execution>\n            <phase>validate</phase>\n            <goals>\n              <goal>format</goal>\n            </goals>\n          </execution>\n        </executions>\n      </plugin>\n      <plugin>\n        <groupId>au.com.acegi</groupId>\n        <artifactId>xml-format-maven-plugin</artifactId>\n        <version>4.1.0</version>\n      </plugin>\n    </plugins>\n  </build>\n  <profiles>\n    <profile>\n      <id>java-21-modules</id>\n      <activation>\n        <jdk>[21,)</jdk>\n      </activation>\n      <modules>\n        <module>transactionoutbox-jooq</module>\n      </modules>\n    </profile>\n    <profile>\n      <id>java-25-modules</id>\n      <activation>\n        <jdk>[25,)</jdk>\n      </activation>\n      <modules>\n        <module>transactionoutbox-virtthreads</module>\n      </modules>\n    </profile>\n    <profile>\n      <id>release</id>\n      <properties>\n        <gpg.executable>gpg2</gpg.executable>\n      </properties>\n      <build>\n        <plugins>\n          <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-source-plugin</artifactId>\n            <version>${maven.source.plugin.version}</version>\n            <executions>\n              <execution>\n                <id>attach-sources</id>\n                <goals>\n                  <goal>jar</goal>\n                </goals>\n              </execution>\n            </executions>\n          </plugin>\n          <plugin>\n            <groupId>org.sonatype.central</groupId>\n            <artifactId>central-publishing-maven-plugin</artifactId>\n            <version>0.10.0</version>\n            <extensions>true</extensions>\n            <configuration>\n              <publishingServerId>central</publishingServerId>\n              <autoPublish>true</autoPublish>\n              <waitUntil>published</waitUntil>\n            </configuration>\n          </plugin>\n          <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-gpg-plugin</artifactId>\n            <version>${maven.gpg.plugin.version}</version>\n            <executions>\n              <execution>\n                <id>sign-artifacts</id>\n                <phase>verify</phase>\n                <goals>\n                  <goal>sign</goal>\n                </goals>\n                <configuration>\n                  <keyname>${gpg.keyname}</keyname>\n                  <passphraseServerId>${gpg.keyname}</passphraseServerId>\n                  <gpgArguments>\n                    <arg>--pinentry-mode</arg>\n                    <arg>loopback</arg>\n                  </gpgArguments>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n      <distributionManagement>\n        <snapshotRepository>\n          <id>ossrh</id>\n          <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n        </snapshotRepository>\n        <repository>\n          <id>ossrh</id>\n          <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n        </repository>\n      </distributionManagement>\n    </profile>\n    <profile>\n      <id>delombok</id>\n      <properties>\n        <src.dir>target/generated-sources/delombok</src.dir>\n        <skip.format>true</skip.format>\n      </properties>\n      <pluginRepositories>\n        <pluginRepository>\n          <id>projectlombok.org</id>\n          <url>https://projectlombok.org/edge-releases</url>\n        </pluginRepository>\n      </pluginRepositories>\n      <build>\n        <plugins>\n          <plugin>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok-maven-plugin</artifactId>\n            <version>1.18.20.0</version>\n            <dependencies>\n              <dependency>\n                <groupId>org.projectlombok</groupId>\n                <artifactId>lombok</artifactId>\n                <version>${lombok.version}</version>\n              </dependency>\n            </dependencies>\n            <executions>\n              <execution>\n                <id>delombok</id>\n                <phase>generate-sources</phase>\n                <goals>\n                  <goal>delombok</goal>\n                </goals>\n                <configuration>\n                  <addOutputDirectory>false</addOutputDirectory>\n                  <sourceDirectory>src/main/java</sourceDirectory>\n                </configuration>\n              </execution>\n            </executions>\n          </plugin>\n          <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-javadoc-plugin</artifactId>\n            <version>3.12.0</version>\n            <configuration>\n              <failOnError>true</failOnError>\n              <quiet>true</quiet>\n              <defaultVersion>${project.version}</defaultVersion>\n              <sourcepath>target/generated-sources/delombok</sourcepath>\n            </configuration>\n            <executions>\n              <execution>\n                <id>attach-javadocs</id>\n                <goals>\n                  <goal>jar</goal>\n                </goals>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-nodb-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <excludes>\n                <exclude>**/*Oracle*.java</exclude>\n                <exclude>**/*Postgres*.java</exclude>\n                <exclude>**/*MySql5*.java</exclude>\n                <exclude>**/*MySql8*.java</exclude>\n                <exclude>**/*MSSqlServer*.java</exclude>\n              </excludes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-oracle18-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <includes>\n                <include>**/*Oracle18*.java</include>\n              </includes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-oracle21-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <includes>\n                <include>**/*Oracle21*.java</include>\n              </includes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-postgres-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <includes>\n                <include>**/*Postgres*.java</include>\n              </includes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-mysql5-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <includes>\n                <include>**/*MySql5*.java</include>\n              </includes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-mysql8-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <includes>\n                <include>**/*MySql8*.java</include>\n              </includes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>only-mssqlserver-tests</id>\n      <build>\n        <plugins>\n          <plugin>\n            <artifactId>maven-surefire-plugin</artifactId>\n            <configuration>\n              <includes>\n                <include>**/*MSSqlServer*.java</include>\n              </includes>\n            </configuration>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n    <profile>\n      <id>noformat</id>\n      <build>\n        <plugins>\n          <plugin>\n            <groupId>com.spotify.fmt</groupId>\n            <artifactId>fmt-maven-plugin</artifactId>\n            <executions>\n              <execution>\n                <phase>none</phase>\n              </execution>\n            </executions>\n          </plugin>\n          <plugin>\n            <groupId>org.codehaus.mojo</groupId>\n            <artifactId>flatten-maven-plugin</artifactId>\n            <executions>\n              <execution>\n                <id>flatten</id>\n                <phase>none</phase>\n              </execution>\n              <execution>\n                <id>flatten.clean</id>\n                <phase>none</phase>\n              </execution>\n            </executions>\n          </plugin>\n        </plugins>\n      </build>\n    </profile>\n  </profiles>\n  <licenses>\n    <license>\n      <name>The Apache License, Version 2.0</name>\n      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n    </license>\n  </licenses>\n  <developers>\n    <developer>\n      <name>Graham Crockford</name>\n      <email>graham@gruelbox.com</email>\n      <organization>Gruelbox</organization>\n      <organizationUrl>https://gruelbox.com</organizationUrl>\n    </developer>\n  </developers>\n  <issueManagement>\n    <system>GitHub Issues</system>\n    <url>https://github.com/gruelbox/transaction-outbox/issues</url>\n  </issueManagement>\n  <scm>\n    <connection>scm:git:https://github.com/gruelbox/transaction-outbox.git</connection>\n    <developerConnection>scm:git:git@github.com:gruelbox/transaction-outbox.git</developerConnection>\n    <url>https://github.com/gruelbox/transaction-outbox</url>\n    <tag>HEAD</tag>\n  </scm>\n  <distributionManagement>\n    <repository>\n      <id>github</id>\n      <name>GitHub OWNER Apache Maven Packages</name>\n      <url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>\n    </repository>\n  </distributionManagement>\n</project>\n"
  },
  {
    "path": "settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <servers>\n    <server>\n      <id>github</id>\n      <username>${env.GITHUB_ACTOR}</username>\n      <password>${env.GITHUB_TOKEN}</password>\n    </server>\n    <server>\n      <id>central</id>\n      <username>${env.SONATYPE_USERNAME}</username>\n      <password>${env.SONATYPE_PASSWORD}</password>\n    </server>\n  </servers>\n  <profiles>\n    <profile>\n      <id>ossrh</id>\n      <activation>\n        <activeByDefault>true</activeByDefault>\n      </activation>\n      <properties>\n        <gpg.keyname>${env.GPG_KEYNAME}</gpg.keyname>\n        <gpg.executable>${env.GPG_EXECUTABLE}</gpg.executable>\n        <gpg.passphrase>${env.GPG_PASSPHRASE}</gpg.passphrase>\n      </properties>\n    </profile>\n  </profiles>\n</settings>\n"
  },
  {
    "path": "transactionoutbox-acceptance/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Acceptance Tests</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-acceptance</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (core library)</description>\n  <dependencies>\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- Compile time -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test dependencies -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-testing</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>testcontainers</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>junit-jupiter</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>postgresql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>oracle-xe</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mysql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.postgresql</groupId>\n      <artifactId>postgresql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.oracle.database.jdbc</groupId>\n      <artifactId>ojdbc11</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.mysql</groupId>\n      <artifactId>mysql-connector-j</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.h2database</groupId>\n      <artifactId>h2</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mssqlserver</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.microsoft.sqlserver</groupId>\n      <artifactId>mssql-jdbc</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestComplexConfigurationExample.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.sql.Connection;\nimport java.time.Duration;\nimport java.util.Currency;\nimport java.util.Set;\nimport java.util.concurrent.ForkJoinPool;\nimport javax.sql.DataSource;\nimport lombok.Value;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mockito;\nimport org.slf4j.MDC;\nimport org.slf4j.event.Level;\n\n/**\n * Just syntax-checks the example given in the README to give a warning if the example needs to\n * change.\n */\nclass TestComplexConfigurationExample {\n\n  @Test\n  @Disabled\n  void test() {\n\n    DataSource dataSource = Mockito.mock(DataSource.class);\n    ServiceLocator myServiceLocator = Mockito.mock(ServiceLocator.class);\n    EventPublisher eventPublisher = Mockito.mock(EventPublisher.class);\n\n    TransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);\n\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            // The most complex part to set up for most will be synchronizing with your existing\n            // transaction\n            // management. Pre-rolled implementations are available for jOOQ and Spring (see above\n            // for more information)\n            // and you can use those examples to synchronize with anything else by defining your own\n            // TransactionManager.\n            // Or, if you have no formal transaction management at the moment, why not start, using\n            // transaction-outbox's\n            // built-in one?\n            .transactionManager(transactionManager)\n            // Modify how requests are persisted to the database.\n            .persistor(\n                DefaultPersistor.builder()\n                    // Selecting the right SQL dialect ensures that features such as SKIP LOCKED are\n                    // used correctly.\n                    .dialect(Dialect.POSTGRESQL_9)\n                    // Override the table name (defaults to \"TXNO_OUTBOX\")\n                    .tableName(\"transactionOutbox\")\n                    // Shorten the time we will wait for write locks (defaults to 2)\n                    .writeLockTimeoutSeconds(1)\n                    // Disable automatic creation and migration of the outbox table, forcing the\n                    // application to manage\n                    // migrations itself\n                    .migrate(false)\n                    // Allow the SaleType enum and Money class to be used in arguments (see example\n                    // below)\n                    .serializer(\n                        DefaultInvocationSerializer.builder()\n                            .serializableTypes(Set.of(SaleType.class, Money.class))\n                            .build())\n                    .build())\n            .instantiator(Instantiator.using(myServiceLocator::createInstance))\n            // Change the log level used when work cannot be submitted to a saturated queue to INFO\n            // level (the default\n            // is WARN, which you should probably consider a production incident). You can also\n            // change the Executor used\n            // for submitting work to a shared thread pool used by the rest of your application.\n            // Fully-custom Submitter\n            // implementations are also easy to implement.\n            .submitter(\n                ExecutorSubmitter.builder()\n                    .executor(ForkJoinPool.commonPool())\n                    .logLevelWorkQueueSaturation(Level.INFO)\n                    .build())\n            // Lower the log level when a task fails temporarily from the default WARN.\n            .logLevelTemporaryFailure(Level.INFO)\n            // 10 attempts at a task before it is blocked (and would require intervention)\n            .blockAfterAttempts(10)\n            // When calling flush(), select 0.5m records at a time.\n            .flushBatchSize(500_000)\n            // Flush once every 15 minutes only\n            .attemptFrequency(Duration.ofMinutes(15))\n            // Include Slf4j's Mapped Diagnostic Context in tasks. This means that anything in the\n            // MDC when schedule()\n            // is called will be recreated in the task when it runs. Very useful for tracking things\n            // like user ids and\n            // request ids across invocations.\n            .serializeMdc(true)\n            // We can intercept task successes, single failures and blocked tasks. The most common\n            // use is\n            // to catch blocked tasks.\n            // and raise alerts for these to be investigated. A Slack interactive message is\n            // particularly effective here\n            // since it can be wired up to call unblock() automatically.\n            .listener(\n                new TransactionOutboxListener() {\n\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    eventPublisher.publish(new OutboxTaskProcessedEvent(entry.getId()));\n                  }\n\n                  @Override\n                  public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n                    eventPublisher.publish(new BlockedOutboxTaskEvent(entry.getId()));\n                  }\n                })\n            .build();\n\n    // Usage example, using the in-built transaction manager\n    MDC.put(\"SESSIONKEY\", \"Foo\");\n    try {\n      transactionManager.inTransaction(\n          tx -> {\n            writeSomeChanges(tx.connection());\n            outbox\n                .schedule(getClass())\n                .performRemoteCall(SaleType.SALE, Money.of(10, Currency.getInstance(\"USD\")));\n          });\n    } finally {\n      MDC.clear();\n    }\n  }\n\n  void performRemoteCall(\n      @SuppressWarnings({\"unused\", \"SameParameterValue\"}) SaleType saleType,\n      @SuppressWarnings(\"unused\") Money amount) {}\n\n  private void writeSomeChanges(@SuppressWarnings(\"unused\") Connection connection) {}\n\n  private interface ServiceLocator {\n    <T> T createInstance(Class<T> clazz);\n  }\n\n  private interface EventPublisher {\n    void publish(Object o);\n  }\n\n  @Value\n  private static class BlockedOutboxTaskEvent {\n    String id;\n  }\n\n  @Value\n  private static class OutboxTaskProcessedEvent {\n    String id;\n  }\n\n  @SuppressWarnings(\"unused\")\n  private enum SaleType {\n    SALE,\n    REFUND\n  }\n\n  private interface Money {\n    static Money of(\n        @SuppressWarnings(\"unused\") int amount, @SuppressWarnings(\"unused\") Currency currency) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestH2.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport com.gruelbox.transactionoutbox.testing.InterfaceProcessor;\nimport com.gruelbox.transactionoutbox.testing.LatchListener;\nimport com.gruelbox.transactionoutbox.testing.OrderedEntryListener;\nimport java.lang.reflect.InvocationTargetException;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.MDC;\n\n@SuppressWarnings(\"WeakerAccess\")\nclass TestH2 extends AbstractAcceptanceTest {\n\n  static final ThreadLocal<Boolean> inWrappedInvocation = ThreadLocal.withInitial(() -> false);\n\n  @Test\n  final void delayedExecutionImmediateSubmission() throws InterruptedException {\n\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(Instantiator.using(clazz -> (InterfaceProcessor) (foo, bar) -> {}))\n            .listener(new OrderedEntryListener(latch, new CountDownLatch(1)))\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .attemptFrequency(Duration.ofSeconds(60))\n            .build();\n\n    outbox.initialize();\n    clearOutbox();\n\n    var start = Instant.now();\n    transactionManager.inTransaction(\n        () ->\n            outbox\n                .with()\n                .delayForAtLeast(Duration.ofSeconds(1))\n                .schedule(InterfaceProcessor.class)\n                .process(1, \"bar\"));\n    assertTrue(latch.await(5, TimeUnit.SECONDS));\n    assertTrue(start.plus(Duration.ofSeconds(1)).isBefore(Instant.now()));\n  }\n\n  @Test\n  final void delayedExecutionFlushOnly() throws Exception {\n\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(Instantiator.using(clazz -> (InterfaceProcessor) (foo, bar) -> {}))\n            .listener(new OrderedEntryListener(latch, new CountDownLatch(1)))\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .attemptFrequency(Duration.ofSeconds(1))\n            .build();\n\n    outbox.initialize();\n    clearOutbox();\n\n    transactionManager.inTransaction(\n        () ->\n            outbox\n                .with()\n                .delayForAtLeast(Duration.ofSeconds(2))\n                .schedule(InterfaceProcessor.class)\n                .process(1, \"bar\"));\n    assertFalse(latch.await(3, TimeUnit.SECONDS));\n\n    withRunningFlusher(outbox, () -> assertTrue(latch.await(3, TimeUnit.SECONDS)));\n  }\n\n  @Test\n  final void wrapInvocations() throws InterruptedException {\n\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) -> {\n                              if (!inWrappedInvocation.get()) {\n                                throw new IllegalStateException(\"Not in a wrapped invocation\");\n                              }\n                            }))\n            .listener(\n                new LatchListener(latch)\n                    .andThen(\n                        new TransactionOutboxListener() {\n                          @Override\n                          public void wrapInvocation(Invocator invocator)\n                              throws IllegalAccessException,\n                                  IllegalArgumentException,\n                                  InvocationTargetException {\n                            inWrappedInvocation.set(true);\n                            try {\n                              invocator.run();\n                            } finally {\n                              inWrappedInvocation.remove();\n                            }\n                          }\n                        }))\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .build();\n\n    outbox.initialize();\n    clearOutbox();\n\n    transactionManager.inTransaction(\n        () -> outbox.schedule(InterfaceProcessor.class).process(1, \"bar\"));\n    assertTrue(latch.await(5, TimeUnit.SECONDS));\n  }\n\n  @Test\n  final void wrapInvocationsWithMDC() throws InterruptedException {\n\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) -> {\n                              if (!Boolean.parseBoolean(MDC.get(\"BAR\"))) {\n                                throw new IllegalStateException(\"Not in a wrapped invocation\");\n                              }\n                            }))\n            .listener(\n                new LatchListener(latch)\n                    .andThen(\n                        new TransactionOutboxListener() {\n                          @Override\n                          public void wrapInvocation(Invocator invocator)\n                              throws IllegalAccessException,\n                                  IllegalArgumentException,\n                                  InvocationTargetException {\n                            MDC.put(\"BAR\", \"true\");\n                            try {\n                              invocator.run();\n                            } finally {\n                              MDC.remove(\"BAR\");\n                            }\n                          }\n                        }))\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .build();\n\n    outbox.initialize();\n    clearOutbox();\n\n    transactionManager.inTransaction(\n        () -> outbox.schedule(InterfaceProcessor.class).process(1, \"bar\"));\n    assertTrue(latch.await(5, TimeUnit.SECONDS));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2019.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MSSQLServerContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestMSSqlServer2019 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MSSQLServerContainer<>(\"mcr.microsoft.com/mssql/server:2019-latest\")\n          .acceptLicense()\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MS_SQL_SERVER)\n        .driverClassName(\"com.microsoft.sqlserver.jdbc.SQLServerDriver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMSSqlServer2022.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MSSQLServerContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestMSSqlServer2022 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MSSQLServerContainer<>(\"mcr.microsoft.com/mssql/server:2022-latest\")\n          .acceptLicense()\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MS_SQL_SERVER)\n        .driverClassName(\"com.microsoft.sqlserver.jdbc.SQLServerDriver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql5.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestMySql5 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MySQLContainer<>(\"mysql:5\")\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true)\n          .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MY_SQL_5)\n        .driverClassName(\"com.mysql.cj.jdbc.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestMySql8.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestMySql8 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MySQLContainer<>(\"mysql:8\")\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true)\n          .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MY_SQL_8)\n        .driverClassName(\"com.mysql.cj.jdbc.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle18.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.OracleContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestOracle18 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings(\"rawtypes\")\n  private static final JdbcDatabaseContainer container =\n      new OracleContainer(\"gvenzl/oracle-xe:18-slim-faststart\")\n          .withStartupTimeout(Duration.ofHours(1));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.ORACLE)\n        .driverClassName(\"oracle.jdbc.OracleDriver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n\n  @Override\n  protected String createTestTable() {\n    return \"CREATE TABLE TEST_TABLE (topic VARCHAR(50), ix NUMBER, foo INTEGER, CONSTRAINT TEST_TABLE_sequencing_pk PRIMARY KEY (topic, ix))\";\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestOracle21.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.OracleContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestOracle21 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings(\"rawtypes\")\n  private static final JdbcDatabaseContainer container =\n      new OracleContainer(\"gvenzl/oracle-xe:21-slim-faststart\")\n          .withStartupTimeout(Duration.ofHours(1));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.ORACLE)\n        .driverClassName(\"oracle.jdbc.OracleDriver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n\n  @Override\n  protected String createTestTable() {\n    return \"CREATE TABLE TEST_TABLE (topic VARCHAR(50), ix NUMBER, foo INTEGER, CONSTRAINT TEST_TABLE_sequencing_pk PRIMARY KEY (topic, ix))\";\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres11.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestPostgres11 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:11\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres12.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestPostgres12 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:12\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres13.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestPostgres13 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:13\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres14.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestPostgres14 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:14\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres15.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestPostgres15 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:15\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestPostgres16.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestPostgres16 extends AbstractAcceptanceTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:16\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestRequestSerialization.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport com.gruelbox.transactionoutbox.testing.LatchListener;\nimport com.gruelbox.transactionoutbox.testing.TestUtils;\nimport java.util.Set;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport lombok.Getter;\nimport lombok.Setter;\nimport org.junit.jupiter.api.Test;\n\npublic class TestRequestSerialization {\n\n  /**\n   * Ensures that we are serializing and deserializing any request before processing it. Otherwise\n   * work could get processed locally successfully but fail when retried since the serialized\n   * version of the request is not equivalent to the original.\n   */\n  @Test\n  final void workAlwaysSerialized() throws Exception {\n    TransactionManager transactionManager = simpleTxnManager();\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(\n                DefaultPersistor.builder()\n                    .dialect(connectionDetails().dialect())\n                    .serializer(\n                        DefaultInvocationSerializer.builder()\n                            .serializableTypes(Set.of(Arg.class))\n                            .build())\n                    .build())\n            .listener(new LatchListener(latch))\n            .build();\n\n    clearOutbox();\n\n    Arg arg = new Arg();\n    arg.hiddenData = \"HIDDEN\";\n    arg.serializedData = \"SERIALIZED\";\n\n    transactionManager.inTransaction(() -> outbox.schedule(ComplexProcessor.class).process(arg));\n    assertTrue(latch.await(15, TimeUnit.SECONDS));\n  }\n\n  protected AbstractAcceptanceTest.ConnectionDetails connectionDetails() {\n    return AbstractAcceptanceTest.ConnectionDetails.builder()\n        .dialect(Dialect.H2)\n        .driverClassName(\"org.h2.Driver\")\n        .url(\n            \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE\")\n        .user(\"test\")\n        .password(\"test\")\n        .build();\n  }\n\n  private TransactionManager simpleTxnManager() {\n    return TransactionManager.fromConnectionDetails(\n        connectionDetails().driverClassName(),\n        connectionDetails().url(),\n        connectionDetails().user(),\n        connectionDetails().password());\n  }\n\n  private void clearOutbox() {\n    TestUtils.runSql(simpleTxnManager(), \"DELETE FROM TXNO_OUTBOX\");\n  }\n\n  static class ComplexProcessor {\n\n    public void process(Arg arg) {\n      if (arg.hiddenData != null) {\n        throw new IllegalStateException(\n            \"Running with state that could not possibly have been serialized\");\n      }\n      if (!\"SERIALIZED\".equals(arg.serializedData)) {\n        throw new IllegalStateException(\"No serialized state\");\n      }\n    }\n  }\n\n  @Getter\n  @Setter\n  static class Arg {\n    transient String hiddenData;\n    String serializedData;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/TestStubbing.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.math.BigDecimal;\nimport java.time.Clock;\nimport java.time.LocalDateTime;\nimport java.time.ZoneOffset;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport lombok.Value;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n/** Checks that stubbing {@link TransactionOutbox} works cleanly. */\nclass TestStubbing {\n\n  @Test\n  void testStubbingWithThreadLocalContext() {\n    StubThreadLocalTransactionManager transactionManager = new StubThreadLocalTransactionManager();\n    TransactionOutbox outbox = createOutbox(transactionManager);\n\n    Interface.invocations.clear();\n\n    transactionManager.inTransaction(\n        () -> {\n          outbox\n              .schedule(Interface.class)\n              .doThing(1, \"2\", new BigDecimal[] {BigDecimal.ONE, BigDecimal.TEN});\n          outbox.schedule(Interface.class).doThing(2, \"3\", new BigDecimal[] {});\n          outbox.schedule(Interface.class).doThing(3, null, null);\n        });\n    transactionManager.inTransaction(() -> outbox.schedule(Interface.class).doThing(4, null, null));\n\n    Object expected1 = List.of(1, \"2\", List.of(BigDecimal.ONE, BigDecimal.TEN));\n    Object expected2 = List.of(2, \"3\", List.of());\n    List<Object> expected3 = new ArrayList<>();\n    expected3.add(3);\n    expected3.add(null);\n    expected3.add(null);\n    List<Object> expected4 = new ArrayList<>();\n    expected4.add(4);\n    expected4.add(null);\n    expected4.add(null);\n    assertThat(Interface.invocations, contains(expected1, expected2, expected3, expected4));\n  }\n\n  @Test\n  void testStubbingWithExplicitContextInvalidContext() {\n    StubParameterContextTransactionManager<Context> transactionManager =\n        new StubParameterContextTransactionManager<>(Context.class, () -> new Context(1L));\n    TransactionOutbox outbox = createOutbox(transactionManager);\n\n    Assertions.assertThrows(\n        IllegalArgumentException.class,\n        () ->\n            transactionManager.inTransaction(\n                tx -> outbox.schedule(Interface.class).doThing(1, new Context(2L))));\n  }\n\n  @Test\n  void testStubbingWithExplicitContextPassingTransaction() {\n    StubParameterContextTransactionManager<Context> transactionManager =\n        new StubParameterContextTransactionManager<>(Context.class, () -> new Context(1L));\n    TransactionOutbox outbox = createOutbox(transactionManager);\n\n    Interface.invocations.clear();\n\n    transactionManager.inTransaction(tx -> outbox.schedule(Interface.class).doThing(1, tx));\n\n    assertThat(Interface.invocations, hasSize(1));\n    assertThat(Interface.invocations.get(0).get(0), equalTo(1));\n    assertThat(Interface.invocations.get(0).get(1), isA(Transaction.class));\n  }\n\n  @Test\n  void testStubbingWithExplicitContextPassingContext() {\n    StubParameterContextTransactionManager<Context> transactionManager =\n        new StubParameterContextTransactionManager<>(Context.class, () -> new Context(1L));\n    TransactionOutbox outbox = createOutbox(transactionManager);\n\n    Interface.invocations.clear();\n\n    transactionManager.inTransaction(\n        tx -> outbox.schedule(Interface.class).doThing(1, (Context) tx.context()));\n\n    assertThat(Interface.invocations, hasSize(1));\n    assertThat(Interface.invocations.get(0).get(0), equalTo(1));\n    assertThat(Interface.invocations.get(0).get(1), isA(Context.class));\n  }\n\n  private TransactionOutbox createOutbox(TransactionManager transactionManager) {\n    return TransactionOutbox.builder()\n        .instantiator(Instantiator.usingReflection())\n        .persistor(StubPersistor.builder().build())\n        .submitter(Submitter.withExecutor(Runnable::run))\n        .transactionManager(transactionManager)\n        .clockProvider(\n            () ->\n                Clock.fixed(\n                    LocalDateTime.of(2020, 3, 1, 12, 0).toInstant(ZoneOffset.UTC),\n                    ZoneOffset.UTC)) // Fix the clock\n        .build();\n  }\n\n  static class Interface {\n\n    static List<List<Object>> invocations = new ArrayList<>();\n\n    void doThing(int arg1, String arg2, BigDecimal[] arg3) {\n      ArrayList<Object> args = new ArrayList<>();\n      args.add(arg1);\n      args.add(arg2);\n      args.add(arg3 == null ? null : Arrays.asList(arg3));\n      invocations.add(args);\n    }\n\n    void doThing(@SuppressWarnings(\"SameParameterValue\") int arg1, Transaction transaction) {\n      assertThat(transaction, notNullValue());\n      invocations.add(List.of(arg1, transaction));\n    }\n\n    void doThing(@SuppressWarnings(\"SameParameterValue\") int arg1, Context context) {\n      assertThat(context, notNullValue());\n      invocations.add(List.of(arg1, context));\n    }\n  }\n\n  @Value\n  static class Context {\n    long id;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorH2.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\nclass TestDefaultPersistorH2 extends AbstractPersistorTest {\n\n  private final DefaultPersistor persistor = DefaultPersistor.builder().dialect(Dialect.H2).build();\n  private final TransactionManager txManager =\n      TransactionManager.fromConnectionDetails(\n          \"org.h2.Driver\",\n          \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=2000;LOB_TIMEOUT=2000;MV_STORE=TRUE\",\n          \"test\",\n          \"test\");\n\n  @Override\n  protected DefaultPersistor persistor() {\n    return persistor;\n  }\n\n  @Override\n  protected TransactionManager txManager() {\n    return txManager;\n  }\n\n  @Override\n  protected Dialect dialect() {\n    return Dialect.H2;\n  }\n\n  @Override\n  public void testSkipLocked() throws Exception {\n    // Not supported.\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMSSqlServer2019.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MSSQLServerContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\nclass TestDefaultPersistorMSSqlServer2019 extends AbstractPersistorTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MSSQLServerContainer<>(\"mcr.microsoft.com/mssql/server:2019-latest\")\n          .acceptLicense()\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true);\n\n  private final DefaultPersistor persistor =\n      DefaultPersistor.builder().dialect(Dialect.MS_SQL_SERVER).build();\n  private final TransactionManager txManager =\n      TransactionManager.fromConnectionDetails(\n          \"com.microsoft.sqlserver.jdbc.SQLServerDriver\",\n          container.getJdbcUrl(),\n          container.getUsername(),\n          container.getPassword());\n\n  @Override\n  protected DefaultPersistor persistor() {\n    return persistor;\n  }\n\n  @Override\n  protected TransactionManager txManager() {\n    return txManager;\n  }\n\n  @Override\n  protected Dialect dialect() {\n    return Dialect.MS_SQL_SERVER;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql5.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\nclass TestDefaultPersistorMySql5 extends AbstractPersistorTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MySQLContainer<>(\"mysql:5\")\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true)\n          .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  private final DefaultPersistor persistor =\n      DefaultPersistor.builder().dialect(Dialect.MY_SQL_5).build();\n  private final TransactionManager txManager =\n      TransactionManager.fromConnectionDetails(\n          \"com.mysql.cj.jdbc.Driver\",\n          container.getJdbcUrl(),\n          container.getUsername(),\n          container.getPassword());\n\n  @Override\n  protected DefaultPersistor persistor() {\n    return persistor;\n  }\n\n  @Override\n  protected TransactionManager txManager() {\n    return txManager;\n  }\n\n  @Override\n  protected Dialect dialect() {\n    return Dialect.MY_SQL_5;\n  }\n\n  @Override\n  public void testSkipLocked() throws Exception {\n    // Not supported.\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorMySql8.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\nclass TestDefaultPersistorMySql8 extends AbstractPersistorTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MySQLContainer<>(\"mysql:8\")\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true)\n          .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  private final DefaultPersistor persistor =\n      DefaultPersistor.builder().dialect(Dialect.MY_SQL_8).build();\n  private final TransactionManager txManager =\n      TransactionManager.fromConnectionDetails(\n          \"com.mysql.cj.jdbc.Driver\",\n          container.getJdbcUrl(),\n          container.getUsername(),\n          container.getPassword());\n\n  @Override\n  protected DefaultPersistor persistor() {\n    return persistor;\n  }\n\n  @Override\n  protected TransactionManager txManager() {\n    return txManager;\n  }\n\n  @Override\n  protected Dialect dialect() {\n    return Dialect.MY_SQL_8;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorOracle18.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.OracleContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\nclass TestDefaultPersistorOracle18 extends AbstractPersistorTest {\n\n  @Container\n  @SuppressWarnings(\"rawtypes\")\n  private static final JdbcDatabaseContainer container =\n      new OracleContainer(\"gvenzl/oracle-xe:18-slim-faststart\")\n          .withStartupTimeout(Duration.ofHours(1))\n          .withReuse(true);\n\n  private final DefaultPersistor persistor =\n      DefaultPersistor.builder().dialect(Dialect.ORACLE).build();\n\n  private final TransactionManager txManager =\n      TransactionManager.fromConnectionDetails(\n          \"oracle.jdbc.OracleDriver\",\n          container.getJdbcUrl(),\n          container.getUsername(),\n          container.getPassword());\n\n  @Override\n  protected DefaultPersistor persistor() {\n    return persistor;\n  }\n\n  @Override\n  protected TransactionManager txManager() {\n    return txManager;\n  }\n\n  @Override\n  protected Dialect dialect() {\n    return Dialect.ORACLE;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-acceptance/src/test/java/com/gruelbox/transactionoutbox/acceptance/persistor/TestDefaultPersistorPostgres16.java",
    "content": "package com.gruelbox.transactionoutbox.acceptance.persistor;\n\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractPersistorTest;\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\nclass TestDefaultPersistorPostgres16 extends AbstractPersistorTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:16\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  private final DefaultPersistor persistor =\n      DefaultPersistor.builder().dialect(Dialect.POSTGRESQL_9).build();\n  private final TransactionManager txManager =\n      TransactionManager.fromConnectionDetails(\n          \"org.postgresql.Driver\",\n          container.getJdbcUrl(),\n          container.getUsername(),\n          container.getPassword());\n\n  @Override\n  protected DefaultPersistor persistor() {\n    return persistor;\n  }\n\n  @Override\n  protected TransactionManager txManager() {\n    return txManager;\n  }\n\n  @Override\n  protected Dialect dialect() {\n    return Dialect.POSTGRESQL_9;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Core</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-core</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (core library)</description>\n  <dependencies>\n    <!-- Run time -->\n    <dependency>\n      <groupId>org.slf4j</groupId>\n      <artifactId>slf4j-api</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>net.bytebuddy</groupId>\n      <artifactId>byte-buddy</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.objenesis</groupId>\n      <artifactId>objenesis</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.google.code.gson</groupId>\n      <artifactId>gson</artifactId>\n    </dependency>\n    <!-- Compile time -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test dependencies -->\n    <dependency>\n      <groupId>org.hamcrest</groupId>\n      <artifactId>hamcrest-core</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-engine</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-params</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.mockito</groupId>\n      <artifactId>mockito-all</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-classic</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-core</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.zaxxer</groupId>\n      <artifactId>HikariCP</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.h2database</groupId>\n      <artifactId>h2</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/AlreadyScheduledException.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Duration;\n\n/**\n * Thrown when we attempt to schedule an invocation with a unique client id which has already been\n * used within {@link TransactionOutbox.TransactionOutboxBuilder#retentionThreshold(Duration)}.\n */\npublic class AlreadyScheduledException extends RuntimeException {\n  AlreadyScheduledException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ConnectionProvider.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\n\n/**\n * Source for JDBC connections to be provided to a {@link TransactionManager}. It is not required\n * for a {@link TransactionManager} to use {@link ConnectionProvider}, and when integrating with\n * existing applications with transaction management, it is indeed unlikely to do so.\n */\n@SuppressWarnings(\"WeakerAccess\")\npublic interface ConnectionProvider {\n\n  /**\n   * Requests a new connection, or an available connection from a pool. The caller is responsible\n   * for calling {@link Connection#close()}.\n   *\n   * @return The connection.\n   */\n  Connection obtainConnection();\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DataSourceConnectionProvider.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.sql.Connection;\nimport javax.sql.DataSource;\nimport lombok.Builder;\n\n/**\n * A {@link ConnectionProvider} which requests connections from a {@link DataSource}. This is\n * suitable for applications using connection pools or container-provided JDBC.\n *\n * <p>Usage:\n *\n * <pre>ConnectionProvider provider = DataSourceConnectionProvider.builder()\n *   .dataSource(ds)\n *   .build()</pre>\n */\n@Builder\nfinal class DataSourceConnectionProvider implements ConnectionProvider {\n\n  private final DataSource dataSource;\n\n  @Override\n  public Connection obtainConnection() {\n    return Utils.uncheckedly(dataSource::getConnection);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultDialect.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.Statement;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.experimental.Accessors;\n\n@AllArgsConstructor(access = AccessLevel.PRIVATE)\nclass DefaultDialect implements Dialect {\n\n  static Builder builder(String name) {\n    return new Builder(name);\n  }\n\n  @Getter private final String name;\n  @Getter private final String deleteExpired;\n  @Getter private final String delete;\n  @Getter private final String selectBatch;\n  @Getter private final String lock;\n  @Getter private final String checkSql;\n  @Getter private final String fetchNextInAllTopics;\n  @Getter private final String fetchNextInSelectedTopics;\n  @Getter private final String fetchCurrentVersion;\n  @Getter private final String fetchNextSequence;\n  private final Collection<Migration> migrations;\n\n  @Override\n  public String booleanValue(boolean criteriaValue) {\n    return criteriaValue ? Boolean.TRUE.toString() : Boolean.FALSE.toString();\n  }\n\n  @Override\n  public void createVersionTableIfNotExists(Connection connection) throws SQLException {\n    try (Statement s = connection.createStatement()) {\n      s.execute(\n          \"CREATE TABLE IF NOT EXISTS TXNO_VERSION (id INT DEFAULT 0, version INT, PRIMARY KEY (id))\");\n    }\n  }\n\n  @Override\n  public String toString() {\n    return name;\n  }\n\n  @Override\n  public Stream<Migration> getMigrations() {\n    return migrations.stream();\n  }\n\n  @Setter\n  @Accessors(fluent = true)\n  static final class Builder {\n    private final String name;\n    private String delete = \"DELETE FROM {{table}} WHERE id = ? and version = ?\";\n    private String deleteExpired =\n        \"DELETE FROM {{table}} WHERE nextAttemptTime < ? AND processed = true AND blocked = false\"\n            + \" LIMIT {{batchSize}}\";\n    private String selectBatch =\n        \"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? \"\n            + \"AND blocked = false AND processed = false AND topic = '*' LIMIT {{batchSize}}\";\n    private String lock =\n        \"SELECT id, invocation FROM {{table}} WHERE id = ? AND version = ? FOR UPDATE\";\n    private String checkSql = \"SELECT 1\";\n    private Map<Integer, Migration> migrations;\n    private Function<Boolean, String> booleanValueFrom;\n    private SQLAction createVersionTableBy;\n    private String fetchNextInAllTopics =\n        \"SELECT {{allFields}} FROM {{table}} a\"\n            + \" WHERE processed = false AND topic <> '*' AND nextAttemptTime < ?\"\n            + \" AND seq = (\"\n            + \"SELECT MIN(seq) FROM {{table}} b WHERE b.topic=a.topic AND b.processed = false\"\n            + \") LIMIT {{batchSize}}\";\n    private String fetchNextInSelectedTopics =\n        \"SELECT {{allFields}} FROM {{table}} a\"\n            + \" WHERE processed = false AND topic IN ({{topicNames}}) AND nextAttemptTime < ?\"\n            + \" AND seq = (\"\n            + \"SELECT MIN(seq) FROM {{table}} b WHERE b.topic=a.topic AND b.processed = false\"\n            + \") LIMIT {{batchSize}}\";\n    private String fetchCurrentVersion = \"SELECT version FROM TXNO_VERSION FOR UPDATE\";\n    private String fetchNextSequence = \"SELECT seq FROM TXNO_SEQUENCE WHERE topic = ? FOR UPDATE\";\n\n    Builder(String name) {\n      this.name = name;\n      this.migrations = new TreeMap<>();\n      migrations.put(\n          1,\n          new Migration(\n              1,\n              \"Create outbox table\",\n              \"CREATE TABLE TXNO_OUTBOX (\\n\"\n                  + \"    id VARCHAR(36) PRIMARY KEY,\\n\"\n                  + \"    invocation TEXT,\\n\"\n                  + \"    nextAttemptTime TIMESTAMP(6),\\n\"\n                  + \"    attempts INT,\\n\"\n                  + \"    blacklisted BOOLEAN,\\n\"\n                  + \"    version INT\\n\"\n                  + \")\"));\n      migrations.put(\n          2,\n          new Migration(\n              2,\n              \"Add unique request id\",\n              \"ALTER TABLE TXNO_OUTBOX ADD COLUMN uniqueRequestId VARCHAR(100) NULL UNIQUE\"));\n      migrations.put(\n          3,\n          new Migration(\n              3, \"Add processed flag\", \"ALTER TABLE TXNO_OUTBOX ADD COLUMN processed BOOLEAN\"));\n      migrations.put(\n          4,\n          new Migration(\n              4,\n              \"Add flush index\",\n              \"CREATE INDEX IX_TXNO_OUTBOX_1 ON TXNO_OUTBOX (processed, blacklisted, nextAttemptTime)\"));\n      migrations.put(\n          5,\n          new Migration(\n              5,\n              \"Increase size of uniqueRequestId\",\n              \"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN uniqueRequestId VARCHAR(250)\"));\n      migrations.put(\n          6,\n          new Migration(\n              6,\n              \"Rename column blacklisted to blocked\",\n              \"ALTER TABLE TXNO_OUTBOX CHANGE COLUMN blacklisted blocked VARCHAR(250)\"));\n      migrations.put(\n          7,\n          new Migration(\n              7,\n              \"Add lastAttemptTime column to outbox\",\n              \"ALTER TABLE TXNO_OUTBOX ADD COLUMN lastAttemptTime TIMESTAMP(6) NULL AFTER invocation\"));\n      migrations.put(\n          8,\n          new Migration(\n              8,\n              \"Update length of invocation column on outbox for MySQL dialects only.\",\n              \"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN invocation MEDIUMTEXT\"));\n      migrations.put(\n          9,\n          new Migration(\n              9,\n              \"Add topic\",\n              \"ALTER TABLE TXNO_OUTBOX ADD COLUMN topic VARCHAR(250) NOT NULL DEFAULT '*'\"));\n      migrations.put(\n          10,\n          new Migration(10, \"Add sequence\", \"ALTER TABLE TXNO_OUTBOX ADD COLUMN seq BIGINT NULL\"));\n      migrations.put(\n          11,\n          new Migration(\n              11,\n              \"Add sequence table\",\n              \"CREATE TABLE TXNO_SEQUENCE (topic VARCHAR(250) NOT NULL, seq BIGINT NOT NULL, PRIMARY KEY (topic, seq))\"));\n      migrations.put(\n          12,\n          new Migration(\n              12,\n              \"Add flush index to support ordering\",\n              \"CREATE INDEX IX_TXNO_OUTBOX_2 ON TXNO_OUTBOX (topic, processed, seq)\"));\n      migrations.put(13, new Migration(13, \"Enforce UTF8 collation for outbox messages\", null));\n    }\n\n    Builder setMigration(Migration migration) {\n      this.migrations.put(migration.getVersion(), migration);\n      return this;\n    }\n\n    Builder changeMigration(int version, String sql) {\n      return setMigration(this.migrations.get(version).withSql(sql));\n    }\n\n    Builder disableMigration(@SuppressWarnings(\"SameParameterValue\") int version) {\n      return setMigration(this.migrations.get(version).withSql(null));\n    }\n\n    Dialect build() {\n      return new DefaultDialect(\n          name,\n          deleteExpired,\n          delete,\n          selectBatch,\n          lock,\n          checkSql,\n          fetchNextInAllTopics,\n          fetchNextInSelectedTopics,\n          fetchCurrentVersion,\n          fetchNextSequence,\n          migrations.values()) {\n        @Override\n        public String booleanValue(boolean criteriaValue) {\n          if (booleanValueFrom != null) {\n            return booleanValueFrom.apply(criteriaValue);\n          }\n          return super.booleanValue(criteriaValue);\n        }\n\n        @Override\n        public void createVersionTableIfNotExists(Connection connection) throws SQLException {\n          if (createVersionTableBy != null) {\n            createVersionTableBy.doAction(connection);\n          } else {\n            super.createVersionTableIfNotExists(connection);\n          }\n        }\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultInvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.google.gson.*;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonToken;\nimport com.google.gson.stream.JsonWriter;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.Writer;\nimport java.lang.reflect.Modifier;\nimport java.lang.reflect.Type;\nimport java.math.BigDecimal;\nimport java.text.ParseException;\nimport java.text.ParsePosition;\nimport java.time.*;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.GregorianCalendar;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TimeZone;\nimport java.util.UUID;\nimport lombok.Builder;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * A locked-down serializer which supports a limited list of primitives and simple JDK value types.\n * Only the following are supported:\n *\n * <ul>\n *   <li>{@link Invocation} itself\n *   <li>Primitive types such as {@code int} or {@code double} or the boxed equivalents\n *   <li>{@link String}\n *   <li>{@link java.util.Date}\n *   <li>{@link java.util.UUID}\n *   <li>The {@code java.time} classes:\n *       <ul>\n *         <li>{@link java.time.DayOfWeek}\n *         <li>{@link java.time.Duration}\n *         <li>{@link java.time.Instant}\n *         <li>{@link java.time.LocalDate}\n *         <li>{@link java.time.LocalDateTime}\n *         <li>{@link java.time.ZonedDateTime}\n *         <li>{@link java.time.Month}\n *         <li>{@link java.time.MonthDay}\n *         <li>{@link java.time.Period}\n *         <li>{@link java.time.Year}\n *         <li>{@link java.time.YearMonth}\n *         <li>{@link java.time.ZoneOffset}\n *         <li>{@link java.time.DayOfWeek}\n *         <li>{@link java.time.temporal.ChronoUnit}\n *       </ul>\n *   <li>Arrays specifically typed to one of the above types\n *   <li>Any types specifically passed in, which must be GSON compatible.\n * </ul>\n */\n@Slf4j\npublic final class DefaultInvocationSerializer implements InvocationSerializer {\n\n  private final Gson gson;\n\n  @Builder\n  DefaultInvocationSerializer(Set<Class<?>> serializableTypes, Integer version) {\n    this.gson =\n        new GsonBuilder()\n            .registerTypeAdapter(\n                Invocation.class,\n                new InvocationJsonSerializer(\n                    serializableTypes == null ? Set.of() : serializableTypes,\n                    version == null ? 2 : version))\n            .registerTypeAdapter(Date.class, new UtcDateTypeAdapter())\n            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter())\n            .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter())\n            .registerTypeAdapter(Instant.class, new InstantTypeAdapter())\n            .registerTypeAdapter(Duration.class, new DurationTypeAdapter())\n            .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter())\n            .registerTypeAdapter(MonthDay.class, new MonthDayTypeAdapter())\n            .registerTypeAdapter(Period.class, new PeriodTypeAdapter())\n            .registerTypeAdapter(Year.class, new YearTypeAdapter())\n            .registerTypeAdapter(YearMonth.class, new YearMonthAdapter())\n            .excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)\n            .create();\n  }\n\n  @Override\n  public void serializeInvocation(Invocation invocation, Writer writer) {\n    try {\n      gson.toJson(invocation, writer);\n    } catch (Exception e) {\n      throw new IllegalArgumentException(\"Cannot serialize \" + invocation, e);\n    }\n  }\n\n  @Override\n  public Invocation deserializeInvocation(Reader reader) throws IOException {\n    try {\n      return gson.fromJson(reader, Invocation.class);\n    } catch (JsonIOException | JsonSyntaxException exception) {\n      throw new IOException(exception);\n    }\n  }\n\n  private static final class InvocationJsonSerializer\n      implements JsonSerializer<Invocation>, JsonDeserializer<Invocation> {\n\n    private final int version;\n    private final Map<Class<?>, String> classToName = new HashMap<>();\n    private final Map<String, Class<?>> nameToClass = new HashMap<>();\n\n    InvocationJsonSerializer(Set<Class<?>> serializableClasses, int version) {\n      this.version = version;\n      addClassPair(byte.class, \"byte\");\n      addClassPair(short.class, \"short\");\n      addClassPair(int.class, \"int\");\n      addClassPair(long.class, \"long\");\n      addClassPair(float.class, \"float\");\n      addClassPair(double.class, \"double\");\n      addClassPair(boolean.class, \"boolean\");\n      addClassPair(char.class, \"char\");\n\n      addClass(Byte.class);\n      addClass(Short.class);\n      addClass(Integer.class);\n      addClass(Long.class);\n      addClass(Float.class);\n      addClass(Double.class);\n      addClass(Boolean.class);\n      addClass(Character.class);\n\n      addClass(BigDecimal.class);\n      addClass(String.class);\n      addClass(Date.class);\n      addClass(UUID.class);\n\n      addClass(DayOfWeek.class);\n      addClass(Duration.class);\n      addClass(Instant.class);\n      addClass(LocalDate.class);\n      addClass(LocalDateTime.class);\n      addClass(ZonedDateTime.class);\n      addClass(Month.class);\n      addClass(MonthDay.class);\n      addClass(Period.class);\n      addClass(Year.class);\n      addClass(YearMonth.class);\n      addClass(ZoneOffset.class);\n      addClass(DayOfWeek.class);\n      addClass(ChronoUnit.class);\n\n      addClass(Transaction.class);\n      addClassPair(TransactionContextPlaceholder.class, \"TransactionContext\");\n\n      serializableClasses.forEach(clazz -> addClassPair(clazz, clazz.getName()));\n    }\n\n    private void addClass(Class<?> clazz) {\n      addClassPair(clazz, clazz.getSimpleName());\n    }\n\n    private void addClassPair(Class<?> clazz, String name) {\n      classToName.put(clazz, name);\n      nameToClass.put(name, clazz);\n      String arrayClassName = toArrayClassName(clazz);\n      Class<?> arrayClass = toClass(clazz.getClassLoader(), arrayClassName);\n      classToName.put(arrayClass, arrayClassName);\n      nameToClass.put(arrayClassName, arrayClass);\n    }\n\n    private String toArrayClassName(Class<?> clazz) {\n      if (clazz.isArray()) {\n        return \"[\" + clazz.getName();\n      } else if (clazz == boolean.class) {\n        return \"[Z\";\n      } else if (clazz == byte.class) {\n        return \"[B\";\n      } else if (clazz == char.class) {\n        return \"[C\";\n      } else if (clazz == double.class) {\n        return \"[D\";\n      } else if (clazz == float.class) {\n        return \"[F\";\n      } else if (clazz == int.class) {\n        return \"[I\";\n      } else if (clazz == long.class) {\n        return \"[J\";\n      } else if (clazz == short.class) {\n        return \"[S\";\n      } else {\n        return \"[L\" + clazz.getName() + \";\";\n      }\n    }\n\n    private Class<?> toClass(ClassLoader classLoader, String name) {\n      try {\n        return classLoader != null ? Class.forName(name, false, classLoader) : Class.forName(name);\n      } catch (ClassNotFoundException e) {\n        throw new RuntimeException(\n            \"Cannot determine array type for \"\n                + name\n                + \" using \"\n                + (classLoader == null ? \"root classloader\" : \"base classloader\"),\n            e);\n      }\n    }\n\n    @Override\n    public JsonElement serialize(Invocation src, Type typeOfSrc, JsonSerializationContext context) {\n      if (version == 1) {\n        log.warn(\"Serializing as deprecated version {}\", version);\n        return serializeV1(src, typeOfSrc, context);\n      }\n      JsonObject obj = new JsonObject();\n      obj.addProperty(\"c\", src.getClassName());\n      obj.addProperty(\"m\", src.getMethodName());\n      JsonArray params = new JsonArray();\n      JsonArray args = new JsonArray();\n      int i = 0;\n      for (Class<?> parameterType : src.getParameterTypes()) {\n        params.add(nameForClass(parameterType));\n        Object arg = src.getArgs()[i];\n        if (arg == null) {\n          JsonObject jsonObject = new JsonObject();\n          jsonObject.add(\"t\", null);\n          jsonObject.add(\"v\", null);\n          args.add(jsonObject);\n        } else {\n          JsonObject jsonObject = new JsonObject();\n          jsonObject.addProperty(\"t\", nameForClass(arg.getClass()));\n          jsonObject.add(\"v\", context.serialize(arg));\n          args.add(jsonObject);\n        }\n        i++;\n      }\n      obj.add(\"p\", params);\n      obj.add(\"a\", args);\n      obj.add(\"x\", context.serialize(src.getMdc()));\n      obj.add(\"s\", context.serialize(src.getSession()));\n      return obj;\n    }\n\n    JsonElement serializeV1(Invocation src, Type typeOfSrc, JsonSerializationContext context) {\n      JsonObject obj = new JsonObject();\n      obj.addProperty(\"c\", src.getClassName());\n      obj.addProperty(\"m\", src.getMethodName());\n      JsonArray params = new JsonArray();\n      int i = 0;\n      for (Class<?> parameterType : src.getParameterTypes()) {\n        JsonObject jsonObject = new JsonObject();\n        jsonObject.addProperty(\"t\", nameForClass(parameterType));\n        jsonObject.add(\"v\", context.serialize(src.getArgs()[i]));\n        params.add(jsonObject);\n        i++;\n      }\n      obj.add(\"p\", params);\n      obj.add(\"x\", context.serialize(src.getMdc()));\n      return obj;\n    }\n\n    @Override\n    public Invocation deserialize(\n        JsonElement json, Type typeOfT, JsonDeserializationContext context)\n        throws JsonParseException {\n\n      JsonObject jsonObject = json.getAsJsonObject();\n      String className = jsonObject.get(\"c\").getAsString();\n      String methodName = jsonObject.get(\"m\").getAsString();\n\n      JsonArray jsonParams = jsonObject.get(\"p\").getAsJsonArray();\n      Class<?>[] params = new Class<?>[jsonParams.size()];\n      for (int i = 0; i < jsonParams.size(); i++) {\n        JsonElement param = jsonParams.get(i);\n        if (param.isJsonObject()) {\n          // For backwards compatibility\n          params[i] = classForName(param.getAsJsonObject().get(\"t\").getAsString());\n        } else {\n          params[i] = classForName(param.getAsString());\n        }\n      }\n\n      JsonElement argsElement = jsonObject.get(\"a\");\n      if (argsElement == null) {\n        // For backwards compatibility\n        argsElement = jsonObject.get(\"p\");\n      }\n      JsonArray jsonArgs = argsElement.getAsJsonArray();\n      Object[] args = new Object[jsonArgs.size()];\n      for (int i = 0; i < jsonArgs.size(); i++) {\n        JsonElement arg = jsonArgs.get(i);\n        JsonElement argType = arg.getAsJsonObject().get(\"t\");\n        if (argType != null) {\n          JsonElement argValue = arg.getAsJsonObject().get(\"v\");\n          Class<?> argClass = classForName(argType.getAsString());\n          try {\n            args[i] = context.deserialize(argValue, argClass);\n          } catch (Exception e) {\n            throw new RuntimeException(\n                \"Failed to deserialize arg [\" + argValue + \"] of type [\" + argType + \"]\", e);\n          }\n        }\n      }\n      Map<String, String> mdc = context.deserialize(jsonObject.get(\"x\"), Map.class);\n      Map<String, String> session = context.deserialize(jsonObject.get(\"s\"), Map.class);\n\n      return new Invocation(className, methodName, params, args, mdc, session);\n    }\n\n    private Class<?> classForName(String name) {\n      var clazz = nameToClass.get(name);\n      if (clazz == null) {\n        throw new IllegalArgumentException(\"Cannot deserialize class - not found: \" + name);\n      }\n      return clazz;\n    }\n\n    private String nameForClass(Class<?> clazz) {\n      var name = classToName.get(clazz);\n      if (name == null) {\n        throw new IllegalArgumentException(\n            \"Cannot serialize class - not found: \" + clazz.getName());\n      }\n      return name;\n    }\n  }\n\n  static final class ZonedDateTimeTypeAdapter extends TypeAdapter<ZonedDateTime> {\n\n    @Override\n    public void write(final JsonWriter out, final ZonedDateTime value) throws IOException {\n      out.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));\n    }\n\n    @Override\n    public ZonedDateTime read(final JsonReader in) throws IOException {\n      return ZonedDateTime.parse(in.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);\n    }\n  }\n\n  static final class LocalDateTimeTypeAdapter extends TypeAdapter<LocalDateTime> {\n\n    @Override\n    public void write(JsonWriter out, LocalDateTime value) throws IOException {\n      out.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));\n    }\n\n    @Override\n    public LocalDateTime read(JsonReader in) throws IOException {\n      return LocalDateTime.parse(in.nextString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);\n    }\n  }\n\n  static final class InstantTypeAdapter extends TypeAdapter<Instant> {\n\n    @Override\n    public void write(JsonWriter out, Instant value) throws IOException {\n      out.value(DateTimeFormatter.ISO_INSTANT.format(value));\n    }\n\n    @Override\n    public Instant read(JsonReader in) throws IOException {\n      return DateTimeFormatter.ISO_INSTANT.parse(in.nextString(), Instant::from);\n    }\n  }\n\n  static final class DurationTypeAdapter extends TypeAdapter<Duration> {\n\n    @Override\n    public void write(JsonWriter out, Duration value) throws IOException {\n      out.value(value.get(ChronoUnit.SECONDS));\n    }\n\n    @Override\n    public Duration read(JsonReader in) throws IOException {\n      return Duration.of(in.nextLong(), ChronoUnit.SECONDS);\n    }\n  }\n\n  static final class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {\n\n    @Override\n    public void write(JsonWriter out, LocalDate value) throws IOException {\n      out.value(DateTimeFormatter.ISO_LOCAL_DATE.format(value));\n    }\n\n    @Override\n    public LocalDate read(JsonReader in) throws IOException {\n      return DateTimeFormatter.ISO_LOCAL_DATE.parse(in.nextString(), LocalDate::from);\n    }\n  }\n\n  static final class MonthDayTypeAdapter extends TypeAdapter<MonthDay> {\n\n    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"d/M\");\n\n    @Override\n    public void write(JsonWriter out, MonthDay value) throws IOException {\n      out.value(value.format(formatter));\n    }\n\n    @Override\n    public MonthDay read(JsonReader in) throws IOException {\n      return MonthDay.parse(in.nextString(), formatter);\n    }\n  }\n\n  static final class PeriodTypeAdapter extends TypeAdapter<Period> {\n\n    @Override\n    public void write(JsonWriter out, Period value) throws IOException {\n      out.value(value.toString());\n    }\n\n    @Override\n    public Period read(JsonReader in) throws IOException {\n      return Period.parse(in.nextString());\n    }\n  }\n\n  static final class YearTypeAdapter extends TypeAdapter<Year> {\n\n    @Override\n    public void write(JsonWriter out, Year value) throws IOException {\n      out.value(value.getValue());\n    }\n\n    @Override\n    public Year read(JsonReader in) throws IOException {\n      return Year.of(in.nextInt());\n    }\n  }\n\n  static final class YearMonthAdapter extends TypeAdapter<YearMonth> {\n\n    @Override\n    public void write(JsonWriter out, YearMonth value) throws IOException {\n      out.value(value.toString());\n    }\n\n    @Override\n    public YearMonth read(JsonReader in) throws IOException {\n      return YearMonth.parse(in.nextString());\n    }\n  }\n\n  static final class UtcDateTypeAdapter extends TypeAdapter<Date> {\n    private final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone(\"UTC\");\n\n    @Override\n    public void write(JsonWriter out, Date date) throws IOException {\n      if (date == null) {\n        out.nullValue();\n      } else {\n        String value = format(date, true, UTC_TIME_ZONE);\n        out.value(value);\n      }\n    }\n\n    @Override\n    public Date read(JsonReader in) throws IOException {\n      try {\n        if (in.peek() == JsonToken.NULL) {\n          in.nextNull();\n          return null;\n        }\n        String date = in.nextString();\n        // Instead of using iso8601Format.parse(value), we use Jackson's date parsing\n        // This is because Android doesn't support XXX because it is JDK 1.6\n        return parse(date, new ParsePosition(0));\n      } catch (ParseException e) {\n        throw new JsonParseException(e);\n      }\n    }\n\n    // Date parsing code from Jackson databind ISO8601Utils.java\n    // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java\n    private static final String GMT_ID = \"GMT\";\n\n    /**\n     * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]\n     *\n     * @param date the date to format\n     * @param millis true to include millis precision otherwise false\n     * @param tz timezone to use for the formatting (GMT will produce 'Z')\n     * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]\n     */\n    private static String format(Date date, boolean millis, TimeZone tz) {\n      Calendar calendar = new GregorianCalendar(tz, Locale.US);\n      calendar.setTime(date);\n\n      // estimate capacity of buffer as close as we can (yeah, that's pedantic ;)\n      int capacity = \"yyyy-MM-ddThh:mm:ss\".length();\n      capacity += millis ? \".sss\".length() : 0;\n      capacity += tz.getRawOffset() == 0 ? \"Z\".length() : \"+hh:mm\".length();\n      StringBuilder formatted = new StringBuilder(capacity);\n\n      padInt(formatted, calendar.get(Calendar.YEAR), \"yyyy\".length());\n      formatted.append('-');\n      padInt(formatted, calendar.get(Calendar.MONTH) + 1, \"MM\".length());\n      formatted.append('-');\n      padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), \"dd\".length());\n      formatted.append('T');\n      padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), \"hh\".length());\n      formatted.append(':');\n      padInt(formatted, calendar.get(Calendar.MINUTE), \"mm\".length());\n      formatted.append(':');\n      padInt(formatted, calendar.get(Calendar.SECOND), \"ss\".length());\n      if (millis) {\n        formatted.append('.');\n        padInt(formatted, calendar.get(Calendar.MILLISECOND), \"sss\".length());\n      }\n\n      int offset = tz.getOffset(calendar.getTimeInMillis());\n      if (offset != 0) {\n        int hours = Math.abs((offset / (60 * 1000)) / 60);\n        int minutes = Math.abs((offset / (60 * 1000)) % 60);\n        formatted.append(offset < 0 ? '-' : '+');\n        padInt(formatted, hours, \"hh\".length());\n        formatted.append(':');\n        padInt(formatted, minutes, \"mm\".length());\n      } else {\n        formatted.append('Z');\n      }\n\n      return formatted.toString();\n    }\n\n    /**\n     * Zero pad a number to a specified length\n     *\n     * @param buffer buffer to use for padding\n     * @param value the integer value to pad if necessary.\n     * @param length the length of the string we should zero pad\n     */\n    private static void padInt(StringBuilder buffer, int value, int length) {\n      String strValue = Integer.toString(value);\n      buffer.append(\"0\".repeat(Math.max(0, length - strValue.length())));\n      buffer.append(strValue);\n    }\n\n    /**\n     * Parse a date from ISO-8601 formatted string. It expects a format\n     * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]\n     *\n     * @param date ISO string to parse in the appropriate format.\n     * @param pos The position to start parsing from, updated to where parsing stopped.\n     * @return the parsed date\n     * @throws ParseException if the date is not in the appropriate format\n     */\n    private static Date parse(String date, ParsePosition pos) throws ParseException {\n      Exception fail;\n      try {\n        int offset = pos.getIndex();\n\n        // extract year\n        int year = parseInt(date, offset, offset += 4);\n        if (checkOffset(date, offset, '-')) {\n          offset += 1;\n        }\n\n        // extract month\n        int month = parseInt(date, offset, offset += 2);\n        if (checkOffset(date, offset, '-')) {\n          offset += 1;\n        }\n\n        // extract day\n        int day = parseInt(date, offset, offset += 2);\n        // default time value\n        int hour = 0;\n        int minutes = 0;\n        int seconds = 0;\n        int milliseconds =\n            0; // always use 0 otherwise returned date will include millis of current time\n        if (checkOffset(date, offset, 'T')) {\n\n          // extract hours, minutes, seconds and milliseconds\n          hour = parseInt(date, offset += 1, offset += 2);\n          if (checkOffset(date, offset, ':')) {\n            offset += 1;\n          }\n\n          minutes = parseInt(date, offset, offset += 2);\n          if (checkOffset(date, offset, ':')) {\n            offset += 1;\n          }\n          // second and milliseconds can be optional\n          if (date.length() > offset) {\n            char c = date.charAt(offset);\n            if (c != 'Z' && c != '+' && c != '-') {\n              seconds = parseInt(date, offset, offset += 2);\n              // milliseconds can be optional in the format\n              if (checkOffset(date, offset, '.')) {\n                milliseconds = parseInt(date, offset += 1, offset += 3);\n              }\n            }\n          }\n        }\n\n        // extract timezone\n        String timezoneId;\n        if (date.length() <= offset) {\n          throw new IllegalArgumentException(\"No time zone indicator\");\n        }\n        char timezoneIndicator = date.charAt(offset);\n        if (timezoneIndicator == '+' || timezoneIndicator == '-') {\n          String timezoneOffset = date.substring(offset);\n          timezoneId = GMT_ID + timezoneOffset;\n          offset += timezoneOffset.length();\n        } else if (timezoneIndicator == 'Z') {\n          timezoneId = GMT_ID;\n          offset += 1;\n        } else {\n          throw new IndexOutOfBoundsException(\"Invalid time zone indicator \" + timezoneIndicator);\n        }\n\n        TimeZone timezone = TimeZone.getTimeZone(timezoneId);\n        if (!timezone.getID().equals(timezoneId)) {\n          throw new IndexOutOfBoundsException();\n        }\n\n        Calendar calendar = new GregorianCalendar(timezone);\n        calendar.setLenient(false);\n        calendar.set(Calendar.YEAR, year);\n        calendar.set(Calendar.MONTH, month - 1);\n        calendar.set(Calendar.DAY_OF_MONTH, day);\n        calendar.set(Calendar.HOUR_OF_DAY, hour);\n        calendar.set(Calendar.MINUTE, minutes);\n        calendar.set(Calendar.SECOND, seconds);\n        calendar.set(Calendar.MILLISECOND, milliseconds);\n\n        pos.setIndex(offset);\n        return calendar.getTime();\n        // If we get a ParseException it'll already have the right message/offset.\n        // Other exception types can convert here.\n      } catch (IndexOutOfBoundsException | IllegalArgumentException e) {\n        fail = e;\n      }\n      String input = (date == null) ? null : (\"'\" + date + \"'\");\n      throw new ParseException(\n          \"Failed to parse date [\" + input + \"]: \" + fail.getMessage(), pos.getIndex());\n    }\n\n    /**\n     * Check if the expected character exist at the given offset in the value.\n     *\n     * @param value the string to check at the specified offset\n     * @param offset the offset to look for the expected character\n     * @param expected the expected character\n     * @return true if the expected character exist at the given offset\n     */\n    private static boolean checkOffset(String value, int offset, char expected) {\n      return (offset < value.length()) && (value.charAt(offset) == expected);\n    }\n\n    /**\n     * Parse an integer located between 2 given offsets in a string\n     *\n     * @param value the string to parse\n     * @param beginIndex the start index for the integer in the string\n     * @param endIndex the end index for the integer in the string\n     * @return the int\n     * @throws NumberFormatException if the value is not a number\n     */\n    private static int parseInt(String value, int beginIndex, int endIndex)\n        throws NumberFormatException {\n      if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {\n        throw new NumberFormatException(value);\n      }\n      // use same logic as in Integer.parseInt() but less generic we're not supporting negative\n      // values\n      int i = beginIndex;\n      int result = 0;\n      int digit;\n      if (i < endIndex) {\n        digit = Character.digit(value.charAt(i++), 10);\n        if (digit < 0) {\n          throw new NumberFormatException(\"Invalid number: \" + value);\n        }\n        result = -digit;\n      }\n      while (i < endIndex) {\n        digit = Character.digit(value.charAt(i++), 10);\n        if (digit < 0) {\n          throw new NumberFormatException(\"Invalid number: \" + value);\n        }\n        result *= 10;\n        result -= digit;\n      }\n      return -result;\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultMigrationManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\n\nimport java.io.PrintWriter;\nimport java.io.Writer;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Simple database migration manager. Inspired by Flyway, Liquibase, Morf etc, just trimmed down for\n * minimum dependencies.\n */\n@Slf4j\nclass DefaultMigrationManager {\n\n  private static final Executor basicExecutor =\n      runnable -> {\n        new Thread(runnable).start();\n      };\n\n  private static CountDownLatch waitLatch;\n  private static CountDownLatch readyLatch;\n\n  static void withLatch(CountDownLatch readyLatch, Consumer<CountDownLatch> runnable) {\n    waitLatch = new CountDownLatch(1);\n    DefaultMigrationManager.readyLatch = readyLatch;\n    try {\n      runnable.accept(waitLatch);\n    } finally {\n      waitLatch = null;\n      DefaultMigrationManager.readyLatch = null;\n    }\n  }\n\n  static void migrate(TransactionManager transactionManager, Dialect dialect) {\n    transactionManager.inTransaction(\n        transaction -> {\n          try {\n            int currentVersion = currentVersion(transaction.connection(), dialect);\n            dialect\n                .getMigrations()\n                .filter(migration -> migration.getVersion() > currentVersion)\n                .forEach(\n                    migration ->\n                        uncheck(\n                            () -> runSql(transactionManager, transaction.connection(), migration)));\n          } catch (Exception e) {\n            throw new RuntimeException(\"Migrations failed\", e);\n          }\n        });\n  }\n\n  static void writeSchema(Writer writer, Dialect dialect) {\n    PrintWriter printWriter = new PrintWriter(writer);\n    dialect\n        .getMigrations()\n        .forEach(\n            migration -> {\n              printWriter.print(\"-- \");\n              printWriter.print(migration.getVersion());\n              printWriter.print(\": \");\n              printWriter.println(migration.getName());\n              if (migration.getSql() == null || migration.getSql().isEmpty()) {\n                printWriter.println(\"-- Nothing for \" + dialect);\n              } else {\n                printWriter.println(migration.getSql());\n              }\n              printWriter.println();\n            });\n    printWriter.flush();\n  }\n\n  private static void runSql(TransactionManager txm, Connection connection, Migration migration)\n      throws SQLException {\n    log.info(\"Running migration {}: {}\", migration.getVersion(), migration.getName());\n\n    if (migration.getSql() != null && !migration.getSql().isEmpty()) {\n      CompletableFuture.runAsync(\n              () -> {\n                try {\n                  txm.inTransactionThrows(\n                      tx -> {\n                        try (var s = tx.connection().prepareStatement(migration.getSql())) {\n                          s.execute();\n                        }\n                      });\n                } catch (SQLException e) {\n                  throw new RuntimeException(e);\n                }\n              },\n              basicExecutor)\n          .join();\n    }\n\n    try (var s = connection.prepareStatement(\"UPDATE TXNO_VERSION SET version = ?\")) {\n      s.setInt(1, migration.getVersion());\n      if (s.executeUpdate() != 1) {\n        throw new IllegalStateException(\"Version table should already exist\");\n      }\n    }\n  }\n\n  private static int currentVersion(Connection connection, Dialect dialect) throws SQLException {\n    dialect.createVersionTableIfNotExists(connection);\n    int version = fetchCurrentVersion(connection, dialect);\n    if (version >= 0) {\n      return version;\n    }\n    try {\n      log.info(\"No version record found. Attempting to create\");\n      if (waitLatch != null) {\n        log.info(\"Stopping at latch\");\n        readyLatch.countDown();\n        if (!waitLatch.await(10, TimeUnit.SECONDS)) {\n          throw new IllegalStateException(\"Latch not released in 10 seconds\");\n        }\n        log.info(\"Latch released\");\n      }\n      try (var s = connection.prepareStatement(\"INSERT INTO TXNO_VERSION (version) VALUES (0)\")) {\n        s.executeUpdate();\n      }\n      log.info(\"Created version record.\");\n      return fetchCurrentVersion(connection, dialect);\n    } catch (Exception e) {\n      log.info(\n          \"Error attempting to create ({} - {}). May have been beaten to it, attempting second fetch\",\n          e.getClass().getSimpleName(),\n          e.getMessage());\n      version = fetchCurrentVersion(connection, dialect);\n      if (version >= 0) {\n        return version;\n      }\n      throw new IllegalStateException(\"Unable to read or create version record\", e);\n    }\n  }\n\n  private static int fetchCurrentVersion(Connection connection, Dialect dialect)\n      throws SQLException {\n    try (PreparedStatement s = connection.prepareStatement(dialect.getFetchCurrentVersion());\n        ResultSet rs = s.executeQuery()) {\n      if (rs.next()) {\n        var version = rs.getInt(1);\n        log.info(\"Current version is {}, obtained lock\", version);\n        if (rs.next()) {\n          throw new IllegalStateException(\"More than one version record\");\n        }\n        return version;\n      }\n      return -1;\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DefaultPersistor.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.io.*;\nimport java.sql.*;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * The default {@link Persistor} for {@link TransactionOutbox}.\n *\n * <p>Saves requests to a relational database table, by default called {@code TXNO_OUTBOX}. This can\n * optionally be automatically created and upgraded by {@link DefaultPersistor}, although this\n * behaviour can be disabled if you wish.\n *\n * <p>More significant changes can be achieved by subclassing, which is explicitly supported. If, on\n * the other hand, you want to use a completely non-relational underlying data store or do something\n * equally esoteric, you may prefer to implement {@link Persistor} from the ground up.\n */\n@Slf4j\n@SuperBuilder\n@AllArgsConstructor(access = AccessLevel.PROTECTED)\npublic class DefaultPersistor implements Persistor, Validatable {\n\n  private static final String ALL_FIELDS =\n      \"id, uniqueRequestId, invocation, topic, seq, lastAttemptTime, nextAttemptTime, attempts, blocked, processed, version\";\n\n  /**\n   * @param writeLockTimeoutSeconds How many seconds to wait before timing out on obtaining a write\n   *     lock. There's no point making this long; it's always better to just back off as quickly as\n   *     possible and try another record. Generally these lock timeouts only kick in if the database\n   *     does not support skip locking.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Builder.Default\n  private final int writeLockTimeoutSeconds = 2;\n\n  /**\n   * @param dialect The database dialect to use. Required.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  private final Dialect dialect;\n\n  /**\n   * @param tableName The database table name. The default is {@code TXNO_OUTBOX}.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Builder.Default\n  private final String tableName = \"TXNO_OUTBOX\";\n\n  /**\n   * @param migrate Set to false to disable automatic database migrations. This may be preferred if\n   *     the default migration behaviour interferes with your existing toolset, and you prefer to\n   *     manage the migrations explicitly (e.g. using FlyWay or Liquibase), or you do not give the\n   *     application DDL permissions at runtime. You may use {@link #writeSchema(Writer)} to access\n   *     the migrations.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Builder.Default\n  private final boolean migrate = true;\n\n  /**\n   * @param serializer The serializer to use for {@link Invocation}s. See {@link\n   *     InvocationSerializer} for more information. Defaults to {@link\n   *     InvocationSerializer#createDefaultJsonSerializer()} with no custom serializable classes.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Builder.Default\n  private final InvocationSerializer serializer =\n      InvocationSerializer.createDefaultJsonSerializer();\n\n  @Override\n  public void validate(Validator validator) {\n    validator.notNull(\"dialect\", dialect);\n    validator.notNull(\"tableName\", tableName);\n  }\n\n  @Override\n  public void migrate(TransactionManager transactionManager) {\n    if (migrate) {\n      DefaultMigrationManager.migrate(transactionManager, dialect);\n    }\n  }\n\n  /**\n   * Provides access to the database schema so that you may optionally use your existing toolset to\n   * manage migrations.\n   *\n   * @param writer The writer to which the migrations are written.\n   */\n  public void writeSchema(Writer writer) {\n    DefaultMigrationManager.writeSchema(writer, dialect);\n  }\n\n  @Override\n  public void save(Transaction tx, TransactionOutboxEntry entry)\n      throws SQLException, AlreadyScheduledException {\n    var insertSql =\n        \"INSERT INTO \"\n            + tableName\n            + \" (\"\n            + ALL_FIELDS\n            + \") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\";\n    var writer = new StringWriter();\n    serializer.serializeInvocation(entry.getInvocation(), writer);\n    if (entry.getTopic() != null) {\n      setNextSequence(tx, entry);\n      log.info(\"Assigned sequence number {} to topic {}\", entry.getSequence(), entry.getTopic());\n    }\n    PreparedStatement stmt = tx.prepareBatchStatement(insertSql);\n    setupInsert(entry, writer, stmt);\n    if (entry.getUniqueRequestId() == null) {\n      stmt.addBatch();\n      log.debug(\"Inserted {} in batch\", entry.description());\n    } else {\n      try {\n        stmt.executeUpdate();\n        log.debug(\"Inserted {} immediately\", entry.description());\n      } catch (Exception e) {\n        if (indexViolation(e)) {\n          throw new AlreadyScheduledException(\n              \"Request \" + entry.description() + \" already exists\", e);\n        }\n        throw e;\n      }\n    }\n  }\n\n  @Override\n  public Invocation serializeAndDeserialize(Invocation invocation) {\n    try (var baos = new ByteArrayOutputStream()) {\n      try (var writer = new OutputStreamWriter(baos, UTF_8)) {\n        serializer.serializeInvocation(invocation, writer);\n      }\n      ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());\n      try (Reader reader = new InputStreamReader(bais, UTF_8)) {\n        return serializer.deserializeInvocation(reader);\n      }\n    } catch (IOException e) {\n      throw new UncheckedException(e);\n    }\n  }\n\n  private void setNextSequence(Transaction tx, TransactionOutboxEntry entry) throws SQLException {\n    //noinspection resource\n    var seqSelect = tx.prepareBatchStatement(dialect.getFetchNextSequence());\n    seqSelect.setString(1, entry.getTopic());\n    try (ResultSet rs = seqSelect.executeQuery()) {\n      if (rs.next()) {\n        entry.setSequence(rs.getLong(1) + 1L);\n        //noinspection resource\n        var seqUpdate =\n            tx.prepareBatchStatement(\"UPDATE TXNO_SEQUENCE SET seq = ? WHERE topic = ?\");\n        seqUpdate.setLong(1, entry.getSequence());\n        seqUpdate.setString(2, entry.getTopic());\n        seqUpdate.executeUpdate();\n      } else {\n        try {\n          entry.setSequence(1L);\n          //noinspection resource\n          var seqInsert =\n              tx.prepareBatchStatement(\"INSERT INTO TXNO_SEQUENCE (topic, seq) VALUES (?, ?)\");\n          seqInsert.setString(1, entry.getTopic());\n          seqInsert.setLong(2, entry.getSequence());\n          seqInsert.executeUpdate();\n        } catch (Exception e) {\n          if (indexViolation(e)) {\n            setNextSequence(tx, entry);\n          } else {\n            throw e;\n          }\n        }\n      }\n    }\n  }\n\n  private boolean indexViolation(Exception e) {\n    return (e instanceof SQLIntegrityConstraintViolationException)\n        || (e.getClass().getName().equals(\"org.postgresql.util.PSQLException\")\n            && e.getMessage().contains(\"constraint\"))\n        || (e.getClass().getName().equals(\"com.microsoft.sqlserver.jdbc.SQLServerException\")\n            && e.getMessage().contains(\"duplicate key\"));\n  }\n\n  private void setupInsert(\n      TransactionOutboxEntry entry, StringWriter writer, PreparedStatement stmt)\n      throws SQLException {\n    stmt.setString(1, entry.getId());\n    stmt.setString(2, entry.getUniqueRequestId());\n    stmt.setString(3, writer.toString());\n    stmt.setString(4, entry.getTopic() == null ? \"*\" : entry.getTopic());\n    if (entry.getSequence() == null) {\n      stmt.setObject(5, null);\n    } else {\n      stmt.setLong(5, entry.getSequence());\n    }\n    stmt.setTimestamp(\n        6, entry.getLastAttemptTime() == null ? null : Timestamp.from(entry.getLastAttemptTime()));\n    stmt.setTimestamp(7, Timestamp.from(entry.getNextAttemptTime()));\n    stmt.setInt(8, entry.getAttempts());\n    stmt.setBoolean(9, entry.isBlocked());\n    stmt.setBoolean(10, entry.isProcessed());\n    stmt.setInt(11, entry.getVersion());\n  }\n\n  @Override\n  public void delete(Transaction tx, TransactionOutboxEntry entry) throws Exception {\n    //noinspection resource\n    try (PreparedStatement stmt =\n        tx.connection().prepareStatement(dialect.getDelete().replace(\"{{table}}\", tableName))) {\n      stmt.setString(1, entry.getId());\n      stmt.setInt(2, entry.getVersion());\n      if (stmt.executeUpdate() != 1) {\n        throw new OptimisticLockException();\n      }\n      log.debug(\"Deleted {}\", entry.description());\n    }\n  }\n\n  @Override\n  public void update(Transaction tx, TransactionOutboxEntry entry) throws Exception {\n    //noinspection resource\n    try (PreparedStatement stmt =\n        tx.connection()\n            .prepareStatement(\n                // language=MySQL\n                \"UPDATE \"\n                    + tableName\n                    + \" \"\n                    + \"SET lastAttemptTime = ?, nextAttemptTime = ?, attempts = ?, blocked = ?, processed = ?, version = ? \"\n                    + \"WHERE id = ? and version = ?\")) {\n      stmt.setTimestamp(\n          1,\n          entry.getLastAttemptTime() == null ? null : Timestamp.from(entry.getLastAttemptTime()));\n      stmt.setTimestamp(2, Timestamp.from(entry.getNextAttemptTime()));\n      stmt.setInt(3, entry.getAttempts());\n      stmt.setBoolean(4, entry.isBlocked());\n      stmt.setBoolean(5, entry.isProcessed());\n      stmt.setInt(6, entry.getVersion() + 1);\n      stmt.setString(7, entry.getId());\n      stmt.setInt(8, entry.getVersion());\n      if (stmt.executeUpdate() != 1) {\n        throw new OptimisticLockException();\n      }\n      entry.setVersion(entry.getVersion() + 1);\n      log.debug(\"Updated {}\", entry.description());\n    }\n  }\n\n  @Override\n  public boolean lock(Transaction tx, TransactionOutboxEntry entry) throws Exception {\n    //noinspection resource\n    try (PreparedStatement stmt =\n        tx.connection()\n            .prepareStatement(\n                dialect\n                    .getLock()\n                    .replace(\"{{table}}\", tableName)\n                    .replace(\"{{allFields}}\", ALL_FIELDS))) {\n      stmt.setString(1, entry.getId());\n      stmt.setInt(2, entry.getVersion());\n      stmt.setQueryTimeout(writeLockTimeoutSeconds);\n      try {\n        try (ResultSet rs = stmt.executeQuery()) {\n          if (!rs.next()) {\n            return false;\n          }\n          // Ensure that subsequent processing uses a deserialized invocation rather than\n          // the object from the caller, which might not serialize well and thus cause a\n          // difference between immediate and retry processing\n          try (Reader invocationStream = rs.getCharacterStream(\"invocation\")) {\n            entry.setInvocation(serializer.deserializeInvocation(invocationStream));\n          }\n          return true;\n        }\n      } catch (SQLTimeoutException e) {\n        log.debug(\"Lock attempt timed out on {}\", entry.description());\n        return false;\n      }\n    }\n  }\n\n  @Override\n  public boolean unblock(Transaction tx, String entryId) throws Exception {\n    //noinspection resource\n    try (PreparedStatement stmt =\n        tx.connection()\n            .prepareStatement(\n                \"UPDATE \"\n                    + tableName\n                    + \" SET attempts = 0, blocked = \"\n                    + dialect.booleanValue(false)\n                    + \" \"\n                    + \"WHERE blocked = \"\n                    + dialect.booleanValue(true)\n                    + \" AND processed = \"\n                    + dialect.booleanValue(false)\n                    + \" AND id = ?\")) {\n      stmt.setString(1, entryId);\n      stmt.setQueryTimeout(writeLockTimeoutSeconds);\n      return stmt.executeUpdate() != 0;\n    }\n  }\n\n  @Override\n  public List<TransactionOutboxEntry> selectBatch(Transaction tx, int batchSize, Instant now)\n      throws Exception {\n    //noinspection resource\n    try (PreparedStatement stmt =\n        tx.connection()\n            .prepareStatement(\n                dialect\n                    .getSelectBatch()\n                    .replace(\"{{table}}\", tableName)\n                    .replace(\"{{batchSize}}\", Integer.toString(batchSize))\n                    .replace(\"{{allFields}}\", ALL_FIELDS))) {\n      stmt.setTimestamp(1, Timestamp.from(now));\n      var result = new ArrayList<TransactionOutboxEntry>(batchSize);\n      gatherResults(stmt, result);\n      return result;\n    }\n  }\n\n  @Override\n  public Collection<TransactionOutboxEntry> selectNextInTopics(\n      Transaction tx, int batchSize, Instant now) throws Exception {\n    var sql =\n        dialect\n            .getFetchNextInAllTopics()\n            .replace(\"{{table}}\", tableName)\n            .replace(\"{{batchSize}}\", Integer.toString(batchSize))\n            .replace(\"{{allFields}}\", ALL_FIELDS);\n    //noinspection resource\n    try (PreparedStatement stmt = tx.connection().prepareStatement(sql)) {\n      stmt.setTimestamp(1, Timestamp.from(now));\n      var results = new ArrayList<TransactionOutboxEntry>();\n      gatherResults(stmt, results);\n      return results;\n    }\n  }\n\n  @Override\n  public Collection<TransactionOutboxEntry> selectNextInSelectedTopics(\n      Transaction tx, List<String> topicNames, int batchSize, Instant now) throws Exception {\n\n    var topicsInParameterList = topicNames.stream().map(it -> \"?\").collect(Collectors.joining(\",\"));\n    var sql =\n        dialect\n            .getFetchNextInSelectedTopics()\n            .replace(\"{{table}}\", tableName)\n            .replace(\"{{topicNames}}\", topicsInParameterList)\n            .replace(\"{{batchSize}}\", Integer.toString(batchSize))\n            .replace(\"{{allFields}}\", ALL_FIELDS);\n    //noinspection resource\n    try (PreparedStatement stmt = tx.connection().prepareStatement(sql)) {\n      var counter = 1;\n      for (var topicName : topicNames) {\n        stmt.setString(counter, topicName);\n        counter++;\n      }\n      stmt.setTimestamp(counter, Timestamp.from(now));\n      var results = new ArrayList<TransactionOutboxEntry>();\n      gatherResults(stmt, results);\n      return results;\n    }\n  }\n\n  @Override\n  public int deleteProcessedAndExpired(Transaction tx, int batchSize, Instant now)\n      throws Exception {\n    //noinspection resource\n    try (PreparedStatement stmt =\n        tx.connection()\n            .prepareStatement(\n                dialect\n                    .getDeleteExpired()\n                    .replace(\"{{table}}\", tableName)\n                    .replace(\"{{batchSize}}\", Integer.toString(batchSize)))) {\n      stmt.setTimestamp(1, Timestamp.from(now));\n      return stmt.executeUpdate();\n    }\n  }\n\n  private void gatherResults(PreparedStatement stmt, Collection<TransactionOutboxEntry> output)\n      throws SQLException, IOException {\n    try (ResultSet rs = stmt.executeQuery()) {\n      while (rs.next()) {\n        output.add(map(rs));\n      }\n      log.debug(\"Found {} results\", output.size());\n    }\n  }\n\n  private TransactionOutboxEntry map(ResultSet rs) throws SQLException, IOException {\n    String topic = rs.getString(\"topic\");\n    Long sequence = rs.getLong(\"seq\");\n    if (rs.wasNull()) {\n      sequence = null;\n    }\n    // Reading invocationStream *must* occur first because some drivers (ex. SQL Server)\n    // implement true streams that are not buffered in memory. Calling any other getter\n    // on ResultSet before invocationStream is read will cause Reader to be closed\n    // prematurely.\n    try (Reader invocationStream = rs.getCharacterStream(\"invocation\")) {\n      Invocation invocation;\n      try {\n        invocation = serializer.deserializeInvocation(invocationStream);\n      } catch (IOException e) {\n        invocation = new FailedDeserializingInvocation(e);\n      }\n      TransactionOutboxEntry entry =\n          TransactionOutboxEntry.builder()\n              .invocation(invocation)\n              .id(rs.getString(\"id\"))\n              .uniqueRequestId(rs.getString(\"uniqueRequestId\"))\n              .topic(\"*\".equals(topic) ? null : topic)\n              .sequence(sequence)\n              .lastAttemptTime(\n                  rs.getTimestamp(\"lastAttemptTime\") == null\n                      ? null\n                      : rs.getTimestamp(\"lastAttemptTime\").toInstant())\n              .nextAttemptTime(rs.getTimestamp(\"nextAttemptTime\").toInstant())\n              .attempts(rs.getInt(\"attempts\"))\n              .blocked(rs.getBoolean(\"blocked\"))\n              .processed(rs.getBoolean(\"processed\"))\n              .version(rs.getInt(\"version\"))\n              .build();\n      log.debug(\"Found {}\", entry);\n      return entry;\n    }\n  }\n\n  // For testing. Assumed low volume.\n  @Override\n  public void clear(Transaction tx) throws SQLException {\n    //noinspection resource\n    try (Statement stmt = tx.connection().createStatement()) {\n      stmt.execute(\"DELETE FROM \" + tableName);\n    }\n  }\n\n  @Override\n  public boolean checkConnection(Transaction tx) throws SQLException {\n    //noinspection resource\n    try (Statement stmt = tx.connection().createStatement();\n        ResultSet rs = stmt.executeQuery(dialect.getCheckSql())) {\n      return rs.next() && (rs.getInt(1) == 1);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Dialect.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.Statement;\nimport java.util.stream.Stream;\n\n/** The SQL dialects supported by {@link DefaultPersistor}. */\npublic interface Dialect {\n  String getDelete();\n\n  /**\n   * @return Format string for the SQL required to delete expired retained records.\n   */\n  String getDeleteExpired();\n\n  String getSelectBatch();\n\n  String getLock();\n\n  String getCheckSql();\n\n  String getFetchNextInAllTopics();\n\n  String getFetchNextInSelectedTopics();\n\n  String getFetchCurrentVersion();\n\n  String getFetchNextSequence();\n\n  String booleanValue(boolean criteriaValue);\n\n  void createVersionTableIfNotExists(Connection connection) throws SQLException;\n\n  Stream<Migration> getMigrations();\n\n  Dialect MY_SQL_5 =\n      DefaultDialect.builder(\"MY_SQL_5\")\n          .changeMigration(\n              13,\n              \"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN invocation mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci\")\n          .build();\n  Dialect MY_SQL_8 =\n      DefaultDialect.builder(\"MY_SQL_8\")\n          .fetchNextInAllTopics(\n              \"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn\"\n                  + \" FROM {{table}} WHERE processed = false AND topic <> '*')\"\n                  + \" SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}\")\n          .fetchNextInSelectedTopics(\n              \"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn\"\n                  + \" FROM {{table}} WHERE processed = false AND topic IN ({{topicNames}}))\"\n                  + \" SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}\")\n          .deleteExpired(\n              \"DELETE FROM {{table}} WHERE nextAttemptTime < ? AND processed = true AND blocked = false\"\n                  + \" LIMIT {{batchSize}}\")\n          .selectBatch(\n              \"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? \"\n                  + \"AND blocked = false AND processed = false AND topic = '*' LIMIT {{batchSize}} FOR UPDATE \"\n                  + \"SKIP LOCKED\")\n          .lock(\n              \"SELECT id, invocation FROM {{table}} WHERE id = ? AND version = ? FOR \"\n                  + \"UPDATE SKIP LOCKED\")\n          .changeMigration(\n              13,\n              \"ALTER TABLE TXNO_OUTBOX MODIFY COLUMN invocation mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci\")\n          .build();\n  Dialect POSTGRESQL_9 =\n      DefaultDialect.builder(\"POSTGRESQL_9\")\n          .fetchNextInAllTopics(\n              \"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn\"\n                  + \" FROM {{table}} WHERE processed = false AND topic <> '*')\"\n                  + \" SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}\")\n          .fetchNextInSelectedTopics(\n              \"WITH raw AS(SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn\"\n                  + \" FROM {{table}} WHERE processed = false AND topic IN ({{topicNames}}))\"\n                  + \" SELECT * FROM raw WHERE rn = 1 AND nextAttemptTime < ? LIMIT {{batchSize}}\")\n          .deleteExpired(\n              \"DELETE FROM {{table}} WHERE id IN \"\n                  + \"(SELECT id FROM {{table}} WHERE nextAttemptTime < ? AND processed = true AND blocked = false LIMIT {{batchSize}})\")\n          .selectBatch(\n              \"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? \"\n                  + \"AND blocked = false AND processed = false AND topic = '*' LIMIT \"\n                  + \"{{batchSize}} FOR UPDATE SKIP LOCKED\")\n          .lock(\n              \"SELECT id, invocation FROM {{table}} WHERE id = ? AND version = ? FOR \"\n                  + \"UPDATE SKIP LOCKED\")\n          .changeMigration(\n              5, \"ALTER TABLE TXNO_OUTBOX ALTER COLUMN uniqueRequestId TYPE VARCHAR(250)\")\n          .changeMigration(6, \"ALTER TABLE TXNO_OUTBOX RENAME COLUMN blacklisted TO blocked\")\n          .changeMigration(7, \"ALTER TABLE TXNO_OUTBOX ADD COLUMN lastAttemptTime TIMESTAMP(6)\")\n          .disableMigration(8)\n          .build();\n\n  Dialect H2 =\n      DefaultDialect.builder(\"H2\")\n          .changeMigration(5, \"ALTER TABLE TXNO_OUTBOX ALTER COLUMN uniqueRequestId VARCHAR(250)\")\n          .changeMigration(6, \"ALTER TABLE TXNO_OUTBOX RENAME COLUMN blacklisted TO blocked\")\n          .disableMigration(8)\n          .build();\n  Dialect ORACLE =\n      DefaultDialect.builder(\"ORACLE\")\n          .fetchNextInAllTopics(\n              \"WITH cte1 AS (SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn\"\n                  + \" FROM {{table}} WHERE processed = 0 AND topic <> '*')\"\n                  + \" SELECT * FROM cte1 WHERE rn = 1 AND nextAttemptTime < ? AND ROWNUM <= {{batchSize}}\")\n          .fetchNextInSelectedTopics(\n              \"WITH cte1 AS (SELECT {{allFields}}, (ROW_NUMBER() OVER(PARTITION BY topic ORDER BY seq)) as rn\"\n                  + \" FROM {{table}} WHERE processed = 0 AND topic IN ({{topicNames}}))\"\n                  + \" SELECT * FROM cte1 WHERE rn = 1 AND nextAttemptTime < ? AND ROWNUM <= {{batchSize}}\")\n          .deleteExpired(\n              \"DELETE FROM {{table}} WHERE nextAttemptTime < ? AND processed = 1 AND blocked = 0 \"\n                  + \"AND ROWNUM <= {{batchSize}}\")\n          .selectBatch(\n              \"SELECT {{allFields}} FROM {{table}} WHERE nextAttemptTime < ? \"\n                  + \"AND blocked = 0 AND processed = 0 AND topic = '*' AND ROWNUM <= {{batchSize}} FOR UPDATE \"\n                  + \"SKIP LOCKED\")\n          .lock(\n              \"SELECT id, invocation FROM {{table}} WHERE id = ? AND version = ? FOR \"\n                  + \"UPDATE SKIP LOCKED\")\n          .checkSql(\"SELECT 1 FROM DUAL\")\n          .changeMigration(\n              1,\n              \"CREATE TABLE TXNO_OUTBOX (\\n\"\n                  + \"    id VARCHAR2(36) PRIMARY KEY,\\n\"\n                  + \"    invocation CLOB,\\n\"\n                  + \"    nextAttemptTime TIMESTAMP(6),\\n\"\n                  + \"    attempts NUMBER,\\n\"\n                  + \"    blacklisted NUMBER(1),\\n\"\n                  + \"    version NUMBER\\n\"\n                  + \")\")\n          .changeMigration(\n              2, \"ALTER TABLE TXNO_OUTBOX ADD uniqueRequestId VARCHAR(100) NULL UNIQUE\")\n          .changeMigration(3, \"ALTER TABLE TXNO_OUTBOX ADD processed NUMBER(1)\")\n          .changeMigration(5, \"ALTER TABLE TXNO_OUTBOX MODIFY uniqueRequestId VARCHAR2(250)\")\n          .changeMigration(6, \"ALTER TABLE TXNO_OUTBOX RENAME COLUMN blacklisted TO blocked\")\n          .changeMigration(7, \"ALTER TABLE TXNO_OUTBOX ADD lastAttemptTime TIMESTAMP(6)\")\n          .disableMigration(8)\n          .changeMigration(9, \"ALTER TABLE TXNO_OUTBOX ADD topic VARCHAR(250) DEFAULT '*' NOT NULL\")\n          .changeMigration(10, \"ALTER TABLE TXNO_OUTBOX ADD seq NUMBER\")\n          .changeMigration(\n              11,\n              \"CREATE TABLE TXNO_SEQUENCE (topic VARCHAR(250) NOT NULL, seq NUMBER NOT NULL, CONSTRAINT PK_TXNO_SEQUENCE PRIMARY KEY (topic, seq))\")\n          .booleanValueFrom(v -> v ? \"1\" : \"0\")\n          .createVersionTableBy(\n              connection -> {\n                try (Statement s = connection.createStatement()) {\n                  try {\n                    s.execute(\"CREATE TABLE TXNO_VERSION (version NUMBER)\");\n                  } catch (SQLException e) {\n                    // oracle code for name already used by an existing object\n                    if (!e.getMessage().contains(\"955\")) {\n                      throw e;\n                    }\n                  }\n                }\n              })\n          .build();\n\n  Dialect MS_SQL_SERVER =\n      DefaultDialect.builder(\"MS_SQL_SERVER\")\n          .lock(\n              \"SELECT id, invocation FROM {{table}} WITH (UPDLOCK, ROWLOCK, READPAST) WHERE id = ? AND version = ?\")\n          .selectBatch(\n              \"SELECT TOP ({{batchSize}}) {{allFields}} FROM {{table}} \"\n                  + \"WITH (UPDLOCK, ROWLOCK, READPAST) WHERE nextAttemptTime < ? AND topic = '*' \"\n                  + \"AND blocked = 0 AND processed = 0\")\n          .delete(\"DELETE FROM {{table}} WITH (ROWLOCK, READPAST) WHERE id = ? and version = ?\")\n          .deleteExpired(\n              \"DELETE  TOP ({{batchSize}}) FROM {{table}} \"\n                  + \"WHERE nextAttemptTime < ? AND processed = 1 AND blocked = 0\")\n          .fetchCurrentVersion(\"SELECT version FROM TXNO_VERSION WITH (UPDLOCK, ROWLOCK, READPAST)\")\n          .fetchNextInAllTopics(\n              \"SELECT TOP {{batchSize}} {{allFields}} FROM {{table}} a\"\n                  + \" WHERE processed = 0 AND topic <> '*' AND nextAttemptTime < ?\"\n                  + \" AND seq = (\"\n                  + \"SELECT MIN(seq) FROM {{table}} b WHERE b.topic=a.topic AND b.processed = 0\"\n                  + \")\")\n          .fetchNextInSelectedTopics(\n              \"SELECT TOP {{batchSize}} {{allFields}} FROM {{table}} a\"\n                  + \" WHERE processed = 0 AND topic IN ({{topicNames}}) AND nextAttemptTime < ?\"\n                  + \" AND seq = (\"\n                  + \"SELECT MIN(seq) FROM {{table}} b WHERE b.topic=a.topic AND b.processed = 0\"\n                  + \")\")\n          .fetchNextSequence(\n              \"SELECT seq FROM TXNO_SEQUENCE WITH (UPDLOCK, ROWLOCK, READPAST) WHERE topic = ?\")\n          .booleanValueFrom(v -> v ? \"1\" : \"0\")\n          .changeMigration(\n              1,\n              \"CREATE TABLE TXNO_OUTBOX (\\n\"\n                  + \"    id VARCHAR(36) PRIMARY KEY,\\n\"\n                  + \"    invocation NVARCHAR(MAX),\\n\"\n                  + \"    nextAttemptTime DATETIME2(6),\\n\"\n                  + \"    attempts INT,\\n\"\n                  + \"    blocked BIT,\\n\"\n                  + \"    version INT,\\n\"\n                  + \"    uniqueRequestId VARCHAR(250),\\n\"\n                  + \"    processed BIT,\\n\"\n                  + \"    lastAttemptTime DATETIME2(6),\\n\"\n                  + \"    topic VARCHAR(250) DEFAULT '*' NOT NULL,\\n\"\n                  + \"    seq INT\\n\"\n                  + \")\")\n          .disableMigration(2)\n          .disableMigration(3)\n          .changeMigration(\n              4,\n              \"CREATE INDEX IX_TXNO_OUTBOX_1 ON TXNO_OUTBOX (processed, blocked, nextAttemptTime)\")\n          .disableMigration(5)\n          .disableMigration(6)\n          .disableMigration(7)\n          .changeMigration(\n              8,\n              \"CREATE UNIQUE INDEX UX_TXNO_OUTBOX_uniqueRequestId ON TXNO_OUTBOX (uniqueRequestId) WHERE uniqueRequestId IS NOT NULL\")\n          .disableMigration(9)\n          .disableMigration(10)\n          .changeMigration(\n              11,\n              \"CREATE TABLE TXNO_SEQUENCE (topic VARCHAR(250) NOT NULL, seq INT NOT NULL, CONSTRAINT \"\n                  + \"PK_TXNO_SEQUENCE PRIMARY KEY (topic, seq))\")\n          .createVersionTableBy(\n              connection -> {\n                try (Statement s = connection.createStatement()) {\n                  s.execute(\n                      \"IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TXNO_VERSION')\\n\"\n                          + \"BEGIN\\n\"\n                          + \"    CREATE TABLE TXNO_VERSION (\\n\"\n                          + \"        version INT\\n\"\n                          + \"    );\"\n                          + \"END\");\n                }\n              })\n          .build();\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/DriverConnectionProvider.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\n\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * A {@link ConnectionProvider} which requests connections directly from {@link DriverManager}.\n *\n * <p>Unlikely to be suitable for most production applications since it doesn't use any sort of\n * connection pool.\n *\n * <p>Usage:\n *\n * <pre>ConnectionProvider provider = SimpleConnectionProvider.builder()\n *   .driverClassName(\"org.postgresql.Driver\")\n *   .url(myJdbcUrl)\n *   .user(\"myusername\")\n *   .password(\"mypassword\")\n *   .build()</pre>\n */\n@SuperBuilder\n@Slf4j\nfinal class DriverConnectionProvider implements ConnectionProvider, Validatable {\n\n  private final String driverClassName;\n  private final String url;\n  private final String user;\n  private final String password;\n\n  private volatile boolean initialized;\n\n  @Override\n  public Connection obtainConnection() {\n    return uncheckedly(\n        () -> {\n          if (!initialized) {\n            synchronized (this) {\n              log.debug(\"Initialising {}\", driverClassName);\n              Class.forName(driverClassName);\n              initialized = true;\n            }\n          }\n          log.debug(\"Opening connection to {}\", url);\n          Connection connection = DriverManager.getConnection(url, user, password);\n          connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);\n          return connection;\n        });\n  }\n\n  @Override\n  public void validate(Validator validator) {\n    validator.notBlank(\"driverClassName\", driverClassName);\n    validator.notBlank(\"url\", url);\n    validator.notBlank(\"user\", user);\n    validator.notBlank(\"password\", password);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ExecutorSubmitter.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.function.Consumer;\nimport lombok.Builder;\nimport lombok.extern.slf4j.Slf4j;\nimport org.slf4j.event.Level;\n\n/**\n * Schedules background work using a local {@link Executor} implementation. Note that the {@link\n * Runnable}s submitted to this will not be {@link java.io.Serializable} so will not be suitable for\n * remoting. Remote submission of work is not yet supported.\n *\n * <p>Note that there are some important aspects that should be considered in the configuration of\n * this executor:\n *\n * <ul>\n *   <li>Should use a BOUNDED blocking queue implementation such as {@link ArrayBlockingQueue},\n *       otherwise under high volume, the queue may get so large it causes out-of-memory errors.\n *   <li>Should use a {@link java.util.concurrent.RejectedExecutionHandler} which either throws\n *       (such as {@link java.util.concurrent.ThreadPoolExecutor.AbortPolicy}), silently fails (such\n *       as {@link java.util.concurrent.ThreadPoolExecutor.DiscardPolicy}) or blocks the calling\n *       thread until a thread is available. It should <strong>not</strong> execute the work in the\n *       calling thread (e.g. {@link java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy},\n *       since this could result in unpredictable effects with tasks assuming they will be run in a\n *       different thread context corrupting thread state. Generally, throwing or silently failing\n *       are preferred since this allows the database to absorb all backpressure, but if you have a\n *       strong reason to choose a blocking policy to enforce upstream backpressure, be aware that\n *       {@link TransactionOutbox#flush()} can potentially block for a long period of time too, so\n *       design any background processing which calls it accordingly (e.g. avoid calling from a\n *       timed scheduled job; perhaps instead simply loop it).\n *   <li>The queue can afford to be quite large in most realistic production deployments, and it is\n *       advised that it be so (10000+).\n * </ul>\n */\n@Slf4j\n@Builder\npublic class ExecutorSubmitter implements Submitter, Validatable {\n\n  /**\n   * @param executor The executor to use.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  private final Executor executor;\n\n  /**\n   * @param logLevelWorkQueueSaturation The log level to use when work submission hits the executor\n   *     queue limit. This usually indicates saturation and may be of greater interest than the\n   *     default {@code DEBUG} level.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Builder.Default\n  private final Level logLevelWorkQueueSaturation = Level.DEBUG;\n\n  @Override\n  public void submit(TransactionOutboxEntry entry, Consumer<TransactionOutboxEntry> localExecutor) {\n    try {\n      executor.execute(() -> localExecutor.accept(entry));\n      log.debug(\"Submitted {} for immediate processing\", entry.description());\n    } catch (RejectedExecutionException e) {\n      Utils.logAtLevel(\n          log,\n          logLevelWorkQueueSaturation,\n          \"Queued {} for processing when executor is available\",\n          entry.description());\n    } catch (Exception e) {\n      log.warn(\n          \"Failed to submit {} for execution. It will be re-attempted later.\",\n          entry.description(),\n          e);\n    }\n  }\n\n  @Override\n  public void validate(Validator validator) {\n    validator.notNull(\"executor\", executor);\n    validator.notNull(\"logLevelWorkQueueSaturation\", logLevelWorkQueueSaturation);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/FailedDeserializingInvocation.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.io.IOException;\nimport java.lang.reflect.InvocationTargetException;\n\n/** Represents an invocation those deserialization failed. */\npublic class FailedDeserializingInvocation extends Invocation {\n\n  /**\n   * @return Indicates an Exception during De-Serialization, that is not persisted.\n   */\n  private final transient IOException exceptionDuringDeserialization;\n\n  public FailedDeserializingInvocation(IOException exceptionDuringDeserialization) {\n    super(\"\", \"\", new Class<?>[] {}, new Object[] {});\n    this.exceptionDuringDeserialization = exceptionDuringDeserialization;\n  }\n\n  @Override\n  void invoke(Object instance, TransactionOutboxListener listener)\n      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {\n    throw new UncheckedException(exceptionDuringDeserialization);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/FunctionInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameInstantiator;\nimport java.util.function.Function;\nimport lombok.experimental.SuperBuilder;\n\n@SuperBuilder\nclass FunctionInstantiator extends AbstractFullyQualifiedNameInstantiator {\n\n  private final Function<Class<?>, Object> fn;\n\n  @Override\n  public Object createInstance(Class<?> clazz) {\n    return fn.apply(clazz);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Instantiator.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.util.function.Function;\n\n/**\n * Provides callbacks for the creation and serialization of classes by {@link TransactionOutbox}.\n */\npublic interface Instantiator {\n\n  /**\n   * Creates an {@link Instantiator} which records the class name as its fully qualified name (e.g.\n   * {@code com.gruelbox.example.EnterpriseBeanProxyFactoryFactory}) and instantiates instances\n   * using reflection and a no-args constructor.\n   *\n   * <p>This is the default used by {@link TransactionOutbox} if nothing else is specified.\n   *\n   * @return A reflection instantiator\n   */\n  static Instantiator usingReflection() {\n    return ReflectionInstantiator.builder().build();\n  }\n\n  /**\n   * Creates an {@link Instantiator} which records the class name as its fully qualified name (e.g.\n   * {@code com.gruelbox.example.EnterpriseBeanProxyFactoryFactory}) and instantiates instances\n   * using the supplied function, which takes the fully qualified name and should return an\n   * instance.\n   *\n   * <p>This is a good option to use with dependency injection frameworks such as Guice:\n   *\n   * <pre>TransactionOutbox outbox = TransactionOutbox.builder()\n   * ...\n   * .instantiator(Instantiator.using(injector::getInstance))\n   * .build();</pre>\n   *\n   * @param fn A function to create an instance of the specified class.\n   * @return A reflection instantiator\n   */\n  static Instantiator using(Function<Class<?>, Object> fn) {\n    return FunctionInstantiator.builder().fn(fn).build();\n  }\n\n  /**\n   * Provides the name of the specified class. This may be the classes fully-qualified name, or may\n   * be an alias of some kind. This is up to the implementer.\n   *\n   * <p>Not using the actual class name can be useful in avoiding a case where queued tasks end up\n   * referencing renamed classes following a refactor. It is also useful for DI frameworks such as\n   * Spring DI, which use named bindings by default.\n   *\n   * @param clazz The class to get the name of.\n   * @return The class name.\n   */\n  String getName(Class<?> clazz);\n\n  /**\n   * Requests an instance of the named class, where the \"name\" is whatever is returned by {@link\n   * #getName(Class)}.\n   *\n   * <p>A common use-case for this method is to return a class from a DI framework such as Guice\n   * (using an injected {code Injector}), but it is perfectly valid to simply instantiate the class\n   * by name and populate its dependencies directly.\n   *\n   * @param name The class \"name\" as returned by {@link #getName(Class)}.\n   * @return An instance of the class.\n   */\n  Object getInstance(String name);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Invocation.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.google.gson.annotations.SerializedName;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport lombok.*;\nimport lombok.experimental.FieldDefaults;\nimport lombok.extern.slf4j.Slf4j;\nimport org.slf4j.MDC;\n\n/**\n * Represents the invocation of a specific method on a named class (where the name is provided by an\n * {@link Instantiator}), with the specified arguments.\n *\n * <p>Optimized for safe serialization via GSON.\n */\n@SuppressWarnings(\"WeakerAccess\")\n@ToString\n@EqualsAndHashCode\n@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)\n@Getter\n@Slf4j\npublic class Invocation {\n\n  /**\n   * @return The class name (as provided/expected by an {@link Instantiator}).\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @SerializedName(\"c\")\n  String className;\n\n  /**\n   * @return The method name. Combined with {@link #parameterTypes}, uniquely identifies the method.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @SerializedName(\"m\")\n  String methodName;\n\n  /**\n   * @return The method parameter types. Combined with {@link #methodName}, uniquely identifies the\n   *     method.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @SerializedName(\"p\")\n  Class<?>[] parameterTypes;\n\n  /**\n   * @return The arguments to call. Must match {@link #parameterTypes}.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @SerializedName(\"a\")\n  Object[] args;\n\n  /**\n   * @return Thread-local context to recreate when running the task.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @SerializedName(\"x\")\n  Map<String, String> mdc;\n\n  /**\n   * @return Free-form data used by add-ons to store additional information related to the request\n   */\n  @SerializedName(\"s\")\n  Map<String, String> session;\n\n  /**\n   * @param className The class name (as provided/expected by an {@link Instantiator}).\n   * @param methodName The method name. Combined with {@link #parameterTypes}, uniquely identifies\n   *     the method.\n   * @param parameterTypes The method parameter types. Combined with {@link #methodName}, uniquely\n   *     identifies the method.\n   * @param args The arguments to call. Must match {@link #parameterTypes}.\n   */\n  public Invocation(String className, String methodName, Class<?>[] parameterTypes, Object[] args) {\n    this(className, methodName, parameterTypes, args, null, null);\n  }\n\n  /**\n   * @param className The class name (as provided/expected by an {@link Instantiator}).\n   * @param methodName The method name. Combined with {@link #parameterTypes}, uniquely identifies\n   *     the method.\n   * @param parameterTypes The method parameter types. Combined with {@link #methodName}, uniquely\n   *     identifies the method.\n   * @param args The arguments to call. Must match {@link #parameterTypes}.\n   * @param mdc Thread-local context to recreate when running the task.\n   * @deprecated Use another constructor.\n   */\n  @Deprecated(forRemoval = true)\n  public Invocation(\n      String className,\n      String methodName,\n      Class<?>[] parameterTypes,\n      Object[] args,\n      Map<String, String> mdc) {\n    this(className, methodName, parameterTypes, args, mdc, null);\n  }\n\n  /**\n   * @param className The class name (as provided/expected by an {@link Instantiator}).\n   * @param methodName The method name. Combined with {@link #parameterTypes}, uniquely identifies\n   *     the method.\n   * @param parameterTypes The method parameter types. Combined with {@link #methodName}, uniquely\n   *     identifies the method.\n   * @param args The arguments to call. Must match {@link #parameterTypes}.\n   * @param mdc Thread-local context to recreate when running the task.\n   * @param session Free-form data used by add-ons to store additional information related to the\n   *     request.\n   */\n  public Invocation(\n      String className,\n      String methodName,\n      Class<?>[] parameterTypes,\n      Object[] args,\n      Map<String, String> mdc,\n      Map<String, String> session) {\n    this.className = className;\n    this.methodName = methodName;\n    this.parameterTypes = parameterTypes;\n    this.args = args;\n    this.mdc = mdc == null ? null : new HashMap<>(mdc);\n    this.session = session == null ? null : new HashMap<>(session);\n  }\n\n  <T> T withinMDC(Callable<T> callable) throws Exception {\n    if (mdc != null && MDC.getMDCAdapter() != null) {\n      var oldMdc = MDC.getCopyOfContextMap();\n      MDC.setContextMap(mdc);\n      try {\n        return callable.call();\n      } finally {\n        if (oldMdc == null) {\n          MDC.clear();\n        } else {\n          MDC.setContextMap(oldMdc);\n        }\n      }\n    } else {\n      return callable.call();\n    }\n  }\n\n  void invoke(Object instance, TransactionOutboxListener listener)\n      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {\n\n    Method method = instance.getClass().getDeclaredMethod(methodName, parameterTypes);\n    method.setAccessible(true);\n    if (log.isDebugEnabled()) {\n      log.debug(\"Invoking method {} with args {}\", method, Arrays.toString(args));\n    }\n    listener.wrapInvocation(\n        new TransactionOutboxListener.Invocator() {\n          @Override\n          public void run()\n              throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {\n            method.invoke(instance, args);\n          }\n\n          @Override\n          public Invocation getInvocation() {\n            return Invocation.this;\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/InvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.Writer;\n\n/**\n * {@link Invocation} objects are inherently difficult to serialize safely since they are\n * unpredictably polymorphic. Allowing them to contain <em>any</em> type reference opens you up to a\n * host of code injection attacks. At the same time, allowing possibly-unstable types into\n * serialized {@link Invocation}s can result in compatibility issues, with still unprocessed entries\n * in the database containing older versions of your classes. To avoid this, it makes sense to\n * specify the types supported and restrict this set of serializable classes to known-stable types\n * such as primitives and common JDK value types. {@link #createDefaultJsonSerializer()} provides\n * exactly this and is used by default. However, if you want to extend this list or use a different\n * serialization format, you can create your own implementation here, at your own risk.\n */\npublic interface InvocationSerializer {\n\n  /**\n   * Creates a locked-down serializer which supports a limited list of primitives and simple JDK\n   * value types. Shortcut to {@link DefaultInvocationSerializer}.\n   *\n   * @return The serializer.\n   */\n  static InvocationSerializer createDefaultJsonSerializer() {\n    return DefaultInvocationSerializer.builder().build();\n  }\n\n  /**\n   * Serializes an invocation to the supplied writer.\n   *\n   * @param invocation The invocation.\n   * @param writer The writer.\n   */\n  void serializeInvocation(Invocation invocation, Writer writer);\n\n  /**\n   * Deserializes an invocation from the supplied reader.\n   *\n   * @param reader The reader.\n   * @return The deserialized invocation.\n   */\n  Invocation deserializeInvocation(Reader reader) throws IOException;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Migration.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport lombok.Value;\n\n/** A database migration script entry. See {@link Dialect#getMigrations()}. */\n@Value\npublic class Migration {\n  int version;\n  String name;\n  String sql;\n\n  public Migration withSql(String sql) {\n    return new Migration(version, name, sql);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/MissingOptionalDependencyException.java",
    "content": "package com.gruelbox.transactionoutbox;\n\npublic class MissingOptionalDependencyException extends RuntimeException {\n\n  public MissingOptionalDependencyException(String groupId, String artifactId) {\n    super(\n        String.format(\n            \"You are trying to use an optional feature, which requires an additional dependency (%s:%s). Please add it to your classpath, and try again.\",\n            groupId, artifactId));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/NoTransactionActiveException.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n/** Thrown if an active transaction is required by a method and no transaction is active. */\n@SuppressWarnings(\"WeakerAccess\")\npublic final class NoTransactionActiveException extends RuntimeException {\n\n  public NoTransactionActiveException() {\n    super();\n  }\n\n  public NoTransactionActiveException(Throwable cause) {\n    super(cause);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/OptimisticLockException.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n/** Thrown when we attempt to update a record which has been modified by another thread. */\npublic class OptimisticLockException extends Exception {}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ParameterContextTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\n\n/**\n * A transaction manager which makes no assumption of a \"current\" {@link Transaction}. This means\n * that {@link TransactionOutbox#schedule(Class)} needs to be given the transaction to use as part\n * of any invoked method's arguments. In turn, that method will need the transaction at the time it\n * is invoked.\n *\n * <p>Call patterns permitted:\n *\n * <pre>\n * // Using TransactionManager\n * transactionManager.inTransaction(tx -&gt;\n *   outbox.schedule(MyClass.class).myMethod(\"foo\", tx));\n *\n * // Using some third party transaction manager\n * wibbleTransactionManager.doInATransaction(context -&gt;\n *   outbox.schedule(MyClass.class).myMethod(\"foo\", context));\n * </pre>\n */\npublic interface ParameterContextTransactionManager<T> extends TransactionManager {\n\n  /**\n   * Given an implementation-specific transaction context, return the active {@link Transaction}.\n   *\n   * @param context The implementation-specific context, of the same type returned by {@link\n   *     #contextType()}.\n   * @return The transaction, or null if the context is not known.\n   */\n  Transaction transactionFromContext(T context);\n\n  /**\n   * @return The type expected by {@link #transactionFromContext(Object)}.\n   */\n  Class<T> contextType();\n\n  /**\n   * Obtains the active transaction by parsing the method arguments for a {@link Transaction} or a\n   * context (any object of type {@link #contextType()}). All such arguments are removed from the\n   * invocation adn replaced with nulls before saving. They will be \"rehydrated\" later upon actual\n   * invocation using the transaction/context at the time of invocation.\n   *\n   * @param method The method called.\n   * @param args The method arguments.\n   * @return The transactional invocation.\n   */\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  default TransactionalInvocation extractTransaction(Method method, Object[] args) {\n    args = Arrays.copyOf(args, args.length);\n    var params = Arrays.copyOf(method.getParameterTypes(), method.getParameterCount());\n    Transaction transaction = null;\n    for (int i = 0; i < args.length; i++) {\n      Object candidate = args[i];\n      if (candidate instanceof Transaction) {\n        transaction = (Transaction) candidate;\n        args[i] = null;\n      } else if (contextType().isInstance(candidate)) {\n        if (transaction == null) {\n          transaction = transactionFromContext((T) candidate);\n          if (transaction == null) {\n            throw new IllegalArgumentException(\n                candidate.getClass().getName()\n                    + \" context passed to \"\n                    + method\n                    + \" does not relate to a known transaction. This either indicates that the context object was not \"\n                    + \"created by normal means or the transaction manager is incorrectly configured.\");\n          }\n        }\n        args[i] = null;\n        params[i] = TransactionContextPlaceholder.class;\n      }\n    }\n    if (transaction == null) {\n      throw new IllegalArgumentException(\n          getClass().getName()\n              + \" requires transaction context (either \"\n              + contextType().getName()\n              + \" or \"\n              + Transaction.class.getName()\n              + \") to be passed as a parameter to any scheduled method.\");\n    }\n    return new TransactionalInvocation(\n        method.getDeclaringClass(), method.getName(), params, args, transaction);\n  }\n\n  /**\n   * Modifies an {@link Invocation} at runtime to rehyrate it with the transaction context in which\n   * the record was locked.\n   *\n   * @param invocation The invocation.\n   * @param transaction The transaction to use.\n   * @return The modified invocation.\n   */\n  @Override\n  default Invocation injectTransaction(Invocation invocation, Transaction transaction) {\n    Object[] args = Arrays.copyOf(invocation.getArgs(), invocation.getArgs().length);\n    Class<?>[] params =\n        Arrays.copyOf(invocation.getParameterTypes(), invocation.getParameterTypes().length);\n    for (int i = 0; i < invocation.getParameterTypes().length; i++) {\n      Class<?> parameterType = invocation.getParameterTypes()[i];\n      if (Transaction.class.isAssignableFrom(parameterType)) {\n        if (args[i] != null) {\n          throw new IllegalArgumentException(\n              String.format(\n                  \"Parameter %s.%s[%d] contains unexpected serialized Transaction\",\n                  invocation.getClassName(), invocation.getMethodName(), i));\n        }\n        args[i] = transaction;\n      } else if (parameterType.equals(TransactionContextPlaceholder.class)) {\n        if (args[i] != null) {\n          throw new IllegalArgumentException(\n              String.format(\n                  \"Parameter %s.%s[%d] contains unexpected serialized Transaction context\",\n                  invocation.getClassName(), invocation.getMethodName(), i));\n        }\n        args[i] = transaction.context();\n        params[i] = contextType();\n      }\n    }\n    return new Invocation(\n        invocation.getClassName(),\n        invocation.getMethodName(),\n        params,\n        args,\n        invocation.getMdc(),\n        invocation.getSession());\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Persistor.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * Saves and loads {@link TransactionOutboxEntry}s. For most use cases, just use {@link\n * DefaultPersistor}. It is parameterisable and designed for extension, so can be easily modified.\n * Creating completely new implementations of {@link Persistor} should be reserved for cases where\n * the underlying data store is of a completely different nature entirely.\n */\npublic interface Persistor {\n\n  /**\n   * Uses the default relational persistor. Shortcut for: <code>\n   * DefaultPersistor.builder().dialect(dialect).build();</code>\n   *\n   * @param dialect The database dialect.\n   * @return The persistor.\n   */\n  static DefaultPersistor forDialect(Dialect dialect) {\n    return DefaultPersistor.builder().dialect(dialect).build();\n  }\n\n  /**\n   * Upgrades any database schema used by the persistor to the latest version. Called on creation of\n   * a {@link TransactionOutbox}.\n   *\n   * @param transactionManager The transactoin manager.\n   */\n  void migrate(TransactionManager transactionManager);\n\n  /**\n   * Saves a new {@link TransactionOutboxEntry}. Should throw {@link AlreadyScheduledException} if\n   * the record already exists based on the {@code id} or {@code uniqueRequestId} (the latter of\n   * which should not treat nulls as duplicates).\n   *\n   * @param tx The current {@link Transaction}.\n   * @param entry The entry to save. All properties on the object should be saved recursively.\n   * @throws Exception Any exception.\n   */\n  void save(Transaction tx, TransactionOutboxEntry entry) throws Exception;\n\n  /**\n   * Used in tests to simulate a database reload.\n   *\n   * @param invocation An invocation.\n   * @return The same invocation passed through a serialize/deserialize loop.\n   */\n  default Invocation serializeAndDeserialize(Invocation invocation) {\n    return invocation;\n  }\n\n  /**\n   * Deletes a {@link TransactionOutboxEntry}.\n   *\n   * <p>A record should only be deleted if <em>both</em> the {@code id} and {@code version} on the\n   * database match that on the object. If no such record is found, {@link OptimisticLockException}\n   * should be thrown.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param entry The entry to be deleted.\n   * @throws OptimisticLockException If no such record is found.\n   * @throws Exception Any other exception.\n   */\n  void delete(Transaction tx, TransactionOutboxEntry entry) throws Exception;\n\n  /**\n   * Modifies an existing {@link TransactionOutboxEntry}. Performs an optimistic lock check on any\n   * existing record via a compare-and-swap operation and throws {@link OptimisticLockException} if\n   * the lock is failed. {@link TransactionOutboxEntry#setVersion(int)} is called before returning\n   * containing the new version of the entry.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param entry The entry to be updated.\n   * @throws OptimisticLockException If no record with same id and version is found.\n   * @throws Exception Any other exception.\n   */\n  void update(Transaction tx, TransactionOutboxEntry entry) throws Exception;\n\n  /**\n   * Attempts to pessimistically lock an existing {@link TransactionOutboxEntry}.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param entry The entry to be locked\n   * @return true if the lock was successful.\n   * @throws OptimisticLockException If no record with same id and version is found.\n   * @throws Exception Any other exception.\n   */\n  boolean lock(Transaction tx, TransactionOutboxEntry entry) throws Exception;\n\n  /**\n   * Clears the blocked flag and resets the attempt count to zero.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param entryId The entry id.\n   * @return true if the update was successful. This will be false if the record was no longer\n   *     blocked or didn't exist anymore.\n   * @throws Exception Any other exception.\n   */\n  boolean unblock(Transaction tx, String entryId) throws Exception;\n\n  /**\n   * Selects up to a specified maximum number of non-blocked records which have passed their {@link\n   * TransactionOutboxEntry#getNextAttemptTime()}. Until a subsequent call to {@link\n   * #lock(Transaction, TransactionOutboxEntry)}, these records may be selected by another instance\n   * for processing.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param batchSize The number of records to select.\n   * @param now The time to use when selecting records.\n   * @return The records.\n   * @throws Exception Any exception.\n   */\n  List<TransactionOutboxEntry> selectBatch(Transaction tx, int batchSize, Instant now)\n      throws Exception;\n\n  /**\n   * Selects the next items in all the open topics as a batch for processing. Does not lock.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param batchSize The maximum number of records to select.\n   * @param now The time to use when selecting records.\n   * @return The records.\n   * @throws Exception Any exception.\n   */\n  Collection<TransactionOutboxEntry> selectNextInTopics(Transaction tx, int batchSize, Instant now)\n      throws Exception;\n\n  /**\n   * Selects the next items in all selected topics as a batch for processing. Does not lock.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param topicNames The topics to select records from.\n   * @param batchSize The maximum number of records to select.\n   * @param now The time to use when selecting records.\n   * @return The records.\n   * @throws Exception Any exception.\n   */\n  Collection<TransactionOutboxEntry> selectNextInSelectedTopics(\n      Transaction tx, List<String> topicNames, int batchSize, Instant now) throws Exception;\n\n  /**\n   * Deletes records which have processed and passed their expiry time, in specified batch sizes.\n   *\n   * @param tx The current {@link Transaction}.\n   * @param batchSize The maximum number of records to select.\n   * @param now The time to use when selecting records.\n   * @return The number of records deleted.\n   * @throws Exception Any exception.\n   */\n  int deleteProcessedAndExpired(Transaction tx, int batchSize, Instant now) throws Exception;\n\n  /**\n   * Checks the connection status of a transaction.\n   *\n   * @param tx The current {@link Transaction}.\n   * @return true if connected and working.\n   */\n  boolean checkConnection(Transaction tx) throws Exception;\n\n  /**\n   * Clears the database. For testing only.\n   *\n   * @param tx The current {@link Transaction}.\n   */\n  void clear(Transaction tx) throws Exception;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ReflectionInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameInstantiator;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.lang.reflect.Constructor;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * {@link Instantiator} which records the class name as its fully-qualified class name, and\n * instantiates via reflection. The class must have a no-args constructor. Likely only of use in\n * simple applications since it does not allow for dependency injection.\n */\n@Slf4j\n@SuperBuilder\nfinal class ReflectionInstantiator extends AbstractFullyQualifiedNameInstantiator {\n\n  @Override\n  public Object createInstance(Class<?> clazz) {\n    log.debug(\"Getting instance of class [{}] via reflection\", clazz.getName());\n    Constructor<?> constructor = Utils.uncheckedly(clazz::getDeclaredConstructor);\n    constructor.setAccessible(true);\n    return Utils.uncheckedly(constructor::newInstance);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/RuntimeTypeAdapterFactory.java",
    "content": "/*\n * Copyright (C) 2011 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.gruelbox.transactionoutbox;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParseException;\nimport com.google.gson.JsonPrimitive;\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.TypeAdapterFactory;\nimport com.google.gson.internal.Streams;\nimport com.google.gson.reflect.TypeToken;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonWriter;\nimport java.io.IOException;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Adapts values whose runtime type may differ from their declaration type. This is necessary when a\n * field's type is not the same type that GSON should create when deserializing that field. For\n * example, consider these types:\n *\n * <pre>{@code\n * abstract class Shape {\n *   int x;\n *   int y;\n * }\n * class Circle extends Shape {\n *   int radius;\n * }\n * class Rectangle extends Shape {\n *   int width;\n *   int height;\n * }\n * class Diamond extends Shape {\n *   int width;\n *   int height;\n * }\n * class Drawing {\n *   Shape bottomShape;\n *   Shape topShape;\n * }\n * }</pre>\n *\n * <p>Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in\n * this drawing a rectangle or a diamond?\n *\n * <pre>{@code\n * {\n *   \"bottomShape\": {\n *     \"width\": 10,\n *     \"height\": 5,\n *     \"x\": 0,\n *     \"y\": 0\n *   },\n *   \"topShape\": {\n *     \"radius\": 2,\n *     \"x\": 4,\n *     \"y\": 1\n *   }\n * }\n * }</pre>\n *\n * This class addresses this problem by adding type information to the serialized JSON and honoring\n * that type information when the JSON is deserialized:\n *\n * <pre>{@code\n * {\n *   \"bottomShape\": {\n *     \"type\": \"Diamond\",\n *     \"width\": 10,\n *     \"height\": 5,\n *     \"x\": 0,\n *     \"y\": 0\n *   },\n *   \"topShape\": {\n *     \"type\": \"Circle\",\n *     \"radius\": 2,\n *     \"x\": 4,\n *     \"y\": 1\n *   }\n * }\n * }</pre>\n *\n * Both the type field name ({@code \"type\"}) and the type labels ({@code \"Rectangle\"}) are\n * configurable.\n *\n * <h3>Registering Types</h3>\n *\n * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the\n * {@link #of} factory method. If you don't supply an explicit type field name, {@code \"type\"} will\n * be used.\n *\n * <pre>{@code\n * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory\n *     = RuntimeTypeAdapterFactory.of(Shape.class, \"type\");\n * }</pre>\n *\n * Next register all of your subtypes. Every subtype must be explicitly registered. This protects\n * your application from injection attacks. If you don't supply an explicit type label, the type's\n * simple name will be used.\n *\n * <pre>{@code\n * shapeAdapterFactory.registerSubtype(Rectangle.class, \"Rectangle\");\n * shapeAdapterFactory.registerSubtype(Circle.class, \"Circle\");\n * shapeAdapterFactory.registerSubtype(Diamond.class, \"Diamond\");\n * }</pre>\n *\n * Finally, register the type adapter factory in your application's GSON builder:\n *\n * <pre>{@code\n * Gson gson = new GsonBuilder()\n *     .registerTypeAdapterFactory(shapeAdapterFactory)\n *     .create();\n * }</pre>\n *\n * Like {@code GsonBuilder}, this API supports chaining:\n *\n * <pre>{@code\n * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)\n *     .registerSubtype(Rectangle.class)\n *     .registerSubtype(Circle.class)\n *     .registerSubtype(Diamond.class);\n * }</pre>\n *\n * <h3>Serialization and deserialization</h3>\n *\n * In order to serialize and deserialize a polymorphic object, you must specify the base type\n * explicitly.\n *\n * <pre>{@code\n * Diamond diamond = new Diamond();\n * String json = gson.toJson(diamond, Shape.class);\n * }</pre>\n *\n * And then:\n *\n * <pre>{@code\n * Shape shape = gson.fromJson(json, Shape.class);\n * }</pre>\n */\n@Slf4j\nfinal class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {\n  private final Class<?> baseType;\n  private final String typeFieldName;\n  private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();\n  private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();\n  private final boolean maintainType;\n\n  private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {\n    if (typeFieldName == null || baseType == null) {\n      throw new NullPointerException();\n    }\n    this.baseType = baseType;\n    this.typeFieldName = typeFieldName;\n    this.maintainType = maintainType;\n  }\n\n  /**\n   * Creates a new runtime type adapter for {@code baseType} using {@code \"type\"} as the type field\n   * name.\n   */\n  static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {\n    return new RuntimeTypeAdapterFactory<>(baseType, \"type\", false);\n  }\n\n  /**\n   * Registers {@code type} identified by {@code label}. Labels are case sensitive.\n   *\n   * @throws IllegalArgumentException if either {@code type} or {@code label} have already been\n   *     registered on this type adapter.\n   */\n  RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {\n    if (type == null || label == null) {\n      throw new NullPointerException();\n    }\n    if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {\n      throw new IllegalArgumentException(\"types and labels must be unique\");\n    }\n    labelToSubtype.put(label, type);\n    subtypeToLabel.put(type, label);\n    return this;\n  }\n\n  @Override\n  public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {\n    if (type.getRawType() != baseType) {\n      return null;\n    }\n    log.debug(\"Looking for adapter for {}\", type);\n    final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();\n    final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();\n    for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {\n      TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));\n      labelToDelegate.put(entry.getKey(), delegate);\n      subtypeToDelegate.put(entry.getValue(), delegate);\n    }\n\n    return new TypeAdapter<R>() {\n      @Override\n      public R read(JsonReader in) {\n        log.debug(\"Reading\");\n        JsonElement jsonElement = Streams.parse(in);\n        JsonElement labelJsonElement;\n        if (maintainType) {\n          labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);\n        } else {\n          labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);\n        }\n\n        if (labelJsonElement == null) {\n          throw new JsonParseException(\n              \"cannot deserialize \"\n                  + baseType\n                  + \" because it does not define a field named \"\n                  + typeFieldName);\n        }\n        String label = labelJsonElement.getAsString();\n        @SuppressWarnings(\"unchecked\") // registration requires that subtype extends T\n        TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);\n        if (delegate == null) {\n          throw new JsonParseException(\n              \"cannot deserialize \"\n                  + baseType\n                  + \" subtype named \"\n                  + label\n                  + \"; did you forget to register a subtype?\");\n        }\n        return delegate.fromJsonTree(jsonElement);\n      }\n\n      @Override\n      public void write(JsonWriter out, R value) throws IOException {\n        Class<?> srcType = value.getClass();\n        String label = subtypeToLabel.get(srcType);\n        @SuppressWarnings(\"unchecked\") // registration requires that subtype extends T\n        TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);\n        if (delegate == null) {\n          throw new JsonParseException(\n              \"cannot serialize \" + srcType.getName() + \"; did you forget to register a subtype?\");\n        }\n        JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();\n\n        if (maintainType) {\n          Streams.write(jsonObject, out);\n          return;\n        }\n\n        JsonObject clone = new JsonObject();\n\n        if (jsonObject.has(typeFieldName)) {\n          throw new JsonParseException(\n              \"cannot serialize \"\n                  + srcType.getName()\n                  + \" because it already defines a field named \"\n                  + typeFieldName);\n        }\n        clone.add(typeFieldName, new JsonPrimitive(label));\n\n        for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {\n          clone.add(e.getKey(), e.getValue());\n        }\n        Streams.write(clone, out);\n      }\n    }.nullSafe();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/SQLAction.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\n\n@FunctionalInterface\ninterface SQLAction {\n  void doAction(Connection connection) throws SQLException;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/SimpleTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractThreadLocalTransactionManager;\nimport com.gruelbox.transactionoutbox.spi.SimpleTransaction;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * A simple {@link TransactionManager} implementation suitable for applications with no existing\n * transaction management.\n */\n@SuperBuilder\n@Slf4j\nfinal class SimpleTransactionManager\n    extends AbstractThreadLocalTransactionManager<SimpleTransaction> {\n\n  private final ConnectionProvider connectionProvider;\n\n  @Override\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) throws E {\n    return withTransaction(\n        atx -> {\n          T result = processAndCommitOrRollback(work, (SimpleTransaction) atx);\n          ((SimpleTransaction) atx).processHooks();\n          return result;\n        });\n  }\n\n  private <T, E extends Exception> T processAndCommitOrRollback(\n      ThrowingTransactionalSupplier<T, E> work, SimpleTransaction transaction) throws E {\n    try {\n      log.debug(\"Processing work\");\n      T result = work.doWork(transaction);\n      transaction.flushBatches();\n      log.debug(\"Committing transaction\");\n      transaction.commit();\n      return result;\n    } catch (Exception e) {\n      try {\n        log.warn(\n            \"Exception in transactional block ({}{}). Rolling back. See later messages for detail\",\n            e.getClass().getSimpleName(),\n            e.getMessage() == null ? \"\" : (\" - \" + e.getMessage()));\n        transaction.rollback();\n      } catch (Exception ex) {\n        log.warn(\"Failed to roll back\", ex);\n      }\n      throw e;\n    }\n  }\n\n  private <T, E extends Exception> T withTransaction(ThrowingTransactionalSupplier<T, E> work)\n      throws E {\n    try (Connection connection = connectionProvider.obtainConnection();\n        SimpleTransaction transaction = pushTransaction(new SimpleTransaction(connection, null))) {\n      log.debug(\"Got connection {}\", connection);\n      boolean autoCommit = transaction.connection().getAutoCommit();\n      if (autoCommit) {\n        log.debug(\"Setting auto-commit false\");\n        Utils.uncheck(() -> transaction.connection().setAutoCommit(false));\n      }\n      try {\n        return work.doWork(transaction);\n      } finally {\n        connection.setAutoCommit(autoCommit);\n      }\n    } catch (SQLException e) {\n      throw new RuntimeException(e);\n    } finally {\n      popTransaction();\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubParameterContextTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.ProxyFactory;\nimport com.gruelbox.transactionoutbox.spi.SimpleTransaction;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.sql.Connection;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.Supplier;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * A stub transaction manager that assumes no underlying database, and a transaction context of the\n * specified type.\n */\n@Slf4j\npublic class StubParameterContextTransactionManager<C>\n    implements ParameterContextTransactionManager<C> {\n\n  private final Class<C> contextClass;\n  private final Supplier<C> contextFactory;\n  private final ConcurrentMap<C, Transaction> contextMap = new ConcurrentHashMap<>();\n\n  /**\n   * @param contextClass The class that represents the context. Must support equals/hashCode.\n   * @param contextFactory Generates context instances when transactions are started.\n   */\n  public StubParameterContextTransactionManager(Class<C> contextClass, Supplier<C> contextFactory) {\n    this.contextClass = contextClass;\n    this.contextFactory = contextFactory;\n  }\n\n  @Override\n  public Transaction transactionFromContext(C context) {\n    return contextMap.get(context);\n  }\n\n  @Override\n  public final Class<C> contextType() {\n    return contextClass;\n  }\n\n  @Override\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) throws E {\n    return withTransaction(\n        atx -> {\n          T result = work.doWork(atx);\n          ((SimpleTransaction) atx).processHooks();\n          return result;\n        });\n  }\n\n  private <T, E extends Exception> T withTransaction(ThrowingTransactionalSupplier<T, E> work)\n      throws E {\n    Connection mockConnection = Utils.createLoggingProxy(new ProxyFactory(), Connection.class);\n    C context = contextFactory.get();\n    try (SimpleTransaction transaction = new SimpleTransaction(mockConnection, context)) {\n      contextMap.put(context, transaction);\n      return work.doWork(transaction);\n    } finally {\n      contextMap.remove(context);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubPersistor.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.List;\nimport lombok.Builder;\n\n/** Stub implementation of {@link Persistor}. */\n@Builder\npublic class StubPersistor implements Persistor {\n\n  StubPersistor() {}\n\n  @Override\n  public void migrate(TransactionManager transactionManager) {\n    // No-op\n  }\n\n  @Override\n  public void save(Transaction tx, TransactionOutboxEntry entry) {\n    // No-op\n  }\n\n  @Override\n  public void delete(Transaction tx, TransactionOutboxEntry entry) {\n    // No-op\n  }\n\n  @Override\n  public void update(Transaction tx, TransactionOutboxEntry entry) {\n    // No-op\n  }\n\n  @Override\n  public boolean lock(Transaction tx, TransactionOutboxEntry entry) {\n    return true;\n  }\n\n  @Override\n  public boolean unblock(Transaction tx, String entryId) {\n    return true;\n  }\n\n  @Override\n  public List<TransactionOutboxEntry> selectBatch(Transaction tx, int batchSize, Instant now) {\n    return List.of();\n  }\n\n  @Override\n  public Collection<TransactionOutboxEntry> selectNextInTopics(\n      Transaction tx, int flushBatchSize, Instant now) {\n    return List.of();\n  }\n\n  @Override\n  public Collection<TransactionOutboxEntry> selectNextInSelectedTopics(\n      Transaction tx, List<String> topicNames, int batchSize, Instant now) throws Exception {\n    return List.of();\n  }\n\n  @Override\n  public int deleteProcessedAndExpired(Transaction tx, int batchSize, Instant now) {\n    return 0;\n  }\n\n  @Override\n  public void clear(Transaction tx) {}\n\n  @Override\n  public boolean checkConnection(Transaction tx) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/StubThreadLocalTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractThreadLocalTransactionManager;\nimport com.gruelbox.transactionoutbox.spi.ProxyFactory;\nimport com.gruelbox.transactionoutbox.spi.SimpleTransaction;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.sql.Connection;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * A stub transaction manager that assumes no underlying database and thread local transaction\n * management.\n */\n@Slf4j\npublic class StubThreadLocalTransactionManager\n    extends AbstractThreadLocalTransactionManager<SimpleTransaction> {\n\n  public StubThreadLocalTransactionManager() {\n    // Nothing to do\n  }\n\n  @Override\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) throws E {\n    return withTransaction(\n        atx -> {\n          T result = work.doWork(atx);\n          ((SimpleTransaction) atx).processHooks();\n          return result;\n        });\n  }\n\n  private <T, E extends Exception> T withTransaction(ThrowingTransactionalSupplier<T, E> work)\n      throws E {\n    Connection mockConnection = Utils.createLoggingProxy(new ProxyFactory(), Connection.class);\n    try (SimpleTransaction transaction =\n        pushTransaction(new SimpleTransaction(mockConnection, null))) {\n      return work.doWork(transaction);\n    } finally {\n      popTransaction();\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Submitter.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ForkJoinPool;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\n\n/** Called by {@link TransactionOutbox} to submit work for background processing. */\npublic interface Submitter {\n\n  /**\n   * Schedules background work using a local {@link Executor} implementation.\n   *\n   * <p>Shortcut for {@code ExecutorSubmitter.builder().executor(executor).build()}.\n   *\n   * @param executor The executor.\n   * @return The submitter.\n   */\n  static Submitter withExecutor(Executor executor) {\n    return ExecutorSubmitter.builder().executor(executor).build();\n  }\n\n  /**\n   * Schedules background worh with a {@link ThreadPoolExecutor}, sized to match {@link\n   * ForkJoinPool#commonPool()} (or one thread, whichever is the larger), with a maximum queue size\n   * of 16384 before work is discarded.\n   *\n   * @return The submitter.\n   */\n  static Submitter withDefaultExecutor() {\n    // JDK bug means this warning can't be fixed\n    //noinspection Convert2Diamond\n    return withExecutor(\n        new ThreadPoolExecutor(\n            1,\n            Math.max(1, ForkJoinPool.commonPool().getParallelism()),\n            0L,\n            TimeUnit.MILLISECONDS,\n            new ArrayBlockingQueue<Runnable>(16384)));\n  }\n\n  /**\n   * Submits a transaction outbox task for processing. The {@link TransactionOutboxEntry} is\n   * provided, along with a {@code localExecutor} which can run the work immediately. An\n   * implementation may validly do any of the following:\n   *\n   * <ul>\n   *   <li>Submit a call to {@code localExecutor} in a local thread, e.g. using an {@link Executor}.\n   *       This is what implementations returned by {@link #withExecutor(Executor)} or {@link\n   *       #withDefaultExecutor()} will do, and is recommended in almost all cases.\n   *   <li>Serialize the {@link TransactionOutboxEntry}, send it to another instance (e.g. via a\n   *       queue) and have the handler code call {@link\n   *       TransactionOutbox#processNow(TransactionOutboxEntry)}. Such an approach should not\n   *       generally be necessary since {@link TransactionOutbox#flush()} is designed to be called\n   *       repeatedly on multiple instances. This means there is a degree of load balancing built\n   *       into the system, but when dealing with very high load, very low run-time tasks, this can\n   *       get overwhelmed and direct multi-instance queuing can help balance the load at source.\n   *       <strong>Note:</strong> it is recommended that the {@code invocation} property of the\n   *       {@link TransactionOutboxEntry} be serialized using {@link\n   *       InvocationSerializer#createDefaultJsonSerializer()}\n   *   <li>Pass the {@code entry} directly to the {@code localExecutor}. This will run the work\n   *       immediately in the calling thread and is therefore generally not recommended; the calling\n   *       thread will be either the thread calling {@link TransactionOutbox#schedule(Class)}\n   *       (effectively making the work synchronous) or the background poll thread (limiting work in\n   *       progress to one). It can, however, be useful for test cases.\n   * </ul>\n   *\n   * @param entry The entry to process.\n   * @param localExecutor Provides a means of running the work directly locally (it is effectively\n   *     just a call to {@link TransactionOutbox#processNow(TransactionOutboxEntry)}).\n   */\n  void submit(TransactionOutboxEntry entry, Consumer<TransactionOutboxEntry> localExecutor);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThreadLocalContextTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.lang.reflect.Method;\n\n/**\n * A transaction manager which assumes there is a single \"current\" {@link Transaction} on a thread\n * (presumably saved in a {@link ThreadLocal}) which can be both used by {@link\n * TransactionOutbox#schedule(Class)} as the current context to write records using {@link\n * Persistor} <em>and</em> used by scheduled methods themselves to write changes within the\n * transaction started as a result of reading and locking the request.\n *\n * <p>Call pattern permitted:\n *\n * <pre>transactionManager.inTransaction(() -&gt; outbox.schedule(MyClass.ckass).myMethod(\"foo\");\n * </pre>\n *\n * <p>Adds the {@link #requireTransactionReturns(ThrowingTransactionalSupplier)} and {@link\n * #requireTransaction(ThrowingTransactionalWork)} methods, which extract the current transaction\n * from the thread context and pass it on, throwing {@link NoTransactionActiveException} if there is\n * no current transaction.\n */\npublic interface ThreadLocalContextTransactionManager extends TransactionManager {\n\n  /**\n   * Runs the specified work in the context of the \"current\" transaction (the definition of which is\n   * up to the implementation).\n   *\n   * @param work Code which must be called while the transaction is active.\n   * @param <E> The exception type.\n   * @throws E If any exception is thrown by {@link Runnable}.\n   * @throws NoTransactionActiveException If a transaction is not currently active.\n   */\n  default <E extends Exception> void requireTransaction(ThrowingTransactionalWork<E> work)\n      throws E, NoTransactionActiveException {\n    requireTransactionReturns(ThrowingTransactionalSupplier.fromWork(work));\n  }\n\n  /**\n   * Runs the specified work in the context of the \"current\" transaction (the definition of which is\n   * up to the implementation).\n   *\n   * @param work Code which must be called while the transaction is active.\n   * @param <T> The type returned.\n   * @param <E> The exception type.\n   * @return The value returned by {@code work}.\n   * @throws E If any exception is thrown by {@link Runnable}.\n   * @throws NoTransactionActiveException If a transaction is not currently active.\n   * @throws UnsupportedOperationException If the transaction manager does not support thread-local\n   *     context.\n   */\n  <T, E extends Exception> T requireTransactionReturns(ThrowingTransactionalSupplier<T, E> work)\n      throws E, NoTransactionActiveException;\n\n  /**\n   * Obtains the active transaction by using {@link\n   * #requireTransactionReturns(ThrowingTransactionalSupplier)}, thus requiring nothing to be passed\n   * in the method invocation. No changes are made to the invocation.\n   *\n   * @param method The method called.\n   * @param args The method arguments.\n   * @return The transactional invocation.\n   */\n  @Override\n  default TransactionalInvocation extractTransaction(Method method, Object[] args) {\n    return requireTransactionReturns(\n        transaction ->\n            new TransactionalInvocation(\n                method.getDeclaringClass(),\n                method.getName(),\n                method.getParameterTypes(),\n                args,\n                transaction));\n  }\n\n  /**\n   * The transaction is not needed as part of an invocation, so the invocation is left unmodified.\n   *\n   * @param invocation The invocation.\n   * @return The unmodified invocation.\n   */\n  @Override\n  default Invocation injectTransaction(Invocation invocation, Transaction transaction) {\n    return invocation;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingRunnable.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n/** A runnable... that throws. */\n@FunctionalInterface\npublic interface ThrowingRunnable {\n\n  void run() throws Exception;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingTransactionalSupplier.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface ThrowingTransactionalSupplier<T, E extends Exception> {\n\n  static <F extends Exception> ThrowingTransactionalSupplier<Void, F> fromRunnable(\n      Runnable runnable) {\n    return transaction -> {\n      runnable.run();\n      return null;\n    };\n  }\n\n  static <F extends Exception> ThrowingTransactionalSupplier<Void, F> fromWork(\n      ThrowingTransactionalWork<F> work) {\n    return transaction -> {\n      work.doWork(transaction);\n      return null;\n    };\n  }\n\n  static ThrowingTransactionalSupplier<Void, RuntimeException> fromWork(TransactionalWork work) {\n    return transaction -> {\n      work.doWork(transaction);\n      return null;\n    };\n  }\n\n  static <T> ThrowingTransactionalSupplier<T, RuntimeException> fromSupplier(\n      TransactionalSupplier<T> work) {\n    return work::doWork;\n  }\n\n  T doWork(Transaction transaction) throws E;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/ThrowingTransactionalWork.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface ThrowingTransactionalWork<E extends Exception> {\n\n  void doWork(Transaction transaction) throws E;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Transaction.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\n\n/** Access and manipulation of a currently-active transaction. */\npublic interface Transaction {\n\n  /**\n   * @return The connection for the transaction.\n   */\n  Connection connection();\n\n  /**\n   * @param <T> The context type. Coerced on read.\n   * @return A {@link TransactionManager}-specific object representing the context of this\n   *     transaction. Intended for use with {@link TransactionManager} implementations that support\n   *     explicitly-passed transaction context injection into method arguments.\n   */\n  default <T> T context() {\n    return null;\n  }\n\n  /**\n   * Creates a prepared statement which will be cached and re-used within a transaction. Any batch\n   * on these statements is executed before the transaction is committed, and automatically closed.\n   *\n   * @param sql The SQL statement\n   * @return The statement.\n   */\n  PreparedStatement prepareBatchStatement(String sql);\n\n  /**\n   * Will be called to perform work immediately after the current transaction is committed. This\n   * should occur in the same thread and will generally not be long-lasting.\n   *\n   * @param runnable The code to run post-commit.\n   */\n  void addPostCommitHook(Runnable runnable);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionContextPlaceholder.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n/**\n * Marker for {@link Invocation} arguments holding transaction context. These will be rehydrated\n * with the real context type at runtime.\n */\ninterface TransactionContextPlaceholder {}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\n\nimport java.lang.reflect.Method;\nimport javax.sql.DataSource;\n\n/**\n * Key interface giving {@link TransactionOutbox} access to JDBC.\n *\n * <p>In practice, most implementations should extend {@link ThreadLocalContextTransactionManager}\n * or {@link ParameterContextTransactionManager}.\n */\npublic interface TransactionManager {\n\n  /**\n   * Creates a simple transaction manager which uses the specified {@link DataSource} to source\n   * connections. A new connection is requested for each transaction.\n   *\n   * <p>Transactions will be solely controlled through {@link TransactionManager}, so this may be\n   * suitable for new applications with no other transaction management. Otherwise, a custom {@link\n   * TransactionManager} implementation should be used.\n   *\n   * @param dataSource The data source.\n   * @return The transaction manager.\n   */\n  static ThreadLocalContextTransactionManager fromDataSource(DataSource dataSource) {\n    return SimpleTransactionManager.builder()\n        .connectionProvider(DataSourceConnectionProvider.builder().dataSource(dataSource).build())\n        .build();\n  }\n\n  /**\n   * Creates a simple transaction manager which uses the specified connection details to request a\n   * new connection from the {@link java.sql.DriverManager} every time a new transaction starts.\n   *\n   * <p>Transactions will be solely controlled through {@link TransactionManager}, and without\n   * pooling, performance will be poor. Generally, {@link #fromDataSource(DataSource)} using a\n   * pooling {@code DataSource} such as that provided by Hikari is preferred.\n   *\n   * @param driverClass The driver class name (e.g. {@code com.mysql.cj.jdbc.Driver}).\n   * @param url The JDBC url.\n   * @param username The username.\n   * @param password The password.\n   * @return The transaction manager.\n   */\n  static ThreadLocalContextTransactionManager fromConnectionDetails(\n      String driverClass, String url, String username, String password) {\n    return SimpleTransactionManager.builder()\n        .connectionProvider(\n            DriverConnectionProvider.builder()\n                .driverClassName(driverClass)\n                .url(url)\n                .user(username)\n                .password(password)\n                .build())\n        .build();\n  }\n\n  /**\n   * Should do any work necessary to start a (new) transaction, call {@code runnable} and then\n   * either commit on success or rollback on failure, flushing and closing any prepared statements\n   * prior to a commit and firing post commit hooks immediately afterwards\n   *\n   * @param runnable Code which must be called while the transaction is active..\n   */\n  default void inTransaction(Runnable runnable) {\n    uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromRunnable(runnable)));\n  }\n\n  /**\n   * Should do any work necessary to start a (new) transaction, call {@code runnable} and then\n   * either commit on success or rollback on failure, flushing and closing any prepared statements\n   * prior to a commit and firing post commit hooks immediately afterwards\n   *\n   * @param work Code which must be called while the transaction is active..\n   */\n  default void inTransaction(TransactionalWork work) {\n    uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromWork(work)));\n  }\n\n  /**\n   * Should do any work necessary to start a (new) transaction, call {@code runnable} and then\n   * either commit on success or rollback on failure, flushing and closing any prepared statements\n   * prior to a commit and firing post commit hooks immediately afterwards.\n   *\n   * @param <T> The type returned.\n   * @param supplier Code which must be called while the transaction is active.\n   * @return The result of {@code supplier}.\n   */\n  default <T> T inTransactionReturns(TransactionalSupplier<T> supplier) {\n    return uncheckedly(\n        () -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromSupplier(supplier)));\n  }\n\n  /**\n   * Should do any work necessary to start a (new) transaction, call {@code runnable} and then\n   * either commit on success or rollback on failure, flushing and closing any prepared statements\n   * prior to a commit and firing post commit hooks immediately afterwards.\n   *\n   * @param work Code which must be called while the transaction is active.\n   * @param <E> The exception type.\n   * @throws E If any exception is thrown by {@link Runnable}.\n   */\n  @SuppressWarnings(\"SameReturnValue\")\n  default <E extends Exception> void inTransactionThrows(ThrowingTransactionalWork<E> work)\n      throws E {\n    inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromWork(work));\n  }\n\n  /**\n   * Should do any work necessary to start a (new) transaction, call {@code work} and then either\n   * commit on success or rollback on failure, flushing and closing any prepared statements prior to\n   * a commit and firing post commit hooks immediately afterwards.\n   *\n   * @param <T> The type returned.\n   * @param work Code which must be called while the transaction is active.\n   * @param <E> The exception type.\n   * @return The result of {@code supplier}.\n   * @throws E If any exception is thrown by {@link Runnable}.\n   */\n  <T, E extends Exception> T inTransactionReturnsThrows(ThrowingTransactionalSupplier<T, E> work)\n      throws E;\n\n  /**\n   * All transaction managers need to be able to take a method call at the time it is scheduled and\n   * determine the {@link Transaction} to use to pass to {@link Persistor} and save the request.\n   * They can do this either by examining some current application state or by parsing the method\n   * and arguments.\n   *\n   * @param method The method called.\n   * @param args The method arguments.\n   * @return The extracted transaction and any modifications to the method and arguments.\n   */\n  TransactionalInvocation extractTransaction(Method method, Object[] args);\n\n  /**\n   * Makes any modifications to an invocation at runtime necessary to inject the current transaction\n   * or transaction context.\n   *\n   * @param invocation The invocation.\n   * @param transaction The transaction that the invocation will be run in.\n   * @return The modified invocation.\n   */\n  Invocation injectTransaction(Invocation invocation, Transaction transaction);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutbox.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.concurrent.Executor;\nimport java.util.function.Supplier;\nimport lombok.ToString;\nimport org.slf4j.MDC;\nimport org.slf4j.event.Level;\n\n/**\n * An implementation of the <a\n * href=\"https://microservices.io/patterns/data/transactional-outbox.html\">Transactional Outbox</a>\n * pattern for Java. See <a href=\"https://github.com/gruelbox/transaction-outbox\">README</a> for\n * usage instructions.\n */\npublic interface TransactionOutbox {\n\n  /**\n   * @return A builder for creating a new instance of {@link TransactionOutbox}.\n   */\n  static TransactionOutboxBuilder builder() {\n    return TransactionOutboxImpl.builder();\n  }\n\n  /**\n   * Performs initial setup, making the instance usable. If {@link\n   * TransactionOutboxBuilder#initializeImmediately(boolean)} is true, which is the default, this\n   * method is called automatically when the instance is constructed.\n   */\n  void initialize();\n\n  /**\n   * The main entry point for submitting new transaction outbox tasks.\n   *\n   * <p>Returns a proxy of {@code T} which, when called, will instantly return and schedule a call\n   * of the <em>real</em> method to occur after the current transaction is committed (as such a\n   * transaction needs to be active and accessible from the transaction manager supplied to {@link\n   * TransactionOutboxBuilder#transactionManager(TransactionManager)}),\n   *\n   * <p>Usage:\n   *\n   * <pre>transactionOutbox.schedule(MyService.class)\n   *   .runMyMethod(\"with\", \"some\", \"arguments\");</pre>\n   *\n   * <p>This will write a record to the database using the supplied {@link Persistor} and {@link\n   * Instantiator}, using the current database transaction, which will get rolled back if the rest\n   * of the transaction is, and thus never processed. However, if the transaction is committed, the\n   * real method will be called immediately afterwards using the submitter supplied to {@link\n   * TransactionOutboxBuilder#submitter(Submitter)}. Should that fail, the call will be reattempted\n   * whenever {@link #flush()} is called, provided at least supplied {@link\n   * TransactionOutboxBuilder#attemptFrequency(Duration)} has passed since the time the task was\n   * last attempted.\n   *\n   * @param clazz The class to proxy.\n   * @param <T> The type to proxy.\n   * @return The proxy of {@code T}.\n   */\n  <T> T schedule(Class<T> clazz);\n\n  /**\n   * Starts building a schedule request with parameterization. See {@link\n   * ParameterizedScheduleBuilder#schedule(Class)} for more information.\n   *\n   * @return Builder.\n   */\n  ParameterizedScheduleBuilder with();\n\n  /**\n   * Flush in a single thread. Calls {@link #flush(Executor)} with an {@link Executor} which runs\n   * all work in the current thread.\n   *\n   * @see #flush(Executor)\n   * @return true if any work was flushed.\n   */\n  default boolean flush() {\n    return flush(Runnable::run);\n  }\n\n  /**\n   * Identifies any stale tasks queued using {@link #schedule(Class)} (those which were queued more\n   * than supplied {@link TransactionOutboxBuilder#attemptFrequency(Duration)} ago and have been\n   * tried less than {@link TransactionOutboxBuilder#blockAfterAttempts(int)} )} times) and attempts\n   * to resubmit them.\n   *\n   * <p>As long as the {@link TransactionOutboxBuilder#submitter(Submitter)} is non-blocking (e.g.\n   * uses a bounded queue with a {@link java.util.concurrent.RejectedExecutionHandler} which throws\n   * such as {@link java.util.concurrent.ThreadPoolExecutor.AbortPolicy}), this method will return\n   * quickly. However, if the {@link TransactionOutboxBuilder#submitter(Submitter)} uses a bounded\n   * queue with a blocking policy, this method could block for a long time, depending on how long\n   * the scheduled work takes and how large {@link TransactionOutboxBuilder#flushBatchSize(int)} is.\n   *\n   * <p>Calls {@link TransactionManager#inTransactionReturns(TransactionalSupplier)} to start a new\n   * transaction for the fetch.\n   *\n   * <p>Additionally, expires any records completed prior to the {@link\n   * TransactionOutboxBuilder#retentionThreshold(Duration)}.\n   *\n   * @param executor to be used for parallelising work (note that the method overall is blocking and\n   *     this is solely ued for fork-join semantics).\n   * @return true if any work was flushed.\n   */\n  boolean flush(Executor executor);\n\n  /**\n   * Flushes a specific topic (or set of topics)\n   *\n   * @param executor to be used for parallelising work (note that the method overall is blocking and\n   *     this is solely ued for fork-join semantics).\n   * @param topicNames the list of specific topics to flush\n   * @return true if any work was flushed\n   */\n  default boolean flushTopics(Executor executor, String... topicNames) {\n    return flushTopics(executor, Arrays.asList(topicNames));\n  }\n\n  /**\n   * Flushes a specific topic (or set of topics)\n   *\n   * @param executor to be used for parallelising work (note that the method overall is blocking and\n   *     this is solely ued for fork-join semantics).\n   * @param topicNames the list of specific topics to flush\n   * @return true if any work was flushed\n   */\n  boolean flushTopics(Executor executor, List<String> topicNames);\n\n  /**\n   * Unblocks a blocked entry and resets the attempt count so that it will be retried again.\n   * Requires an active transaction and a transaction manager that supports thread local context.\n   *\n   * @param entryId The entry id.\n   * @return True if the request to unblock the entry was successful. May return false if another\n   *     thread unblocked the entry first.\n   */\n  boolean unblock(String entryId);\n\n  /**\n   * Clears a failed entry of its failed state and resets the attempt count so that it will be\n   * retried again. Requires an active transaction and a transaction manager that supports supplied\n   * context.\n   *\n   * @param entryId The entry id.\n   * @param transactionContext The transaction context ({@link TransactionManager} implementation\n   *     specific).\n   * @return True if the request to unblock the entry was successful. May return false if another\n   *     thread unblocked the entry first.\n   */\n  @SuppressWarnings(\"unused\")\n  boolean unblock(String entryId, Object transactionContext);\n\n  /**\n   * Processes an entry immediately in the current thread. Intended for use in custom\n   * implementations of {@link Submitter} and should not generally otherwise be called.\n   *\n   * @param entry The entry.\n   */\n  @SuppressWarnings(\"WeakerAccess\")\n  void processNow(TransactionOutboxEntry entry);\n\n  /** Builder for {@link TransactionOutbox}. */\n  @ToString\n  abstract class TransactionOutboxBuilder {\n\n    protected TransactionManager transactionManager;\n    protected Instantiator instantiator;\n    protected Submitter submitter;\n    protected Duration attemptFrequency;\n    protected int blockAfterAttempts;\n    protected int flushBatchSize;\n    protected Supplier<Clock> clockProvider;\n    protected TransactionOutboxListener listener;\n    protected Persistor persistor;\n    protected Level logLevelTemporaryFailure;\n    protected Boolean serializeMdc;\n    protected Duration retentionThreshold;\n    protected Boolean initializeImmediately;\n\n    protected TransactionOutboxBuilder() {}\n\n    /**\n     * @param transactionManager Provides {@link TransactionOutbox} with the ability to start,\n     *     commit and roll back transactions as well as interact with running transactions started\n     *     outside.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder transactionManager(TransactionManager transactionManager) {\n      this.transactionManager = transactionManager;\n      return this;\n    }\n\n    /**\n     * @param instantiator Responsible for describing a class as a name and creating instances of\n     *     that class at runtime from the name. See {@link Instantiator} for more information.\n     *     Defaults to {@link Instantiator#usingReflection()}.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder instantiator(Instantiator instantiator) {\n      this.instantiator = instantiator;\n      return this;\n    }\n\n    /**\n     * @param submitter Used for scheduling background work. If no submitter is specified, {@link\n     *     TransactionOutbox} will use {@link Submitter#withDefaultExecutor()}. See {@link\n     *     Submitter#withExecutor(Executor)} for more information on designing bespoke submitters\n     *     for remoting.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder submitter(Submitter submitter) {\n      this.submitter = submitter;\n      return this;\n    }\n\n    /**\n     * @param attemptFrequency How often tasks should be re-attempted. This should be balanced with\n     *     {@link #flushBatchSize} and the frequency with which {@link #flush()} is called to\n     *     achieve optimum throughput. Defaults to 2 minutes.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder attemptFrequency(Duration attemptFrequency) {\n      this.attemptFrequency = attemptFrequency;\n      return this;\n    }\n\n    /**\n     * @param blockAfterAttempts how many attempts a task should be retried before it is permanently\n     *     blocked. Defaults to 5.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder blockAfterAttempts(int blockAfterAttempts) {\n      this.blockAfterAttempts = blockAfterAttempts;\n      return this;\n    }\n\n    /**\n     * @param flushBatchSize How many items should be attempted in each flush. This should be\n     *     balanced with {@link #attemptFrequency} and the frequency with which {@link #flush()} is\n     *     called to achieve optimum throughput. Defaults to 4096.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder flushBatchSize(int flushBatchSize) {\n      this.flushBatchSize = flushBatchSize;\n      return this;\n    }\n\n    /**\n     * @param clockProvider The {@link Clock} source. Generally best left alone except when testing.\n     *     Defaults to the system clock.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder clockProvider(Supplier<Clock> clockProvider) {\n      this.clockProvider = clockProvider;\n      return this;\n    }\n\n    /**\n     * @param listener Event listener. Allows client code to react to tasks running, failing or\n     *     getting blocked.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder listener(TransactionOutboxListener listener) {\n      this.listener = listener;\n      return this;\n    }\n\n    /**\n     * @param persistor The method {@link TransactionOutbox} uses to interact with the database.\n     *     This encapsulates all {@link TransactionOutbox} interaction with the database outside\n     *     transaction management (which is handled by the {@link TransactionManager}). Defaults to\n     *     a multi-platform SQL implementation that should not need to be changed in most cases. If\n     *     re-implementing this interface, read the documentation on {@link Persistor} carefully.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder persistor(Persistor persistor) {\n      this.persistor = persistor;\n      return this;\n    }\n\n    /**\n     * @param logLevelTemporaryFailure The log level to use when logging temporary task failures.\n     *     Includes a full stack trace. Defaults to {@code WARN} level, but you may wish to reduce\n     *     it to a lower level if you consider warnings to be incidents.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder logLevelTemporaryFailure(Level logLevelTemporaryFailure) {\n      this.logLevelTemporaryFailure = logLevelTemporaryFailure;\n      return this;\n    }\n\n    /**\n     * @param serializeMdc Determines whether to include any Slf4j {@link MDC} (Mapped Diagnostic\n     *     Context) in serialized invocations and recreate the state in submitted tasks. Defaults to\n     *     true.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder serializeMdc(Boolean serializeMdc) {\n      this.serializeMdc = serializeMdc;\n      return this;\n    }\n\n    /**\n     * @param retentionThreshold The length of time that any request with a unique client id will be\n     *     remembered, such that if the same request is repeated within the threshold period, {@link\n     *     AlreadyScheduledException} will be thrown.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder retentionThreshold(Duration retentionThreshold) {\n      this.retentionThreshold = retentionThreshold;\n      return this;\n    }\n\n    /**\n     * @param initializeImmediately If true, {@link TransactionOutbox#initialize()} is called\n     *     automatically on creation (this is the default). Set to false in environments where\n     *     structured startup means that the database should not be accessed until later.\n     * @return Builder.\n     */\n    public TransactionOutboxBuilder initializeImmediately(boolean initializeImmediately) {\n      this.initializeImmediately = initializeImmediately;\n      return this;\n    }\n\n    /**\n     * Creates and initialises the {@link TransactionOutbox}.\n     *\n     * @return The outbox implementation.\n     */\n    public abstract TransactionOutbox build();\n  }\n\n  interface ParameterizedScheduleBuilder {\n\n    /**\n     * Specifies a unique id for the request. This defaults to {@code null}, but if non-null, will\n     * cause the request to be retained in the database after completion for the specified {@link\n     * TransactionOutboxBuilder#retentionThreshold(Duration)}, during which time any duplicate\n     * requests to schedule the same request id will throw {@link AlreadyScheduledException}. This\n     * allows tasks to be scheduled idempotently even if the request itself is not idempotent (e.g.\n     * from a message queue listener, which can usually only work reliably on an \"at least once\"\n     * basis).\n     *\n     * @param uniqueRequestId The unique request id. May be {@code null}, but if non-null may be a\n     *     maximum of 250 characters in length. It is advised that if these ids are client-supplied,\n     *     they be prepended with some sort of context identifier to ensure global uniqueness.\n     * @return Builder.\n     */\n    ParameterizedScheduleBuilder uniqueRequestId(String uniqueRequestId);\n\n    /**\n     * Specifies that the request should be applied in a strictly-ordered fashion within the\n     * specified topic.\n     *\n     * <p>This is useful for a number of applications, such as feeding messages into an ordered\n     * pipeline such as a FIFO queue or Kafka topic, or for reliable data replication, such as when\n     * feeding a data warehouse or distributed cache.\n     *\n     * <p>Note that using this option has a number of consequences:\n     *\n     * <ul>\n     *   <li>Requests are not processed immediately when submitting a request, as normal, and are\n     *       processed by {@link TransactionOutbox#flush()} only. As a result there will be\n     *       increased delay between the source transaction being committed and the request being\n     *       processed.\n     *   <li>If a request fails, no further requests will be processed <em>in that topic</em> until\n     *       a subsequent retry allows the failing request to succeed, to preserve ordered\n     *       processing. This means it is possible for topics to become entirely frozen in the event\n     *       that a request fails repeatedly. For this reason, it is essential to use a {@link\n     *       TransactionOutboxListener} to watch for failing requests and investigate quickly. Note\n     *       that other topics will be unaffected.\n     *   <lI>For the same reason, {@link TransactionOutboxBuilder#blockAfterAttempts} is ignored for\n     *       all requests that use this option. The only safe way to recover from a failing request\n     *       is to make the request succeed.\n     *   <li>A single topic can only be processed in single-threaded fashion, so if your requests\n     *       use a small number of topics, scalability will be affected since the degree of\n     *       parallelism will be reduced.\n     *   <li>Throughput is significantly reduced and database load increased more generally, even\n     *       with larger numbers of topics, since records are only processed one-at-a-time rather\n     *       than in batches, which is less optimised.\n     *   <li>In general, <a\n     *       href=\"https://shermanonsoftware.com/2019/09/04/your-database-is-not-a-queue/\">databases\n     *       are not well optimised for this sort of thing</a>. Don't expect miracles. If you need\n     *       more throughput, you probably need to think twice about your architecture. Consider the\n     *       <a href=\"https://martinfowler.com/eaaDev/EventSourcing.html\">event sourcing\n     *       pattern</a>, for example, where the message queue is the primary data store rather than\n     *       a secondary, and remove the need for an outbox entirely.\n     * </ul>\n     *\n     * @param topic a free-text string up to 250 characters.\n     * @return Builder.\n     */\n    ParameterizedScheduleBuilder ordered(String topic);\n\n    /**\n     * Instructs the scheduler to delay processing the task until after the specified duration. This\n     * can be used for simple job scheduling or to introduce an asynchronous delay into chains of\n     * tasks.\n     *\n     * <p>Note that any delay is <em>not precise</em> and accuracy is primarily determined by the\n     * frequency at which {@link #flush(Executor)} or {@link #flush()} are called. Do not use this\n     * for time-sensitive tasks, particularly if the duration exceeds {@link\n     * TransactionOutboxBuilder#attemptFrequency(Duration)} (see more on this below).\n     *\n     * <p>A note on implementation: tasks (when {@link #ordered(String)} is not used) are normally\n     * submitted for processing on the local JVM immediately after transaction commit. By default,\n     * when a delay is introduced, the work is instead submitted to a {@link\n     * java.util.concurrent.ScheduledExecutorService} for processing after the specified delay.\n     * However, if the delay is long enough that the work would likely get picked up by a {@link\n     * #flush()} on this JVM or another, this is pointless and wasteful. Unfortunately, we don't\n     * know exactly how frequently {@link #flush()} will be called! To mitigate this, Any task\n     * submitted with a delay in excess of {@link\n     * TransactionOutboxBuilder#attemptFrequency(Duration)} will be assumed to get picked up by a\n     * future flush.\n     *\n     * @param duration The minimum delay duration.\n     * @return Builder.\n     */\n    ParameterizedScheduleBuilder delayForAtLeast(Duration duration);\n\n    /**\n     * Equivalent to {@link TransactionOutbox#schedule(Class)}, but applying additional parameters\n     * to the request as configured using {@link TransactionOutbox#with()}.\n     *\n     * <p>Usage example:\n     *\n     * <pre>transactionOutbox.with()\n     * .uniqueRequestId(\"my-request\")\n     * .schedule(MyService.class)\n     * .runMyMethod(\"with\", \"some\", \"arguments\");</pre>\n     *\n     * @param clazz The class to proxy.\n     * @param <T> The type to proxy.\n     * @return The proxy of {@code T}.\n     */\n    <T> T schedule(Class<T> clazz);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxEntry.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static java.util.stream.Collectors.joining;\n\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.time.Instant;\nimport java.util.Arrays;\nimport lombok.*;\nimport lombok.experimental.SuperBuilder;\n\n/**\n * Internal representation of a {@link TransactionOutbox} task. Generally only directly of interest\n * to implementers of SPIs such as {@link Persistor} or {@link Submitter}.\n */\n@SuperBuilder(toBuilder = true)\n@EqualsAndHashCode\n@ToString\npublic class TransactionOutboxEntry implements Validatable {\n\n  /**\n   * @param id The id of the record. Usually a UUID.\n   * @return The id of the record. Usually a UUID.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  private final String id;\n\n  /**\n   * @param uniqueRequestId A unique, client-supplied key for the entry. If supplied, it must be\n   *     globally unique\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  private final String uniqueRequestId;\n\n  /**\n   * @param topic An optional scope for ordered sequencing.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  private final String topic;\n\n  /**\n   * @param sequence The ordered sequence within the {@code topic}.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private Long sequence;\n\n  /**\n   * @param invocation The method invocation to perform.\n   * @return The method invocation to perform.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter(AccessLevel.PACKAGE)\n  private Invocation invocation;\n\n  /**\n   * @param lastAttemptTime The timestamp at which the task was last processed.\n   * @return The timestamp at which the task was last processed.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private Instant lastAttemptTime;\n\n  /**\n   * @param nextAttemptTime The timestamp after which the task is available for re-attempting.\n   * @return The timestamp after which the task is available for re-attempting.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private Instant nextAttemptTime;\n\n  /**\n   * @param attempts The number of unsuccessful attempts so far made to run the task.\n   * @return The number of unsuccessful attempts so far made to run the task.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private int attempts;\n\n  /**\n   * @param blocked True if the task has exceeded the configured maximum number of attempts.\n   * @return True if the task has exceeded the configured maximum number of attempts.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private boolean blocked;\n\n  /**\n   * @param processed True if the task has been processed but has been retained to prevent duplicate\n   *     requests.\n   * @return True if the task has been processed but has been retained to prevent * duplicate\n   *     requests.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private boolean processed;\n\n  /**\n   * @param version The optimistic locking version. Monotonically increasing with each update.\n   * @return The optimistic locking version. Monotonically increasing with each update.\n   */\n  @SuppressWarnings(\"JavaDoc\")\n  @Getter\n  @Setter\n  private int version;\n\n  @EqualsAndHashCode.Exclude @ToString.Exclude private volatile boolean initialized;\n  @EqualsAndHashCode.Exclude @ToString.Exclude private String description;\n\n  /**\n   * @return A textual description of the task.\n   */\n  public String description() {\n    if (!this.initialized) {\n      synchronized (this) {\n        if (!this.initialized) {\n          String description =\n              String.format(\n                  \"%s.%s(%s) [%s]%s%s\",\n                  invocation.getClassName(),\n                  invocation.getMethodName(),\n                  invocation.getArgs() == null\n                      ? null\n                      : Arrays.stream(invocation.getArgs())\n                          .map(Utils::stringify)\n                          .collect(joining(\", \")),\n                  id,\n                  uniqueRequestId == null ? \"\" : \" uid=[\" + uniqueRequestId + \"]\",\n                  topic == null ? \"\" : \" seq=[\" + topic + \"/\" + sequence + \"]\");\n          this.description = description;\n          this.initialized = true;\n          return description;\n        }\n      }\n    }\n    return this.description;\n  }\n\n  @Override\n  public void validate(Validator validator) {\n    validator.notNull(\"id\", id);\n    validator.nullOrNotBlank(\"uniqueRequestId\", uniqueRequestId);\n    validator.nullOrNotBlank(\"topic\", topic);\n    validator.notNull(\"invocation\", invocation);\n    validator.positiveOrZero(\"attempts\", attempts);\n    validator.positiveOrZero(\"version\", version);\n    validator.isTrue(\"topic\", !\"*\".equals(topic), \"Topic may not be *\");\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxImpl.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.logAtLevel;\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\nimport static java.time.temporal.ChronoUnit.MILLIS;\nimport static java.time.temporal.ChronoUnit.MINUTES;\n\nimport com.gruelbox.transactionoutbox.spi.ProxyFactory;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.lang.reflect.InvocationTargetException;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport lombok.AccessLevel;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\nimport lombok.ToString;\nimport lombok.experimental.Accessors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.slf4j.MDC;\nimport org.slf4j.event.Level;\n\n@Slf4j\n@RequiredArgsConstructor(access = AccessLevel.PRIVATE)\nfinal class TransactionOutboxImpl implements TransactionOutbox, Validatable {\n\n  /**\n   * Used in tests to ensure that the full serialization/deserialization loop is done every time\n   * work is processed. This prevents tests passing which would fail if the work were delayed and\n   * handled after being loaded from disk.\n   */\n  static final AtomicBoolean FORCE_SERIALIZE_AND_DESERIALIZE_BEFORE_USE = new AtomicBoolean();\n\n  private final TransactionManager transactionManager;\n  private final Persistor persistor;\n  private final Instantiator instantiator;\n  private final Submitter submitter;\n  private final Duration attemptFrequency;\n  private final Level logLevelTemporaryFailure;\n  private final int blockAfterAttempts;\n  private final int flushBatchSize;\n  private final Supplier<Clock> clockProvider;\n  private final TransactionOutboxListener listener;\n  private final boolean serializeMdc;\n  private final Validator validator;\n  private final Duration retentionThreshold;\n  private final AtomicBoolean initialized = new AtomicBoolean();\n  private final ProxyFactory proxyFactory = new ProxyFactory();\n  private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();\n\n  @Override\n  public void validate(Validator validator) {\n    validator.notNull(\"transactionManager\", transactionManager);\n    validator.valid(\"persistor\", persistor);\n    validator.valid(\"instantiator\", instantiator);\n    validator.valid(\"submitter\", submitter);\n    validator.notNull(\"attemptFrequency\", attemptFrequency);\n    validator.notNull(\"logLevelTemporaryFailure\", logLevelTemporaryFailure);\n    validator.min(\"blockAfterAttempts\", blockAfterAttempts, 1);\n    validator.min(\"flushBatchSize\", flushBatchSize, 1);\n    validator.notNull(\"clockProvider\", clockProvider);\n    validator.notNull(\"listener\", listener);\n    validator.notNull(\"retentionThreshold\", retentionThreshold);\n  }\n\n  static TransactionOutboxBuilder builder() {\n    return new TransactionOutboxBuilderImpl();\n  }\n\n  @Override\n  public void initialize() {\n    if (initialized.compareAndSet(false, true)) {\n      try {\n        persistor.migrate(transactionManager);\n      } catch (Exception e) {\n        initialized.set(false);\n        throw e;\n      }\n    }\n  }\n\n  @Override\n  public <T> T schedule(Class<T> clazz) {\n    return schedule(clazz, null, null, null);\n  }\n\n  @Override\n  public ParameterizedScheduleBuilder with() {\n    return new ParameterizedScheduleBuilderImpl();\n  }\n\n  private boolean doFlush(Function<Transaction, Collection<TransactionOutboxEntry>> batchSource) {\n    var batch =\n        transactionManager.inTransactionReturns(\n            transaction -> {\n              var entries = batchSource.apply(transaction);\n              List<TransactionOutboxEntry> result = new ArrayList<>(entries.size());\n              for (var entry : entries) {\n                log.debug(\"Triggering {}\", entry.description());\n                try {\n                  pushBack(transaction, entry);\n                  result.add(entry);\n                } catch (OptimisticLockException e) {\n                  log.debug(\"Beaten to optimistic lock on {}\", entry.description());\n                }\n              }\n              return result;\n            });\n    log.debug(\"Got batch of {}\", batch.size());\n    batch.forEach(this::submitNow);\n    log.debug(\"Submitted batch\");\n    return !batch.isEmpty();\n  }\n\n  @Override\n  public boolean flush(Executor executor) {\n    if (!initialized.get()) {\n      throw new IllegalStateException(\"Not initialized\");\n    }\n    Instant now = clockProvider.get().instant();\n    List<CompletableFuture<Boolean>> futures = new ArrayList<>();\n\n    futures.add(\n        CompletableFuture.supplyAsync(\n            () -> {\n              log.debug(\"Flushing stale tasks\");\n              return doFlush(\n                  tx -> uncheckedly(() -> persistor.selectBatch(tx, flushBatchSize, now)));\n            },\n            executor));\n\n    futures.add(\n        CompletableFuture.runAsync(() -> expireIdempotencyProtection(now), executor)\n            .thenApply(it -> false));\n\n    futures.add(\n        CompletableFuture.supplyAsync(\n            () -> {\n              log.debug(\"Flushing topics\");\n              return doFlush(\n                  tx -> uncheckedly(() -> persistor.selectNextInTopics(tx, flushBatchSize, now)));\n            },\n            executor));\n\n    return futures.stream()\n        .reduce((f1, f2) -> f1.thenCombine(f2, (d1, d2) -> d1 || d2))\n        .map(CompletableFuture::join)\n        .orElse(false);\n  }\n\n  @Override\n  public boolean flushTopics(Executor executor, List<String> topicNames) {\n    if (!initialized.get()) {\n      throw new IllegalStateException(\"Not initialized\");\n    }\n    Instant now = clockProvider.get().instant();\n\n    log.debug(\"Flushing selected topics {}\", topicNames);\n    return doFlush(\n        tx ->\n            uncheckedly(\n                () -> persistor.selectNextInSelectedTopics(tx, topicNames, flushBatchSize, now)));\n  }\n\n  private void expireIdempotencyProtection(Instant now) {\n    long totalRecordsDeleted = 0;\n    int recordsDeleted;\n    do {\n      recordsDeleted =\n          transactionManager.inTransactionReturns(\n              tx ->\n                  uncheckedly(() -> persistor.deleteProcessedAndExpired(tx, flushBatchSize, now)));\n      totalRecordsDeleted += recordsDeleted;\n    } while (recordsDeleted > 0);\n    if (totalRecordsDeleted > 0) {\n      String duration =\n          String.format(\n              \"%dd:%02dh:%02dm:%02ds\",\n              retentionThreshold.toDaysPart(),\n              retentionThreshold.toHoursPart(),\n              retentionThreshold.toMinutesPart(),\n              retentionThreshold.toSecondsPart());\n      log.info(\n          \"Expired idempotency protection on {} requests completed more than {} ago\",\n          totalRecordsDeleted,\n          duration);\n    } else {\n      log.debug(\"No records found to delete as of {}\", now);\n    }\n  }\n\n  @Override\n  public boolean unblock(String entryId) {\n    if (!initialized.get()) {\n      throw new IllegalStateException(\"Not initialized\");\n    }\n    if (!(transactionManager instanceof ThreadLocalContextTransactionManager)) {\n      throw new UnsupportedOperationException(\n          \"This method requires a ThreadLocalContextTransactionManager\");\n    }\n    log.info(\"Unblocking entry {} for retry.\", entryId);\n    try {\n      return ((ThreadLocalContextTransactionManager) transactionManager)\n          .requireTransactionReturns(tx -> persistor.unblock(tx, entryId));\n    } catch (Exception e) {\n      throw (RuntimeException) Utils.uncheckAndThrow(e);\n    }\n  }\n\n  @Override\n  @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n  public boolean unblock(String entryId, Object transactionContext) {\n    if (!initialized.get()) {\n      throw new IllegalStateException(\"Not initialized\");\n    }\n    if (!(transactionManager instanceof ParameterContextTransactionManager)) {\n      throw new UnsupportedOperationException(\n          \"This method requires a ParameterContextTransactionManager\");\n    }\n    log.info(\"Unblocking entry {} for retry\", entryId);\n    try {\n      if (transactionContext instanceof Transaction) {\n        return persistor.unblock((Transaction) transactionContext, entryId);\n      }\n      Transaction transaction =\n          ((ParameterContextTransactionManager) transactionManager)\n              .transactionFromContext(transactionContext);\n      return persistor.unblock(transaction, entryId);\n    } catch (Exception e) {\n      throw (RuntimeException) Utils.uncheckAndThrow(e);\n    }\n  }\n\n  private <T> T schedule(\n      Class<T> clazz, String uniqueRequestId, String topic, Duration delayForAtLeast) {\n    if (!initialized.get()) {\n      throw new IllegalStateException(\"Not initialized\");\n    }\n    return proxyFactory.createProxy(\n        clazz,\n        (method, args) ->\n            uncheckedly(\n                () -> {\n                  var extracted = transactionManager.extractTransaction(method, args);\n                  TransactionOutboxEntry entry =\n                      newEntry(\n                          extracted.getClazz(),\n                          extracted.getMethodName(),\n                          extracted.getParameters(),\n                          extracted.getArgs(),\n                          uniqueRequestId,\n                          topic);\n                  if (delayForAtLeast != null) {\n                    entry.setNextAttemptTime(entry.getNextAttemptTime().plus(delayForAtLeast));\n                  }\n                  validator.validate(entry);\n                  persistor.save(extracted.getTransaction(), entry);\n                  extracted\n                      .getTransaction()\n                      .addPostCommitHook(\n                          () -> {\n                            listener.scheduled(entry);\n                            if (entry.getTopic() != null) {\n                              log.debug(\"Queued {} in topic {}\", entry.description(), topic);\n                            } else if (delayForAtLeast == null) {\n                              submitNow(entry);\n                              log.debug(\n                                  \"Scheduled {} for post-commit execution\", entry.description());\n                            } else if (delayForAtLeast.compareTo(attemptFrequency) < 0) {\n                              scheduler.schedule(\n                                  () -> submitNow(entry),\n                                  delayForAtLeast.toMillis(),\n                                  TimeUnit.MILLISECONDS);\n                              log.info(\n                                  \"Scheduled {} for post-commit execution after at least {}\",\n                                  entry.description(),\n                                  delayForAtLeast);\n                            } else {\n                              log.info(\n                                  \"Queued {} for execution after at least {}\",\n                                  entry.description(),\n                                  delayForAtLeast);\n                            }\n                          });\n                  return null;\n                }));\n  }\n\n  private void submitNow(TransactionOutboxEntry entry) {\n    submitter.submit(entry, this::processNow);\n  }\n\n  @Override\n  @SuppressWarnings(\"WeakerAccess\")\n  public void processNow(TransactionOutboxEntry entry) {\n    listener.wrapInvocationAndInit(\n        new TransactionOutboxListener.Invocator() {\n          @Override\n          public void run() {\n            processNowInternal(entry);\n          }\n\n          @Override\n          public Invocation getInvocation() {\n            return entry.getInvocation();\n          }\n        });\n  }\n\n  private void processNowInternal(TransactionOutboxEntry entry) {\n    initialize();\n    Boolean success = null;\n    try {\n      success =\n          entry\n              .getInvocation()\n              .withinMDC(\n                  () ->\n                      transactionManager.inTransactionReturnsThrows(\n                          tx -> {\n                            if (!persistor.lock(tx, entry)) {\n                              return false;\n                            }\n                            log.info(\"Processing {}\", entry.description());\n                            invoke(entry, tx);\n                            if (entry.getUniqueRequestId() == null) {\n                              persistor.delete(tx, entry);\n                            } else {\n                              log.debug(\n                                  \"Deferring deletion of {} by {}\",\n                                  entry.description(),\n                                  retentionThreshold);\n                              entry.setProcessed(true);\n                              entry.setLastAttemptTime(Instant.now(clockProvider.get()));\n                              entry.setNextAttemptTime(after(retentionThreshold));\n                              persistor.update(tx, entry);\n                            }\n                            return true;\n                          }));\n    } catch (InvocationTargetException e) {\n      updateAttemptCount(entry, e.getCause());\n    } catch (Exception e) {\n      updateAttemptCount(entry, e);\n    }\n    if (success != null) {\n      if (success) {\n        log.info(\"Processed {}\", entry.description());\n        listener.success(entry);\n      } else {\n        log.debug(\"Skipped task {} - may be locked or already processed\", entry.getId());\n      }\n    }\n  }\n\n  private void invoke(TransactionOutboxEntry entry, Transaction transaction)\n      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {\n    Object instance = instantiator.getInstance(entry.getInvocation().getClassName());\n    log.debug(\"Created instance {}\", instance);\n    transactionManager\n        .injectTransaction(entry.getInvocation(), transaction)\n        .invoke(instance, listener);\n  }\n\n  private TransactionOutboxEntry newEntry(\n      Class<?> clazz,\n      String methodName,\n      Class<?>[] params,\n      Object[] args,\n      String uniqueRequestId,\n      String topic) {\n\n    var invocation =\n        new Invocation(\n            instantiator.getName(clazz),\n            methodName,\n            params,\n            args,\n            serializeMdc && (MDC.getMDCAdapter() != null) ? MDC.getCopyOfContextMap() : null,\n            listener.extractSession());\n    if (FORCE_SERIALIZE_AND_DESERIALIZE_BEFORE_USE.get()) {\n      invocation = persistor.serializeAndDeserialize(invocation);\n    }\n\n    return TransactionOutboxEntry.builder()\n        .id(UUID.randomUUID().toString())\n        .invocation(invocation)\n        .lastAttemptTime(null)\n        .nextAttemptTime(clockProvider.get().instant())\n        .uniqueRequestId(uniqueRequestId)\n        .topic(topic)\n        .build();\n  }\n\n  private void pushBack(Transaction transaction, TransactionOutboxEntry entry)\n      throws OptimisticLockException {\n    try {\n      entry.setLastAttemptTime(clockProvider.get().instant());\n      entry.setNextAttemptTime(after(attemptFrequency));\n      validator.validate(entry);\n      persistor.update(transaction, entry);\n    } catch (OptimisticLockException e) {\n      throw e;\n    } catch (Exception e) {\n      Utils.uncheckAndThrow(e);\n    }\n  }\n\n  private Instant after(Duration duration) {\n    return clockProvider.get().instant().plus(duration).truncatedTo(MILLIS);\n  }\n\n  private void updateAttemptCount(TransactionOutboxEntry entry, Throwable cause) {\n    try {\n      entry.setAttempts(entry.getAttempts() + 1);\n      var blocked = (entry.getTopic() == null) && (entry.getAttempts() >= blockAfterAttempts);\n      entry.setBlocked(blocked);\n      transactionManager.inTransactionThrows(tx -> pushBack(tx, entry));\n      listener.failure(entry, cause);\n      if (blocked) {\n        log.error(\n            \"Blocking failing entry {} after {} attempts: {}\",\n            entry.getId(),\n            entry.getAttempts(),\n            entry.description(),\n            cause);\n        listener.blocked(entry, cause);\n      } else {\n        logAtLevel(\n            log,\n            logLevelTemporaryFailure,\n            \"Temporarily failed to process entry {} : {}\",\n            entry.getId(),\n            entry.description(),\n            cause);\n      }\n    } catch (Exception e) {\n      log.error(\n          \"Failed to update attempt count for {}. It may be retried more times than expected.\",\n          entry.description(),\n          e);\n    }\n  }\n\n  @ToString\n  static class TransactionOutboxBuilderImpl extends TransactionOutboxBuilder {\n\n    TransactionOutboxBuilderImpl() {\n      super();\n    }\n\n    public TransactionOutboxImpl build() {\n      Validator validator = new Validator(this.clockProvider);\n      TransactionOutboxImpl impl =\n          new TransactionOutboxImpl(\n              transactionManager,\n              persistor,\n              Utils.firstNonNull(instantiator, Instantiator::usingReflection),\n              Utils.firstNonNull(submitter, Submitter::withDefaultExecutor),\n              Utils.firstNonNull(attemptFrequency, () -> Duration.of(2, MINUTES)),\n              Utils.firstNonNull(logLevelTemporaryFailure, () -> Level.WARN),\n              blockAfterAttempts < 1 ? 5 : blockAfterAttempts,\n              flushBatchSize < 1 ? 4096 : flushBatchSize,\n              clockProvider == null ? Clock::systemDefaultZone : clockProvider,\n              Utils.firstNonNull(listener, () -> TransactionOutboxListener.EMPTY),\n              serializeMdc == null || serializeMdc,\n              validator,\n              retentionThreshold == null ? Duration.ofDays(7) : retentionThreshold);\n      validator.validate(impl);\n      if (initializeImmediately == null || initializeImmediately) {\n        impl.initialize();\n      }\n      return impl;\n    }\n  }\n\n  @Accessors(fluent = true, chain = true)\n  @Setter\n  private class ParameterizedScheduleBuilderImpl implements ParameterizedScheduleBuilder {\n\n    private String uniqueRequestId;\n    private String ordered;\n    private Duration delayForAtLeast;\n\n    @Override\n    public <T> T schedule(Class<T> clazz) {\n      if (uniqueRequestId != null && uniqueRequestId.length() > 250) {\n        throw new IllegalArgumentException(\"uniqueRequestId may be up to 250 characters\");\n      }\n      return TransactionOutboxImpl.this.schedule(clazz, uniqueRequestId, ordered, delayForAtLeast);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionOutboxListener.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/** A listener for events fired by {@link TransactionOutbox}. */\npublic interface TransactionOutboxListener {\n\n  TransactionOutboxListener EMPTY = new TransactionOutboxListener() {};\n\n  /**\n   * Fired when a transaction outbox task is scheduled.\n   *\n   * <p>This event is not guaranteed to fire in the event of a JVM failure or power loss. It is\n   * fired <em>after</em> the commit to the database adding the scheduled task but before the task\n   * is submitted for processing. It will, except in extreme circumstances (although this is not\n   * guaranteed), fire prior to any subsequent {@link #success(TransactionOutboxEntry)} or {@link\n   * #failure(TransactionOutboxEntry, Throwable)}.\n   *\n   * @param entry The outbox entry scheduled.\n   */\n  default void scheduled(TransactionOutboxEntry entry) {\n    // No-op\n  }\n\n  /**\n   * Implement this method to intercept and decorate all outbox invocations. In general, you should\n   * call {@code invocation.run()} which actually calls the underlying method, unless you are\n   * deliberately trying to suppress the method call.\n   *\n   * <p>This method is called immediately before your method is invoked. A fair bit of work has\n   * already been done by this point (MDC initialised, transaction started, database record locked\n   * etc) so it's a good place to do database-related things, but a poor place to initialise things\n   * like session state (such as OTEL traces). For that, use {@link\n   * #wrapInvocationAndInit(Invocator)}.\n   *\n   * @param invocator A runnable which performs the work of the scheduled task.\n   * @throws IllegalAccessException If thrown by the method invocation.\n   * @throws IllegalArgumentException If thrown by the method invocation.\n   * @throws InvocationTargetException If thrown by the method invocation.\n   */\n  default void wrapInvocation(Invocator invocator)\n      throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {\n    invocator.run();\n  }\n\n  /**\n   * Wraps an entire invocation, including the work do obtain a database lock. This is a good place\n   * to initialise session state from {@link Invocation#getSession()} (using {@link\n   * Invocator#getInvocation()}) since then all subsequent database activity is performed within\n   * that session.\n   *\n   * <p>NOTE that there is no guarantee that your method will actually be invoked at this point.\n   * There is no active database transaction, the database record hasn't been locked and important\n   * checks haven't been performed. This is intended purely for state management.\n   *\n   * @param invocator A runnable which performs the work of the scheduled task.\n   */\n  default void wrapInvocationAndInit(Invocator invocator) {\n    try {\n      invocator.run();\n    } catch (IllegalAccessException | InvocationTargetException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  interface Invocator {\n    void run() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;\n\n    default void runUnchecked() {\n      try {\n        run();\n      } catch (IllegalAccessException | InvocationTargetException e) {\n        throw new UncheckedException(e);\n      }\n    }\n\n    /**\n     * @return The full {@link Invocation} object, for use in {@link #wrapInvocation(Invocator)} and\n     *     {@link #wrapInvocationAndInit(Invocator)}.\n     */\n    Invocation getInvocation();\n  }\n\n  /**\n   * Fired when a transaction outbox task is successfully completed <em>and</em> recorded as such in\n   * the database such that it will not be re-attempted. Note that:\n   *\n   * <ul>\n   *   <li>{@link TransactionOutbox} uses \"at least once\" semantics, so the actual processing of a\n   *       task may complete any number of times before this event is fired.\n   *   <li>This event is not guaranteed to fire in the event of a JVM failure or power loss. It is\n   *       fired <em>after</em> the commit to the database removing the completed task and all bets\n   *       are off after this point.\n   * </ul>\n   *\n   * @param entry The outbox entry completed.\n   */\n  default void success(TransactionOutboxEntry entry) {\n    // No-op\n  }\n\n  /**\n   * Fired when a transaction outbox task fails. This may occur multiple times until the maximum\n   * number of retries, at which point this will be fired <em>and then</em> {@link\n   * #blocked(TransactionOutboxEntry, Throwable)}. This event is not guaranteed to fire in the event\n   * of a JVM failure or power loss. It is fired <em>after</em> the commit to the database marking\n   * the task as failed.\n   *\n   * @param entry The outbox entry failed.\n   * @param cause The cause of the most recent failure.\n   */\n  default void failure(TransactionOutboxEntry entry, Throwable cause) {\n    // No-op\n  }\n\n  /**\n   * Fired when a transaction outbox task has passed the maximum number of retries and has been\n   * blocked. This event is not guaranteed to fire in the event of a JVM failure or power loss. It\n   * is fired <em>after</em> the commit to the database marking the task as blocked.\n   *\n   * @param entry The outbox entry to be marked as blocked.\n   * @param cause The cause of the most recent failure.\n   */\n  default void blocked(TransactionOutboxEntry entry, Throwable cause) {\n    // No-op\n  }\n\n  /**\n   * Implement this to provide session state that you want to include with a persisted request. This\n   * is a free-form {@link Map} which you can then read in {@link #wrapInvocation(Invocator)} using\n   * {@link Invocator#getInvocation()}.\n   *\n   * @return Your session information.\n   */\n  default Map<String, String> extractSession() {\n    return null;\n  }\n\n  /**\n   * Chains this listener with another and returns the result.\n   *\n   * @param other The other listener. It will always be called after this one.\n   * @return The combined listener.\n   */\n  default TransactionOutboxListener andThen(TransactionOutboxListener other) {\n    var self = this;\n    return new TransactionOutboxListener() {\n\n      @Override\n      public void scheduled(TransactionOutboxEntry entry) {\n        self.scheduled(entry);\n        other.scheduled(entry);\n      }\n\n      @Override\n      public void wrapInvocation(Invocator invocator)\n          throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {\n        self.wrapInvocation(\n            new Invocator() {\n              @Override\n              public void run()\n                  throws IllegalAccessException,\n                      IllegalArgumentException,\n                      InvocationTargetException {\n                other.wrapInvocation(invocator);\n              }\n\n              @Override\n              public Invocation getInvocation() {\n                return invocator.getInvocation();\n              }\n            });\n      }\n\n      @Override\n      public void wrapInvocationAndInit(Invocator invocator) {\n        self.wrapInvocationAndInit(\n            new Invocator() {\n              @Override\n              public void run() {\n                other.wrapInvocationAndInit(invocator);\n              }\n\n              @Override\n              public Invocation getInvocation() {\n                return invocator.getInvocation();\n              }\n            });\n      }\n\n      @Override\n      public Map<String, String> extractSession() {\n        var mine = self.extractSession();\n        var theirs = other.extractSession();\n        if (mine == null) return theirs;\n        if (theirs == null) return mine;\n        Map<String, String> result = new HashMap<>(mine);\n        result.putAll(theirs);\n        return result;\n      }\n\n      @Override\n      public void success(TransactionOutboxEntry entry) {\n        self.success(entry);\n        other.success(entry);\n      }\n\n      @Override\n      public void failure(TransactionOutboxEntry entry, Throwable cause) {\n        self.failure(entry, cause);\n        other.failure(entry, cause);\n      }\n\n      @Override\n      public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n        self.blocked(entry, cause);\n        other.blocked(entry, cause);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalInvocation.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport lombok.Value;\n\n/**\n * Describes a method invocation along with the transaction scope in which it should be performed.\n */\n@Value\npublic class TransactionalInvocation {\n  Class<?> clazz;\n  String methodName;\n  Class<?>[] parameters;\n  Object[] args;\n  Transaction transaction;\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalSupplier.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface TransactionalSupplier<T> {\n\n  static <U> TransactionalSupplier<U> fromWork(TransactionalWork work) {\n    return transaction -> {\n      work.doWork(transaction);\n      return null;\n    };\n  }\n\n  T doWork(Transaction transaction);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/TransactionalWork.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n@FunctionalInterface\npublic interface TransactionalWork {\n\n  void doWork(Transaction transaction);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/UncheckedException.java",
    "content": "package com.gruelbox.transactionoutbox;\n\n/** A wrapped {@link Exception} where unchecked exceptions are caught and propagated as runtime. */\n@SuppressWarnings(\"WeakerAccess\")\npublic class UncheckedException extends RuntimeException {\n\n  public UncheckedException(Throwable cause) {\n    super(cause);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/Validatable.java",
    "content": "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",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport java.time.Clock;\nimport java.util.function.Supplier;\n\nclass Validator {\n\n  private final String path;\n  private final Supplier<Clock> clockProvider;\n\n  Validator(Supplier<Clock> clockProvider) {\n    this.clockProvider = clockProvider;\n    this.path = \"\";\n  }\n\n  private Validator(String className, Validator validator) {\n    this.clockProvider = validator.clockProvider;\n    this.path = className;\n  }\n\n  public void validate(Validatable validatable) {\n    validatable.validate(new Validator(validatable.getClass().getSimpleName(), this));\n  }\n\n  public void valid(String propertyName, Object object) {\n    notNull(propertyName, object);\n    if (!(object instanceof Validatable)) {\n      return;\n    }\n    ((Validatable) object)\n        .validate(new Validator(path.isEmpty() ? propertyName : (path + \".\" + propertyName), this));\n  }\n\n  public void notNull(String propertyName, Object object) {\n    if (object == null) {\n      error(propertyName, \"may not be null\");\n    }\n  }\n\n  public void isTrue(String propertyName, boolean condition, String message, Object... args) {\n    if (!condition) {\n      error(propertyName, String.format(message, args));\n    }\n  }\n\n  public void nullOrNotBlank(String propertyName, String object) {\n    if (object != null && object.isEmpty()) {\n      error(propertyName, \"may be either null or non-blank\");\n    }\n  }\n\n  public void notBlank(String propertyName, String object) {\n    notNull(propertyName, object);\n    if (object.isEmpty()) {\n      error(propertyName, \"may not be blank\");\n    }\n  }\n\n  public void positiveOrZero(String propertyName, int object) {\n    min(propertyName, object, 0);\n  }\n\n  public void min(String propertyName, int object, int minimumValue) {\n    if (object < minimumValue) {\n      error(propertyName, \"must be greater than \" + minimumValue);\n    }\n  }\n\n  private void error(String propertyName, String message) {\n    throw new IllegalArgumentException(\n        (path.isEmpty() ? \"\" : path + \".\") + propertyName + \" \" + message);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractFullyQualifiedNameInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox.spi;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\n\nimport com.gruelbox.transactionoutbox.Instantiator;\nimport lombok.AccessLevel;\nimport lombok.NoArgsConstructor;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Abstract {@link Instantiator} implementation which simplifies the creation of implementations\n * which instantiate based on the clazz FQN.\n */\n@Slf4j\n@SuperBuilder\n@NoArgsConstructor(access = AccessLevel.PROTECTED)\npublic abstract class AbstractFullyQualifiedNameInstantiator implements Instantiator {\n\n  @Override\n  public final String getName(Class<?> clazz) {\n    return clazz.getName();\n  }\n\n  @Override\n  public final Object getInstance(String name) {\n    log.debug(\"Getting class by name [{}]\", name);\n    return createInstance(uncheckedly(() -> Class.forName(name)));\n  }\n\n  protected abstract Object createInstance(Class<?> clazz);\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/AbstractThreadLocalTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox.spi;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.util.Deque;\nimport java.util.LinkedList;\nimport java.util.Optional;\nimport lombok.AccessLevel;\nimport lombok.NoArgsConstructor;\nimport lombok.experimental.SuperBuilder;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\n@SuperBuilder\n@NoArgsConstructor(access = AccessLevel.PROTECTED)\npublic abstract class AbstractThreadLocalTransactionManager<TX extends SimpleTransaction>\n    implements ThreadLocalContextTransactionManager {\n\n  private final ThreadLocal<Deque<TX>> transactions = ThreadLocal.withInitial(LinkedList::new);\n\n  @Override\n  public final void inTransaction(Runnable runnable) {\n    ThreadLocalContextTransactionManager.super.inTransaction(runnable);\n  }\n\n  @Override\n  public final void inTransaction(TransactionalWork work) {\n    ThreadLocalContextTransactionManager.super.inTransaction(work);\n  }\n\n  @Override\n  public final <T> T inTransactionReturns(TransactionalSupplier<T> supplier) {\n    return ThreadLocalContextTransactionManager.super.inTransactionReturns(supplier);\n  }\n\n  @Override\n  public final <E extends Exception> void inTransactionThrows(ThrowingTransactionalWork<E> work)\n      throws E {\n    ThreadLocalContextTransactionManager.super.inTransactionThrows(work);\n  }\n\n  @Override\n  public <T, E extends Exception> T requireTransactionReturns(\n      ThrowingTransactionalSupplier<T, E> work) throws E, NoTransactionActiveException {\n    return work.doWork(peekTransaction().orElseThrow(NoTransactionActiveException::new));\n  }\n\n  public final TX pushTransaction(TX transaction) {\n    transactions.get().push(transaction);\n    return transaction;\n  }\n\n  public final TX popTransaction() {\n    TX result = transactions.get().pop();\n    if (transactions.get().isEmpty()) {\n      transactions.remove();\n    }\n    return result;\n  }\n\n  public Optional<TX> peekTransaction() {\n    return Optional.ofNullable(transactions.get().peek());\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/ProxyFactory.java",
    "content": "package com.gruelbox.transactionoutbox.spi;\n\nimport com.gruelbox.transactionoutbox.MissingOptionalDependencyException;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.util.function.BiFunction;\nimport lombok.extern.slf4j.Slf4j;\nimport net.bytebuddy.ByteBuddy;\nimport net.bytebuddy.TypeCache;\nimport net.bytebuddy.TypeCache.Sort;\nimport net.bytebuddy.description.modifier.Visibility;\nimport net.bytebuddy.dynamic.loading.ClassLoadingStrategy;\nimport net.bytebuddy.implementation.InvocationHandlerAdapter;\nimport net.bytebuddy.matcher.ElementMatchers;\nimport org.objenesis.Objenesis;\nimport org.objenesis.ObjenesisStd;\nimport org.objenesis.instantiator.ObjectInstantiator;\n\n@Slf4j\npublic class ProxyFactory {\n\n  private final Objenesis objenesis = setupObjenesis();\n  private final TypeCache<Class<?>> byteBuddyCache = setupByteBuddyCache();\n\n  private static boolean hasDefaultConstructor(Class<?> clazz) {\n    try {\n      clazz.getConstructor();\n      return true;\n    } catch (NoSuchMethodException e) {\n      return false;\n    }\n  }\n\n  private TypeCache<Class<?>> setupByteBuddyCache() {\n    try {\n      return new TypeCache<>(Sort.WEAK);\n    } catch (NoClassDefFoundError error) {\n      log.info(\n          \"ByteBuddy is not on the classpath, so only interfaces can be used with transaction-outbox\");\n      return null;\n    }\n  }\n\n  private ObjenesisStd setupObjenesis() {\n    try {\n      return new ObjenesisStd();\n    } catch (NoClassDefFoundError error) {\n      log.info(\n          \"Objenesis is not on the classpath, so only interfaces or classes with default constructors can be used with transaction-outbox\");\n      return null;\n    }\n  }\n\n  @SuppressWarnings({\"unchecked\", \"cast\"})\n  public <T> T createProxy(Class<T> clazz, BiFunction<Method, Object[], T> processor) {\n    if (clazz.isInterface()) {\n      // Fastest - we can just proxy an interface directly\n      return (T)\n          Proxy.newProxyInstance(\n              clazz.getClassLoader(),\n              new Class[] {clazz},\n              (proxy, method, args) -> processor.apply(method, args));\n    } else {\n      Class<? extends T> proxy = buildByteBuddyProxyClass(clazz);\n      return constructProxy(clazz, processor, proxy);\n    }\n  }\n\n  private <T> T constructProxy(\n      Class<T> clazz, BiFunction<Method, Object[], T> processor, Class<? extends T> proxy) {\n    final T instance;\n    if (hasDefaultConstructor(clazz)) {\n      instance = Utils.uncheckedly(() -> proxy.getDeclaredConstructor().newInstance());\n    } else {\n      if (objenesis == null) {\n        throw new MissingOptionalDependencyException(\"org.objenesis\", \"objenesis\");\n      }\n      ObjectInstantiator<? extends T> instantiator = objenesis.getInstantiatorOf(proxy);\n      instance = instantiator.newInstance();\n    }\n    Utils.uncheck(\n        () -> {\n          var field = instance.getClass().getDeclaredField(\"handler\");\n          field.set(\n              instance,\n              (InvocationHandler) (proxy1, method, args) -> processor.apply(method, args));\n        });\n    return instance;\n  }\n\n  @SuppressWarnings({\"unchecked\", \"cast\"})\n  private <T> Class<? extends T> buildByteBuddyProxyClass(Class<T> clazz) {\n    if (byteBuddyCache == null) {\n      throw new MissingOptionalDependencyException(\"net.bytebuddy\", \"byte-buddy\");\n    }\n    return (Class<? extends T>)\n        byteBuddyCache.findOrInsert(\n            clazz.getClassLoader(),\n            clazz,\n            () ->\n                new ByteBuddy()\n                    .subclass(clazz)\n                    .defineField(\"handler\", InvocationHandler.class, Visibility.PUBLIC)\n                    .method(ElementMatchers.isDeclaredBy(clazz))\n                    .intercept(InvocationHandlerAdapter.toField(\"handler\"))\n                    .make()\n                    .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)\n                    .getLoaded());\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/SimpleTransaction.java",
    "content": "package com.gruelbox.transactionoutbox.spi;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.safelyClose;\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\n\nimport com.gruelbox.transactionoutbox.Transaction;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.AccessLevel;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\n@AllArgsConstructor(access = AccessLevel.PUBLIC)\npublic final class SimpleTransaction implements Transaction, AutoCloseable {\n\n  private final List<Runnable> postCommitHooks = new ArrayList<>();\n  private final Map<String, PreparedStatement> preparedStatements = new HashMap<>();\n  private final Connection connection;\n  private final Object context;\n\n  @Override\n  public Connection connection() {\n    return connection;\n  }\n\n  @Override\n  public void addPostCommitHook(Runnable runnable) {\n    postCommitHooks.add(runnable);\n  }\n\n  @Override\n  public PreparedStatement prepareBatchStatement(String sql) {\n    return preparedStatements.computeIfAbsent(\n        sql, s -> Utils.uncheckedly(() -> connection.prepareStatement(s)));\n  }\n\n  public void flushBatches() {\n    if (!preparedStatements.isEmpty()) {\n      log.debug(\"Flushing batches\");\n      for (PreparedStatement statement : preparedStatements.values()) {\n        uncheck(statement::executeBatch);\n      }\n    }\n  }\n\n  public void processHooks() {\n    if (!postCommitHooks.isEmpty()) {\n      log.debug(\"Running post-commit hooks\");\n      postCommitHooks.forEach(Runnable::run);\n    }\n  }\n\n  public void commit() {\n    uncheck(connection::commit);\n  }\n\n  public void rollback() throws SQLException {\n    connection.rollback();\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public <T> T context() {\n    return (T) context;\n  }\n\n  @Override\n  public void close() {\n    if (!preparedStatements.isEmpty()) {\n      log.debug(\"Closing batch statements\");\n      safelyClose(preparedStatements.values());\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/main/java/com/gruelbox/transactionoutbox/spi/Utils.java",
    "content": "package com.gruelbox.transactionoutbox.spi;\n\nimport static java.util.stream.Collectors.joining;\n\nimport com.gruelbox.transactionoutbox.ThrowingRunnable;\nimport com.gruelbox.transactionoutbox.UncheckedException;\nimport java.util.Arrays;\nimport java.util.concurrent.Callable;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.slf4j.Logger;\nimport org.slf4j.event.Level;\n\n@Slf4j\npublic class Utils {\n\n  @SuppressWarnings({\"SameParameterValue\", \"WeakerAccess\", \"UnusedReturnValue\"})\n  public static boolean safelyRun(String gerund, ThrowingRunnable runnable) {\n    try {\n      runnable.run();\n      return true;\n    } catch (Exception e) {\n      log.error(\"Error when {}\", gerund, e);\n      return false;\n    }\n  }\n\n  @SuppressWarnings(\"unused\")\n  public static void safelyClose(AutoCloseable... closeables) {\n    safelyClose(Arrays.asList(closeables));\n  }\n\n  public static void safelyClose(Iterable<? extends AutoCloseable> closeables) {\n    closeables.forEach(\n        d -> {\n          if (d == null) return;\n          safelyRun(\"closing resource\", d::close);\n        });\n  }\n\n  public static void uncheck(ThrowingRunnable runnable) {\n    try {\n      runnable.run();\n    } catch (Exception e) {\n      uncheckAndThrow(e);\n    }\n  }\n\n  public static <T> T uncheckedly(Callable<T> runnable) {\n    try {\n      return runnable.call();\n    } catch (Exception e) {\n      return uncheckAndThrow(e);\n    }\n  }\n\n  public static <T> T uncheckAndThrow(Throwable e) {\n    if (e instanceof RuntimeException) {\n      throw (RuntimeException) e;\n    }\n    if (e instanceof Error) {\n      throw (Error) e;\n    }\n    throw new UncheckedException(e);\n  }\n\n  public static <T> T createLoggingProxy(ProxyFactory proxyFactory, Class<T> clazz) {\n    return proxyFactory.createProxy(\n        clazz,\n        (method, args) -> {\n          log.info(\n              \"Called mock \" + clazz.getSimpleName() + \".{}({})\",\n              method.getName(),\n              args == null\n                  ? \"\"\n                  : Arrays.stream(args)\n                      .map(it -> it == null ? \"null\" : it.toString())\n                      .collect(Collectors.joining(\", \")));\n          return null;\n        });\n  }\n\n  public static <T> T firstNonNull(T one, Supplier<T> two) {\n    if (one == null) return two.get();\n    return one;\n  }\n\n  public static void logAtLevel(Logger logger, Level level, String message, Object... args) {\n    switch (level) {\n      case ERROR:\n        logger.error(message, args);\n        break;\n      case WARN:\n        logger.warn(message, args);\n        break;\n      case INFO:\n        logger.info(message, args);\n        break;\n      case DEBUG:\n        logger.debug(message, args);\n        break;\n      case TRACE:\n        logger.trace(message, args);\n        break;\n      default:\n        logger.warn(message, args);\n        break;\n    }\n  }\n\n  public static String stringify(Object o) {\n    if (o == null) {\n      return \"null\";\n    }\n    if (o.getClass().isArray()) {\n      return \"[\" + Arrays.stream((Object[]) o).map(Utils::stringify).collect(joining(\", \")) + \"]\";\n    }\n    if (o instanceof String) {\n      return \"\\\"\" + o + \"\\\"\";\n    }\n    return o.toString();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/AbstractTestDefaultInvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.io.UncheckedIOException;\nimport java.time.*;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n@SuppressWarnings(\"RedundantCast\")\n@Slf4j\nabstract class AbstractTestDefaultInvocationSerializer {\n\n  protected static final String CLASS_NAME = \"foo\";\n  protected static final String METHOD_NAME = \"bar\";\n\n  private final DefaultInvocationSerializer serializer;\n\n  protected AbstractTestDefaultInvocationSerializer(Integer version) {\n    this.serializer =\n        DefaultInvocationSerializer.builder()\n            .serializableTypes(Set.of(ExampleCustomEnum.class, ExampleCustomClass.class))\n            .version(version)\n            .build();\n  }\n\n  @Test\n  void testNoArgs() {\n    check(new Invocation(String.class.getName(), \"toString\", new Class<?>[0], new Object[0]));\n  }\n\n  @Test\n  void testArrays() {\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            new Class<?>[] {int[].class},\n            new Object[] {new int[] {1, 2, 3}}));\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            new Class<?>[] {Integer[].class},\n            new Object[] {new Integer[] {1, 2, 3}}));\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            new Class<?>[] {String[].class},\n            new Object[] {new String[] {\"1\", \"2\", \"3\"}}));\n  }\n\n  @Test\n  void testPrimitives() {\n    Class<?>[] primitives = {\n      byte.class,\n      short.class,\n      int.class,\n      long.class,\n      float.class,\n      double.class,\n      boolean.class,\n      char.class\n    };\n    Object[] values = {(byte) 1, (short) 2, 3, 4L, 1.23F, 1.23D, true, '-'};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testBoxedPrimitives() {\n    Class<?>[] primitives = {\n      Byte.class,\n      Short.class,\n      Integer.class,\n      Long.class,\n      Float.class,\n      Double.class,\n      Boolean.class,\n      Character.class,\n      String.class\n    };\n    Object[] values = {\n      (Byte) (byte) 1,\n      (Short) (short) 2,\n      (Integer) 3,\n      (Long) 4L,\n      (Float) 1.23F,\n      (Double) 1.23D,\n      (Boolean) true,\n      (Character) '-',\n      \"Foo\"\n    };\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaDateEnums() {\n    Class<?>[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class};\n    Object[] values = {DayOfWeek.FRIDAY, Month.APRIL, ChronoUnit.DAYS};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaDateEnumsNulls() {\n    Class<?>[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class};\n    Object[] values = {null, null, null};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaUtilDate() {\n    Class<?>[] primitives = {Date.class, Date.class};\n    Object[] values = {new Date(), null};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaTimeClasses() {\n    Class<?>[] primitives = {\n      Duration.class,\n      Instant.class,\n      LocalDate.class,\n      LocalDateTime.class,\n      MonthDay.class,\n      Period.class,\n      Year.class,\n      YearMonth.class,\n      ZonedDateTime.class\n    };\n    Object[] values = {\n      Duration.ofDays(1),\n      Instant.now(),\n      LocalDate.now(),\n      LocalDateTime.now(),\n      MonthDay.of(1, 1),\n      Period.ofMonths(1),\n      Year.now(),\n      YearMonth.now(),\n      ZonedDateTime.now()\n    };\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaTimeClassesNulls() {\n    Class<?>[] primitives = {\n      Duration.class,\n      Instant.class,\n      LocalDate.class,\n      LocalDateTime.class,\n      MonthDay.class,\n      Period.class,\n      Year.class,\n      YearMonth.class,\n      ZonedDateTime.class\n    };\n    Object[] values = new Object[9];\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testCustomEnum() {\n    Class<?>[] primitives = {ExampleCustomEnum.class, ExampleCustomEnum.class};\n    Object[] values = {ExampleCustomEnum.ONE, ExampleCustomEnum.TWO};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testCustomEnumNulls() {\n    Class<?>[] primitives = {ExampleCustomEnum.class};\n    Object[] values = {null};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testCustomComplexClass() {\n    Class<?>[] primitives = {ExampleCustomClass.class, ExampleCustomClass.class};\n    Object[] values = {\n      new ExampleCustomClass(\"Foo\", \"Bar\"), new ExampleCustomClass(\"Bish\", \"Bash\")\n    };\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testMDCAndSession() {\n    Class<?>[] primitives = {Integer.class};\n    Object[] values = {1};\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            primitives,\n            values,\n            Map.of(\"A\", \"1\", \"B\", \"2\"),\n            Map.of(\"C\", \"3\")));\n  }\n\n  @Test\n  void testSession() {\n    Class<?>[] primitives = {Integer.class};\n    Object[] values = {1};\n    check(\n        new Invocation(\n            CLASS_NAME, METHOD_NAME, primitives, values, null, Map.of(\"A\", \"1\", \"B\", \"2\")));\n  }\n\n  @Test\n  void testUUID() {\n    Class<?>[] primitives = {UUID.class};\n    Object[] values = {UUID.randomUUID()};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testUUIDNull() {\n    Class<?>[] primitives = {UUID.class};\n    Object[] values = {null};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testDeserializationException() {\n    assertThrows(\n        IOException.class, () -> serializer.deserializeInvocation(new StringReader(\"unparseable\")));\n  }\n\n  void check(Invocation invocation) {\n    Invocation deserialized = serdeser(invocation);\n    Assertions.assertEquals(deserialized, serdeser(invocation));\n    Assertions.assertEquals(invocation, deserialized);\n  }\n\n  Invocation serdeser(Invocation invocation) {\n    try {\n      var writer = new StringWriter();\n      serializer.serializeInvocation(invocation, writer);\n      log.info(\"Serialised as: {}\", writer);\n      return serializer.deserializeInvocation(new StringReader(writer.toString()));\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  enum ExampleCustomEnum {\n    ONE,\n    TWO\n  }\n\n  @Getter\n  static class ExampleCustomClass {\n\n    private final String arg1;\n    private final String arg2;\n\n    ExampleCustomClass(String arg1, String arg2) {\n      this.arg1 = arg1;\n      this.arg2 = arg2;\n    }\n\n    @Override\n    public String toString() {\n      return \"ExampleCustomClass{\" + \"arg1='\" + arg1 + '\\'' + \", arg2='\" + arg2 + '\\'' + '}';\n    }\n\n    @Override\n    public boolean equals(Object o) {\n      if (this == o) return true;\n      if (o == null || getClass() != o.getClass()) return false;\n      ExampleCustomClass that = (ExampleCustomClass) o;\n      return Objects.equals(arg1, that.arg1) && Objects.equals(arg2, that.arg2);\n    }\n\n    @Override\n    public int hashCode() {\n      return Objects.hash(arg1, arg2);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultInvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Nested;\n\n@Slf4j\nclass TestDefaultInvocationSerializer {\n\n  @Nested\n  class Version1 extends AbstractTestDefaultInvocationSerializer {\n    public Version1() {\n      super(1);\n    }\n\n    @Override\n    void testSession() {\n      // no-op\n    }\n\n    @Override\n    void testMDCAndSession() {\n      // no-op\n    }\n  }\n\n  @Nested\n  class Version2 extends AbstractTestDefaultInvocationSerializer {\n    public Version2() {\n      super(2);\n    }\n  }\n\n  @Nested\n  class NullVersion extends AbstractTestDefaultInvocationSerializer {\n    public NullVersion() {\n      super(null);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultMigrationManager.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static com.gruelbox.transactionoutbox.Dialect.H2;\nimport static org.junit.jupiter.api.Assertions.fail;\n\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.util.concurrent.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\npublic class TestDefaultMigrationManager {\n\n  private static HikariDataSource dataSource;\n\n  @BeforeAll\n  static void beforeAll() {\n    HikariConfig config = new HikariConfig();\n    config.setJdbcUrl(\n        \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE;DATABASE_TO_UPPER=FALSE\");\n    config.setUsername(\"test\");\n    config.setPassword(\"test\");\n    config.addDataSourceProperty(\"cachePrepStmts\", \"true\");\n    dataSource = new HikariDataSource(config);\n  }\n\n  @AfterAll\n  static void afterAll() {\n    dataSource.close();\n  }\n\n  @Test\n  void parallelMigrations() {\n    CountDownLatch readyLatch = new CountDownLatch(2);\n    DefaultMigrationManager.withLatch(\n        readyLatch,\n        waitLatch -> {\n          Executor executor = runnable -> new Thread(runnable).start();\n          TransactionManager txm = TransactionManager.fromDataSource(dataSource);\n          CompletableFuture<?> threads =\n              CompletableFuture.allOf(\n                  CompletableFuture.runAsync(\n                      () -> {\n                        try {\n                          DefaultMigrationManager.migrate(txm, H2);\n                        } catch (Exception e) {\n                          log.error(\"Thread 1 failed\", e);\n                          throw e;\n                        }\n                      },\n                      executor),\n                  CompletableFuture.runAsync(\n                      () -> {\n                        try {\n                          DefaultMigrationManager.migrate(txm, H2);\n                        } catch (Exception e) {\n                          log.error(\"Thread 2 failed\", e);\n                          throw e;\n                        }\n                      },\n                      executor));\n          try {\n            if (!readyLatch.await(15, TimeUnit.SECONDS)) {\n              throw new TimeoutException();\n            }\n            waitLatch.countDown();\n          } catch (InterruptedException | TimeoutException e) {\n            fail(\"Timed out or interrupted waiting for ready latch\");\n          } finally {\n            threads.join();\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestDefaultPersistorConfiguration.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\n\nimport java.io.StringWriter;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport org.junit.jupiter.api.Test;\n\nclass TestDefaultPersistorConfiguration {\n  @Test\n  final void whenMigrateIsFalseDoNotMigrate() throws Exception {\n    TransactionManager transactionManager = simpleTxnManager();\n    runSql(transactionManager, \"DROP ALL OBJECTS\");\n\n    TransactionOutbox.builder()\n        .transactionManager(transactionManager)\n        .persistor(DefaultPersistor.builder().dialect(Dialect.H2).migrate(false).build())\n        .build();\n\n    transactionManager.inTransactionThrows(\n        tx -> {\n          try (Statement statement = tx.connection().createStatement()) {\n            try (ResultSet rs =\n                statement.executeQuery(\n                    \"SELECT COUNT(*)\"\n                        + \" FROM INFORMATION_SCHEMA.TABLES\"\n                        + \" WHERE TABLE_NAME IN ('TXNO_OUTBOX', 'TXNO_VERSION')\")) {\n              rs.next();\n              assertThat(rs.getInt(1), is(0));\n            }\n          }\n        });\n  }\n\n  @Test\n  final void writeSchema() {\n    StringWriter stringWriter = new StringWriter();\n\n    DefaultPersistor defaultPersistor = DefaultPersistor.builder().dialect(Dialect.H2).build();\n    defaultPersistor.writeSchema(stringWriter);\n\n    String migrations = stringWriter.toString();\n\n    assertThat(migrations, startsWith(\"-- 1: Create outbox table\"));\n    assertThat(\n        migrations,\n        containsString(\n            \"-- 2: Add unique request id\"\n                + System.lineSeparator()\n                + \"ALTER TABLE TXNO_OUTBOX ADD COLUMN uniqueRequestId VARCHAR(100) NULL UNIQUE\"));\n    assertThat(\n        migrations,\n        containsString(\n            \"-- 8: Update length of invocation column on outbox for MySQL dialects only.\"\n                + System.lineSeparator()\n                + \"-- Nothing for H2\"));\n  }\n\n  private TransactionManager simpleTxnManager() {\n    return TransactionManager.fromConnectionDetails(\n        \"org.h2.Driver\",\n        \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE\",\n        \"test\",\n        \"test\");\n  }\n\n  private void runSql(\n      TransactionManager transactionManager, @SuppressWarnings(\"SameParameterValue\") String sql) {\n    transactionManager.inTransaction(\n        tx -> {\n          try {\n            try (Statement statement = tx.connection().createStatement()) {\n              statement.execute(sql);\n            }\n          } catch (Exception e) {\n            throw new RuntimeException(e);\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestProxyGeneration.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.gruelbox.transactionoutbox.spi.ProxyFactory;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass TestProxyGeneration {\n\n  private ProxyFactory proxyFactory;\n\n  @BeforeEach\n  void setUp() {\n    proxyFactory = new ProxyFactory();\n  }\n\n  /** Reflection */\n  @Test\n  void testReflection() {\n    AtomicBoolean called = new AtomicBoolean();\n    Interface proxy =\n        proxyFactory.createProxy(\n            Interface.class,\n            (method, args) -> {\n              called.set(true);\n              return null;\n            });\n    proxy.doThing();\n    assertTrue(called.get());\n  }\n\n  /** ByteBuddy */\n  @Test\n  void testByteBuddy() {\n    AtomicBoolean called = new AtomicBoolean();\n    Child proxy =\n        proxyFactory.createProxy(\n            Child.class,\n            (method, args) -> {\n              called.set(true);\n              return null;\n            });\n    proxy.doThing();\n    assertTrue(called.get());\n  }\n\n  /** This fails without Objenesis. */\n  @Test\n  void testObjensis() {\n    AtomicBoolean called = new AtomicBoolean();\n    Parent proxy =\n        proxyFactory.createProxy(\n            Parent.class,\n            (method, args) -> {\n              called.set(true);\n              return null;\n            });\n    proxy.doThing();\n    assertTrue(called.get());\n  }\n\n  interface Interface {\n    void doThing();\n  }\n\n  static class Child {\n    void doThing() {\n      // No-op\n    }\n  }\n\n  static class Parent {\n\n    private final Child child;\n\n    Parent(Child child) {\n      this.child = child;\n    }\n\n    void doThing() {\n      // No-op\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/java/com/gruelbox/transactionoutbox/TestValidator.java",
    "content": "package com.gruelbox.transactionoutbox;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\n\nimport java.math.BigDecimal;\nimport java.time.Clock;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport org.junit.jupiter.api.Test;\n\nclass TestValidator {\n\n  private static final Invocation COMPLEX_INVOCATION =\n      new Invocation(\n          \"Foo\",\n          \"Bar\",\n          new Class<?>[] {int.class, BigDecimal.class, String.class},\n          new Object[] {1, BigDecimal.TEN, null});\n\n  private final Instant now = Instant.now();\n  private final Validator validator = new Validator(() -> Clock.fixed(now, ZoneId.of(\"+4\")));\n\n  @Test\n  void testEntryDateFuture() {\n    TransactionOutboxEntry entry =\n        TransactionOutboxEntry.builder()\n            .id(\"FOO\")\n            .invocation(COMPLEX_INVOCATION)\n            .nextAttemptTime(now.plusMillis(1))\n            .build();\n    assertDoesNotThrow(() -> validator.validate(entry));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-core/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level SESSION-KEY=%X{SESSION-KEY} %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "transactionoutbox-guice/README.md",
    "content": "# transaction-outbox-guice\n\n[![Guice on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-guice/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-guice)\n[![Guice Javadoc](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-guice.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-guice)\n[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot&sort=semver)](#development-snapshots)\n\nExtension for [transaction-outbox-core](../README.md) which integrates with Guice.\n\n## Installation\n\n### Stable releases\n\n#### Maven\n\n```xml\n<dependency>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-guice</artifactId>\n  <version>7.0.707</version>\n</dependency>\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-guice:7.0.707'\n```\n\n### Development snapshots\n\nSee [transactionoutbox-core](../README.md) for more information.\n\n## Standard usage\n\n### Configuration\n\nTo get a `TransactionOutbox` for use throughout your application, add a `Singleton` binding for your chosen transaction manager and then wire in `GuiceInstantiator` as follows:\n\n```java\n@Provides\n@Singleton\nTransactionOutbox transactionOutbox(Injector injector, TransactionManager transactionManager) {\n  return TransactionOutbox.builder()\n    .transactionManager(transactionManager)\n    .persistor(Persistor.forDialect(Dialect.MY_SQL_8))\n    .instantiator(GuiceInstantiator.builder().injector(injector).build())\n    .build();\n}\n```\n\n### Usage\n\n```java\n@Inject private TransactionOutbox outbox;\n\nvoid doSomething() {\n  // Obtains a MyService from the injector\n  outbox.schedule(MyService.class).doAThing(1, 2, 3);\n}\n```\n\n## Remote injection\n\nAlternatively, you may prefer to hide the use of `TransactionOutbox` and create injectable \"remote\" implementations of specific services. This is a stylistic choice, and is more a Guice thing than a `TransactionOutbox` thing, but is presented here for illustration.\n\n### Configuration\n\nCreate a suitable binding annotation to specify that you want to inject the remote version of a service:\n\n```java\n@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\n@BindingAnnotation\npublic @interface Remote {}\n```\n\nBind `TransactionOutbox` as per the example above, and add two more bindings to expose the \"real\" and \"remote\" versions of the service:\n\n```java\n@Provides\n@Remote\n@Singleton // Can help performance\nMyService remote(TransactionOutbox outbox) {\n  return outbox.schedule(MyService.class);\n}\n\n@Provides\nMyService local() {\n  return new MyService();\n}\n```\n\n### Usage\n\nNow you can inject the remote implementation and use it to schedule work. The following is exactly equivalent to the usage example above, just using an injected remote:\n\n```java\n@Inject\n@Remote\nprivate MyService myService;\n\nvoid doSomething() {\n  myService.doAThing(1, 2, 3);\n}\n```\n"
  },
  {
    "path": "transactionoutbox-guice/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Guice</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-guice</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (Guice extension library)</description>\n  <properties>\n    <guice.version>5.2.4.RELEASE</guice.version>\n  </properties>\n  <dependencies>\n    <!-- Run time -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>com.google.inject</groupId>\n      <artifactId>guice</artifactId>\n      <version>7.0.0</version>\n      <exclusions>\n        <!-- There's a vulnerability in the version provided by Guice 7.0.0 -->\n        <exclusion>\n          <groupId>com.google.guava</groupId>\n          <artifactId>guava</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <!-- There's a vulnerability in the version provided by Guice 6.0.0 -->\n    <dependency>\n      <groupId>com.google.guava</groupId>\n      <artifactId>guava</artifactId>\n      <version>33.5.0-jre</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- Compile time -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test -->\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-classic</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-core</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.hamcrest</groupId>\n      <artifactId>hamcrest-core</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-engine</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.mockito</groupId>\n      <artifactId>mockito-all</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-guice/src/main/java/com/gruelbox/transactionoutbox/guice/GuiceInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox.guice;\n\nimport com.google.inject.Injector;\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameInstantiator;\nimport lombok.experimental.SuperBuilder;\n\n/** Instantiator that uses the Guice {@link Injector} to source objects. */\n@SuperBuilder\npublic class GuiceInstantiator extends AbstractFullyQualifiedNameInstantiator {\n\n  private final Injector injector;\n\n  @Override\n  protected Object createInstance(Class<?> clazz) {\n    return injector.getInstance(clazz);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-guice/src/test/java/com/gruelbox/transactionoutbox/guice/acceptance/TestGuiceBinding.java",
    "content": "package com.gruelbox.transactionoutbox.guice.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.BindingAnnotation;\nimport com.google.inject.Guice;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Provides;\nimport com.google.inject.Singleton;\nimport com.gruelbox.transactionoutbox.StubPersistor;\nimport com.gruelbox.transactionoutbox.StubThreadLocalTransactionManager;\nimport com.gruelbox.transactionoutbox.Submitter;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport com.gruelbox.transactionoutbox.TransactionOutboxListener;\nimport com.gruelbox.transactionoutbox.guice.GuiceInstantiator;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Demonstrates an alternative approach to using {@link TransactionOutbox} using binding annotations\n * to inject a remoted version of a service.\n */\n@Slf4j\nclass TestGuiceBinding {\n\n  /** The real service */\n  @Inject MyService local;\n\n  /** The remoted version */\n  @Inject @Remote MyService remote;\n\n  /** We need this to schedule the work */\n  @Inject TransactionManager transactionManager;\n\n  @Test\n  void testProviderInjection() {\n    AtomicBoolean processedWithRemote = new AtomicBoolean();\n    Injector injector = Guice.createInjector(new DemoModule(processedWithRemote));\n    injector.injectMembers(this);\n\n    transactionManager.inTransaction(\n        () -> {\n          remote.process();\n          log.info(\"Queued request\");\n        });\n\n    assertTrue(processedWithRemote.get());\n    assertTrue(local.processed.get());\n  }\n\n  /** The service we're going to remote */\n  static class MyService {\n    AtomicBoolean processed = new AtomicBoolean();\n\n    void process() {\n      processed.set(true);\n      log.info(\"Processed local\");\n    }\n  }\n\n  /** Binding annotation for the remote version of the service. */\n  @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})\n  @Retention(RetentionPolicy.RUNTIME)\n  @BindingAnnotation\n  public @interface Remote {}\n\n  /** Sets up the bindings */\n  static final class DemoModule extends AbstractModule {\n\n    private final AtomicBoolean processedWithRemote;\n\n    DemoModule(AtomicBoolean processedWithRemote) {\n      this.processedWithRemote = processedWithRemote;\n    }\n\n    @Provides\n    @Singleton\n    TransactionManager manager() {\n      return new StubThreadLocalTransactionManager();\n    }\n\n    @Provides\n    @Singleton\n    TransactionOutbox outbox(Injector injector, TransactionManager transactionManager) {\n      return TransactionOutbox.builder()\n          .instantiator(GuiceInstantiator.builder().injector(injector).build())\n          .persistor(StubPersistor.builder().build())\n          .submitter(Submitter.withExecutor(Runnable::run))\n          .transactionManager(transactionManager)\n          .listener(\n              new TransactionOutboxListener() {\n                @Override\n                public void success(TransactionOutboxEntry entry) {\n                  processedWithRemote.set(true);\n                }\n              })\n          .build();\n    }\n\n    @Provides\n    @Remote\n    @Singleton\n    MyService remote(TransactionOutbox outbox) {\n      return outbox.schedule(MyService.class);\n    }\n\n    @Provides\n    @Singleton\n    MyService local() {\n      return new MyService();\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-guice/src/test/java/com/gruelbox/transactionoutbox/guice/acceptance/TestGuiceInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox.guice.acceptance;\n\nimport com.google.inject.Guice;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.gruelbox.transactionoutbox.guice.GuiceInstantiator;\nimport org.hamcrest.MatcherAssert;\nimport org.hamcrest.Matchers;\nimport org.junit.jupiter.api.Test;\n\nclass TestGuiceInstantiator {\n\n  @Test\n  void testInjection() {\n    Injector injector = Guice.createInjector();\n    GuiceInstantiator guiceInstantiator = GuiceInstantiator.builder().injector(injector).build();\n    Object instance = guiceInstantiator.getInstance(Parent.class.getName());\n    MatcherAssert.assertThat(instance, Matchers.isA(Parent.class));\n  }\n\n  static final class Child {}\n\n  static final class Parent {\n\n    private final Child child;\n\n    @Inject\n    Parent(Child child) {\n      this.child = child;\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-guice/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level SESSION-KEY=%X{SESSION-KEY} %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "transactionoutbox-jackson/README.md",
    "content": "# transaction-outbox-jackson\n\n[![Jackson on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-jackson/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-guice)\n[![Jackson Javadoc](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-jackson.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-guice)\n[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot&sort=semver)](#development-snapshots)\n\nExtension for [transaction-outbox-core](../README.md) which uses Jackson for serialisation.\n\nIf you are confident in trusting your database, then this serializer has a number of advantages: it is as\nconfigurable as whatever Jackson's `ObjectMapper` can handle, and explicitly handles n-depth polymorphic trees. This\nlargely means that you can throw pretty much anything at it and it will \"just work\".\n\nHowever, if there is any risk that you might not trust the source of the serialized `Invocation`,\n_do not use this_. This serializer is vulnerable to\n[deserialization of untrusted data](https://github.com/gruelbox/transaction-outbox/issues/236#issuecomment-1024929436),\nwhich is why it is not included in the core library.\n\n## Installation\n\n### Stable releases\n\n#### Maven\n\n```xml\n<dependency>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-jackson</artifactId>\n  <version>7.0.707</version>\n</dependency>\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-jackson:7.0.707'\n```\n\n### Development snapshots\n\nSee [transactionoutbox-core](../README.md) for more information.\n\n## Configuration\n\n### Fresh projects\n\nIf starting with a fresh project, you don't need to worry about compatibility with DefaultInvocationSerializer, so configure as follows:\n\n```java\nvar outbox = TransactionOutbox.builder()\n  .persistor(DefaultPersistor.builder()\n    .dialect(Dialect.H2)\n    .serializer(JacksonInvocationSerializer.builder()\n        .mapper(new ObjectMapper())\n        .build())\n    .build())\n```\n\n### Existing projects using DefaultInvocationSerializer\n\nIf you're already using Transaction Outbox, you may have outbox tasks queued which your application needs to continue to be capable of loading.\nTo handle this, pass through an instance of `DefaultInvocationSerializer` which matches what you used previously:\n\n```java\nvar outbox = TransactionOutbox.builder()\n  .persistor(DefaultPersistor.builder()\n    .dialect(Dialect.H2)\n    .serializer(JacksonInvocationSerializer.builder()\n      .mapper(new ObjectMapper())\n      .defaultInvocationSerializer(DefaultInvocationSerializer.builder()\n        .serializableTypes(Set.of(Foo.class, Bar.class))\n        .build())\n      .build())\n    .build())\n```\n\n## Usage\n\nYou can now go wild with your scheduled method arguments:\n\n```java\noutbox.schedule(getClass())\n  .process(List.of(LocalDate.of(2000,1,1), \"a\", \"b\", 2));\n```\n"
  },
  {
    "path": "transactionoutbox-jackson/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Jackson</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-jackson</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (Jackson extension library)</description>\n  <properties>\n    <jackson.version>2.21.0</jackson.version>\n    <commons.lang.version>3.20.0</commons.lang.version>\n  </properties>\n  <dependencies>\n    <!-- Run time -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>com.fasterxml.jackson.core</groupId>\n      <artifactId>jackson-databind</artifactId>\n      <version>${jackson.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.apache.commons</groupId>\n      <artifactId>commons-lang3</artifactId>\n      <version>${commons.lang.version}</version>\n    </dependency>\n    <!-- Compile time -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-testing</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.fasterxml.jackson.datatype</groupId>\n      <artifactId>jackson-datatype-guava</artifactId>\n      <version>${jackson.version}</version>\n      <scope>test</scope>\n      <exclusions>\n        <!-- There's a vulnerability in the version provided by Jackson -->\n        <exclusion>\n          <groupId>com.google.guava</groupId>\n          <artifactId>guava</artifactId>\n        </exclusion>\n      </exclusions>\n    </dependency>\n    <!-- There's a vulnerability in the version provided by Jackson -->\n    <dependency>\n      <groupId>com.google.guava</groupId>\n      <artifactId>guava</artifactId>\n      <version>33.5.0-jre</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.fasterxml.jackson.datatype</groupId>\n      <artifactId>jackson-datatype-jdk8</artifactId>\n      <version>${jackson.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.fasterxml.jackson.datatype</groupId>\n      <artifactId>jackson-datatype-jsr310</artifactId>\n      <version>${jackson.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.h2database</groupId>\n      <artifactId>h2</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/CustomInvocationDeserializer.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.fasterxml.jackson.databind.jsontype.TypeDeserializer;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.regex.Pattern;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ClassUtils;\n\n@Slf4j\nclass CustomInvocationDeserializer extends StdDeserializer<Invocation> {\n\n  private static final Pattern setPattern =\n      Pattern.compile(\"\\\\{\\\\w*\\\"(java.util.ImmutableCollections\\\\$Set[\\\\dN]+)\\\"\\\\w*:\");\n  private static final Pattern mapPattern =\n      Pattern.compile(\"\\\\{\\\\w*\\\"(java.util.ImmutableCollections\\\\$Map[\\\\dN]+)\\\"\\\\w*:\");\n  private static final Pattern listPattern =\n      Pattern.compile(\"\\\\{\\\\w*\\\"(java.util.ImmutableCollections\\\\$List[\\\\dN]+)\\\"\\\\w*:\");\n\n  protected CustomInvocationDeserializer(Class<?> vc) {\n    super(vc);\n  }\n\n  CustomInvocationDeserializer() {\n    this(Invocation.class);\n  }\n\n  @Override\n  public Invocation deserializeWithType(\n      JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)\n      throws IOException {\n    return deserialize(p, ctxt);\n  }\n\n  @Override\n  public Invocation deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {\n    JsonNode node = p.getCodec().readTree(p);\n    String className = node.get(\"className\").textValue();\n    String methodName = node.get(\"methodName\").textValue();\n    ArrayNode paramTypes = ((ArrayNode) node.get(\"parameterTypes\"));\n    JsonNode arguments = node.get(\"args\");\n    JsonNode processedArguments = replaceImmutableCollections(arguments, p);\n    Class<?>[] types = new Class<?>[paramTypes.size()];\n\n    for (int i = 0; i < paramTypes.size(); i++) {\n      try {\n        types[i] = ClassUtils.getClass(paramTypes.get(i).asText());\n      } catch (ClassNotFoundException e) {\n        throw new RuntimeException(e);\n      }\n    }\n    Object[] args = p.getCodec().treeToValue(processedArguments, Object[].class);\n\n    Map<String, String> mdc =\n        p.getCodec()\n            .readValue(p.getCodec().treeAsTokens(node.get(\"mdc\")), new TypeReference<>() {});\n\n    var sessionNode = node.get(\"session\");\n    Map<String, String> session = null;\n    if (sessionNode != null && !sessionNode.isNull()) {\n      session =\n          p.getCodec().readValue(p.getCodec().treeAsTokens(sessionNode), new TypeReference<>() {});\n    }\n    return new Invocation(className, methodName, types, args, mdc, session);\n  }\n\n  private JsonNode replaceImmutableCollections(JsonNode arguments, JsonParser p)\n      throws IOException {\n    String args = arguments.toString();\n    args = setPattern.matcher(args).replaceAll(\"{\\\"java.util.HashSet\\\":\");\n    args = mapPattern.matcher(args).replaceAll(\"{\\\"java.util.HashMap\\\":\");\n    args = listPattern.matcher(args).replaceAll(\"{\\\"java.util.ArrayList\\\":\");\n    JsonParser parser = p.getCodec().getFactory().createParser(args);\n    return p.getCodec().readTree(parser);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/CustomInvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.jsontype.TypeSerializer;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport java.io.IOException;\n\nclass CustomInvocationSerializer extends StdSerializer<Invocation> {\n\n  public CustomInvocationSerializer() {\n    this(Invocation.class);\n  }\n\n  protected CustomInvocationSerializer(Class<Invocation> t) {\n    super(t);\n  }\n\n  @Override\n  public void serializeWithType(\n      Invocation value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer)\n      throws IOException {\n    serialize(value, gen, serializers);\n  }\n\n  @Override\n  public void serialize(Invocation value, JsonGenerator gen, SerializerProvider provider)\n      throws IOException {\n    gen.writeStartObject();\n    gen.writeStringField(\"className\", value.getClassName());\n    gen.writeStringField(\"methodName\", value.getMethodName());\n    gen.writeArrayFieldStart(\"parameterTypes\");\n    for (Class<?> parameterType : value.getParameterTypes()) {\n      gen.writeString(parameterType.getCanonicalName());\n    }\n    gen.writeEndArray();\n    gen.writeObjectField(\"args\", value.getArgs());\n    gen.writeObjectField(\"mdc\", value.getMdc());\n    gen.writeObjectField(\"session\", value.getSession());\n    gen.writeEndObject();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/JacksonInvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.gruelbox.transactionoutbox.DefaultInvocationSerializer;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport com.gruelbox.transactionoutbox.InvocationSerializer;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.io.Writer;\nimport lombok.Builder;\n\n/**\n * A general-purpose {@link InvocationSerializer} which can handle pretty much anything that you\n * throw at it.\n *\n * <p>Note that if there is any risk that you might not trust the source of the serialized {@link\n * Invocation}, <strong>do not use this</strong>. This serializer is vulnerable to a\n * <em>deserialization of untrusted data</em> vulnerability (more information <a\n * href=\"https://github.com/gruelbox/transaction-outbox/issues/236#issuecomment-1024929436\">here</a>)\n * which is why it is not included in the core library.\n */\npublic final class JacksonInvocationSerializer implements InvocationSerializer {\n  private final ObjectMapper mapper;\n  private final InvocationSerializer defaultInvocationSerializer;\n\n  @Builder\n  private JacksonInvocationSerializer(\n      ObjectMapper mapper, DefaultInvocationSerializer defaultInvocationSerializer) {\n    this.mapper = mapper.copy();\n    this.defaultInvocationSerializer = defaultInvocationSerializer;\n    this.mapper.setDefaultTyping(TransactionOutboxJacksonModule.typeResolver());\n    this.mapper.registerModule(new TransactionOutboxJacksonModule());\n  }\n\n  @Override\n  public void serializeInvocation(Invocation invocation, Writer writer) {\n    try {\n      mapper.writeValue(writer, invocation);\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public Invocation deserializeInvocation(Reader reader) throws IOException {\n    // read ahead to check if old style\n    BufferedReader br = new BufferedReader(reader);\n    if (checkForOldSerialization(br)) {\n      if (defaultInvocationSerializer == null) {\n        throw new UnsupportedOperationException(\n            \"Can't deserialize GSON-format tasks without a \"\n                + DefaultInvocationSerializer.class.getSimpleName()\n                + \". Supply one when building \"\n                + getClass().getSimpleName());\n      }\n      return defaultInvocationSerializer.deserializeInvocation(br);\n    }\n    return mapper.readValue(br, Invocation.class);\n  }\n\n  private boolean checkForOldSerialization(BufferedReader reader) throws IOException {\n    reader.mark(1);\n    char[] chars = new char[6];\n    int charsRead = reader.read(chars, 0, 6);\n\n    String result = \"\";\n    if (charsRead != -1) {\n      result = new String(chars, 0, charsRead);\n    }\n    reader.reset();\n    return result.startsWith(\"{\\\"c\\\":\");\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/TransactionOutboxEntryDeserializer.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.ObjectCodec;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.util.Map;\n\nclass TransactionOutboxEntryDeserializer extends JsonDeserializer<TransactionOutboxEntry> {\n\n  @Override\n  public TransactionOutboxEntry deserialize(JsonParser p, DeserializationContext c)\n      throws IOException {\n    ObjectCodec oc = p.getCodec();\n    JsonNode entry = oc.readTree(p);\n    var invocation = entry.get(\"invocation\");\n    return TransactionOutboxEntry.builder()\n        .id(entry.get(\"id\").asText())\n        .lastAttemptTime(mapNullableInstant(entry.get(\"lastAttemptTime\"), c))\n        .nextAttemptTime(mapNullableInstant(entry.get(\"nextAttemptTime\"), c))\n        .attempts(entry.get(\"attempts\").asInt())\n        .blocked(entry.get(\"blocked\").asBoolean())\n        .processed(entry.get(\"processed\").asBoolean())\n        .uniqueRequestId(mapNullableString(entry.get(\"uniqueRequestId\")))\n        .version(entry.get(\"version\").asInt())\n        .invocation(\n            new Invocation(\n                invocation.get(\"className\").asText(),\n                invocation.get(\"methodName\").asText(),\n                c.readTreeAsValue(invocation.get(\"parameterTypes\"), Class[].class),\n                c.readTreeAsValue(invocation.get(\"args\"), Object[].class),\n                mapNullableStringMap(invocation.get(\"mdc\"), c),\n                mapNullableStringMap(invocation.get(\"session\"), c)))\n        .build();\n  }\n\n  private String mapNullableString(JsonNode node) {\n    if (node == null || node.isNull()) {\n      return null;\n    }\n    return node.asText();\n  }\n\n  private Instant mapNullableInstant(JsonNode node, DeserializationContext c) throws IOException {\n    if (node == null || node.isNull()) {\n      return null;\n    }\n    return c.readTreeAsValue(node, Instant.class);\n  }\n\n  private Map<String, String> mapNullableStringMap(JsonNode node, DeserializationContext c)\n      throws IOException {\n    if (node == null || node.isNull()) {\n      return null;\n    }\n    return c.readTreeAsValue(\n        node, c.getTypeFactory().constructType(new TypeReference<Map<String, String>>() {}));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/main/java/com/gruelbox/transactionoutbox/jackson/TransactionOutboxJacksonModule.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL;\n\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.core.Version;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.Module;\nimport com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;\nimport com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;\nimport com.fasterxml.jackson.databind.module.SimpleDeserializers;\nimport com.fasterxml.jackson.databind.module.SimpleSerializers;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\n\npublic class TransactionOutboxJacksonModule extends Module {\n\n  @Override\n  public String getModuleName() {\n    return \"TransactionOutboxJacksonModule\";\n  }\n\n  @Override\n  public Version version() {\n    return Version.unknownVersion();\n  }\n\n  @Override\n  public void setupModule(SetupContext setupContext) {\n    SimpleSerializers serializers = new SimpleSerializers();\n    serializers.addSerializer(Invocation.class, new CustomInvocationSerializer());\n    setupContext.addSerializers(serializers);\n\n    SimpleDeserializers deserializers = new SimpleDeserializers();\n    deserializers.addDeserializer(Invocation.class, new CustomInvocationDeserializer());\n    deserializers.addDeserializer(\n        TransactionOutboxEntry.class, new TransactionOutboxEntryDeserializer());\n    setupContext.addDeserializers(deserializers);\n  }\n\n  public static TypeResolverBuilder<?> typeResolver() {\n    return new ObjectMapper.DefaultTypeResolverBuilder(\n            NON_FINAL,\n            BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).build())\n        .init(JsonTypeInfo.Id.CLASS, null)\n        .inclusion(JsonTypeInfo.As.WRAPPER_OBJECT);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/MonetaryAmount.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport java.math.BigDecimal;\nimport java.util.Objects;\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok.NoArgsConstructor;\nimport lombok.Setter;\n\n@AllArgsConstructor\n@NoArgsConstructor\n@Getter\n@Setter\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic final class MonetaryAmount {\n\n  private static final String GBP = \"GBP\";\n  private static final BigDecimal BY_HUNDRED = BigDecimal.valueOf(100);\n\n  public static final MonetaryAmount ZERO_GBP = new MonetaryAmount(BigDecimal.ZERO, GBP);\n  public static final MonetaryAmount TEN_GBP = new MonetaryAmount(BigDecimal.TEN, GBP);\n  public static final MonetaryAmount ONE_HUNDRED_GBP =\n      new MonetaryAmount(BigDecimal.valueOf(100), GBP);\n\n  private BigDecimal amount;\n\n  private String currency;\n\n  public static MonetaryAmount ofGbp(final String amount) {\n    return new MonetaryAmount(new BigDecimal(amount), \"GBP\");\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (o == null || getClass() != o.getClass()) return false;\n\n    MonetaryAmount that = (MonetaryAmount) o;\n\n    if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) return false;\n    return Objects.equals(currency, that.currency);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = amount != null ? amount.hashCode() : 0;\n    result = 31 * result + (currency != null ? currency.hashCode() : 0);\n    return result;\n  }\n\n  @Override\n  public String toString() {\n    return currency + \" \" + amount.stripTrailingZeros().toPlainString();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/SerializationStressTestInput.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.*;\n\n@Getter\n@Setter\n@NoArgsConstructor\n@EqualsAndHashCode\n@ToString\npublic class SerializationStressTestInput {\n  private boolean enabled = false;\n  private MonetaryAmount amount = MonetaryAmount.ONE_HUNDRED_GBP;\n  private Set<String> investments = Set.of(\"investment1\", \"investment2\", \"investment3\");\n  private Map<String, MonetaryAmount> investmentAmounts =\n      Map.of(\n          \"investment1\",\n          MonetaryAmount.ofGbp(\"33\"),\n          \"investment2\",\n          MonetaryAmount.ofGbp(\"34\"),\n          \"investment3\",\n          MonetaryAmount.ofGbp(\"33\"));\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/TestJacksonInvocationSerializer.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.gruelbox.transactionoutbox.DefaultInvocationSerializer;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.io.UncheckedIOException;\nimport java.time.*;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@SuppressWarnings(\"RedundantCast\")\nclass TestJacksonInvocationSerializer {\n\n  private JacksonInvocationSerializer underTest;\n\n  private static final String CLASS_NAME = \"foo\";\n  private static final String METHOD_NAME = \"bar\";\n\n  @BeforeEach\n  void beforeEach() {\n    ObjectMapper mapper = new ObjectMapper();\n    mapper.registerModule(new GuavaModule());\n    mapper.registerModule(new Jdk8Module());\n    mapper.registerModule(new JavaTimeModule());\n    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);\n\n    underTest =\n        JacksonInvocationSerializer.builder()\n            .mapper(mapper)\n            .defaultInvocationSerializer(\n                DefaultInvocationSerializer.builder()\n                    .serializableTypes(Set.of(Invocation.class))\n                    .build())\n            .build();\n  }\n\n  void check(Invocation invocation) {\n    Invocation deserialized = serdeser(invocation);\n    assertEquals(deserialized, serdeser(invocation));\n    assertEquals(invocation, deserialized);\n  }\n\n  Invocation serdeser(Invocation invocation) {\n    try {\n      var writer = new StringWriter();\n      underTest.serializeInvocation(invocation, writer);\n      return underTest.deserializeInvocation(new StringReader(writer.toString()));\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  @Test\n  void testNoArgs() {\n    check(new Invocation(String.class.getName(), \"toString\", new Class<?>[0], new Object[0]));\n  }\n\n  @Test\n  void testArrays() {\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            new Class<?>[] {int[].class},\n            new Object[] {new int[] {1, 2, 3}}));\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            new Class<?>[] {Integer[].class},\n            new Object[] {new Integer[] {1, 2, 3}}));\n    check(\n        new Invocation(\n            CLASS_NAME,\n            METHOD_NAME,\n            new Class<?>[] {String[].class},\n            new Object[] {new String[] {\"1\", \"2\", \"3\"}}));\n  }\n\n  @Test\n  void testPrimitives() {\n    Class<?>[] primitives = {\n      short.class, int.class, long.class, float.class, double.class, boolean.class,\n    };\n    Object[] values = {(short) 2, 3, 4L, 1.23F, 1.23D, true};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testBoxedPrimitives() {\n    Class<?>[] primitives = {\n      Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, String.class\n    };\n    Object[] values = {\n      (Short) (short) 2,\n      (Integer) 3,\n      (Long) 4L,\n      (Float) 1.23F,\n      (Double) 1.23D,\n      (Boolean) true,\n      \"Foo\"\n    };\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaDateEnums() {\n    Class<?>[] primitives = {DayOfWeek.class, Month.class, ChronoUnit.class};\n    Object[] values = {DayOfWeek.FRIDAY, Month.APRIL, ChronoUnit.DAYS};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaUtilDate() {\n    Class<?>[] primitives = {Date.class};\n    Object[] values = {new Date()};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void testJavaTimeClasses() {\n    Class<?>[] primitives = {\n      Duration.class,\n      Instant.class,\n      LocalDate.class,\n      LocalDateTime.class,\n      MonthDay.class,\n      Period.class,\n      Year.class,\n      YearMonth.class\n    };\n    Object[] values = {\n      Duration.ofDays(1),\n      Instant.now().truncatedTo(ChronoUnit.MICROS),\n      LocalDate.now(),\n      LocalDateTime.now(),\n      MonthDay.of(1, 1),\n      Period.ofMonths(1),\n      Year.now(),\n      YearMonth.now()\n    };\n    check(new Invocation(CLASS_NAME, METHOD_NAME, primitives, values));\n  }\n\n  @Test\n  void deserializes_new_representation_correctly() throws IOException {\n    StringReader reader =\n        new StringReader(\n            \"{\\\"c\\\":\\\"com.gruelbox.transactionoutbox.jackson.Service\\\",\\\"m\\\":\\\"parseDate\\\",\\\"p\\\":[\\\"String\\\"],\\\"a\\\":[{\\\"t\\\":\\\"String\\\",\\\"v\\\":\\\"2021-05-11\\\"}],\\\"x\\\":{\\\"REQUEST-ID\\\":\\\"someRequestId\\\"},\\\"s\\\":{\\\"SESSION-ID\\\":\\\"someSessionId\\\"}}\");\n    Invocation invocation = underTest.deserializeInvocation(reader);\n    assertEquals(\n        new Invocation(\n            \"com.gruelbox.transactionoutbox.jackson.Service\",\n            \"parseDate\",\n            new Class<?>[] {String.class},\n            new Object[] {\"2021-05-11\"},\n            Map.of(\"REQUEST-ID\", \"someRequestId\"),\n            Map.of(\"SESSION-ID\", \"someSessionId\")),\n        invocation);\n  }\n\n  @Test\n  void deserializes_old_representation_correctly() throws IOException {\n    StringReader reader =\n        new StringReader(\n            \"{\\\"c\\\":\\\"com.gruelbox.transactionoutbox.jackson.Service\\\",\\\"m\\\":\\\"parseDate\\\",\\\"p\\\":[\\\"String\\\"],\\\"a\\\":[{\\\"t\\\":\\\"String\\\",\\\"v\\\":\\\"2021-05-11\\\"}],\\\"x\\\":{\\\"REQUEST-ID\\\":\\\"someRequestId\\\"}}\");\n    Invocation invocation = underTest.deserializeInvocation(reader);\n    assertEquals(\n        new Invocation(\n            \"com.gruelbox.transactionoutbox.jackson.Service\",\n            \"parseDate\",\n            new Class<?>[] {String.class},\n            new Object[] {\"2021-05-11\"},\n            Map.of(\"REQUEST-ID\", \"someRequestId\"),\n            null),\n        invocation);\n  }\n\n  @Test\n  void serializes_new_representation_stress_test() {\n    Class<?>[] parameterTypes = new Class<?>[] {SerializationStressTestInput.class};\n    Object[] args = new Object[] {new SerializationStressTestInput()};\n\n    check(new Invocation(CLASS_NAME, METHOD_NAME, parameterTypes, args, null, null));\n  }\n\n  @Test\n  void serializes_new_representation_list() {\n    Class<?>[] parameterTypes = new Class<?>[] {List.class};\n    Object[] args = new Object[] {List.of(MonetaryAmount.ofGbp(\"200\"))};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, parameterTypes, args, null, null));\n  }\n\n  @Test\n  void serializes_new_representation_set() {\n    Class<?>[] parameterTypes = new Class<?>[] {Set.class};\n    Object[] args = new Object[] {Set.of(MonetaryAmount.ofGbp(\"200\"))};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, parameterTypes, args, null, null));\n  }\n\n  @Test\n  void serializes_new_representation_map() {\n    Class<?>[] parameterTypes = new Class<?>[] {Set.class};\n    Object[] args = new Object[] {Map.of(\"investmentValue\", MonetaryAmount.ofGbp(\"200\"))};\n    check(new Invocation(CLASS_NAME, METHOD_NAME, parameterTypes, args, null, null));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/TestTransactionOutboxEntrySerialization.java",
    "content": "package com.gruelbox.transactionoutbox.jackson;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.gruelbox.transactionoutbox.Invocation;\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\npublic class TestTransactionOutboxEntrySerialization {\n\n  @Test\n  void test() throws JsonProcessingException {\n    ObjectMapper objectMapper = new ObjectMapper();\n    objectMapper.setDefaultTyping(TransactionOutboxJacksonModule.typeResolver());\n    objectMapper.registerModule(new TransactionOutboxJacksonModule());\n    objectMapper.registerModule(new JavaTimeModule());\n\n    var entry =\n        TransactionOutboxEntry.builder()\n            .invocation(\n                new Invocation(\n                    \"c\",\n                    \"m\",\n                    new Class<?>[] {Map.class},\n                    new Object[] {\n                      Map.of(\n                          \"x\", MonetaryAmount.ofGbp(\"200\"),\n                          \"y\", 3,\n                          \"z\", List.of(1, 2, 3))\n                    },\n                    null,\n                    null))\n            .attempts(1)\n            .blocked(true)\n            .id(\"X\")\n            .description(\"Stuff\")\n            .nextAttemptTime(Instant.now().truncatedTo(ChronoUnit.MILLIS))\n            .uniqueRequestId(\"Y\")\n            .build();\n    var s = objectMapper.writeValueAsString(entry);\n\n    var deserialized = objectMapper.readValue(s, TransactionOutboxEntry.class);\n    assertEquals(entry, deserialized);\n  }\n\n  @Test\n  void testWithSessionAndMdc() throws JsonProcessingException {\n    ObjectMapper objectMapper = new ObjectMapper();\n    objectMapper.setDefaultTyping(TransactionOutboxJacksonModule.typeResolver());\n    objectMapper.registerModule(new TransactionOutboxJacksonModule());\n    objectMapper.registerModule(new JavaTimeModule());\n\n    var entry =\n        TransactionOutboxEntry.builder()\n            .invocation(\n                new Invocation(\n                    \"c\",\n                    \"m\",\n                    new Class<?>[] {Map.class},\n                    new Object[] {\n                      Map.of(\n                          \"x\", MonetaryAmount.ofGbp(\"200\"),\n                          \"y\", 3,\n                          \"z\", List.of(1, 2, 3))\n                    },\n                    Map.of(\"a\", \"1\"),\n                    Map.of(\"b\", \"2\", \"c\", \"3\")))\n            .attempts(1)\n            .blocked(true)\n            .id(\"X\")\n            .description(\"Stuff\")\n            .nextAttemptTime(Instant.now().truncatedTo(ChronoUnit.MILLIS))\n            .uniqueRequestId(\"Y\")\n            .build();\n    var s = objectMapper.writeValueAsString(entry);\n    log.info(s);\n    var deserialized = objectMapper.readValue(s, TransactionOutboxEntry.class);\n    assertEquals(entry, deserialized);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/test/java/com/gruelbox/transactionoutbox/jackson/acceptance/TestJacksonSerializer.java",
    "content": "package com.gruelbox.transactionoutbox.jackson.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.jackson.JacksonInvocationSerializer;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport com.gruelbox.transactionoutbox.testing.LatchListener;\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nclass TestJacksonSerializer extends AbstractAcceptanceTest {\n\n  private final CountDownLatch latch = new CountDownLatch(1);\n  private final ThreadLocal<Map<String, String>> sessionVariable = new ThreadLocal<>();\n\n  @Override\n  protected Persistor persistor() {\n    return DefaultPersistor.builder()\n        .dialect(connectionDetails().dialect())\n        .serializer(\n            JacksonInvocationSerializer.builder()\n                .mapper(\n                    new ObjectMapper()\n                        .registerModule(new GuavaModule())\n                        .registerModule(new Jdk8Module())\n                        .registerModule(new JavaTimeModule())\n                        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true))\n                .build())\n        .build();\n  }\n\n  void process(List<Object> difficultDataStructure) {\n    assertEquals(List.of(LocalDate.of(2000, 1, 1), \"a\", \"b\", 2), difficultDataStructure);\n    assertEquals(Map.of(\"sessionVar\", \"foobar\"), sessionVariable.get());\n  }\n\n  @Test\n  void testPolymorphicDeserialization() throws Exception {\n    var transactionManager = txManager();\n    var outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(persistor())\n            .instantiator(Instantiator.using(clazz -> TestJacksonSerializer.this))\n            .listener(\n                new LatchListener(latch)\n                    .andThen(\n                        new TransactionOutboxListener() {\n                          @Override\n                          public Map<String, String> extractSession() {\n                            return Map.of(\"sessionVar\", \"foobar\");\n                          }\n\n                          @Override\n                          public void wrapInvocationAndInit(Invocator invocator) {\n                            sessionVariable.set(invocator.getInvocation().getSession());\n                            try {\n                              invocator.runUnchecked();\n                            } finally {\n                              sessionVariable.remove();\n                            }\n                          }\n                        }))\n            .build();\n    transactionManager.inTransaction(\n        () -> outbox.schedule(getClass()).process(List.of(LocalDate.of(2000, 1, 1), \"a\", \"b\", 2)));\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jackson/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level SESSION-KEY=%X{SESSION-KEY} %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "transactionoutbox-jooq/README.md",
    "content": "# transaction-outbox-jooq\n\n[![jOOQ on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-jooq/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-jooq)\n[![jOOQ Javadoc](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-jooq.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-jooq)\n[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot&sort=semver)](#development-snapshots)\n\nExtension for [transaction-outbox-core](../README.md) which integrates with jOOQ for transaction management.\n\nLike Transaction Outbox, jOOQ is intended to play nicely with any other transaction management approach, but provides its own as an option. If you are already using jOOQ's `TransactionProvider` via `DSLContext.transaction(...)` throughout your application, you can continue to do so with this extension.\n\njOOQ gives you the option to either use thread-local transaction management or explicitly pass a contextual `DSLContext` or `Configuration` down your stack. You can do the same thing with `TransactionOutbox`.\n\n## Installation\n\n### Stable releases\n\n#### Maven\n\n```xml\n<dependency>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-jooq</artifactId>\n  <version>7.0.707</version>\n</dependency>\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-jooq:7.0.707'\n```\n\n### Development snapshots\n\nSee [transactionoutbox-core](../README.md) for more information.\n\n## Using thread-local transactions\n\n### Configuration\n\nFirst, configure jOOQ to use thread-local transaction management:\n\n```java\nvar jooqConfig = new DefaultConfiguration();\nvar connectionProvider = new DataSourceConnectionProvider(dataSource);\njooqConfig.setConnectionProvider(connectionProvider);\njooqConfig.setSQLDialect(SQLDialect.H2);\njooqConfig.setTransactionProvider(new ThreadLocalTransactionProvider(connectionProvider, true));\n```\n\nNow connect `JooqTransactionListener`, which is the bridge between jOOQ and `TransactionOutbox`, and create the `DSLContext`:\n\n```java\nvar listener = JooqTransactionManager.createListener();\njooqConfig.set(listener);\nvar dsl = DSL.using(jooqConfig);\n```\n\nFinally create the `TransactionOutbox`:\n\n```java\nvar outbox = TransactionOutbox.builder()\n    .transactionManager(JooqTransactionManager.create(dsl, listener))\n    .persistor(Persistor.forDialect(Dialect.MY_SQL_8))\n    .build();\n}\n```\n\n### Usage\n\nYou can now use jOOQ and Transaction Outbox together, assuming thread-bound transactions.\n\n```java\ndsl.transaction(() -> {\n  customerDao.save(new Customer(1L, \"Martin\", \"Carthy\"));\n  customerDao.save(new Customer(2L, \"Dave\", \"Pegg\"));\n  outbox.schedule(MyClass.class).publishCustomerCreatedEvent(1L);\n  outbox.schedule(MyClass.class).publishCustomerCreatedEvent(2L);\n});\n```\n\n## Using explicit transaction context\n\nIf you prefer not to use thread-local transactions, you are already taking on the burden of passing jOOQ `Configuration`s or `DSLContext`s down your stack. This is supported with `TransactionOutbox`, but requires a little explanation.\n\n### Configuration\n\nWithout the need to synchronise the thread context, setup is a bit easier:\n\n```java\n// Create the DSLContext and connect the listener\nvar dsl = DSL.using(dataSource, SQLDialect.H2);\ndsl.configuration().set(JooqTransactionManager.createListener());\n\n// Create the outbox\nvar outbox = TransactionOutbox.builder()\n    .transactionManager(JooqTransactionManager.create(dsl))\n    .persistor(Persistor.forDialect(Dialect.MY_SQL_8))\n    .build();\n```\n\n### Usage\n\nThe call pattern in the thread-local context example above will now not work:\n\n```java\noutbox.schedule(MyClass.class).publishCustomerCreatedEvent(1L);\n```\n\n`TransactionOutbox` needs the currently active transaction context to write the database record. To do so, you need to change the scheduled method itself to receive a `Configuration`:\n\n```java\nvoid publishCustomerCreatedEvent(long id, Configuration cfg2) {\n  cfg.dsl().insertInto(...)...\n}\n```\n\nThen call accordingly:\n\n```java\ndsl.transaction(cfg1 -> {\n  new CustomerDao(cfg1).save(new Customer(1L, \"Martin\", \"Carthy\"));\n  new CustomerDao(cfg1).save(new Customer(2L, \"Dave\", \"Pegg\"));\n  outbox.schedule(MyClass.class).publishCustomerCreatedEvent(1L, cfg1);\n  outbox.schedule(MyClass.class).publishCustomerCreatedEvent(2L, cfg1);\n});\n```\n\nIn the example above, `cfg1` is the transaction context in which the request is written to the database, and `cfg2` is the context in which it is executed, which will be a different transaction at some later time. `cfg1` is stripped from the request before writing it to the database and replaced with `cfg2` at run time.\n\nThe reason for passing the `Configuration` in the scheduled method call itself (rather than the `schedule()` method call) is twofold:\n\n1.  It is very common for tasks to need access to the transaction context _at the time they are run_ in order to participate in that transaction. That way, if any part of the outbox task is rolled back, any work we do inside it is also rolled back.\n2.  If the method were not scheduled by `TransactionOutbox`, but instead called directly, it would need the `Configuration` passed to it anyway. By working this way we ensure that the API for calling directly or scheduled is the same, and therefore the two implementations are interchangeable.\n"
  },
  {
    "path": "transactionoutbox-jooq/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox jOOQ</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-jooq</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (jOOQ extension library)</description>\n  <properties>\n    <guice.version>5.2.4.RELEASE</guice.version>\n    <maven.compiler.source>21</maven.compiler.source>\n    <maven.compiler.target>21</maven.compiler.target>\n  </properties>\n  <dependencies>\n    <!-- Runtime -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.jooq</groupId>\n      <artifactId>jooq</artifactId>\n      <version>3.20.11</version>\n    </dependency>\n    <!-- Compile -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-testing</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.h2database</groupId>\n      <artifactId>h2</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>testcontainers</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>junit-jupiter</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>postgresql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>oracle-xe</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mysql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.postgresql</groupId>\n      <artifactId>postgresql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.oracle.database.jdbc</groupId>\n      <artifactId>ojdbc11</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.mysql</groupId>\n      <artifactId>mysql-connector-j</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mssqlserver</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.microsoft.sqlserver</groupId>\n      <artifactId>mssql-jdbc</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/DefaultJooqTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.ParameterContextTransactionManager;\nimport com.gruelbox.transactionoutbox.ThrowingTransactionalSupplier;\nimport com.gruelbox.transactionoutbox.Transaction;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\n\n/**\n * jOOQ transaction manager which uses explicitly-passed transaction context. Suitable for use with\n * {@link org.jooq.impl.DefaultTransactionProvider}. Relies on {@link JooqTransactionListener} being\n * connected to the {@link DSLContext}.\n */\n@Slf4j\nfinal class DefaultJooqTransactionManager\n    implements ParameterContextTransactionManager<Configuration> {\n\n  private final DSLContext dsl;\n\n  DefaultJooqTransactionManager(DSLContext dsl) {\n    this.dsl = dsl;\n  }\n\n  @Override\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) {\n    return dsl.transactionResult(cfg -> work.doWork(transactionFromContext(cfg)));\n  }\n\n  @Override\n  public Transaction transactionFromContext(Configuration context) {\n    Object txn = context.data(JooqTransactionListener.TXN_KEY);\n    if (txn == null) {\n      throw new IllegalStateException(\n          JooqTransactionListener.class.getSimpleName() + \" is not attached to the DSL\");\n    }\n    return (Transaction) txn;\n  }\n\n  @Override\n  public Class<Configuration> contextType() {\n    return Configuration.class;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/JooqTransactionListener.java",
    "content": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.spi.SimpleTransaction;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.TransactionContext;\nimport org.jooq.TransactionListener;\n\n/** A jOOQ {@link TransactionListener} which synchronises a {@link JooqTransactionManager}. */\n@Slf4j\npublic class JooqTransactionListener implements TransactionListener {\n\n  static final String TXN_KEY = JooqTransactionListener.class.getName() + \".txn\";\n\n  private ThreadLocalJooqTransactionManager jooqTransactionManager;\n\n  protected JooqTransactionListener() {}\n\n  void setJooqTransactionManager(ThreadLocalJooqTransactionManager jooqTransactionManager) {\n    this.jooqTransactionManager = jooqTransactionManager;\n  }\n\n  @Override\n  public void beginStart(TransactionContext ctx) {\n    // No-op\n  }\n\n  @Override\n  public void beginEnd(TransactionContext ctx) {\n    ctx.dsl()\n        .connection(\n            connection -> {\n              SimpleTransaction transaction =\n                  new SimpleTransaction(connection, ctx.dsl().configuration());\n              ctx.dsl().configuration().data(TXN_KEY, transaction);\n              if (jooqTransactionManager != null) {\n                jooqTransactionManager.pushTransaction(transaction);\n              }\n            });\n  }\n\n  @Override\n  public void commitStart(TransactionContext ctx) {\n    log.debug(\"Transaction commit start\");\n    try {\n      getTransaction(ctx).flushBatches();\n    } finally {\n      getTransaction(ctx).close();\n    }\n  }\n\n  @Override\n  public void commitEnd(TransactionContext ctx) {\n    log.debug(\"Transaction commit end\");\n    SimpleTransaction transaction = getTransaction(ctx);\n    safePopThreadLocals();\n    transaction.processHooks();\n  }\n\n  @Override\n  public void rollbackStart(TransactionContext ctx) {\n    log.debug(\"Transaction rollback\");\n    getTransaction(ctx).close();\n  }\n\n  @Override\n  public void rollbackEnd(TransactionContext ctx) {\n    safePopThreadLocals();\n  }\n\n  private SimpleTransaction getTransaction(TransactionContext ctx) {\n    return (SimpleTransaction) ctx.dsl().configuration().data(TXN_KEY);\n  }\n\n  private void safePopThreadLocals() {\n    if (jooqTransactionManager != null) {\n      jooqTransactionManager.popTransaction();\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/JooqTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.ParameterContextTransactionManager;\nimport com.gruelbox.transactionoutbox.ThreadLocalContextTransactionManager;\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\n\n/**\n * Transaction manager which uses jOOQ's transaction management. In order to wire into JOOQ's\n * transaction lifecycle, a slightly convoluted construction process is required which involves\n * first creating a {@link JooqTransactionListener}, including it in the JOOQ {@link Configuration}\n * while constructing the root {@link DSLContext}, and then finally linking the listener to the new\n * {@link JooqTransactionManager}:\n *\n * <pre>\n * DataSourceConnectionProvider connectionProvider = new DataSourceConnectionProvider(dataSource);\n * DefaultConfiguration configuration = new DefaultConfiguration();\n * configuration.setConnectionProvider(connectionProvider);\n * configuration.setSQLDialect(SQLDialect.H2);\n * configuration.setTransactionProvider(new ThreadLocalTransactionProvider(connectionProvider));\n * JooqTransactionListener listener = JooqTransactionManager.createListener();\n * configuration.set(listener);\n * DSLContext dsl = DSL.using(configuration);\n * return JooqTransactionManager.create(dsl, listener);</pre>\n */\npublic interface JooqTransactionManager extends TransactionManager {\n\n  /**\n   * Creates the {@link org.jooq.TransactionListener} to wire into the {@link DSLContext}. See\n   * class-level documentation for more detail.\n   *\n   * @return The transaction listener.\n   */\n  static JooqTransactionListener createListener() {\n    return new JooqTransactionListener();\n  }\n\n  /**\n   * Creates a transaction manager which uses thread-local context. Attaches to the supplied {@link\n   * JooqTransactionListener} to receive notifications of transactions starting and finishing on the\n   * local thread so that {@link TransactionOutbox#schedule(Class)} can be called for methods that\n   * don't explicitly inject a {@link Configuration}, e.g.:\n   *\n   * <pre>dsl.transaction(() -&gt; outbox.schedule(Foo.class).process(\"bar\"));</pre>\n   *\n   * @param dslContext The DSL context.\n   * @param listener The listener, linked to the DSL context.\n   * @return The transaction manager.\n   */\n  static ThreadLocalContextTransactionManager create(\n      DSLContext dslContext, JooqTransactionListener listener) {\n    var result = new ThreadLocalJooqTransactionManager(dslContext);\n    listener.setJooqTransactionManager(result);\n    return result;\n  }\n\n  /**\n   * Creates a transaction manager which uses explicitly-passed context, allowing multiple active\n   * contexts in the current thread and contexts which are passed between threads. Requires a {@link\n   * Configuration} for the transaction context or a {@link org.jooq.Transaction} to be used to be\n   * passed any method called via {@link TransactionOutbox#schedule(Class)}. Example:\n   *\n   * <pre>\n   * void doSchedule() {\n   *   // ctx1 is used to write the request to the DB\n   *   dsl.transaction(ctx1 -&gt; outbox.schedule(getClass()).process(\"bar\", ctx1));\n   * }\n   *\n   * // ctx2 is injected at run time\n   * void process(String arg, org.jooq.Configuration ctx2) {\n   *   ...\n   * }</pre>\n   *\n   * <p>Or:\n   *\n   * <pre>\n   * void doSchedule() {\n   *   // tx1 is used to write the request to the DB\n   *   transactionManager.inTransaction(tx1 -&gt; outbox.schedule(getClass()).process(\"bar\", tx1));\n   * }\n   *\n   * // tx2 is injected at run time\n   * void process(String arg, Transaction tx2) {\n   *   ...\n   * }</pre>\n   *\n   * @param dslContext The DSL context.\n   * @return The transaction manager.\n   */\n  static ParameterContextTransactionManager<Configuration> create(DSLContext dslContext) {\n    return new DefaultJooqTransactionManager(dslContext);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/main/java/com/gruelbox/transactionoutbox/jooq/ThreadLocalJooqTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox.jooq;\n\nimport com.gruelbox.transactionoutbox.ThrowingTransactionalSupplier;\nimport com.gruelbox.transactionoutbox.spi.AbstractThreadLocalTransactionManager;\nimport com.gruelbox.transactionoutbox.spi.SimpleTransaction;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\n\n/**\n * jOOQ transaction manager which uses thread-local context. Best used with {@link\n * org.jooq.impl.ThreadLocalTransactionProvider}. Relies on a {@link JooqTransactionListener} being\n * attached to the {@link DSLContext}.\n */\n@Slf4j\nfinal class ThreadLocalJooqTransactionManager\n    extends AbstractThreadLocalTransactionManager<SimpleTransaction>\n    implements JooqTransactionManager {\n\n  private final DSLContext parentDsl;\n\n  ThreadLocalJooqTransactionManager(DSLContext parentDsl) {\n    this.parentDsl = parentDsl;\n  }\n\n  @Override\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) {\n    DSLContext dsl =\n        peekTransaction()\n            .map(SimpleTransaction::context)\n            .map(Configuration.class::cast)\n            .map(Configuration::dsl)\n            .orElse(parentDsl);\n    return dsl.transactionResult(\n        config ->\n            config\n                .dsl()\n                .connectionResult(connection -> work.doWork(peekTransaction().orElseThrow())));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/AbstractJooqAcceptanceTest.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport com.gruelbox.transactionoutbox.testing.AbstractAcceptanceTest;\nimport org.jooq.DSLContext;\nimport org.junit.jupiter.api.Test;\n\nabstract class AbstractJooqAcceptanceTest extends AbstractAcceptanceTest {\n\n  protected DSLContext dsl;\n\n  @Override\n  protected TransactionManager txManager() {\n    throw new IllegalStateException(\"Needs to be defined\");\n  }\n\n  @Test\n  abstract void testNestedDirectInvocation() throws Exception;\n\n  @Test\n  abstract void testNestedViaListener() throws Exception;\n\n  @Test\n  abstract void testNestedWithInnerFailure() throws Exception;\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/AbstractJooqAcceptanceThreadLocalTest.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport static java.util.concurrent.CompletableFuture.runAsync;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionListener;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionManager;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.SQLDialect;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.DataSourceConnectionProvider;\nimport org.jooq.impl.DefaultConfiguration;\nimport org.jooq.impl.ThreadLocalTransactionProvider;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nabstract class AbstractJooqAcceptanceThreadLocalTest extends AbstractJooqAcceptanceTest {\n  private ThreadLocalContextTransactionManager txm;\n\n  @Override\n  protected final ThreadLocalContextTransactionManager txManager() {\n    return txm;\n  }\n\n  protected SQLDialect jooqDialect() {\n    return SQLDialect.H2;\n  }\n\n  @BeforeEach\n  final void beforeEach() {\n    DataSourceConnectionProvider connectionProvider = new DataSourceConnectionProvider(dataSource);\n    DefaultConfiguration configuration = new DefaultConfiguration();\n    configuration.setConnectionProvider(connectionProvider);\n    configuration.setSQLDialect(jooqDialect());\n    configuration.setTransactionProvider(\n        new ThreadLocalTransactionProvider(connectionProvider, true));\n    JooqTransactionListener listener = JooqTransactionManager.createListener();\n    configuration.set(listener);\n    dsl = DSL.using(configuration);\n    txm = JooqTransactionManager.create(dsl, listener);\n    JooqTestUtils.createTestTable(dsl);\n  }\n\n  @Test\n  @Override\n  void testNestedDirectInvocation() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    ThreadLocalContextTransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          transactionManager.inTransactionThrows(\n              tx1 -> {\n                outbox.schedule(Worker.class).process(1);\n\n                transactionManager.inTransactionThrows(\n                    tx2 -> outbox.schedule(Worker.class).process(2));\n\n                // Neither should be fired - the second job is in a nested transaction\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          // Should be fired after commit\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(2, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertTrue(latch2.await(2, TimeUnit.SECONDS)))))\n              .get();\n        });\n\n    JooqTestUtils.assertRecordExists(dsl, 1);\n    JooqTestUtils.assertRecordExists(dsl, 2);\n  }\n\n  @Test\n  @Override\n  void testNestedViaListener() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    ThreadLocalContextTransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          dsl.transaction(\n              ctx -> {\n                outbox.schedule(Worker.class).process(1);\n                ctx.dsl().transaction(() -> outbox.schedule(Worker.class).process(2));\n\n                // Neither should be fired - the second job is in a nested transaction\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          // Both should be fired after commit\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(10, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertTrue(latch2.await(10, TimeUnit.SECONDS)))))\n              .get();\n        });\n    JooqTestUtils.assertRecordExists(dsl, 1);\n    JooqTestUtils.assertRecordExists(dsl, 2);\n  }\n\n  /**\n   * Ensures that given the rollback of an inner transaction, any outbox work scheduled in the inner\n   * transaction is rolled back while the outer transaction's works.\n   */\n  @Test\n  @Override\n  void testNestedWithInnerFailure() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    ThreadLocalContextTransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          dsl.transaction(\n              ctx -> {\n                outbox.schedule(Worker.class).process(1);\n\n                assertThrows(\n                    UnsupportedOperationException.class,\n                    () ->\n                        ctx.dsl()\n                            .transaction(\n                                () -> {\n                                  outbox.schedule(Worker.class).process(2);\n                                  throw new UnsupportedOperationException();\n                                }));\n\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(2, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n              .get();\n        });\n  }\n\n  @SuppressWarnings(\"EmptyMethod\")\n  static class Worker {\n\n    private final ThreadLocalContextTransactionManager transactionManager;\n\n    Worker(ThreadLocalContextTransactionManager transactionManager) {\n      this.transactionManager = transactionManager;\n    }\n\n    @SuppressWarnings(\"SameParameterValue\")\n    void process(int i) {\n      JooqTestUtils.writeRecord(transactionManager, i);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/JooqTestUtils.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.gruelbox.transactionoutbox.ThreadLocalContextTransactionManager;\nimport com.gruelbox.transactionoutbox.Transaction;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Record;\nimport org.jooq.Table;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.SQLDataType;\n\n@Slf4j\nclass JooqTestUtils {\n\n  private static final Table<Record> TEST_TABLE = DSL.table(\"TEST_TABLE_JOOQ\");\n  private static final String VAL = \"val\";\n\n  static void createTestTable(DSLContext dsl) {\n    log.info(\"Creating table\");\n    dsl.dropTableIfExists(TEST_TABLE).execute();\n    dsl.createTable(TEST_TABLE).column(VAL, SQLDataType.INTEGER).execute();\n  }\n\n  static void writeRecord(Configuration configuration, int value) {\n    log.info(\"Inserting record {}\", value);\n    configuration.dsl().insertInto(TEST_TABLE).values(value).execute();\n  }\n\n  static void writeRecord(Transaction transaction, int value) {\n    Configuration configuration = transaction.context();\n    writeRecord(configuration, value);\n  }\n\n  static void writeRecord(ThreadLocalContextTransactionManager transactionManager, int value) {\n    transactionManager.requireTransaction(tx -> writeRecord(tx, value));\n  }\n\n  static void assertRecordExists(DSLContext dsl, int value) {\n    assertTrue(\n        dsl.select().from(TEST_TABLE).where(DSL.field(VAL).eq(value)).fetchOptional().isPresent());\n  }\n\n  static void assertRecordNotExists(\n      DSLContext dsl, @SuppressWarnings(\"SameParameterValue\") int value) {\n    assertFalse(\n        dsl.select().from(TEST_TABLE).where(DSL.field(VAL).eq(value)).fetchOptional().isPresent());\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalH2.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\nclass TestJooqThreadLocalH2 extends AbstractJooqAcceptanceThreadLocalTest {}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMSSqlServer2019.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.SQLDialect;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MSSQLServerContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Slf4j\n@Testcontainers\nclass TestJooqThreadLocalMSSqlServer2019 extends AbstractJooqAcceptanceThreadLocalTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\", \"unchecked\"})\n  private static final JdbcDatabaseContainer<?> container =\n      new MSSQLServerContainer<>(\"mcr.microsoft.com/mssql/server:2019-latest\")\n          .acceptLicense()\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MS_SQL_SERVER)\n        .driverClassName(\"com.microsoft.sqlserver.jdbc.SQLServerDriver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n\n  @Override\n  protected SQLDialect jooqDialect() {\n    return SQLDialect.DEFAULT;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMySql5.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.SQLDialect;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Slf4j\n@Testcontainers\nclass TestJooqThreadLocalMySql5 extends AbstractJooqAcceptanceThreadLocalTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\", \"unchecked\"})\n  private static final JdbcDatabaseContainer<?> container =\n      (JdbcDatabaseContainer<?>)\n          new MySQLContainer(\"mysql:5\")\n              .withStartupTimeout(Duration.ofMinutes(5))\n              .withReuse(true)\n              .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MY_SQL_5)\n        .driverClassName(\"com.mysql.cj.jdbc.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n\n  @Override\n  protected SQLDialect jooqDialect() {\n    return SQLDialect.MYSQL;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalMySql8.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.SQLDialect;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Slf4j\n@Testcontainers\nclass TestJooqThreadLocalMySql8 extends AbstractJooqAcceptanceThreadLocalTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\", \"unchecked\"})\n  private static final JdbcDatabaseContainer<?> container =\n      (JdbcDatabaseContainer<?>)\n          new MySQLContainer(\"mysql:8\")\n              .withStartupTimeout(Duration.ofMinutes(5))\n              .withReuse(true)\n              .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.MY_SQL_8)\n        .driverClassName(\"com.mysql.cj.jdbc.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n\n  @Override\n  protected SQLDialect jooqDialect() {\n    return SQLDialect.MYSQL;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqThreadLocalPostgres16.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.SQLDialect;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Slf4j\n@Testcontainers\nclass TestJooqThreadLocalPostgres16 extends AbstractJooqAcceptanceThreadLocalTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:16\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n\n  @Override\n  protected SQLDialect jooqDialect() {\n    return SQLDialect.POSTGRES;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static com.gruelbox.transactionoutbox.jooq.acceptance.JooqTestUtils.createTestTable;\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport static com.gruelbox.transactionoutbox.testing.TestUtils.runSql;\nimport static java.util.concurrent.CompletableFuture.runAsync;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionManager;\nimport com.gruelbox.transactionoutbox.testing.LatchListener;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.SQLDialect;\nimport org.jooq.impl.DSL;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nclass TestJooqTransactionManagerWithDefaultProviderAndExplicitlyPassedContext {\n\n  private HikariDataSource dataSource;\n  private DSLContext dsl;\n  private static ThreadLocal<String> sessionVar = new ThreadLocal<>();\n\n  @BeforeEach\n  void beforeEach() {\n    TestingMode.enable();\n    dataSource = pooledDataSource();\n    dsl = createDsl();\n    createTestTable(dsl);\n  }\n\n  @AfterEach\n  void afterEach() {\n    TestingMode.disable();\n    dataSource.close();\n  }\n\n  private HikariDataSource pooledDataSource() {\n    HikariConfig config = new HikariConfig();\n    config.setJdbcUrl(\n        \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=2000;LOB_TIMEOUT=2000;MV_STORE=TRUE;DATABASE_TO_UPPER=FALSE\");\n    config.setUsername(\"test\");\n    config.setPassword(\"test\");\n    config.addDataSourceProperty(\"cachePrepStmts\", \"true\");\n    return new HikariDataSource(config);\n  }\n\n  private DSLContext createDsl() {\n    dsl = DSL.using(dataSource, SQLDialect.H2);\n    dsl.configuration().set(JooqTransactionManager.createListener());\n    return dsl;\n  }\n\n  private TransactionManager createTransactionManager() {\n    return JooqTransactionManager.create(dsl);\n  }\n\n  @Test\n  void testSimplePassingTransaction() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(createTransactionManager())\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .listener(new LatchListener(latch))\n            .build();\n\n    clearOutbox(createTransactionManager());\n\n    createTransactionManager()\n        .inTransaction(\n            tx -> {\n              outbox.schedule(Worker.class).process(1, tx);\n              try {\n                // Should not be fired until after commit\n                assertFalse(latch.await(2, TimeUnit.SECONDS));\n              } catch (InterruptedException e) {\n                fail(\"Interrupted\");\n              }\n            });\n\n    // Should be fired after commit\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n    JooqTestUtils.assertRecordExists(dsl, 1);\n  }\n\n  @Test\n  void testSimplePassingContext() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(createTransactionManager())\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .listener(new LatchListener(latch))\n            .build();\n\n    clearOutbox(createTransactionManager());\n\n    DSLContext dsl = createDsl();\n    dsl.transaction(\n        cx1 -> {\n          outbox.schedule(Worker.class).process(1, cx1);\n          try {\n            // Should not be fired until after commit\n            assertFalse(latch.await(2, TimeUnit.SECONDS));\n          } catch (InterruptedException e) {\n            fail(\"Interrupted\");\n          }\n        });\n\n    // Should be fired after commit\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n    JooqTestUtils.assertRecordExists(dsl, 1);\n  }\n\n  @Test\n  void testNestedPassingContext() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(createTransactionManager())\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox(createTransactionManager());\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          dsl.transaction(\n              ctx -> {\n                outbox.schedule(Worker.class).process(1, ctx);\n                ctx.dsl().transaction(cx1 -> outbox.schedule(Worker.class).process(2, ctx));\n\n                // Neither should be fired - the second job is in a nested transaction\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          // Both should be fired after commit\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(2, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertTrue(latch2.await(2, TimeUnit.SECONDS)))))\n              .get();\n        });\n    JooqTestUtils.assertRecordExists(dsl, 1);\n    JooqTestUtils.assertRecordExists(dsl, 2);\n  }\n\n  @Test\n  void testNestedPassingTransaction() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(createTransactionManager())\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox(createTransactionManager());\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          createTransactionManager()\n              .inTransactionThrows(\n                  tx1 -> {\n                    outbox.schedule(Worker.class).process(1, tx1);\n\n                    createTransactionManager()\n                        .inTransactionThrows(tx2 -> outbox.schedule(Worker.class).process(2, tx2));\n\n                    // The inner transaction should be committed - these are different semantics\n                    CompletableFuture.allOf(\n                            runAsync(\n                                () ->\n                                    uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                            runAsync(\n                                () -> uncheck(() -> assertTrue(latch2.await(2, TimeUnit.SECONDS)))))\n                        .get();\n\n                    JooqTestUtils.assertRecordExists(dsl, 2);\n                  });\n\n          // Should be fired after commit\n          assertTrue(latch1.await(2, TimeUnit.SECONDS));\n        });\n\n    JooqTestUtils.assertRecordExists(dsl, 1);\n  }\n\n  /**\n   * Ensures that given the rollback of an inner transaction, any outbox work scheduled in the inner\n   * transaction is rolled back while the outer transaction's works.\n   */\n  @Test\n  void testNestedWithInnerFailure() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(createTransactionManager())\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox(createTransactionManager());\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          dsl.transaction(\n              ctx -> {\n                outbox.schedule(Worker.class).process(1, ctx);\n\n                assertThrows(\n                    UnsupportedOperationException.class,\n                    () ->\n                        ctx.dsl()\n                            .transaction(\n                                cx2 -> {\n                                  outbox.schedule(Worker.class).process(2, cx2);\n                                  throw new UnsupportedOperationException();\n                                }));\n\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(2, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n              .get();\n        });\n    JooqTestUtils.assertRecordExists(dsl, 1);\n    JooqTestUtils.assertRecordNotExists(dsl, 2);\n  }\n\n  @Test\n  void testSessionVariables() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    var sessionVarLocal = UUID.randomUUID().toString();\n\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(createTransactionManager())\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .listener(\n                new LatchListener(latch)\n                    .andThen(\n                        new TransactionOutboxListener() {\n                          @Override\n                          public Map<String, String> extractSession() {\n                            return Map.of(\"sesvar\", sessionVarLocal);\n                          }\n\n                          @Override\n                          public void wrapInvocationAndInit(Invocator invocator) {\n                            sessionVar.set(invocator.getInvocation().getSession().get(\"sesvar\"));\n                            try {\n                              invocator.runUnchecked();\n                            } finally {\n                              sessionVar.remove();\n                            }\n                          }\n                        }))\n            .build();\n\n    clearOutbox(createTransactionManager());\n    createTransactionManager()\n        .inTransaction(\n            tx -> outbox.schedule(Worker.class).checkSessionPresent(sessionVarLocal, tx));\n\n    // Should be fired after commit\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n  }\n\n  private void clearOutbox(TransactionManager transactionManager) {\n    runSql(transactionManager, \"DELETE FROM TXNO_OUTBOX\");\n  }\n\n  private void withRunningFlusher(TransactionOutbox outbox, ThrowingRunnable runnable)\n      throws Exception {\n    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);\n    try {\n      scheduler.scheduleAtFixedRate(\n          () -> {\n            if (Thread.interrupted()) {\n              return;\n            }\n            outbox.flush();\n          },\n          500,\n          500,\n          TimeUnit.MILLISECONDS);\n      runnable.run();\n    } finally {\n      scheduler.shutdown();\n      assertTrue(scheduler.awaitTermination(20, TimeUnit.SECONDS));\n    }\n  }\n\n  @SuppressWarnings(\"EmptyMethod\")\n  static class Worker {\n\n    @SuppressWarnings(\"SameParameterValue\")\n    void process(int i, Transaction transaction) {\n      JooqTestUtils.writeRecord(transaction, i);\n    }\n\n    void checkSessionPresent(String expected, Transaction transaction) {\n      assertEquals(expected, sessionVar.get());\n    }\n\n    void process(int i, Configuration configuration) {\n      JooqTestUtils.writeRecord(configuration, i);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/java/com/gruelbox/transactionoutbox/jooq/acceptance/TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext.java",
    "content": "package com.gruelbox.transactionoutbox.jooq.acceptance;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport static com.gruelbox.transactionoutbox.testing.TestUtils.runSql;\nimport static java.util.concurrent.CompletableFuture.runAsync;\nimport static org.hamcrest.Matchers.containsInAnyOrder;\nimport static org.hamcrest.Matchers.empty;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionListener;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionManager;\nimport com.gruelbox.transactionoutbox.testing.LatchListener;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.IntStream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hamcrest.MatcherAssert;\nimport org.jooq.DSLContext;\nimport org.jooq.SQLDialect;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.DataSourceConnectionProvider;\nimport org.jooq.impl.DefaultConfiguration;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nclass TestJooqTransactionManagerWithDefaultProviderAndThreadLocalContext {\n\n  private static ThreadLocal<String> sessionVar = new ThreadLocal<>();\n  private final ExecutorService unreliablePool =\n      new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(16));\n\n  private HikariDataSource dataSource;\n  private ThreadLocalContextTransactionManager transactionManager;\n  private DSLContext dsl;\n\n  @BeforeEach\n  void beforeEach() {\n    TestingMode.enable();\n    dataSource = pooledDataSource();\n    transactionManager = createTransactionManager();\n    JooqTestUtils.createTestTable(dsl);\n  }\n\n  @AfterEach\n  void afterEach() {\n    TestingMode.disable();\n    dataSource.close();\n  }\n\n  private HikariDataSource pooledDataSource() {\n    HikariConfig config = new HikariConfig();\n    config.setJdbcUrl(\n        \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=2000;LOB_TIMEOUT=2000;MV_STORE=TRUE;DATABASE_TO_UPPER=FALSE\");\n    config.setUsername(\"test\");\n    config.setPassword(\"test\");\n    config.addDataSourceProperty(\"cachePrepStmts\", \"true\");\n    return new HikariDataSource(config);\n  }\n\n  private ThreadLocalContextTransactionManager createTransactionManager() {\n    DefaultConfiguration configuration = new DefaultConfiguration();\n    configuration.setConnectionProvider(new DataSourceConnectionProvider(dataSource));\n    configuration.setSQLDialect(SQLDialect.H2);\n    JooqTransactionListener listener = JooqTransactionManager.createListener();\n    configuration.set(listener);\n    dsl = DSL.using(configuration);\n    return JooqTransactionManager.create(dsl, listener);\n  }\n\n  @Test\n  void testSimpleDirectInvocation() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    latch.countDown();\n                  }\n                })\n            .build();\n\n    clearOutbox(transactionManager);\n\n    transactionManager.inTransaction(\n        () -> {\n          outbox.schedule(Worker.class).process(1);\n          try {\n            // Should not be fired until after commit\n            assertFalse(latch.await(2, TimeUnit.SECONDS));\n          } catch (InterruptedException e) {\n            fail(\"Interrupted\");\n          }\n        });\n\n    // Should be fired after commit\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n    JooqTestUtils.assertRecordExists(dsl, 1);\n  }\n\n  @Test\n  void testSimpleViaListener() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    latch.countDown();\n                  }\n                })\n            .build();\n\n    clearOutbox(transactionManager);\n\n    dsl.transaction(\n        cx1 -> {\n          outbox.schedule(Worker.class).process(1);\n          try {\n            // Should not be fired until after commit\n            assertFalse(latch.await(2, TimeUnit.SECONDS));\n          } catch (InterruptedException e) {\n            fail(\"Interrupted\");\n          }\n        });\n\n    // Should be fired after commit\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n    JooqTestUtils.assertRecordExists(dsl, 1);\n  }\n\n  @Test\n  void testNestedViaListener() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox(transactionManager);\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          dsl.transaction(\n              ctx -> {\n                outbox.schedule(Worker.class).process(1);\n                ctx.dsl().transaction(cx1 -> outbox.schedule(Worker.class).process(2));\n\n                // Neither should be fired - the second job is in a nested transaction\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          // Both should be fired after commit\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(2, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertTrue(latch2.await(2, TimeUnit.SECONDS)))))\n              .get();\n        });\n    JooqTestUtils.assertRecordExists(dsl, 1);\n    JooqTestUtils.assertRecordExists(dsl, 2);\n  }\n\n  /**\n   * Ensures that given the rollback of an inner transaction, any outbox work scheduled in the inner\n   * transaction is rolled back while the outer transaction's works.\n   */\n  @Test\n  void testNestedWithInnerFailure() throws Exception {\n    CountDownLatch latch1 = new CountDownLatch(1);\n    CountDownLatch latch2 = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .attemptFrequency(Duration.of(1, ChronoUnit.SECONDS))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    if (entry.getInvocation().getArgs()[0].equals(1)) {\n                      latch1.countDown();\n                    } else {\n                      latch2.countDown();\n                    }\n                  }\n                })\n            .build();\n\n    clearOutbox(transactionManager);\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          dsl.transaction(\n              ctx -> {\n                outbox.schedule(Worker.class).process(1);\n\n                assertThrows(\n                    UnsupportedOperationException.class,\n                    () ->\n                        ctx.dsl()\n                            .transaction(\n                                cx2 -> {\n                                  outbox.schedule(Worker.class).process(2);\n                                  throw new UnsupportedOperationException();\n                                }));\n\n                CompletableFuture.allOf(\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch1.await(2, TimeUnit.SECONDS)))),\n                        runAsync(\n                            () -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n                    .get();\n              });\n\n          CompletableFuture.allOf(\n                  runAsync(() -> uncheck(() -> assertTrue(latch1.await(2, TimeUnit.SECONDS)))),\n                  runAsync(() -> uncheck(() -> assertFalse(latch2.await(2, TimeUnit.SECONDS)))))\n              .get();\n        });\n  }\n\n  @Test\n  void retryBehaviour() throws Exception {\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .instantiator(new FailingInstantiator())\n            .submitter(Submitter.withExecutor(unreliablePool))\n            .attemptFrequency(Duration.ofSeconds(1))\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    latch.countDown();\n                  }\n                })\n            .build();\n\n    clearOutbox(transactionManager);\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          transactionManager.inTransaction(() -> outbox.schedule(InterfaceWorker.class).process(3));\n          assertTrue(latch.await(15, TimeUnit.SECONDS));\n        });\n  }\n\n  @Test\n  void highVolumeUnreliable() throws Exception {\n    int count = 10;\n\n    CountDownLatch latch = new CountDownLatch(count * 10);\n    ConcurrentHashMap<Integer, Integer> results = new ConcurrentHashMap<>();\n    ConcurrentHashMap<Integer, Integer> duplicates = new ConcurrentHashMap<>();\n\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .instantiator(new FailingInstantiator())\n            .submitter(Submitter.withExecutor(unreliablePool))\n            .attemptFrequency(Duration.ofSeconds(1))\n            .flushBatchSize(1000)\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    Integer i = (Integer) entry.getInvocation().getArgs()[0];\n                    if (results.putIfAbsent(i, i) != null) {\n                      duplicates.put(i, i);\n                    }\n                    latch.countDown();\n                  }\n                })\n            .build();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          IntStream.range(0, count)\n              .parallel()\n              .forEach(\n                  i ->\n                      dsl.transaction(\n                          cx1 -> {\n                            for (int j = 0; j < 10; j++) {\n                              outbox.schedule(InterfaceWorker.class).process(i * 10 + j);\n                            }\n                          }));\n          assertTrue(latch.await(30, TimeUnit.SECONDS));\n        });\n\n    MatcherAssert.assertThat(\n        \"Should never get duplicates running to full completion\", duplicates.keySet(), empty());\n    MatcherAssert.assertThat(\n        \"Only got: \" + results.keySet(),\n        results.keySet(),\n        containsInAnyOrder(IntStream.range(0, count * 10).boxed().toArray()));\n  }\n\n  @Test\n  void testSessionVariables() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    var sessionVarLocal = UUID.randomUUID().toString();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(Instantiator.using(clazz -> new Worker(transactionManager)))\n            .persistor(Persistor.forDialect(Dialect.H2))\n            .listener(\n                new LatchListener(latch)\n                    .andThen(\n                        new TransactionOutboxListener() {\n                          @Override\n                          public Map<String, String> extractSession() {\n                            return Map.of(\"sesvar\", sessionVarLocal);\n                          }\n\n                          @Override\n                          public void wrapInvocationAndInit(Invocator invocator) {\n                            sessionVar.set(invocator.getInvocation().getSession().get(\"sesvar\"));\n                            try {\n                              invocator.runUnchecked();\n                            } finally {\n                              sessionVar.remove();\n                            }\n                          }\n                        }))\n            .build();\n\n    clearOutbox(transactionManager);\n    transactionManager.inTransaction(\n        tx -> outbox.schedule(Worker.class).checkSessionPresent(sessionVarLocal));\n\n    // Should be fired after commit\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n  }\n\n  private void clearOutbox(TransactionManager transactionManager) {\n    runSql(transactionManager, \"DELETE FROM TXNO_OUTBOX\");\n  }\n\n  private void withRunningFlusher(TransactionOutbox outbox, ThrowingRunnable runnable)\n      throws Exception {\n    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);\n    try {\n      scheduler.scheduleAtFixedRate(\n          () -> {\n            if (Thread.interrupted()) {\n              return;\n            }\n            outbox.flush();\n          },\n          500,\n          500,\n          TimeUnit.MILLISECONDS);\n      runnable.run();\n    } finally {\n      scheduler.shutdown();\n      assertTrue(scheduler.awaitTermination(20, TimeUnit.SECONDS));\n    }\n  }\n\n  interface InterfaceWorker {\n\n    void process(int i);\n  }\n\n  @SuppressWarnings(\"EmptyMethod\")\n  static class Worker {\n\n    private final ThreadLocalContextTransactionManager transactionManager;\n\n    Worker(ThreadLocalContextTransactionManager transactionManager) {\n      this.transactionManager = transactionManager;\n    }\n\n    @SuppressWarnings(\"SameParameterValue\")\n    void process(int i) {\n      JooqTestUtils.writeRecord(transactionManager, i);\n    }\n\n    void checkSessionPresent(String expected) {\n      assertEquals(expected, sessionVar.get());\n    }\n  }\n\n  private static class FailingInstantiator implements Instantiator {\n\n    private final AtomicInteger attempts;\n\n    FailingInstantiator() {\n      this.attempts = new AtomicInteger(0);\n    }\n\n    @Override\n    public String getName(Class<?> clazz) {\n      return clazz.getName();\n    }\n\n    @Override\n    public Object getInstance(String name) {\n      return (InterfaceWorker)\n          (i) -> {\n            if (attempts.incrementAndGet() < 3) {\n              throw new RuntimeException(\"Temporary failure\");\n            }\n          };\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-jooq/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level SESSION-KEY=%X{SESSION-KEY} %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "transactionoutbox-quarkus/README.md",
    "content": "# transactionoutbox-quarkus\n\n\nExtension for [transaction-outbox-core](../README.md) which integrates CDI's DI and Quarkus transaction management.\n\nTested with Quarkus implementation (Arc/Agroal)\n\n## Installation\n\n### Stable releases\n\nThe latest stable release is available from Maven Central.\n\n#### Maven\n\n```xml\n<dependency>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-quarkus</artifactId>\n  <version>7.0.707</version>\n</dependency>\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-quarkus:7.0.707'\n```\n\n### Development snapshots\n\nMaven Central is updated regularly. Alternatively, 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 production builds since they will never be deleted.\n\n#### Maven\n\n```xml\n<repositories>\n  <repository>\n    <id>github-transaction-outbox</id>\n    <name>Gruelbox Github Repository</name>\n    <url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>\n  </repository>\n</repositories>\n```\n\nYou 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`:\n\n```xml\n<servers>\n    <server>\n        <id>github-transaction-outbox</id>\n        <username>${env.GITHUB_USERNAME}</username>\n        <password>${env.GITHUB_TOKEN}</password>\n    </server>\n</servers>\n```\n\nThe 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.\n\n## Configuration\n\nCreate your `TransactionOutbox` as a bean:\n\n```java\n\n@Produces\npublic TransactionOutbox transactionOutbox(QuarkusTransactionManager transactionManager)\n{\n   return TransactionOutbox.builder().instantiator(CdiInstantiator.create()).transactionManager(transactionManager).persistor(Persistor.forDialect(Dialect.H2)).build();\n}\n```\n\n## Usage\n\n```java\n@Transactional\npublic void doStuff() {\n  customerRepository.save(new Customer(1L, \"Martin\", \"Carthy\"));\n  customerRepository.save(new Customer(2L, \"Dave\", \"Pegg\"));\n  outbox.get().schedule(getClass()).publishCustomerCreatedEvent(1L);\n  outbox.get().schedule(getClass()).publishCustomerCreatedEvent(2L);\n}\n\nvoid publishCustomerCreatedEvent(long id) {\n  // Remote call here\n}\n```\n\n"
  },
  {
    "path": "transactionoutbox-quarkus/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Quarkus</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-quarkus</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (Quarkus extension library)</description>\n  <properties>\n    <quarkus.version>3.31.4</quarkus.version>\n  </properties>\n  <dependencies>\n    <!-- Runtime -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>jakarta.enterprise</groupId>\n      <artifactId>jakarta.enterprise.cdi-api</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>jakarta.transaction</groupId>\n      <artifactId>jakarta.transaction-api</artifactId>\n    </dependency>\n    <!-- Test -->\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-junit</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-resteasy</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-arc</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-jdbc-h2</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-agroal</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.quarkus</groupId>\n      <artifactId>quarkus-undertow</artifactId>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n  <dependencyManagement>\n    <dependencies>\n      <dependency>\n        <groupId>io.quarkus.platform</groupId>\n        <artifactId>quarkus-bom</artifactId>\n        <version>${quarkus.version}</version>\n        <type>pom</type>\n        <scope>import</scope>\n      </dependency>\n    </dependencies>\n  </dependencyManagement>\n</project>\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/main/java/com/gruelbox/transactionoutbox/quarkus/CdiInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus;\n\nimport com.gruelbox.transactionoutbox.spi.AbstractFullyQualifiedNameInstantiator;\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.enterprise.inject.spi.CDI;\n\n@ApplicationScoped\npublic class CdiInstantiator extends AbstractFullyQualifiedNameInstantiator {\n\n  @SuppressWarnings(\"unused\")\n  public static CdiInstantiator create() {\n    return new CdiInstantiator();\n  }\n\n  private CdiInstantiator() {}\n\n  @Override\n  protected Object createInstance(Class<?> clazz) {\n    return CDI.current().select(clazz).get();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/main/java/com/gruelbox/transactionoutbox/quarkus/QuarkusTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.inject.Inject;\nimport jakarta.transaction.Status;\nimport jakarta.transaction.Synchronization;\nimport jakarta.transaction.TransactionSynchronizationRegistry;\nimport jakarta.transaction.Transactional;\nimport jakarta.transaction.Transactional.TxType;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport javax.sql.DataSource;\n\n/** Transaction manager which uses cdi and quarkus. */\n@ApplicationScoped\npublic class QuarkusTransactionManager implements ThreadLocalContextTransactionManager {\n\n  private final CdiTransaction transactionInstance = new CdiTransaction();\n\n  private final DataSource datasource;\n\n  private final TransactionSynchronizationRegistry tsr;\n\n  @Inject\n  public QuarkusTransactionManager(DataSource datasource, TransactionSynchronizationRegistry tsr) {\n    this.datasource = datasource;\n    this.tsr = tsr;\n  }\n\n  @Override\n  @Transactional(value = TxType.REQUIRES_NEW)\n  public void inTransaction(Runnable runnable) {\n    uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromRunnable(runnable)));\n  }\n\n  @Override\n  @Transactional(value = TxType.REQUIRES_NEW)\n  public void inTransaction(TransactionalWork work) {\n    uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromWork(work)));\n  }\n\n  @Override\n  @Transactional(value = TxType.REQUIRES_NEW)\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) throws E {\n    return work.doWork(transactionInstance);\n  }\n\n  @Override\n  public <T, E extends Exception> T requireTransactionReturns(\n      ThrowingTransactionalSupplier<T, E> work) throws E, NoTransactionActiveException {\n    if (tsr.getTransactionStatus() != Status.STATUS_ACTIVE) {\n      throw new NoTransactionActiveException();\n    }\n\n    return work.doWork(transactionInstance);\n  }\n\n  private final class CdiTransaction implements Transaction {\n\n    public Connection connection() {\n      try {\n        return datasource.getConnection();\n      } catch (SQLException e) {\n        throw new RuntimeException(e);\n      }\n    }\n\n    @Override\n    public PreparedStatement prepareBatchStatement(String sql) {\n      BatchCountingStatement preparedStatement =\n          Utils.uncheckedly(\n              () -> BatchCountingStatementHandler.countBatches(connection().prepareStatement(sql)));\n\n      tsr.registerInterposedSynchronization(\n          new Synchronization() {\n            @Override\n            public void beforeCompletion() {\n              if (preparedStatement.getBatchCount() != 0) {\n                Utils.uncheck(preparedStatement::executeBatch);\n              }\n            }\n\n            @Override\n            public void afterCompletion(int status) {\n              Utils.safelyClose(preparedStatement);\n            }\n          });\n\n      return preparedStatement;\n    }\n\n    @Override\n    public void addPostCommitHook(Runnable runnable) {\n      tsr.registerInterposedSynchronization(\n          new Synchronization() {\n            @Override\n            public void beforeCompletion() {}\n\n            @Override\n            public void afterCompletion(int status) {\n              runnable.run();\n            }\n          });\n    }\n  }\n\n  private interface BatchCountingStatement extends PreparedStatement {\n    int getBatchCount();\n  }\n\n  private static final class BatchCountingStatementHandler implements InvocationHandler {\n\n    private final PreparedStatement delegate;\n\n    private int count = 0;\n\n    private BatchCountingStatementHandler(PreparedStatement delegate) {\n      this.delegate = delegate;\n    }\n\n    static BatchCountingStatement countBatches(PreparedStatement delegate) {\n      return (BatchCountingStatement)\n          Proxy.newProxyInstance(\n              BatchCountingStatementHandler.class.getClassLoader(),\n              new Class[] {BatchCountingStatement.class},\n              new BatchCountingStatementHandler(delegate));\n    }\n\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n      if (\"getBatchCount\".equals(method.getName())) {\n        return count;\n      }\n      try {\n        return method.invoke(delegate, args);\n      } finally {\n        if (\"addBatch\".equals(method.getName())) {\n          ++count;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/ApplicationConfig.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.quarkus.CdiInstantiator;\nimport com.gruelbox.transactionoutbox.quarkus.QuarkusTransactionManager;\nimport jakarta.enterprise.inject.Produces;\nimport jakarta.ws.rs.core.Application;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class ApplicationConfig extends Application {\n\n  @Override\n  public Set<Class<?>> getClasses() {\n    final Set<Class<?>> classes = new HashSet<Class<?>>();\n\n    classes.add(BusinessService.class);\n\n    return classes;\n  }\n\n  @Produces\n  public TransactionOutbox transactionOutbox(\n      QuarkusTransactionManager transactionManager, RemoteCallService testProxy) {\n    return TransactionOutbox.builder()\n        .instantiator(CdiInstantiator.create())\n        .blockAfterAttempts(1)\n        .listener(\n            new TransactionOutboxListener() {\n              @Override\n              public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n                block(testProxy);\n              }\n            })\n        .transactionManager(transactionManager)\n        .persistor(Persistor.forDialect(Dialect.H2))\n        .build();\n  }\n\n  private void block(RemoteCallService testProxy) {\n    testProxy.block();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/BusinessService.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.inject.Inject;\nimport jakarta.transaction.Transactional;\n\n@ApplicationScoped\npublic class BusinessService {\n  private final DaoImpl dao;\n  private final TransactionOutbox outbox;\n\n  @Inject\n  public BusinessService(DaoImpl dao, TransactionOutbox outbox) {\n    this.dao = dao;\n    this.outbox = outbox;\n  }\n\n  @Transactional\n  public void writeSomeThingAndRemoteCall(String value, boolean throwException) {\n    dao.writeSomethingIntoDatabase(value);\n    RemoteCallService proxy = outbox.schedule(RemoteCallService.class);\n    proxy.callRemote(throwException);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/BusinessServiceTest.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport io.quarkus.test.junit.QuarkusTest;\nimport jakarta.inject.Inject;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@QuarkusTest\npublic class BusinessServiceTest {\n  @Inject private BusinessService res;\n\n  @Inject private RemoteCallService remoteCall;\n\n  @Inject private DaoImpl dao;\n\n  @BeforeEach\n  void purgeDatabase() {\n    dao.purge();\n    remoteCall.setCalled(false);\n    remoteCall.setBlocked(false);\n  }\n\n  @Test\n  void writeOperationAndRemoteCallOK() throws Exception {\n    Assertions.assertFalse(remoteCall.isCalled());\n\n    res.writeSomeThingAndRemoteCall(\"toto\", false);\n\n    Thread.sleep(1000);\n\n    Assertions.assertTrue(remoteCall.isCalled());\n    Assertions.assertFalse(dao.getFromDatabase().isEmpty());\n  }\n\n  @Test\n  void writeOperationOkButRemoteCallErrorShouldBlockRemoteCall() throws Exception {\n    Assertions.assertFalse(remoteCall.isCalled());\n\n    res.writeSomeThingAndRemoteCall(\"toto\", true);\n\n    Thread.sleep(1000);\n\n    Assertions.assertFalse(remoteCall.isCalled());\n    Assertions.assertFalse(dao.getFromDatabase().isEmpty());\n    Assertions.assertTrue(remoteCall.isBlocked());\n  }\n\n  @Test\n  void transactionRollbackSoRemoteCallShouldNotBeMade() throws Exception {\n    Assertions.assertFalse(remoteCall.isCalled());\n    try {\n      res.writeSomeThingAndRemoteCall(\"error\", false);\n      Assertions.fail(\"Should not happen\");\n    } catch (RuntimeException e) {\n      Assertions.assertEquals(\"Persistence error\", e.getMessage());\n    }\n\n    Thread.sleep(1000);\n\n    Assertions.assertFalse(remoteCall.isCalled());\n    Assertions.assertTrue(dao.getFromDatabase().isEmpty());\n    Assertions.assertFalse(remoteCall.isBlocked());\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/DaoImpl.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.inject.Inject;\nimport java.sql.*;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.sql.DataSource;\n\n@ApplicationScoped\npublic class DaoImpl {\n  @Inject DataSource defaultDataSource;\n\n  @SuppressWarnings(\"UnusedReturnValue\")\n  public int writeSomethingIntoDatabase(String something) {\n    if (\"error\".equals(something)) {\n      throw new RuntimeException(\"Persistence error\");\n    }\n    String insertQuery = \"insert into toto values (?);\";\n    try (Connection connexion = defaultDataSource.getConnection();\n        PreparedStatement statement = connexion.prepareStatement(insertQuery)) {\n      statement.setString(1, something);\n      return statement.executeUpdate();\n    } catch (SQLException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  public List<String> getFromDatabase() {\n    List<String> values = new ArrayList<>();\n    try (Connection connexion = defaultDataSource.getConnection();\n        Statement statement = connexion.createStatement()) {\n      ResultSet resultSet = statement.executeQuery(\"select * from toto;\");\n      while (resultSet.next()) {\n        values.add(resultSet.getString(1));\n      }\n      return values;\n    } catch (SQLException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @SuppressWarnings(\"UnusedReturnValue\")\n  public int purge() {\n    try (Connection connexion = defaultDataSource.getConnection();\n        Statement statement = connexion.createStatement()) {\n      return statement.executeUpdate(\"delete from toto;\");\n    } catch (SQLException e) {\n      throw new RuntimeException(e);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/java/com/gruelbox/transactionoutbox/quarkus/acceptance/RemoteCallService.java",
    "content": "package com.gruelbox.transactionoutbox.quarkus.acceptance;\n\nimport jakarta.enterprise.context.ApplicationScoped;\n\n@ApplicationScoped\npublic class RemoteCallService {\n  private boolean called;\n\n  private boolean blocked;\n\n  public void callRemote(boolean throwException) {\n    if (throwException) {\n      throw new RuntimeException(\"Thrown on purpose\");\n    }\n    called = true;\n  }\n\n  public boolean isCalled() {\n    return called;\n  }\n\n  public void setCalled(boolean called) {\n    this.called = called;\n  }\n\n  public void block() {\n    this.blocked = true;\n  }\n\n  public boolean isBlocked() {\n    return blocked;\n  }\n\n  public void setBlocked(boolean blocked) {\n    this.blocked = blocked;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/resources/application.properties",
    "content": "quarkus.application.name=transaction-outbox\nquarkus.http.port = 8082\nquarkus.datasource.db-kind  = h2\nquarkus.datasource.username = test\nquarkus.datasource.password = test\nquarkus.datasource.jdbc.url = jdbc:h2:mem:test;INIT=RUNSCRIPT FROM 'src/test/resources/db/create.sql'\n\nquarkus.test.flat-class-path=true\n\n%test.quarkus.log.level=INFO\n"
  },
  {
    "path": "transactionoutbox-quarkus/src/test/resources/db/create.sql",
    "content": "CREATE TABLE IF NOT EXISTS toto (toto VARCHAR(50) NOT NULL);\n"
  },
  {
    "path": "transactionoutbox-spring/README.md",
    "content": "# transaction-outbox-spring\n\n[![Spring on Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-spring/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-spring)\n[![Spring Javadoc](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-spring.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-spring)\n[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot&sort=semver)](#development-snapshots)\n\nExtension for [transaction-outbox-core](../README.md) which integrates Spring's DI and/or transaction management.\n\nI don't actually use Spring in production, so this is more presented as an example at the moment. Doubtless I've missed a lot of nuances about the flexibility of Spring. Pull requests very welcome.\n\n## Installation\n\n### Stable releases\n\nThe latest stable release is available from Maven Central.\n\n#### Maven\n\n```xml\n<dependency>\n  <groupId>com.gruelbox</groupId>\n  <artifactId>transactionoutbox-spring</artifactId>\n  <version>7.0.707</version>\n</dependency>\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-spring:7.0.707'\n```\n\n### Development snapshots\n\nMaven Central is updated regularly. Alternatively, 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 production builds since they will never be deleted.\n\n#### Maven\n\n```xml\n<repositories>\n  <repository>\n    <id>github-transaction-outbox</id>\n    <name>Gruelbox Github Repository</name>\n    <url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>\n  </repository>\n</repositories>\n```\n\nYou 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`:\n\n```xml\n<servers>\n    <server>\n        <id>github-transaction-outbox</id>\n        <username>${env.GITHUB_USERNAME}</username>\n        <password>${env.GITHUB_TOKEN}</password>\n    </server>\n</servers>\n```\n\nThe 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.\n\n## Example\nAn example application can be found here: https://github.com/gruelbox/transaction-outbox/tree/better-spring-example/transactionoutbox-spring/src/test.\n\n## Configuration\n\nCreate your `TransactionOutbox` as a bean:\n\n```java\n@Bean\n@Lazy\npublic TransactionOutbox transactionOutbox(SpringTransactionManager springTransactionManager,\n                                           SpringInstantiator springInstantiator) {\n  return TransactionOutbox.builder()\n      .instantiator(springInstantiator)\n      .transactionManager(springTransactionManager)\n      .persistor(Persistor.forDialect(Dialect.H2))\n      .build();\n\n```\n\nYou can mix-and-match `SpringInstantiator` ans `SpringTransactionManager` with other implementations in hybrid frameworks.\n\n## Usage\n\n```java\n@Transactional\npublic void doStuff() {\n  customerRepository.save(new Customer(1L, \"Martin\", \"Carthy\"));\n  customerRepository.save(new Customer(2L, \"Dave\", \"Pegg\"));\n  outbox.get().schedule(getClass()).publishCustomerCreatedEvent(1L);\n  outbox.get().schedule(getClass()).publishCustomerCreatedEvent(2L);\n}\n\nvoid publishCustomerCreatedEvent(long id) {\n  // Remote call here\n}\n```\n\nNotice that with a DI framework like Spring in play, you can **self-invoke** on `getClass()` - invoke a method on the same class that's scheduling it.\n"
  },
  {
    "path": "transactionoutbox-spring/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Spring</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-spring</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (Spring extension library)</description>\n  <properties>\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <spring.boot.version>4.0.3</spring.boot.version>\n    <spring.version>7.0.4</spring.version>\n  </properties>\n  <dependencies>\n    <!-- Runtime -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework</groupId>\n      <artifactId>spring-tx</artifactId>\n      <version>${spring.version}</version>\n      <scope>provided</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework</groupId>\n      <artifactId>spring-jdbc</artifactId>\n      <version>${spring.version}</version>\n      <scope>provided</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework</groupId>\n      <artifactId>spring-context</artifactId>\n      <version>${spring.version}</version>\n      <scope>provided</scope>\n    </dependency>\n    <!-- Compile -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-jackson</artifactId>\n      <scope>test</scope>\n      <version>${project.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-classic</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-core</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.h2database</groupId>\n      <artifactId>h2</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.zaxxer</groupId>\n      <artifactId>HikariCP</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-jackson2</artifactId>\n      <version>${spring.boot.version}</version>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-data-jpa</artifactId>\n      <version>${spring.boot.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-web</artifactId>\n      <version>${spring.boot.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-test</artifactId>\n      <version>${spring.boot.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-engine</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-params</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.awaitility</groupId>\n      <artifactId>awaitility</artifactId>\n      <version>4.3.0</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringInstantiator.java",
    "content": "package com.gruelbox.transactionoutbox.spring;\n\nimport com.gruelbox.transactionoutbox.Instantiator;\nimport java.util.Arrays;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.stereotype.Service;\n\n/**\n * Instantiator that uses the spring {@link ApplicationContext} to source objects. It requires that\n * classes scheduled have a unique name in the context, so doesn't often play well with proxies and\n * other auto-generated code such as repositories based on {@code CrudRepository}.\n */\n@Service\npublic class SpringInstantiator implements Instantiator {\n\n  private final ApplicationContext applicationContext;\n\n  @Autowired\n  public SpringInstantiator(ApplicationContext applicationContext) {\n    this.applicationContext = applicationContext;\n  }\n\n  @Override\n  public String getName(Class<?> clazz) {\n    String[] beanNames = applicationContext.getBeanNamesForType(clazz);\n    if (beanNames.length > 1) {\n      throw new IllegalArgumentException(\n          \"Type \"\n              + clazz.getName()\n              + \" exists under multiple names in the context: \"\n              + Arrays.toString(beanNames)\n              + \". Use a unique type.\");\n    }\n    if (beanNames.length == 0) {\n      throw new IllegalArgumentException(\n          \"Type \" + clazz.getName() + \" not available in the context\");\n    }\n    return beanNames[0];\n  }\n\n  @Override\n  public Object getInstance(String name) {\n    return applicationContext.getBean(name);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionManager.java",
    "content": "package com.gruelbox.transactionoutbox.spring;\n\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheck;\nimport static com.gruelbox.transactionoutbox.spi.Utils.uncheckedly;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Proxy;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport javax.sql.DataSource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.jdbc.datasource.DataSourceUtils;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.PlatformTransactionManager;\nimport org.springframework.transaction.TransactionDefinition;\nimport org.springframework.transaction.support.TransactionSynchronization;\nimport org.springframework.transaction.support.TransactionSynchronizationManager;\nimport org.springframework.transaction.support.TransactionTemplate;\n\n/** Transaction manager which uses spring-tx and Hibernate. */\n@Slf4j\n@Service\npublic class SpringTransactionManager implements ThreadLocalContextTransactionManager {\n\n  private final SpringTransaction transactionInstance = new SpringTransaction();\n  private final PlatformTransactionManager platformTransactionManager;\n  private final DataSource dataSource;\n\n  @Autowired\n  public SpringTransactionManager(\n      PlatformTransactionManager platformTransactionManager, DataSource dataSource) {\n    this.platformTransactionManager = platformTransactionManager;\n    this.dataSource = dataSource;\n  }\n\n  @Override\n  public void inTransaction(Runnable runnable) {\n    uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromRunnable(runnable)));\n  }\n\n  @Override\n  public void inTransaction(TransactionalWork work) {\n    uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromWork(work)));\n  }\n\n  @Override\n  public <T> T inTransactionReturns(TransactionalSupplier<T> supplier) {\n    return uncheckedly(\n        () -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromSupplier(supplier)));\n  }\n\n  @Override\n  public <E extends Exception> void inTransactionThrows(ThrowingTransactionalWork<E> work)\n      throws E {\n    inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromWork(work));\n  }\n\n  @Override\n  public <T, E extends Exception> T inTransactionReturnsThrows(\n      ThrowingTransactionalSupplier<T, E> work) throws E {\n    TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager);\n    transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);\n    try {\n      return transactionTemplate.execute(\n          status -> {\n            try {\n              return work.doWork(transactionInstance);\n            } catch (Exception e) {\n              throw new UncheckedException(e);\n            }\n          });\n    } catch (UncheckedException e) {\n      @SuppressWarnings(\"unchecked\")\n      E cause = (E) e.getCause();\n      throw cause;\n    }\n  }\n\n  @Override\n  public <T, E extends Exception> T requireTransactionReturns(\n      ThrowingTransactionalSupplier<T, E> work) throws E, NoTransactionActiveException {\n\n    if (!TransactionSynchronizationManager.isActualTransactionActive()) {\n      throw new NoTransactionActiveException();\n    }\n\n    return work.doWork(transactionInstance);\n  }\n\n  private final class SpringTransaction implements Transaction {\n\n    @Override\n    public Connection connection() {\n      return DataSourceUtils.getConnection(dataSource);\n    }\n\n    @Override\n    public PreparedStatement prepareBatchStatement(String sql) {\n      BatchCountingStatement preparedStatement =\n          Utils.uncheckedly(\n              () -> BatchCountingStatementHandler.countBatches(connection().prepareStatement(sql)));\n      TransactionSynchronizationManager.registerSynchronization(\n          new TransactionSynchronization() {\n            @Override\n            public void beforeCommit(boolean readOnly) {\n              if (preparedStatement.getBatchCount() != 0) {\n                log.debug(\"Flushing batches\");\n                Utils.uncheck(preparedStatement::executeBatch);\n              }\n            }\n\n            @Override\n            public void afterCompletion(int status) {\n              Utils.safelyClose(preparedStatement);\n            }\n          });\n      return preparedStatement;\n    }\n\n    @Override\n    public void addPostCommitHook(Runnable runnable) {\n      TransactionSynchronizationManager.registerSynchronization(\n          new TransactionSynchronization() {\n            @Override\n            public void afterCommit() {\n              runnable.run();\n            }\n          });\n    }\n  }\n\n  private interface BatchCountingStatement extends PreparedStatement {\n    int getBatchCount();\n  }\n\n  private static final class BatchCountingStatementHandler implements InvocationHandler {\n\n    private final PreparedStatement delegate;\n    private int count = 0;\n\n    private BatchCountingStatementHandler(PreparedStatement delegate) {\n      this.delegate = delegate;\n    }\n\n    static BatchCountingStatement countBatches(PreparedStatement delegate) {\n      return (BatchCountingStatement)\n          Proxy.newProxyInstance(\n              BatchCountingStatementHandler.class.getClassLoader(),\n              new Class[] {BatchCountingStatement.class},\n              new BatchCountingStatementHandler(delegate));\n    }\n\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n      if (\"getBatchCount\".equals(method.getName())) {\n        return count;\n      }\n      try {\n        return method.invoke(delegate, args);\n      } catch (InvocationTargetException e) {\n        throw e.getCause();\n      } finally {\n        if (\"addBatch\".equals(method.getName())) {\n          ++count;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/main/java/com/gruelbox/transactionoutbox/spring/SpringTransactionOutboxConfiguration.java",
    "content": "package com.gruelbox.transactionoutbox.spring;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Import;\n\n/**\n * @deprecated Just {@code @Import} the components you need.\n */\n@Configuration\n@Deprecated(forRemoval = true)\n@Import({SpringTransactionManager.class, SpringInstantiator.class})\npublic class SpringTransactionOutboxConfiguration {}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/SpringTransactionManagerTest.java",
    "content": "package com.gruelbox.transactionoutbox.spring;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.verify;\n\nimport com.gruelbox.transactionoutbox.UncheckedException;\nimport java.sql.SQLException;\nimport javax.sql.DataSource;\nimport org.assertj.core.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentMatchers;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.transaction.PlatformTransactionManager;\nimport org.springframework.transaction.TransactionDefinition;\nimport org.springframework.transaction.TransactionException;\nimport org.springframework.transaction.TransactionStatus;\n\n@ExtendWith(MockitoExtension.class)\nclass SpringTransactionManagerTest {\n\n  @Mock private PlatformTransactionManager platformTransactionManager;\n\n  @Mock private DataSource dataSource;\n\n  @Mock private TransactionStatus transactionStatus;\n  @Mock private SpringTransactionManager springTransactionManager;\n\n  @BeforeEach\n  void setUp() {\n    Mockito.when(platformTransactionManager.getTransaction(Mockito.any()))\n        .thenReturn(transactionStatus);\n    springTransactionManager = new SpringTransactionManager(platformTransactionManager, dataSource);\n  }\n\n  private static class MyRuntimeException extends RuntimeException {}\n\n  private static class MyCheckedException extends Exception {}\n\n  private static class MyUncheckedException extends UncheckedException {\n\n    public MyUncheckedException(Throwable cause) {\n      super(cause);\n    }\n  }\n\n  private static class MySpringTransactionException extends TransactionException {\n\n    public MySpringTransactionException(String msg, Throwable cause) {\n      super(msg, cause);\n    }\n  }\n\n  private static class MySqlException extends SQLException {}\n\n  @Test\n  void shouldWorkInNewTransactionAndCommit() {\n    springTransactionManager.inTransactionReturnsThrows(transaction -> true);\n\n    verify(platformTransactionManager)\n        .getTransaction(\n            ArgumentMatchers.assertArg(\n                trxDef -> {\n                  Assertions.assertThat(trxDef).isNotNull();\n                  Assertions.assertThat(trxDef.getPropagationBehavior())\n                      .isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);\n                }));\n    verify(platformTransactionManager).commit(same(transactionStatus));\n  }\n\n  @Test\n  void shouldRollbackOnFailure() {\n    assertThrows(\n        RuntimeException.class,\n        () -> {\n          springTransactionManager.inTransactionReturnsThrows(\n              transaction -> {\n                throw new RuntimeException();\n              });\n        });\n\n    verify(platformTransactionManager)\n        .getTransaction(\n            ArgumentMatchers.assertArg(\n                trxDef -> {\n                  Assertions.assertThat(trxDef).isNotNull();\n                  Assertions.assertThat(trxDef.getPropagationBehavior())\n                      .isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW);\n                }));\n    verify(platformTransactionManager).rollback(same(transactionStatus));\n  }\n\n  @Test\n  void shouldPreserveRuntimeException() {\n    assertThrows(\n        MyRuntimeException.class,\n        () -> {\n          springTransactionManager.inTransactionReturnsThrows(\n              transaction -> {\n                throw new MyRuntimeException();\n              });\n        });\n  }\n\n  @Test\n  void shouldPreserveCheckedException() {\n    assertThrows(\n        MyCheckedException.class,\n        () -> {\n          springTransactionManager.inTransactionReturnsThrows(\n              transaction -> {\n                throw new MyCheckedException();\n              });\n        });\n  }\n\n  @Test\n  void shouldPreserveUncheckedException() {\n    assertThrows(\n        MyUncheckedException.class,\n        () -> {\n          springTransactionManager.inTransactionReturnsThrows(\n              transaction -> {\n                throw new MyUncheckedException(new RuntimeException());\n              });\n        });\n  }\n\n  @Test\n  void shouldPreserveSpringTransactionException() {\n    assertThrows(\n        MySpringTransactionException.class,\n        () -> {\n          springTransactionManager.inTransactionReturnsThrows(\n              transaction -> {\n                throw new MySpringTransactionException(\"expected failure\", new RuntimeException());\n              });\n        });\n  }\n\n  @Test\n  void shouldPreserveSqlException() {\n    assertThrows(\n        MySqlException.class,\n        () -> {\n          springTransactionManager.inTransactionReturnsThrows(\n              transaction -> {\n                throw new MySqlException();\n              });\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/EventuallyConsistentController.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport static org.springframework.http.HttpStatus.NOT_FOUND;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer.Computer;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer.ComputerExternalQueueService;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer.ComputerRepository;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee.Employee;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee.EmployeeExternalQueueService;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee.EmployeeRepository;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.transaction.annotation.Transactional;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ResponseStatusException;\n\n@SuppressWarnings(\"unused\")\n@RestController\npublic class EventuallyConsistentController {\n\n  @Autowired private ComputerRepository computerRepository;\n\n  @Autowired private TransactionOutbox computerTransactionOutbox;\n\n  @Autowired private EmployeeRepository employeeRepository;\n\n  @Autowired private TransactionOutbox employeeTransactionOutbox;\n\n  @SuppressWarnings(\"SameReturnValue\")\n  @PostMapping(\"/computer\")\n  @Transactional(transactionManager = \"computerTransactionManager\")\n  public void createComputer(\n      @RequestBody Computer computer,\n      @RequestParam(name = \"ordered\", required = false) Boolean ordered) {\n\n    computerRepository.save(computer);\n    if (ordered != null && ordered) {\n      computerTransactionOutbox\n          .with()\n          .ordered(\"justonetopic\")\n          .schedule(ComputerExternalQueueService.class)\n          .sendComputerCreatedEvent(computer);\n    } else {\n      computerTransactionOutbox\n          .schedule(ComputerExternalQueueService.class)\n          .sendComputerCreatedEvent(computer);\n    }\n  }\n\n  @GetMapping(\"/computer/{id}\")\n  public Computer getComputer(@PathVariable long id) {\n    return computerRepository\n        .findById(id)\n        .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));\n  }\n\n  @SuppressWarnings(\"SameReturnValue\")\n  @PostMapping(\"/employee\")\n  @Transactional(transactionManager = \"employeeTransactionManager\")\n  public void createEmployee(\n      @RequestBody Employee employee,\n      @RequestParam(name = \"ordered\", required = false) Boolean ordered) {\n\n    employeeRepository.save(employee);\n    if (ordered != null && ordered) {\n      employeeTransactionOutbox\n          .with()\n          .ordered(\"justonetopic\")\n          .schedule(EmployeeExternalQueueService.class)\n          .sendEmployeeCreatedEvent(employee);\n    } else {\n      employeeTransactionOutbox\n          .schedule(EmployeeExternalQueueService.class)\n          .sendEmployeeCreatedEvent(employee);\n    }\n  }\n\n  @GetMapping(\"/employee/{id}\")\n  public Employee getEmployee(@PathVariable long id) {\n    return employeeRepository\n        .findById(id)\n        .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/ExternalsConfiguration.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport com.gruelbox.transactionoutbox.spring.SpringInstantiator;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Import;\n\n@Configuration\n@Import({SpringInstantiator.class})\nclass ExternalsConfiguration {}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/MultipleDataSourcesTest.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer.Computer;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer.Computer.Type;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer.ComputerExternalQueueService;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee.Employee;\nimport com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee.EmployeeExternalQueueService;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.web.client.RestClient;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\nclass MultipleDataSourcesTest {\n\n  @LocalServerPort private int port;\n\n  private RestClient restClient;\n\n  @Autowired private JdbcTemplate employeeJdbcTemplate;\n  @Autowired private JdbcTemplate computerJdbcTemplate;\n  @Autowired private EmployeeExternalQueueService employeeExternalQueueService;\n  @Autowired private ComputerExternalQueueService computerExternalQueueService;\n\n  @BeforeEach\n  void setUp() {\n    this.restClient = RestClient.builder().baseUrl(\"http://localhost:\" + port).build();\n    employeeExternalQueueService.clear();\n    computerExternalQueueService.clear();\n  }\n\n  @Test\n  void testCheckNormalEmployees() {\n    var joe = new Employee(1L, \"Joe\", \"Strummer\");\n    var dave = new Employee(2L, \"Dave\", \"Grohl\");\n    var neil = new Employee(3L, \"Neil\", \"Diamond\");\n    var tupac = new Employee(4L, \"Tupac\", \"Shakur\");\n    var jeff = new Employee(5L, \"Jeff\", \"Mills\");\n\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee\")\n            .body(joe)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee\")\n            .body(dave)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee\")\n            .body(neil)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee\")\n            .body(tupac)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee\")\n            .body(jeff)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n\n    employeeJdbcTemplate.execute(\n        \"UPDATE txno_outbox SET invocation='non-deserializable invocation' WHERE invocation LIKE '%\"\n            + neil.getLastName()\n            + \"%'\");\n\n    await()\n        .atMost(10, SECONDS)\n        .pollDelay(1, SECONDS)\n        .untilAsserted(\n            () ->\n                assertThat(employeeExternalQueueService.getSent())\n                    .containsExactlyInAnyOrder(joe, dave, tupac, jeff));\n  }\n\n  @Test\n  void testCheckNormalComputers() throws InterruptedException {\n    var computerPc1 = new Computer(1L, \"pc-001\", Type.DESKTOP);\n    var computerPc2 = new Computer(2L, \"pc-002\", Type.LAPTOP);\n    var computerPc3 = new Computer(3L, \"pc-003\", Type.LAPTOP);\n    var computerWebserver1 = new Computer(4L, \"webserver-001\", Type.SERVER);\n    var computerWebserver2 = new Computer(5L, \"webserver-002\", Type.SERVER);\n\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer\")\n            .body(computerPc1)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer\")\n            .body(computerPc2)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer\")\n            .body(computerPc3)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer\")\n            .body(computerWebserver1)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer\")\n            .body(computerWebserver2)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n\n    computerJdbcTemplate.execute(\n        \"UPDATE txno_outbox SET invocation='non-deserializable invocation' WHERE invocation LIKE '%\"\n            + computerPc3.getName()\n            + \"%'\");\n\n    await()\n        .atMost(10, SECONDS)\n        .pollDelay(1, SECONDS)\n        .untilAsserted(\n            () ->\n                assertThat(computerExternalQueueService.getSent())\n                    .containsExactlyInAnyOrder(\n                        computerPc1, computerPc2, computerWebserver1, computerWebserver2));\n  }\n\n  @Test\n  void testCheckOrderedEmployees() {\n\n    var joe = new Employee(1L, \"Joe\", \"Strummer\");\n    var dave = new Employee(2L, \"Dave\", \"Grohl\");\n    var neil = new Employee(3L, \"Neil\", \"Diamond\");\n    var tupac = new Employee(4L, \"Tupac\", \"Shakur\");\n    var jeff = new Employee(5L, \"Jeff\", \"Mills\");\n\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee?ordered=true\")\n            .body(joe)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee?ordered=true\")\n            .body(dave)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee?ordered=true\")\n            .body(neil)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee?ordered=true\")\n            .body(tupac)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/employee?ordered=true\")\n            .body(jeff)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n\n    employeeJdbcTemplate.execute(\n        \"UPDATE txno_outbox SET invocation='non-deserializable invocation' WHERE invocation LIKE '%\"\n            + neil.getLastName()\n            + \"%'\");\n\n    await()\n        .atMost(10, SECONDS)\n        .pollDelay(1, SECONDS)\n        .untilAsserted(\n            () -> assertThat(employeeExternalQueueService.getSent()).containsExactly(joe, dave));\n  }\n\n  @Test\n  void testCheckOrderedComputers() {\n\n    var computerPc1 = new Computer(1L, \"pc-001\", Type.DESKTOP);\n    var computerPc2 = new Computer(2L, \"pc-002\", Type.LAPTOP);\n    var computerPc3 = new Computer(3L, \"pc-003\", Type.LAPTOP);\n    var computerWebserver1 = new Computer(4L, \"webserver-001\", Type.SERVER);\n    var computerWebserver2 = new Computer(5L, \"webserver-002\", Type.SERVER);\n\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer?ordered=true\")\n            .body(computerPc1)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer?ordered=true\")\n            .body(computerPc2)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer?ordered=true\")\n            .body(computerPc3)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer?ordered=true\")\n            .body(computerWebserver1)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/computer?ordered=true\")\n            .body(computerWebserver2)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n\n    employeeJdbcTemplate.execute(\n        \"UPDATE txno_outbox SET invocation='non-deserializable invocation' WHERE invocation LIKE '%\"\n            + computerPc3.getName()\n            + \"%'\");\n\n    await()\n        .atMost(10, SECONDS)\n        .pollDelay(1, SECONDS)\n        .untilAsserted(\n            () ->\n                assertThat(computerExternalQueueService.getSent())\n                    .containsExactly(computerPc1, computerPc2));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxBackgroundProcessor.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport java.util.Collection;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Component;\n\n/**\n * Simple implementation of a background processor for {@link TransactionOutbox}. You don't need to\n * use this if you need different semantics, but this is a good start for most purposes.\n */\n@Component\n@Slf4j\n@RequiredArgsConstructor(onConstructor_ = {@Autowired})\nclass TransactionOutboxBackgroundProcessor {\n\n  private final Collection<TransactionOutbox> outboxes;\n\n  @Scheduled(fixedRateString = \"${outbox.repeatEvery}\")\n  void poll() {\n    outboxes.forEach(\n        outbox -> {\n          try {\n            do {\n              log.info(\"Flushing\");\n            } while (outbox.flush());\n          } catch (Exception e) {\n            log.error(\"Error flushing transaction outbox. Pausing\", e);\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxProperties.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport java.time.Duration;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@ConfigurationProperties(\"outbox\")\n@Data\nclass TransactionOutboxProperties {\n  private Duration repeatEvery;\n  private boolean useJackson;\n  private Duration attemptFrequency;\n  private int blockAfterAttempts;\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/TransactionOutboxSpringMultipleDatasourcesDemoApplication.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.Persistor;\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport com.gruelbox.transactionoutbox.jackson.JacksonInvocationSerializer;\nimport com.gruelbox.transactionoutbox.spring.SpringInstantiator;\nimport com.gruelbox.transactionoutbox.spring.SpringTransactionManager;\nimport javax.sql.DataSource;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Lazy;\nimport org.springframework.scheduling.annotation.EnableScheduling;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@SpringBootApplication\n@EnableScheduling\npublic class TransactionOutboxSpringMultipleDatasourcesDemoApplication {\n\n  public static void main(String[] args) {\n    SpringApplication.run(TransactionOutboxSpringMultipleDatasourcesDemoApplication.class, args);\n  }\n\n  @Bean\n  @Lazy\n  Persistor persistor(TransactionOutboxProperties properties, ObjectMapper objectMapper) {\n    if (properties.isUseJackson()) {\n      return DefaultPersistor.builder()\n          .serializer(JacksonInvocationSerializer.builder().mapper(objectMapper).build())\n          .dialect(Dialect.H2)\n          .build();\n    } else {\n      return Persistor.forDialect(Dialect.H2);\n    }\n  }\n\n  @Bean\n  @Lazy\n  TransactionOutbox computerTransactionOutbox(\n      SpringInstantiator instantiator,\n      @Qualifier(\"computerSpringTransactionManager\") SpringTransactionManager transactionManager,\n      TransactionOutboxProperties properties,\n      Persistor persistor) {\n    return TransactionOutbox.builder()\n        .instantiator(instantiator)\n        .transactionManager(transactionManager)\n        .persistor(persistor)\n        .attemptFrequency(properties.getAttemptFrequency())\n        .blockAfterAttempts(properties.getBlockAfterAttempts())\n        .build();\n  }\n\n  @Bean\n  @Lazy\n  TransactionOutbox employeeTransactionOutbox(\n      SpringInstantiator instantiator,\n      @Qualifier(\"employeeSpringTransactionManager\") SpringTransactionManager transactionManager,\n      TransactionOutboxProperties properties,\n      Persistor persistor) {\n    return TransactionOutbox.builder()\n        .instantiator(instantiator)\n        .transactionManager(transactionManager)\n        .persistor(persistor)\n        .attemptFrequency(properties.getAttemptFrequency())\n        .blockAfterAttempts(properties.getBlockAfterAttempts())\n        .build();\n  }\n\n  @Bean\n  public SpringTransactionManager computerSpringTransactionManager(\n      @Qualifier(\"computerTransactionManager\") PlatformTransactionManager transactionManager,\n      @Qualifier(\"computerDataSource\") DataSource dataSource) {\n    return new SpringTransactionManager(transactionManager, dataSource);\n  }\n\n  @Bean\n  public SpringTransactionManager employeeSpringTransactionManager(\n      @Qualifier(\"employeeTransactionManager\") PlatformTransactionManager transactionManager,\n      @Qualifier(\"employeeDataSource\") DataSource dataSource) {\n    return new SpringTransactionManager(transactionManager, dataSource);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/Computer.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Entity\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class Computer {\n\n  public enum Type {\n    LAPTOP,\n    SERVER,\n    DESKTOP;\n  }\n\n  @Id private Long id;\n  @Column private String name;\n  @Column private Type type;\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputerExternalQueueService.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport lombok.Getter;\nimport org.springframework.stereotype.Service;\n\n@Getter\n@Service\npublic class ComputerExternalQueueService {\n\n  private final Set<Long> attempted = new HashSet<>();\n  private final List<Computer> sent = new CopyOnWriteArrayList<>();\n\n  public void sendComputerCreatedEvent(Computer computer) {\n    if (attempted.add(computer.getId())) {\n      throw new RuntimeException(\"Temporary failure, try again\");\n    }\n    sent.add(computer);\n  }\n\n  public void clear() {\n    attempted.clear();\n    sent.clear();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputerRepository.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport org.springframework.data.repository.CrudRepository;\nimport org.springframework.stereotype.Repository;\n\n@Repository\npublic interface ComputerRepository extends CrudRepository<Computer, Long> {}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/computer/ComputersDbConfiguration.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.computer;\n\nimport jakarta.persistence.EntityManagerFactory;\nimport java.util.Map;\nimport javax.sql.DataSource;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.jdbc.autoconfigure.DataSourceProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.orm.jpa.JpaTransactionManager;\nimport org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;\nimport org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@Configuration\n@EnableJpaRepositories(\n    basePackageClasses = Computer.class,\n    entityManagerFactoryRef = \"computerEntityManager\",\n    transactionManagerRef = \"computerTransactionManager\")\npublic class ComputersDbConfiguration {\n\n  @Bean\n  public DataSourceProperties computerDataSourceProperties() {\n    DataSourceProperties properties = new DataSourceProperties();\n    properties.setDriverClassName(org.h2.Driver.class.getName());\n    properties.setUrl(\"jdbc:h2:mem:computer\");\n    properties.setUsername(\"computerUser\");\n    properties.setPassword(\"computerPassword\");\n    return properties;\n  }\n\n  @Bean\n  public DataSource computerDataSource() {\n    return computerDataSourceProperties().initializeDataSourceBuilder().build();\n  }\n\n  @Bean\n  public JdbcTemplate computerJdbcTemplate() {\n    return new JdbcTemplate(computerDataSource());\n  }\n\n  @Bean\n  public LocalContainerEntityManagerFactoryBean computerEntityManager() {\n    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();\n    emf.setDataSource(computerDataSource());\n    emf.setPackagesToScan(Computer.class.getPackage().getName());\n    emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());\n    emf.setJpaPropertyMap(\n        Map.of(\n            \"hibernate.hbm2ddl.auto\", \"update\",\n            \"hibernate.show_sql\", \"true\"));\n    emf.setPersistenceUnitName(\"computer\");\n    return emf;\n  }\n\n  @Bean\n  public PlatformTransactionManager computerTransactionManager(\n      @Qualifier(\"computerEntityManager\") EntityManagerFactory entityManagerFactory) {\n    return new JpaTransactionManager(entityManagerFactory);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/Employee.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Entity\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class Employee {\n  @Id private Long id;\n  @Column private String firstName;\n  @Column private String lastName;\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeeExternalQueueService.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport lombok.Getter;\nimport org.springframework.stereotype.Service;\n\n@Getter\n@Service\npublic class EmployeeExternalQueueService {\n\n  private final Set<Long> attempted = new HashSet<>();\n  private final List<Employee> sent = new CopyOnWriteArrayList<>();\n\n  public void sendEmployeeCreatedEvent(Employee employee) {\n    if (attempted.add(employee.getId())) {\n      throw new RuntimeException(\"Temporary failure, try again\");\n    }\n    sent.add(employee);\n  }\n\n  public void clear() {\n    attempted.clear();\n    sent.clear();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeeRepository.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport org.springframework.data.repository.CrudRepository;\nimport org.springframework.stereotype.Repository;\n\n@Repository\npublic interface EmployeeRepository extends CrudRepository<Employee, Long> {}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/multipledatasources/employee/EmployeesDbConfiguration.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.multipledatasources.employee;\n\nimport jakarta.persistence.EntityManagerFactory;\nimport java.util.Map;\nimport javax.sql.DataSource;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.jdbc.autoconfigure.DataSourceProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.orm.jpa.JpaTransactionManager;\nimport org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;\nimport org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;\nimport org.springframework.transaction.PlatformTransactionManager;\n\n@Configuration\n@EnableJpaRepositories(\n    basePackageClasses = Employee.class,\n    entityManagerFactoryRef = \"employeeEntityManager\",\n    transactionManagerRef = \"employeeTransactionManager\")\npublic class EmployeesDbConfiguration {\n\n  @Bean\n  @ConfigurationProperties(\"spring.datasource.employees\")\n  public DataSourceProperties employeeDataSourceProperties() {\n    DataSourceProperties properties = new DataSourceProperties();\n    properties.setDriverClassName(org.h2.Driver.class.getName());\n    properties.setUrl(\"jdbc:h2:mem:employee\");\n    properties.setUsername(\"employeeUser\");\n    properties.setPassword(\"employeePassword\");\n    return properties;\n  }\n\n  @Bean\n  public DataSource employeeDataSource() {\n    return employeeDataSourceProperties().initializeDataSourceBuilder().build();\n  }\n\n  @Bean\n  public JdbcTemplate employeeJdbcTemplate() {\n    return new JdbcTemplate(employeeDataSource());\n  }\n\n  @Bean\n  public LocalContainerEntityManagerFactoryBean employeeEntityManager() {\n    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();\n    emf.setDataSource(employeeDataSource());\n    emf.setPackagesToScan(Employee.class.getPackage().getName());\n    emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());\n    emf.setJpaPropertyMap(\n        Map.of(\n            \"hibernate.hbm2ddl.auto\", \"update\",\n            \"hibernate.show_sql\", \"true\"));\n    emf.setPersistenceUnitName(\"employee\");\n    return emf;\n  }\n\n  @Bean\n  public PlatformTransactionManager employeeTransactionManager(\n      @Qualifier(\"employeeEntityManager\") EntityManagerFactory entityManagerFactory) {\n    return new JpaTransactionManager(entityManagerFactory);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/Customer.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n@Entity\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\nclass Customer {\n  @Id private Long id;\n  @Column private String firstName;\n  @Column private String lastName;\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/CustomerRepository.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport org.springframework.data.repository.CrudRepository;\nimport org.springframework.stereotype.Repository;\n\n@Repository\ninterface CustomerRepository extends CrudRepository<Customer, Long> {}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/EventuallyConsistentController.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport static org.springframework.http.HttpStatus.NOT_FOUND;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.transaction.annotation.Transactional;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.server.ResponseStatusException;\n\n@SuppressWarnings(\"unused\")\n@RestController\nclass EventuallyConsistentController {\n\n  @Autowired private CustomerRepository customerRepository;\n  @Autowired private TransactionOutbox outbox;\n\n  @SuppressWarnings(\"SameReturnValue\")\n  @PostMapping(path = \"/customer\")\n  @Transactional\n  public void createCustomer(\n      @RequestBody Customer customer,\n      @RequestParam(name = \"ordered\", required = false) Boolean ordered) {\n    customerRepository.save(customer);\n    if (ordered != null && ordered) {\n      outbox\n          .with()\n          .ordered(\"justonetopic\")\n          .schedule(ExternalQueueService.class)\n          .sendCustomerCreatedEvent(customer);\n    } else {\n      outbox.schedule(ExternalQueueService.class).sendCustomerCreatedEvent(customer);\n    }\n  }\n\n  @GetMapping(\"/customer/{id}\")\n  public Customer getCustomer(@PathVariable long id) {\n    return customerRepository\n        .findById(id)\n        .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/EventuallyConsistentControllerTest.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.web.client.RestClient;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\nclass EventuallyConsistentControllerTest {\n\n  @LocalServerPort private int port;\n\n  private RestClient restClient;\n\n  @Autowired private ExternalQueueService externalQueueService;\n\n  @Autowired private JdbcTemplate jdbcTemplate;\n\n  @BeforeEach\n  void setUp() {\n    this.restClient = RestClient.builder().baseUrl(\"http://localhost:\" + port).build();\n    externalQueueService.clear();\n  }\n\n  @Test\n  void testCheckNormal() {\n\n    var joe = new Customer(1L, \"Joe\", \"Strummer\");\n    var dave = new Customer(2L, \"Dave\", \"Grohl\");\n    var neil = new Customer(3L, \"Neil\", \"Diamond\");\n    var tupac = new Customer(4L, \"Tupac\", \"Shakur\");\n    var jeff = new Customer(5L, \"Jeff\", \"Mills\");\n\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer\")\n            .body(joe)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer\")\n            .body(dave)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer\")\n            .body(neil)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer\")\n            .body(tupac)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer\")\n            .body(jeff)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n\n    jdbcTemplate.execute(\n        \"UPDATE txno_outbox SET invocation='non-deserializable invocation' WHERE invocation LIKE '%\"\n            + neil.getLastName()\n            + \"%'\");\n\n    await()\n        .atMost(10, SECONDS)\n        .pollDelay(1, SECONDS)\n        .untilAsserted(\n            () ->\n                assertThat(externalQueueService.getSent())\n                    .containsExactlyInAnyOrder(joe, dave, tupac, jeff));\n  }\n\n  @Test\n  void testCheckOrdered() {\n\n    var joe = new Customer(1L, \"Joe\", \"Strummer\");\n    var dave = new Customer(2L, \"Dave\", \"Grohl\");\n    var neil = new Customer(3L, \"Neil\", \"Diamond\");\n    var tupac = new Customer(4L, \"Tupac\", \"Shakur\");\n    var jeff = new Customer(5L, \"Jeff\", \"Mills\");\n\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer?ordered=true\")\n            .body(joe)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer?ordered=true\")\n            .body(dave)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer?ordered=true\")\n            .body(neil)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer?ordered=true\")\n            .body(tupac)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n    assertTrue(\n        restClient\n            .post()\n            .uri(\"/customer?ordered=true\")\n            .body(jeff)\n            .retrieve()\n            .toBodilessEntity()\n            .getStatusCode()\n            .is2xxSuccessful());\n\n    jdbcTemplate.execute(\n        \"UPDATE txno_outbox SET invocation='non-deserializable invocation' WHERE invocation LIKE '%\"\n            + neil.getLastName()\n            + \"%'\");\n\n    await()\n        .atMost(10, SECONDS)\n        .pollDelay(1, SECONDS)\n        .untilAsserted(() -> assertThat(externalQueueService.getSent()).containsExactly(joe, dave));\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/ExternalQueueService.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport lombok.Getter;\nimport org.springframework.stereotype.Service;\n\n@Getter\n@Service\nclass ExternalQueueService {\n\n  private final Set<Long> attempted = new HashSet<>();\n  private final List<Customer> sent = new CopyOnWriteArrayList<>();\n\n  void sendCustomerCreatedEvent(Customer customer) {\n    if (attempted.add(customer.getId())) {\n      throw new RuntimeException(\"Temporary failure, try again\");\n    }\n    sent.add(customer);\n  }\n\n  public void clear() {\n    attempted.clear();\n    sent.clear();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/ExternalsConfiguration.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.gruelbox.transactionoutbox.spring.SpringInstantiator;\nimport com.gruelbox.transactionoutbox.spring.SpringTransactionManager;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Import;\n\n@Configuration\n@Import({SpringInstantiator.class, SpringTransactionManager.class})\nclass ExternalsConfiguration {}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxBackgroundProcessor.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Component;\n\n/**\n * Simple implementation of a background processor for {@link TransactionOutbox}. You don't need to\n * use this if you need different semantics, but this is a good start for most purposes.\n */\n@Component\n@Slf4j\n@RequiredArgsConstructor(onConstructor_ = {@Autowired})\nclass TransactionOutboxBackgroundProcessor {\n\n  private final TransactionOutbox outbox;\n\n  @Scheduled(fixedRateString = \"${outbox.repeatEvery}\")\n  void poll() {\n    try {\n      do {\n        log.info(\"Flushing\");\n      } while (outbox.flush());\n    } catch (Exception e) {\n      log.error(\"Error flushing transaction outbox. Pausing\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxProperties.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport java.time.Duration;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\n@ConfigurationProperties(\"outbox\")\n@Data\nclass TransactionOutboxProperties {\n  private Duration repeatEvery;\n  private boolean useJackson;\n  private Duration attemptFrequency;\n  private int blockAfterAttempts;\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/TransactionOutboxSpringDemoApplication.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.gruelbox.transactionoutbox.DefaultPersistor;\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.Persistor;\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport com.gruelbox.transactionoutbox.jackson.JacksonInvocationSerializer;\nimport com.gruelbox.transactionoutbox.spring.SpringInstantiator;\nimport com.gruelbox.transactionoutbox.spring.SpringTransactionManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Lazy;\nimport org.springframework.scheduling.annotation.EnableScheduling;\n\n@SpringBootApplication\n@EnableScheduling\npublic class TransactionOutboxSpringDemoApplication {\n\n  public static void main(String[] args) {\n    SpringApplication.run(TransactionOutboxSpringDemoApplication.class, args);\n  }\n\n  @Bean\n  @Lazy\n  Persistor persistor(\n      TransactionOutboxProperties properties,\n      @SuppressWarnings(\"SpringJavaInjectionPointsAutowiringInspection\")\n          ObjectMapper objectMapper) {\n    if (properties.isUseJackson()) {\n      return DefaultPersistor.builder()\n          .serializer(JacksonInvocationSerializer.builder().mapper(objectMapper).build())\n          .dialect(Dialect.H2)\n          .build();\n    } else {\n      return Persistor.forDialect(Dialect.H2);\n    }\n  }\n\n  @Bean\n  @Lazy\n  TransactionOutbox transactionOutbox(\n      SpringInstantiator instantiator,\n      SpringTransactionManager transactionManager,\n      TransactionOutboxProperties properties,\n      Persistor persistor) {\n    return TransactionOutbox.builder()\n        .instantiator(instantiator)\n        .transactionManager(transactionManager)\n        .persistor(persistor)\n        .attemptFrequency(properties.getAttemptFrequency())\n        .blockAfterAttempts(properties.getBlockAfterAttempts())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/example/simple/Utils.java",
    "content": "package com.gruelbox.transactionoutbox.spring.example.simple;\n\nimport com.gruelbox.transactionoutbox.ThrowingRunnable;\nimport java.util.Arrays;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nclass Utils {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);\n\n  @SuppressWarnings({\"SameParameterValue\", \"WeakerAccess\", \"UnusedReturnValue\"})\n  static boolean safelyRun(String gerund, ThrowingRunnable runnable) {\n    try {\n      runnable.run();\n      return true;\n    } catch (Exception e) {\n      LOGGER.error(\"Error when {}\", gerund, e);\n      return false;\n    }\n  }\n\n  @SuppressWarnings(\"unused\")\n  static void safelyClose(AutoCloseable... closeables) {\n    safelyClose(Arrays.asList(closeables));\n  }\n\n  private static void safelyClose(Iterable<? extends AutoCloseable> closeables) {\n    closeables.forEach(\n        d -> {\n          if (d == null) return;\n          safelyRun(\"closing resource\", d::close);\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/MyRemoteService.java",
    "content": "package com.gruelbox.transactionoutbox.spring.it;\n\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class MyRemoteService {\n\n  public void execute() {}\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/SpringTransactionManagerIT.java",
    "content": "package com.gruelbox.transactionoutbox.spring.it;\n\nimport com.gruelbox.transactionoutbox.AlreadyScheduledException;\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.transaction.PlatformTransactionManager;\nimport org.springframework.transaction.support.TransactionTemplate;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\npublic class SpringTransactionManagerIT {\n\n  @Autowired private TransactionOutbox outbox;\n\n  @Autowired private PlatformTransactionManager transactionManager;\n\n  @Test\n  public void shouldThrowAlreadyScheduledException() {\n    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);\n\n    transactionTemplate.execute(\n        status -> {\n          outbox\n              .with()\n              .uniqueRequestId(\"my-unique-request\")\n              .schedule(MyRemoteService.class)\n              .execute();\n          return null;\n        });\n    transactionTemplate.execute(\n        status -> {\n          // Make sure we can't repeat the same work, and that we get expected exception\n          Assertions.assertThrows(\n              AlreadyScheduledException.class,\n              () ->\n                  outbox\n                      .with()\n                      .uniqueRequestId(\"my-unique-request\")\n                      .schedule(MyRemoteService.class)\n                      .execute());\n          return null;\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/java/com/gruelbox/transactionoutbox/spring/it/TestApplication.java",
    "content": "package com.gruelbox.transactionoutbox.spring.it;\n\nimport com.gruelbox.transactionoutbox.Dialect;\nimport com.gruelbox.transactionoutbox.Persistor;\nimport com.gruelbox.transactionoutbox.TransactionOutbox;\nimport com.gruelbox.transactionoutbox.spring.SpringInstantiator;\nimport com.gruelbox.transactionoutbox.spring.SpringTransactionManager;\nimport java.time.Duration;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.context.annotation.Lazy;\n\n@SpringBootApplication\n@Import({SpringInstantiator.class, SpringTransactionManager.class})\npublic class TestApplication {\n\n  public static void main(String[] args) {\n    SpringApplication.run(TestApplication.class, args);\n  }\n\n  @Bean\n  @Lazy\n  Persistor persistor() {\n    return Persistor.forDialect(Dialect.H2);\n  }\n\n  @Bean\n  @Lazy\n  TransactionOutbox transactionOutbox(\n      SpringInstantiator instantiator,\n      SpringTransactionManager transactionManager,\n      Persistor persistor) {\n    return TransactionOutbox.builder()\n        .instantiator(instantiator)\n        .transactionManager(transactionManager)\n        .persistor(persistor)\n        .attemptFrequency(Duration.ofSeconds(1))\n        .blockAfterAttempts(3)\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/resources/META-INF/persistence.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<persistence xmlns=\"http://xmlns.jcp.org/xml/ns/persistence\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://xmlns.jcp.org/xml/ns/persistence   http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd\" version=\"2.1\">\n  <persistence-unit name=\"txno_test\">\n    <description>Hibernate EntityManager Demo</description>\n    <class>com.gruelbox.transactionoutbox.acceptance.Dummy</class>\n    <exclude-unlisted-classes>true</exclude-unlisted-classes>\n    <properties>\n      <property name=\"hibernate.dialect\" value=\"org.hibernate.dialect.H2Dialect\"/>\n      <property name=\"hibernate.hbm2ddl.auto\" value=\"update\"/>\n      <property name=\"javax.persistence.jdbc.driver\" value=\"org.h2.Driver\"/>\n      <property name=\"javax.persistence.jdbc.url\" value=\"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE\"/>\n      <property name=\"javax.persistence.jdbc.user\" value=\"test\"/>\n      <property name=\"javax.persistence.jdbc.password\" value=\"test\"/>\n    </properties>\n  </persistence-unit>\n</persistence>\n"
  },
  {
    "path": "transactionoutbox-spring/src/test/resources/application.properties",
    "content": "server.port=8081\noutbox.repeatEvery=PT1S\noutbox.attemptFrequency=PT0.5S\noutbox.blockAfterAttempts=100\noutbox.useJackson=true"
  },
  {
    "path": "transactionoutbox-spring/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "transactionoutbox-testing/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Testing</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-testing</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (core library)</description>\n  <dependencies>\n    <!-- Run time -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-classic</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>ch.qos.logback</groupId>\n      <artifactId>logback-core</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.hamcrest</groupId>\n      <artifactId>hamcrest-core</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-engine</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-api</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter-params</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.mockito</groupId>\n      <artifactId>mockito-all</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.zaxxer</groupId>\n      <artifactId>HikariCP</artifactId>\n      <scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>io.opentelemetry</groupId>\n      <artifactId>opentelemetry-sdk-testing</artifactId>\n      <scope>compile</scope>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/TestingMode.java",
    "content": "package com.gruelbox.transactionoutbox;\n\npublic class TestingMode {\n\n  public static void enable() {\n    TransactionOutboxImpl.FORCE_SERIALIZE_AND_DESERIALIZE_BEFORE_USE.set(true);\n  }\n\n  public static void disable() {\n    TransactionOutboxImpl.FORCE_SERIALIZE_AND_DESERIALIZE_BEFORE_USE.set(false);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractAcceptanceTest.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static java.util.stream.Collectors.toList;\nimport static java.util.stream.Collectors.toMap;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.containsInAnyOrder;\nimport static org.hamcrest.Matchers.empty;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.spi.Utils;\nimport com.zaxxer.hikari.HikariDataSource;\nimport io.opentelemetry.api.GlobalOpenTelemetry;\nimport io.opentelemetry.api.OpenTelemetry;\nimport io.opentelemetry.api.common.AttributeKey;\nimport io.opentelemetry.api.trace.Span;\nimport io.opentelemetry.api.trace.SpanContext;\nimport io.opentelemetry.api.trace.TraceFlags;\nimport io.opentelemetry.api.trace.TraceState;\nimport io.opentelemetry.context.Scope;\nimport io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;\nimport io.opentelemetry.sdk.trace.data.SpanData;\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.stream.IntStream;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.MDC;\n\n@Slf4j\npublic abstract class AbstractAcceptanceTest extends BaseTest {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAcceptanceTest.class);\n\n  private ExecutorService unreliablePool;\n  private ExecutorService singleThreadPool;\n\n  private static final Random random = new Random();\n\n  @BeforeEach\n  void beforeEachBase() {\n    unreliablePool =\n        new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(16));\n    singleThreadPool = Executors.newSingleThreadExecutor();\n  }\n\n  @AfterEach\n  void afterEachBase() throws InterruptedException {\n    unreliablePool.shutdown();\n    singleThreadPool.shutdown();\n    assertTrue(unreliablePool.awaitTermination(30, SECONDS));\n    assertTrue(singleThreadPool.awaitTermination(30, SECONDS));\n  }\n\n  @Test\n  final void testMDCPassedToTask() throws InterruptedException {\n    CountDownLatch latch = new CountDownLatch(1);\n    var transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) -> {\n                              log.info(\"Processing ({}, {})\", foo, bar);\n                              assertEquals(\"Foo\", MDC.get(\"SESSION-KEY\"));\n                            }))\n            .listener(new LatchListener(latch))\n            .persistor(StubPersistor.builder().build())\n            .build();\n\n    MDC.put(\"SESSION-KEY\", \"Foo\");\n    try {\n      transactionManager.inTransaction(\n          () -> outbox.schedule(InterfaceProcessor.class).process(3, \"Whee\"));\n    } finally {\n      MDC.clear();\n    }\n\n    assertTrue(latch.await(2, TimeUnit.SECONDS));\n  }\n\n  @Test\n  final void sequencing() throws Exception {\n    int countPerTopic = 20;\n    int topicCount = 5;\n\n    AtomicInteger insertIndex = new AtomicInteger();\n    CountDownLatch latch = new CountDownLatch(countPerTopic * topicCount);\n    ThreadLocalContextTransactionManager transactionManager =\n        (ThreadLocalContextTransactionManager) txManager();\n\n    transactionManager.inTransaction(\n        tx -> {\n          //noinspection resource\n          try (var stmt = tx.connection().createStatement()) {\n            stmt.execute(\"DROP TABLE TEST_TABLE\");\n          } catch (SQLException e) {\n            // ignore\n          }\n        });\n\n    transactionManager.inTransaction(\n        tx -> {\n          //noinspection resource\n          try (var stmt = tx.connection().createStatement()) {\n            stmt.execute(createTestTable());\n          } catch (SQLException e) {\n            throw new RuntimeException(e);\n          }\n        });\n\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .submitter(Submitter.withExecutor(unreliablePool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .instantiator(\n                new RandomFailingInstantiator(\n                    (foo, bar) -> {\n                      transactionManager.requireTransaction(\n                          tx -> {\n                            //noinspection resource\n                            try (var stmt =\n                                tx.connection()\n                                    .prepareStatement(\n                                        \"INSERT INTO TEST_TABLE (topic, ix, foo) VALUES(?, ?, ?)\")) {\n                              stmt.setString(1, bar);\n                              stmt.setInt(2, insertIndex.incrementAndGet());\n                              stmt.setInt(3, foo);\n                              stmt.executeUpdate();\n                            } catch (SQLException e) {\n                              throw new RuntimeException(e);\n                            }\n                          });\n                    }))\n            .persistor(persistor())\n            .listener(new LatchListener(latch))\n            .initializeImmediately(false)\n            .flushBatchSize(4)\n            .build();\n\n    outbox.initialize();\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          transactionManager.inTransaction(\n              () -> {\n                for (int i = 1; i <= countPerTopic; i++) {\n                  for (int j = 1; j <= topicCount; j++) {\n                    outbox\n                        .with()\n                        .ordered(\"topic\" + j)\n                        .schedule(InterfaceProcessor.class)\n                        .process(i, \"topic\" + j);\n                  }\n                }\n              });\n          assertTrue(latch.await(30, SECONDS));\n        });\n\n    var output = new HashMap<String, ArrayList<Integer>>();\n    transactionManager.inTransaction(\n        tx -> {\n          //noinspection resource\n          try (var stmt = tx.connection().createStatement();\n              var rs = stmt.executeQuery(\"SELECT topic, foo FROM TEST_TABLE ORDER BY ix\")) {\n            while (rs.next()) {\n              ArrayList<Integer> values =\n                  output.computeIfAbsent(rs.getString(1), k -> new ArrayList<>());\n              values.add(rs.getInt(2));\n            }\n          } catch (SQLException e) {\n            throw new RuntimeException(e);\n          }\n        });\n\n    var indexes = IntStream.range(1, countPerTopic + 1).boxed().collect(toList());\n    var expected =\n        IntStream.range(1, topicCount + 1)\n            .mapToObj(i -> \"topic\" + i)\n            .collect(toMap(it -> it, it -> indexes));\n    assertEquals(expected, output);\n  }\n\n  /**\n   * Uses a simple direct transaction manager and connection manager and attempts to fire an\n   * interface using a custom instantiator.\n   */\n  @Test\n  final void simpleConnectionProviderCustomInstantiatorInterfaceClass()\n      throws InterruptedException {\n\n    TransactionManager transactionManager = txManager();\n\n    CountDownLatch latch = new CountDownLatch(1);\n    CountDownLatch chainedLatch = new CountDownLatch(1);\n    AtomicBoolean gotScheduled = new AtomicBoolean();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) -> LOGGER.info(\"Processing ({}, {})\", foo, bar)))\n            .submitter(Submitter.withExecutor(unreliablePool))\n            .listener(\n                new LatchListener(latch)\n                    .andThen(\n                        new TransactionOutboxListener() {\n\n                          @Override\n                          public void scheduled(TransactionOutboxEntry entry) {\n                            log.info(\"Got scheduled event\");\n                            gotScheduled.set(true);\n                          }\n\n                          @Override\n                          public void success(TransactionOutboxEntry entry) {\n                            chainedLatch.countDown();\n                          }\n                        }))\n            .persistor(persistor())\n            .initializeImmediately(false)\n            .build();\n\n    outbox.initialize();\n    clearOutbox();\n\n    transactionManager.inTransaction(\n        () -> {\n          outbox.schedule(InterfaceProcessor.class).process(3, \"Whee\");\n          try {\n            // Should not be fired until after commit\n            assertFalse(latch.await(2, SECONDS));\n          } catch (InterruptedException e) {\n            fail(\"Interrupted\");\n          }\n        });\n\n    // Should be fired after commit\n    assertTrue(chainedLatch.await(2, SECONDS));\n    assertTrue(latch.await(1, SECONDS));\n    assertTrue(gotScheduled.get());\n  }\n\n  @Test\n  final void noAutomaticInitialization() {\n\n    TransactionManager transactionManager = txManager();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) -> LOGGER.info(\"Processing ({}, {})\", foo, bar)))\n            .submitter(Submitter.withDefaultExecutor())\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .initializeImmediately(false)\n            .build();\n\n    Persistor.forDialect(connectionDetails().dialect()).migrate(txManager());\n    clearOutbox();\n\n    Assertions.assertThrows(\n        IllegalStateException.class,\n        () ->\n            transactionManager.inTransaction(\n                () -> outbox.schedule(InterfaceProcessor.class).process(3, \"Whee\")));\n  }\n\n  @Test\n  void duplicateRequests() {\n\n    TransactionManager transactionManager = txManager();\n\n    List<String> ids = new ArrayList<>();\n    AtomicReference<Clock> clockProvider = new AtomicReference<>(Clock.systemDefaultZone());\n\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    ids.add((String) entry.getInvocation().getArgs()[0]);\n                  }\n                })\n            .submitter(Submitter.withExecutor(Runnable::run))\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .retentionThreshold(Duration.ofDays(2))\n            .clockProvider(clockProvider::get)\n            .build();\n\n    clearOutbox();\n\n    // Schedule some work\n    transactionManager.inTransaction(\n        () ->\n            outbox\n                .with()\n                .uniqueRequestId(\"context-clientkey1\")\n                .schedule(ClassProcessor.class)\n                .process(\"1\"));\n\n    // Make sure we can schedule more work with a different client key\n    transactionManager.inTransaction(\n        () ->\n            outbox\n                .with()\n                .uniqueRequestId(\"context-clientkey2\")\n                .schedule(ClassProcessor.class)\n                .process(\"2\"));\n\n    // Make sure we can't repeat the same work\n    transactionManager.inTransaction(\n        () ->\n            Assertions.assertThrows(\n                AlreadyScheduledException.class,\n                () ->\n                    outbox\n                        .with()\n                        .uniqueRequestId(\"context-clientkey1\")\n                        .schedule(ClassProcessor.class)\n                        .process(\"3\")));\n\n    // Run the clock forward to just under the retention threshold\n    clockProvider.set(\n        Clock.fixed(\n            clockProvider.get().instant().plus(Duration.ofDays(2)).minusSeconds(60),\n            clockProvider.get().getZone()));\n    outbox.flush();\n\n    // Make sure we can schedule more work with a different client key\n    transactionManager.inTransaction(\n        () ->\n            outbox\n                .with()\n                .uniqueRequestId(\"context-clientkey4\")\n                .schedule(ClassProcessor.class)\n                .process(\"4\"));\n\n    // Make sure we still can't repeat the same work\n    transactionManager.inTransaction(\n        () ->\n            Assertions.assertThrows(\n                AlreadyScheduledException.class,\n                () ->\n                    outbox\n                        .with()\n                        .uniqueRequestId(\"context-clientkey1\")\n                        .schedule(ClassProcessor.class)\n                        .process(\"5\")));\n\n    // Run the clock over the threshold\n    clockProvider.set(\n        Clock.fixed(clockProvider.get().instant().plusSeconds(120), clockProvider.get().getZone()));\n    outbox.flush();\n\n    // We should now be able to add the work\n    transactionManager.inTransaction(\n        () ->\n            outbox\n                .with()\n                .uniqueRequestId(\"context-clientkey1\")\n                .schedule(ClassProcessor.class)\n                .process(\"6\"));\n\n    assertThat(ids, containsInAnyOrder(\"1\", \"2\", \"4\", \"6\"));\n  }\n\n  /**\n   * Uses a simple data source transaction manager and attempts to fire a concrete class via\n   * reflection.\n   */\n  @Test\n  final void dataSourceConnectionProviderReflectionInstantiatorConcreteClass()\n      throws InterruptedException {\n    try (HikariDataSource ds = dataSource) {\n\n      CountDownLatch latch = new CountDownLatch(1);\n\n      TransactionManager transactionManager = TransactionManager.fromDataSource(ds);\n      TransactionOutbox outbox =\n          TransactionOutbox.builder()\n              .transactionManager(transactionManager)\n              .persistor(Persistor.forDialect(connectionDetails().dialect()))\n              .listener(new LatchListener(latch))\n              .build();\n\n      clearOutbox();\n      ClassProcessor.PROCESSED.clear();\n      String myId = UUID.randomUUID().toString();\n\n      transactionManager.inTransaction(() -> outbox.schedule(ClassProcessor.class).process(myId));\n\n      assertTrue(latch.await(2, SECONDS));\n      assertEquals(List.of(myId), ClassProcessor.PROCESSED);\n    }\n  }\n\n  /**\n   * Implements a custom transaction manager. Any required changes to this test are a sign that we\n   * need to bump the major revision.\n   */\n  @Test\n  final void customTransactionManager()\n      throws ClassNotFoundException, SQLException, InterruptedException {\n\n    Class.forName(connectionDetails().driverClassName());\n    try (Connection connection =\n        DriverManager.getConnection(\n            connectionDetails().url(),\n            connectionDetails().user(),\n            connectionDetails().password())) {\n\n      connection.setAutoCommit(false);\n      connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);\n\n      ArrayList<Runnable> postCommitHooks = new ArrayList<>();\n      ArrayList<PreparedStatement> preparedStatements = new ArrayList<>();\n      CountDownLatch latch = new CountDownLatch(1);\n\n      Transaction transaction =\n          new Transaction() {\n            @Override\n            public Connection connection() {\n              return connection;\n            }\n\n            @Override\n            @SneakyThrows\n            public PreparedStatement prepareBatchStatement(String sql) {\n              var stmt = connection.prepareStatement(sql);\n              preparedStatements.add(stmt);\n              return stmt;\n            }\n\n            @Override\n            public void addPostCommitHook(Runnable runnable) {\n              postCommitHooks.add(runnable);\n            }\n          };\n\n      TransactionManager transactionManager =\n          new ThreadLocalContextTransactionManager() {\n            @Override\n            public <T, E extends Exception> T inTransactionReturnsThrows(\n                ThrowingTransactionalSupplier<T, E> work) throws E {\n              return work.doWork(transaction);\n            }\n\n            @Override\n            public <T, E extends Exception> T requireTransactionReturns(\n                ThrowingTransactionalSupplier<T, E> work) throws E, NoTransactionActiveException {\n              return work.doWork(transaction);\n            }\n          };\n\n      TransactionOutbox outbox =\n          TransactionOutbox.builder()\n              .transactionManager(transactionManager)\n              .listener(new LatchListener(latch))\n              .persistor(Persistor.forDialect(connectionDetails().dialect()))\n              .build();\n\n      clearOutbox();\n      ClassProcessor.PROCESSED.clear();\n      String myId = UUID.randomUUID().toString();\n\n      try {\n        outbox.schedule(ClassProcessor.class).process(myId);\n        preparedStatements.forEach(\n            it -> {\n              try {\n                it.executeBatch();\n                it.close();\n              } catch (SQLException e) {\n                throw new RuntimeException(e);\n              }\n            });\n        connection.commit();\n      } catch (Exception e) {\n        connection.rollback();\n        throw e;\n      }\n      postCommitHooks.forEach(Runnable::run);\n\n      assertTrue(latch.await(2, SECONDS));\n      assertEquals(List.of(myId), ClassProcessor.PROCESSED);\n    }\n  }\n\n  /**\n   * Runs a piece of work which will fail several times before working successfully. Ensures that\n   * the work runs eventually.\n   */\n  @Test\n  final void retryBehaviour() throws Exception {\n    TransactionManager transactionManager = txManager();\n    CountDownLatch latch = new CountDownLatch(1);\n    AtomicInteger attempts = new AtomicInteger();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(new FailingInstantiator(attempts))\n            .submitter(Submitter.withExecutor(singleThreadPool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .listener(new LatchListener(latch))\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          transactionManager.inTransaction(\n              () -> outbox.schedule(InterfaceProcessor.class).process(3, \"Whee\"));\n          assertTrue(latch.await(15, SECONDS));\n        },\n        singleThreadPool);\n  }\n\n  @Test\n  final void flushOnlyASpecifiedTopic() throws Exception {\n    TransactionManager transactionManager = txManager();\n    CountDownLatch successLatch = new CountDownLatch(1);\n    var processedEntryListener = new ProcessedEntryListener(successLatch);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) ->\n                                LOGGER.info(\n                                    \"Entered the method to process successfully. Processing ({}, {})\",\n                                    foo,\n                                    bar)))\n            .submitter(Submitter.withExecutor(singleThreadPool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .listener(processedEntryListener)\n            .build();\n\n    clearOutbox();\n\n    var selectedTopic = \"SELECTED_TOPIC\";\n    transactionManager.inTransaction(\n        () -> {\n          outbox\n              .with()\n              .ordered(selectedTopic)\n              .schedule(InterfaceProcessor.class)\n              .process(1, \"Whoo\");\n          outbox\n              .with()\n              .ordered(\"IGNORED_TOPIC\")\n              .schedule(InterfaceProcessor.class)\n              .process(2, \"Wheeeee\");\n        });\n    assertFalse(\n        successLatch.await(5, SECONDS),\n        \"At this point, nothing should have been picked up for processing\");\n\n    outbox.flushTopics(singleThreadPool, selectedTopic);\n\n    assertTrue(successLatch.await(5, SECONDS), \"Should have successfully processed something\");\n\n    var successes = processedEntryListener.getSuccessfulEntries();\n    var failures = processedEntryListener.getFailingEntries();\n\n    // then we only expect the selected topic we're flushing to have had eventually succeeded\n    // as the other work would not have been picked up for a retry\n    assertEquals(1, successes.stream().map(TransactionOutboxEntry::getTopic).distinct().count());\n    assertEquals(selectedTopic, successes.get(0).getTopic());\n\n    // no failures expected\n    assertEquals(0, failures.size());\n  }\n\n  @Test\n  final void onSchedulingFailure_BubbleExceptionsUp() throws Exception {\n    Assumptions.assumeTrue(\n        Dialect.MY_SQL_8.equals(connectionDetails().dialect())\n            || Dialect.MY_SQL_5.equals(connectionDetails().dialect()));\n\n    TransactionManager transactionManager = txManager();\n    CountDownLatch latch = new CountDownLatch(1);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .instantiator(\n                Instantiator.using(\n                    clazz ->\n                        (InterfaceProcessor)\n                            (foo, bar) ->\n                                LOGGER.info(\n                                    \"Entered the method to process successfully. Processing ({}, {})\",\n                                    foo,\n                                    bar)))\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .submitter(Submitter.withExecutor(unreliablePool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .listener(new LatchListener(latch))\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () ->\n            assertThrows(\n                Exception.class,\n                () ->\n                    transactionManager.inTransaction(\n                        () ->\n                            outbox\n                                .with()\n                                .uniqueRequestId(\"some_unique_id\")\n                                .schedule(InterfaceProcessor.class)\n                                .process(1, \"This invocation is too long\".repeat(650000)))));\n  }\n\n  @Test\n  final void lastAttemptTime_updatesEveryTime() throws Exception {\n    TransactionManager transactionManager = txManager();\n    CountDownLatch successLatch = new CountDownLatch(1);\n    CountDownLatch blockLatch = new CountDownLatch(1);\n    AtomicInteger attempts = new AtomicInteger();\n    var orderedEntryListener = new OrderedEntryListener(successLatch, blockLatch);\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(new FailingInstantiator(attempts))\n            .submitter(Submitter.withExecutor(singleThreadPool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .listener(orderedEntryListener)\n            .blockAfterAttempts(2)\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          transactionManager.inTransaction(\n              () -> outbox.schedule(InterfaceProcessor.class).process(3, \"Whee\"));\n          assertTrue(blockLatch.await(20, SECONDS), \"Entry was not blocked\");\n          assertTrue(\n              (Boolean)\n                  transactionManager.inTransactionReturns(\n                      tx -> outbox.unblock(orderedEntryListener.getBlocked().getId())));\n          assertTrue(successLatch.await(20, SECONDS), \"Timeout waiting for success\");\n          var events = orderedEntryListener.getEvents();\n          log.info(\"The entry life cycle is: {}\", events);\n\n          // then we are only dealing in terms of a single outbox entry.\n          assertEquals(1, events.stream().map(TransactionOutboxEntry::getId).distinct().count());\n          // the first, scheduled entry has no lastAttemptTime set\n          assertNull(events.get(0).getLastAttemptTime());\n          // all subsequent entries (2 x failures (second of which 'blocks'), 1x success updates\n          // against db) have a distinct lastAttemptTime set on them.\n          assertEquals(\n              3,\n              events.stream()\n                  .skip(1)\n                  .map(TransactionOutboxEntry::getLastAttemptTime)\n                  .distinct()\n                  .count());\n        },\n        singleThreadPool);\n  }\n\n  /**\n   * Runs a piece of work which will fail enough times to enter a blocked state but will then pass\n   * when re-tried after it is unblocked.\n   */\n  @Test\n  final void blockAndThenUnblockForRetry() throws Exception {\n    TransactionManager transactionManager = txManager();\n    CountDownLatch successLatch = new CountDownLatch(1);\n    CountDownLatch blockLatch = new CountDownLatch(1);\n    LatchListener latchListener = new LatchListener(successLatch, blockLatch);\n    AtomicInteger attempts = new AtomicInteger();\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(new FailingInstantiator(attempts))\n            .submitter(Submitter.withExecutor(singleThreadPool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .listener(latchListener)\n            .blockAfterAttempts(2)\n            .build();\n\n    clearOutbox();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          transactionManager.inTransaction(\n              () -> outbox.schedule(InterfaceProcessor.class).process(3, \"Whee\"));\n          assertTrue(blockLatch.await(5, SECONDS));\n          assertTrue(\n              (Boolean)\n                  transactionManager.inTransactionReturns(\n                      tx -> outbox.unblock(latchListener.getBlocked().getId())));\n          assertTrue(successLatch.await(5, SECONDS));\n        },\n        singleThreadPool);\n  }\n\n  /** Hammers high-volume, frequently failing tasks to ensure that they all get run. */\n  @Test\n  final void highVolumeUnreliable() throws Exception {\n    int count = 10;\n\n    TransactionManager transactionManager = txManager();\n    CountDownLatch latch = new CountDownLatch(count * 10);\n    ConcurrentHashMap<Integer, Integer> results = new ConcurrentHashMap<>();\n    ConcurrentHashMap<Integer, Integer> duplicates = new ConcurrentHashMap<>();\n\n    TransactionOutbox outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(Persistor.forDialect(connectionDetails().dialect()))\n            .instantiator(new RandomFailingInstantiator())\n            .submitter(Submitter.withExecutor(unreliablePool))\n            .attemptFrequency(Duration.ofMillis(500))\n            .flushBatchSize(1000)\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    Integer i = (Integer) entry.getInvocation().getArgs()[0];\n                    if (results.putIfAbsent(i, i) != null) {\n                      duplicates.put(i, i);\n                    }\n                    latch.countDown();\n                  }\n                })\n            .build();\n\n    withRunningFlusher(\n        outbox,\n        () -> {\n          IntStream.range(0, count)\n              .parallel()\n              .forEach(\n                  i ->\n                      transactionManager.inTransaction(\n                          () -> {\n                            for (int j = 0; j < 10; j++) {\n                              outbox.schedule(InterfaceProcessor.class).process(i * 10 + j, \"Whee\");\n                            }\n                          }));\n          assertTrue(latch.await(30, SECONDS), \"Latch not opened in time\");\n        });\n\n    assertThat(\n        \"Should never get duplicates running to full completion\", duplicates.keySet(), empty());\n    assertThat(\n        \"Only got: \" + results.keySet(),\n        results.keySet(),\n        containsInAnyOrder(IntStream.range(0, count * 10).boxed().toArray()));\n  }\n\n  protected String createTestTable() {\n    return \"CREATE TABLE TEST_TABLE (topic VARCHAR(50), ix INTEGER, foo INTEGER, PRIMARY KEY (topic, ix))\";\n  }\n\n  private static class FailingInstantiator implements Instantiator {\n\n    private final AtomicInteger attempts;\n\n    FailingInstantiator(AtomicInteger attempts) {\n      this.attempts = attempts;\n    }\n\n    @Override\n    public String getName(Class<?> clazz) {\n      return \"BEEF\";\n    }\n\n    @Override\n    public Object getInstance(String name) {\n      if (!\"BEEF\".equals(name)) {\n        throw new UnsupportedOperationException();\n      }\n      return (InterfaceProcessor)\n          (foo, bar) -> {\n            LOGGER.info(\"Processing ({}, {})\", foo, bar);\n            if (attempts.incrementAndGet() < 3) {\n              throw new RuntimeException(\"Temporary failure\");\n            }\n            LOGGER.info(\"Processed ({}, {})\", foo, bar);\n          };\n    }\n  }\n\n  private static class RandomFailingInstantiator implements Instantiator {\n\n    private final InterfaceProcessor interfaceProcessor;\n\n    RandomFailingInstantiator() {\n      this.interfaceProcessor = (foo, bar) -> {};\n    }\n\n    RandomFailingInstantiator(InterfaceProcessor interfaceProcessor) {\n      this.interfaceProcessor = interfaceProcessor;\n    }\n\n    @Override\n    public String getName(Class<?> clazz) {\n      return clazz.getName();\n    }\n\n    @Override\n    public Object getInstance(String name) {\n      if (InterfaceProcessor.class.getName().equals(name)) {\n        return (InterfaceProcessor)\n            (foo, bar) -> {\n              if (random.nextInt(10) == 5) {\n                throw new RuntimeException(\"Temporary failure of InterfaceProcessor\");\n              }\n              interfaceProcessor.process(foo, bar);\n            };\n      } else {\n        throw new UnsupportedOperationException();\n      }\n    }\n  }\n\n  @Test\n  void runWithParentOtelSpan() throws Exception {\n    OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();\n    OpenTelemetry otel = otelTesting.getOpenTelemetry();\n    otelTesting.beforeAll(null);\n    try {\n      otelTesting.beforeEach(null);\n\n      var latch = new CountDownLatch(1);\n      AtomicReference<SpanContext> remotedSpan = new AtomicReference<>();\n\n      var txManager = txManager();\n      var outbox =\n          TransactionOutbox.builder()\n              .transactionManager(txManager)\n              .persistor(Persistor.forDialect(connectionDetails().dialect()))\n              .instantiator(\n                  Instantiator.using(\n                      clazz ->\n                          (InterfaceProcessor)\n                              (foo, bar) -> {\n                                remotedSpan.set(Span.current().getSpanContext());\n                              }))\n              .attemptFrequency(Duration.ofMillis(500))\n              .listener(new OtelListener().andThen(new LatchListener(latch)))\n              .blockAfterAttempts(2)\n              .build();\n\n      // Start a parent span, which should be propagated to the instantiator above\n      Span parentSpan = otel.getTracer(\"parent-tracer\").spanBuilder(\"parent-span\").startSpan();\n      String parentTraceId = null;\n      try (Scope scope = parentSpan.makeCurrent()) {\n        parentTraceId = Span.current().getSpanContext().getTraceId();\n        txManager.inTransaction(() -> outbox.schedule(InterfaceProcessor.class).process(1, \"1\"));\n      } finally {\n        parentSpan.end();\n      }\n\n      // Wait for the job to complete\n      assertTrue(latch.await(10, TimeUnit.SECONDS));\n\n      SpanData remotedSpanData = null;\n      for (int i = 0; i < 5; i++) {\n        remotedSpanData =\n            otelTesting.getSpans().stream()\n                .filter(it -> it.getSpanId().equals(remotedSpan.get().getSpanId()))\n                .findFirst()\n                .orElse(null);\n        if (remotedSpanData == null) {\n          if (i == 4) {\n            throw new RuntimeException(\"No matching span\");\n          } else {\n            Thread.sleep(500);\n          }\n        }\n      }\n\n      // Check they ran with linked traces and the correct class/method/args\n      assertTrue(\n          remotedSpanData.getLinks().stream()\n              .findFirst()\n              .orElseThrow(() -> new RuntimeException(\"No linked trace\"))\n              .getSpanContext()\n              .getTraceId()\n              .equals(parentTraceId));\n      assertTrue(\n          remotedSpanData\n              .getName()\n              .equals(\"com.gruelbox.transactionoutbox.testing.InterfaceProcessor.process\"));\n      assertTrue(remotedSpanData.getAttributes().get(AttributeKey.stringKey(\"arg0\")).equals(\"1\"));\n      assertTrue(\n          remotedSpanData.getAttributes().get(AttributeKey.stringKey(\"arg1\")).equals(\"\\\"1\\\"\"));\n    } finally {\n      otelTesting.afterAll(null);\n    }\n  }\n\n  @Test\n  void runWithoutParentOtelSpan() throws Exception {\n    OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();\n    OpenTelemetry otel = otelTesting.getOpenTelemetry();\n    otelTesting.beforeAll(null);\n    try {\n      otelTesting.beforeEach(null);\n\n      var latch = new CountDownLatch(1);\n      AtomicReference<SpanContext> remotedSpan = new AtomicReference<>();\n\n      var txManager = txManager();\n      var outbox =\n          TransactionOutbox.builder()\n              .transactionManager(txManager)\n              .persistor(Persistor.forDialect(connectionDetails().dialect()))\n              .instantiator(\n                  Instantiator.using(\n                      clazz ->\n                          (InterfaceProcessor)\n                              (foo, bar) -> remotedSpan.set(Span.current().getSpanContext())))\n              .attemptFrequency(Duration.ofMillis(500))\n              .listener(new OtelListener().andThen(new LatchListener(latch)))\n              .blockAfterAttempts(2)\n              .build();\n\n      // Run with no parent span\n      txManager.inTransaction(() -> outbox.schedule(InterfaceProcessor.class).process(1, \"1\"));\n\n      // Wait for the job to complete\n      assertTrue(latch.await(10, TimeUnit.SECONDS));\n\n      SpanData remotedSpanData = null;\n      for (int i = 0; i < 5; i++) {\n        remotedSpanData =\n            otelTesting.getSpans().stream()\n                .filter(it -> it.getSpanId().equals(remotedSpan.get().getSpanId()))\n                .findFirst()\n                .orElse(null);\n        if (remotedSpanData == null) {\n          if (i == 4) {\n            throw new RuntimeException(\"No matching span\");\n          } else {\n            Thread.sleep(500);\n          }\n        }\n      }\n\n      // Check they ran with linked traces and the correct class/method/args\n      assertFalse(remotedSpanData.getLinks().stream().findFirst().isPresent());\n      assertTrue(\n          remotedSpanData\n              .getName()\n              .equals(\"com.gruelbox.transactionoutbox.testing.InterfaceProcessor.process\"));\n      assertTrue(remotedSpanData.getAttributes().get(AttributeKey.stringKey(\"arg0\")).equals(\"1\"));\n      assertTrue(\n          remotedSpanData.getAttributes().get(AttributeKey.stringKey(\"arg1\")).equals(\"\\\"1\\\"\"));\n\n    } finally {\n      otelTesting.afterAll(null);\n    }\n  }\n\n  /** Example {@link TransactionOutboxListener} to propagate traces */\n  static class OtelListener implements TransactionOutboxListener {\n\n    /** Serialises the current context into {@link Invocation#getSession()}. */\n    @Override\n    public Map<String, String> extractSession() {\n      var result = new HashMap<String, String>();\n      SpanContext spanContext = Span.current().getSpanContext();\n      if (!spanContext.isValid()) {\n        return null;\n      }\n      result.put(\"traceId\", spanContext.getTraceId());\n      result.put(\"spanId\", spanContext.getSpanId());\n      log.info(\"Extracted: {}\", result);\n      return result;\n    }\n\n    /**\n     * Deserialises {@link Invocation#getSession()} and sets it as the current context so that any\n     * new span started by the method we invoke will treat it as the parent span\n     */\n    @Override\n    public void wrapInvocationAndInit(Invocator invocator) {\n      Invocation inv = invocator.getInvocation();\n      var spanBuilder =\n          GlobalOpenTelemetry.get()\n              .getTracer(\"transaction-outbox\")\n              .spanBuilder(String.format(\"%s.%s\", inv.getClassName(), inv.getMethodName()))\n              .setNoParent();\n      for (var i = 0; i < inv.getArgs().length; i++) {\n        spanBuilder.setAttribute(\"arg\" + i, Utils.stringify(inv.getArgs()[i]));\n      }\n      if (inv.getSession() != null) {\n        var traceId = inv.getSession().get(\"traceId\");\n        var spanId = inv.getSession().get(\"spanId\");\n        if (traceId != null && spanId != null) {\n          spanBuilder.addLink(\n              SpanContext.createFromRemoteParent(\n                  traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()));\n        }\n      }\n      var span = spanBuilder.startSpan();\n      try (Scope scope = span.makeCurrent()) {\n        invocator.runUnchecked();\n      } finally {\n        span.end();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/AbstractPersistorTest.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport static java.time.temporal.ChronoUnit.MILLIS;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.gruelbox.transactionoutbox.*;\nimport java.io.IOException;\nimport java.math.BigDecimal;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.concurrent.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hamcrest.Description;\nimport org.hamcrest.TypeSafeMatcher;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\npublic abstract class AbstractPersistorTest {\n\n  private final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);\n\n  protected abstract Dialect dialect();\n\n  protected abstract Persistor persistor();\n\n  protected abstract TransactionManager txManager();\n\n  protected void validateState() {}\n\n  @BeforeEach\n  public void beforeEach() throws Exception {\n    Boolean connected =\n        txManager().inTransactionReturnsThrows(tx -> persistor().checkConnection(tx));\n    assertTrue(connected);\n    persistor().migrate(txManager());\n    log.info(\"Validating state\");\n    validateState();\n    log.info(\"Clearing old records\");\n    txManager().inTransactionThrows(persistor()::clear);\n    log.info(\"Cleared\");\n  }\n\n  @Test\n  public void testInsertAndSelect() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    Thread.sleep(1100);\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 100, now.plusMillis(1)), contains(entry)));\n  }\n\n  @Test\n  public void testInsertWithUniqueRequestIdFailureBubblesExceptionUp() {\n    var invalidEntry =\n        createEntry(\"FOO\", now, false).toBuilder()\n            .uniqueRequestId(\"INTENTIONALLY_TOO_LONG_TO_CAUSE_BLOW_UP\".repeat(10))\n            .build();\n    assertThrows(\n        RuntimeException.class,\n        () -> txManager().inTransactionThrows(tx -> persistor().save(tx, invalidEntry)));\n  }\n\n  @Test\n  public void testInsertDuplicate() throws Exception {\n    TransactionOutboxEntry entry1 = createEntry(\"FOO1\", now, false, \"context-clientkey1\");\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry1));\n    Thread.sleep(1100);\n    txManager()\n        .inTransactionThrows(\n            tx ->\n                assertThat(persistor().selectBatch(tx, 100, now.plusMillis(1)), contains(entry1)));\n\n    TransactionOutboxEntry entry2 = createEntry(\"FOO2\", now, false, \"context-clientkey2\");\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry2));\n    Thread.sleep(1100);\n    txManager()\n        .inTransactionThrows(\n            tx ->\n                assertThat(\n                    persistor().selectBatch(tx, 100, now.plusMillis(1)),\n                    containsInAnyOrder(entry1, entry2)));\n\n    TransactionOutboxEntry entry3 = createEntry(\"FOO3\", now, false, \"context-clientkey1\");\n    Assertions.assertThrows(\n        AlreadyScheduledException.class,\n        () -> txManager().inTransactionThrows(tx -> persistor().save(tx, entry3)));\n    txManager()\n        .inTransactionThrows(\n            tx ->\n                assertThat(\n                    persistor().selectBatch(tx, 100, now.plusMillis(1)),\n                    containsInAnyOrder(entry1, entry2)));\n  }\n\n  @Test\n  public void testBatchLimitUnderThreshold() throws Exception {\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, createEntry(\"FOO1\", now, false));\n              persistor().save(tx, createEntry(\"FOO2\", now, false));\n              persistor().save(tx, createEntry(\"FOO3\", now, false));\n            });\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 2, now.plusMillis(1)), hasSize(2)));\n  }\n\n  @Test\n  public void testBatchLimitMatchingThreshold() throws Exception {\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, createEntry(\"FOO1\", now, false));\n              persistor().save(tx, createEntry(\"FOO2\", now, false));\n              persistor().save(tx, createEntry(\"FOO3\", now, false));\n            });\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 3, now.plusMillis(1)), hasSize(3)));\n  }\n\n  @Test\n  public void testBatchLimitOverThreshold() throws Exception {\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, createEntry(\"FOO1\", now, false));\n              persistor().save(tx, createEntry(\"FOO2\", now, false));\n              persistor().save(tx, createEntry(\"FOO3\", now, false));\n            });\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 4, now.plusMillis(1)), hasSize(3)));\n  }\n\n  @Test\n  public void testBatchHorizon() throws Exception {\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, createEntry(\"FOO1\", now, false));\n              persistor().save(tx, createEntry(\"FOO2\", now, false));\n              persistor().save(tx, createEntry(\"FOO3\", now.plusMillis(2), false));\n            });\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 3, now.plusMillis(1)), hasSize(2)));\n  }\n\n  @Test\n  public void testBlockedEntriesExcluded() throws Exception {\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, createEntry(\"FOO1\", now, false));\n              persistor().save(tx, createEntry(\"FOO2\", now, false));\n              persistor().save(tx, createEntry(\"FOO3\", now, true));\n            });\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 3, now.plusMillis(1)), hasSize(2)));\n  }\n\n  @Test\n  public void testUnparseableEntriesExcluded() throws Exception {\n\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, createEntry(\"FOO1\", now, false));\n              persistor().save(tx, createEntry(\"FOO2\", now, createUnparseableInvocation()));\n              persistor().save(tx, createEntry(\"FOO3\", now, false));\n            });\n    txManager()\n        .inTransactionThrows(\n            tx -> assertThat(persistor().selectBatch(tx, 3, now.plusMillis(1)), hasSize(3)));\n  }\n\n  static class TransactionOutboxEntryMatcher extends TypeSafeMatcher<TransactionOutboxEntry> {\n    private final TransactionOutboxEntry entry;\n\n    TransactionOutboxEntryMatcher(TransactionOutboxEntry entry) {\n      this.entry = entry;\n    }\n\n    @Override\n    protected boolean matchesSafely(TransactionOutboxEntry other) {\n      return entry.getId().equals(other.getId())\n          && entry.getInvocation().equals(other.getInvocation())\n          && entry.getNextAttemptTime().equals(other.getNextAttemptTime())\n          && entry.getAttempts() == other.getAttempts()\n          && entry.getVersion() == other.getVersion()\n          && entry.isBlocked() == other.isBlocked()\n          && entry.isProcessed() == other.isProcessed();\n    }\n\n    @Override\n    public void describeTo(Description description) {\n      description\n          .appendText(\"Should match on all fields outside of lastAttemptTime :\")\n          .appendText(entry.toString());\n    }\n  }\n\n  TransactionOutboxEntryMatcher matches(TransactionOutboxEntry e) {\n    return new TransactionOutboxEntryMatcher(e);\n  }\n\n  @Test\n  public void testUpdate() throws Exception {\n    var entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    entry.setAttempts(1);\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().update(tx, entry)));\n    var updatedEntry1 =\n        txManager()\n            .inTransactionReturnsThrows(tx -> persistor().selectBatch(tx, 1, now.plusMillis(1)));\n    assertThat(updatedEntry1.size(), equalTo(1));\n    assertThat(updatedEntry1.get(0), matches(entry));\n    assertThat(updatedEntry1.get(0).getLastAttemptTime(), nullValue());\n\n    entry.setAttempts(2);\n    entry.setLastAttemptTime(now);\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().update(tx, entry)));\n\n    var updatedEntry2 =\n        txManager()\n            .inTransactionReturnsThrows(tx -> persistor().selectBatch(tx, 1, now.plusMillis(1)));\n    assertThat(updatedEntry2.size(), equalTo(1));\n    assertThat(updatedEntry2.get(0), matches(entry));\n    assertThat(updatedEntry2.get(0).getLastAttemptTime(), notNullValue());\n  }\n\n  @Test\n  public void testUpdateOptimisticLockFailure() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    TransactionOutboxEntry original = entry.toBuilder().build();\n    entry.setAttempts(1);\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().update(tx, entry)));\n    original.setAttempts(2);\n    txManager()\n        .inTransaction(\n            tx ->\n                assertThrows(\n                    OptimisticLockException.class, () -> persistor().update(tx, original)));\n  }\n\n  @Test\n  public void testDelete() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().delete(tx, entry)));\n  }\n\n  @Test\n  public void testDeleteOptimisticLockFailure() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().delete(tx, entry)));\n    txManager()\n        .inTransaction(\n            tx -> assertThrows(OptimisticLockException.class, () -> persistor().delete(tx, entry)));\n  }\n\n  @Test\n  public void testLock() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    entry.setAttempts(1);\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().update(tx, entry)));\n    txManager().inTransactionThrows(tx -> assertThat(persistor().lock(tx, entry), equalTo(true)));\n  }\n\n  @Test\n  public void testLockOptimisticLockFailure() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n    TransactionOutboxEntry original = entry.toBuilder().build();\n    entry.setAttempts(1);\n    txManager().inTransaction(tx -> assertDoesNotThrow(() -> persistor().update(tx, entry)));\n    txManager()\n        .inTransactionThrows(tx -> assertThat(persistor().lock(tx, original), equalTo(false)));\n  }\n\n  @Test\n  public void testSkipLocked() throws Exception {\n    var entry1 = createEntry(\"FOO1\", now.minusSeconds(1), false);\n    var entry2 = createEntry(\"FOO2\", now.minusSeconds(1), false);\n    var entry3 = createEntry(\"FOO3\", now.minusSeconds(1), false);\n    var entry4 = createEntry(\"FOO4\", now.minusSeconds(1), false);\n\n    txManager()\n        .inTransactionThrows(\n            tx -> {\n              persistor().save(tx, entry1);\n              persistor().save(tx, entry2);\n              persistor().save(tx, entry3);\n              persistor().save(tx, entry4);\n            });\n\n    var gotLockLatch = new CountDownLatch(1);\n    var executorService = Executors.newFixedThreadPool(1);\n    try {\n      Future<?> future =\n          executorService.submit(\n              () -> {\n                log.info(\"Background thread starting\");\n                txManager()\n                    .inTransactionThrows(\n                        tx -> {\n                          log.info(\"Background thread attempting select batch\");\n                          var batch = persistor().selectBatch(tx, 2, now);\n                          assertThat(batch, hasSize(2));\n                          log.info(\"Background thread obtained locks, going to sleep\");\n                          gotLockLatch.countDown();\n                          expectTobeInterrupted();\n                          for (TransactionOutboxEntry entry : batch) {\n                            persistor().delete(tx, entry);\n                          }\n                        });\n                return null;\n              });\n\n      // Wait for the background thread to have obtained the lock\n      log.info(\"Waiting for background thread to obtain lock\");\n      assertTrue(gotLockLatch.await(10, TimeUnit.SECONDS));\n\n      // Now try and select all four - we should only get two\n      log.info(\"Attempting to obtain duplicate locks\");\n      txManager()\n          .inTransactionThrows(\n              tx -> {\n                var batch = persistor().selectBatch(tx, 4, now);\n                assertThat(batch, hasSize(2));\n                for (TransactionOutboxEntry entry : batch) {\n                  persistor().delete(tx, entry);\n                }\n              });\n\n      // Kill the other thread\n      log.info(\"Shutting down\");\n      future.cancel(true);\n\n      // Make sure any assertions from the other thread are propagated\n      assertThrows(CancellationException.class, future::get);\n\n      // Ensure that all the records are processed\n      txManager()\n          .inTransactionThrows(tx -> assertThat(persistor().selectBatch(tx, 100, now), empty()));\n\n    } finally {\n      executorService.shutdown();\n      assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS));\n    }\n  }\n\n  @Test\n  public void testLockPessimisticLockFailure() throws Exception {\n    TransactionOutboxEntry entry = createEntry(\"FOO1\", now, false);\n    txManager().inTransactionThrows(tx -> persistor().save(tx, entry));\n\n    CountDownLatch gotLockLatch = new CountDownLatch(1);\n    ExecutorService executorService = Executors.newFixedThreadPool(1);\n    try {\n\n      // Submit another thread which will take a lock and hold it. If it is not\n      // told to stop after 10 seconds it fails.\n      Future<?> future =\n          executorService.submit(\n              () -> {\n                log.info(\"Background thread starting\");\n                txManager()\n                    .inTransactionThrows(\n                        tx -> {\n                          log.info(\"Background thread attempting lock\");\n                          assertDoesNotThrow(() -> persistor().lock(tx, entry));\n                          log.info(\"Background thread obtained lock, going to sleep\");\n                          gotLockLatch.countDown();\n                          expectTobeInterrupted();\n                        });\n              });\n\n      // Wait for the background thread to have obtained the lock\n      log.info(\"Waiting for background thread to obtain lock\");\n      assertTrue(gotLockLatch.await(10, TimeUnit.SECONDS));\n\n      // Now try and take the lock, which should fail\n      log.info(\"Attempting to obtain duplicate lock\");\n      txManager().inTransactionThrows(tx -> assertFalse(persistor().lock(tx, entry)));\n\n      // Kill the other thread\n      log.info(\"Shutting down\");\n      future.cancel(true);\n\n      // Make sure any assertions from the other thread are propagated\n      assertThrows(CancellationException.class, future::get);\n\n    } finally {\n      executorService.shutdown();\n      assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS));\n    }\n  }\n\n  private TransactionOutboxEntry createEntry(String id, Instant nextAttemptTime, boolean blocked) {\n    return TransactionOutboxEntry.builder()\n        .id(id)\n        .invocation(createInvocation())\n        .blocked(blocked)\n        .lastAttemptTime(null)\n        .nextAttemptTime(nextAttemptTime.truncatedTo(MILLIS))\n        .build();\n  }\n\n  private TransactionOutboxEntry createEntry(\n      String id,\n      Instant nextAttemptTime,\n      @SuppressWarnings(\"SameParameterValue\") boolean blocked,\n      String uniqueId) {\n    return TransactionOutboxEntry.builder()\n        .id(id)\n        .invocation(createInvocation())\n        .blocked(blocked)\n        .lastAttemptTime(null)\n        .nextAttemptTime(nextAttemptTime.truncatedTo(MILLIS))\n        .uniqueRequestId(uniqueId)\n        .build();\n  }\n\n  private TransactionOutboxEntry createEntry(\n      String id, Instant nextAttemptTime, Invocation invocation) {\n    return TransactionOutboxEntry.builder()\n        .id(id)\n        .invocation(invocation)\n        .blocked(false)\n        .lastAttemptTime(null)\n        .nextAttemptTime(nextAttemptTime.truncatedTo(MILLIS))\n        .build();\n  }\n\n  private Invocation createInvocation() {\n    return new Invocation(\n        \"Foo\",\n        \"Bar\",\n        new Class<?>[] {int.class, BigDecimal.class, String.class},\n        new Object[] {1, BigDecimal.TEN, null});\n  }\n\n  private Invocation createUnparseableInvocation() {\n    return new FailedDeserializingInvocation(new IOException());\n  }\n\n  private void expectTobeInterrupted() {\n    try {\n      Thread.sleep(10000);\n      throw new RuntimeException(\"Background thread not killed within 10 seconds\");\n    } catch (InterruptedException e) {\n      log.info(\"Background thread interrupted correctly\");\n    } catch (Exception e) {\n      log.error(\"Background thread failed\", e);\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/BaseTest.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.sql.SQLException;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport lombok.Builder;\nimport lombok.Value;\nimport lombok.experimental.Accessors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\n\n@Slf4j\npublic abstract class BaseTest {\n\n  protected HikariDataSource dataSource;\n  private ExecutorService flushExecutor;\n\n  @BeforeEach\n  final void baseBeforeEach() {\n    HikariConfig config = new HikariConfig();\n    config.setJdbcUrl(connectionDetails().url());\n    config.setUsername(connectionDetails().user());\n    config.setPassword(connectionDetails().password());\n    config.addDataSourceProperty(\"cachePrepStmts\", \"true\");\n    dataSource = new HikariDataSource(config);\n    flushExecutor = Executors.newFixedThreadPool(4);\n    TestingMode.enable();\n  }\n\n  @AfterEach\n  final void baseAfterEach() throws InterruptedException {\n    TestingMode.disable();\n    flushExecutor.shutdown();\n    Assertions.assertTrue(flushExecutor.awaitTermination(30, TimeUnit.SECONDS));\n    dataSource.close();\n  }\n\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(Dialect.H2)\n        .driverClassName(\"org.h2.Driver\")\n        .url(\n            \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DEFAULT_LOCK_TIMEOUT=60000;LOB_TIMEOUT=2000;MV_STORE=TRUE;DATABASE_TO_UPPER=FALSE\")\n        .user(\"test\")\n        .password(\"test\")\n        .build();\n  }\n\n  protected TransactionManager txManager() {\n    return TransactionManager.fromDataSource(dataSource);\n  }\n\n  protected Persistor persistor() {\n    return Persistor.forDialect(connectionDetails().dialect());\n  }\n\n  protected void clearOutbox() {\n    DefaultPersistor persistor = Persistor.forDialect(connectionDetails().dialect());\n    TransactionManager transactionManager = txManager();\n    transactionManager.inTransaction(\n        tx -> {\n          try {\n            persistor.clear(tx);\n          } catch (SQLException e) {\n            throw new RuntimeException(e);\n          }\n        });\n  }\n\n  protected void withRunningFlusher(TransactionOutbox outbox, ThrowingRunnable runnable)\n      throws Exception {\n    withRunningFlusher(outbox, runnable, flushExecutor);\n  }\n\n  protected void withRunningFlusher(\n      TransactionOutbox outbox, ThrowingRunnable runnable, Executor executor) throws Exception {\n    withRunningFlusher(outbox, runnable, executor, null);\n  }\n\n  protected void withRunningFlusher(\n      TransactionOutbox outbox, ThrowingRunnable runnable, Executor executor, String topicName)\n      throws Exception {\n    Thread backgroundThread =\n        new Thread(\n            () -> {\n              while (!Thread.interrupted()) {\n                try {\n                  // Keep flushing work until there's nothing left to flush\n                  log.info(\"Starting flush...\");\n                  while (topicName == null\n                      ? outbox.flush(executor)\n                      : outbox.flushTopics(executor, topicName)) {\n                    log.info(\"More work to do...\");\n                  }\n                  log.info(\"Done!\");\n                } catch (Exception e) {\n                  log.error(\"Error flushing transaction outbox\", e);\n                }\n                try {\n                  //noinspection BusyWait\n                  Thread.sleep(250);\n                } catch (InterruptedException e) {\n                  break;\n                }\n              }\n            });\n    backgroundThread.start();\n    try {\n      runnable.run();\n    } finally {\n      backgroundThread.interrupt();\n      backgroundThread.join();\n    }\n  }\n\n  @Value\n  @Accessors(fluent = true)\n  @Builder\n  public static class ConnectionDetails {\n    String driverClassName;\n    String url;\n    String user;\n    String password;\n    Dialect dialect;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/ClassProcessor.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ClassProcessor {\n\n  private static final Logger LOGGER = LoggerFactory.getLogger(ClassProcessor.class);\n\n  static final List<String> PROCESSED = new CopyOnWriteArrayList<>();\n\n  void process(String itemId) {\n    LOGGER.info(\"Processing work: {}\", itemId);\n    PROCESSED.add(itemId);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/InterfaceProcessor.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\npublic interface InterfaceProcessor {\n  void process(int foo, String bar);\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/LatchListener.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport com.gruelbox.transactionoutbox.TransactionOutboxListener;\nimport java.util.concurrent.CountDownLatch;\nimport lombok.Getter;\n\npublic final class LatchListener implements TransactionOutboxListener {\n  private final CountDownLatch successLatch;\n  private final CountDownLatch blockedLatch;\n\n  @Getter private volatile TransactionOutboxEntry blocked;\n\n  public LatchListener(CountDownLatch successLatch, CountDownLatch markFailedLatch) {\n    this.successLatch = successLatch;\n    this.blockedLatch = markFailedLatch;\n  }\n\n  public LatchListener(CountDownLatch successLatch) {\n    this.successLatch = successLatch;\n    this.blockedLatch = new CountDownLatch(1);\n  }\n\n  @Override\n  public void success(TransactionOutboxEntry entry) {\n    successLatch.countDown();\n  }\n\n  @Override\n  public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n    this.blocked = entry;\n    blockedLatch.countDown();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/OrderedEntryListener.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport com.gruelbox.transactionoutbox.TransactionOutboxListener;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.CountDownLatch;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Collects an ordered list of all entry events (*excluding blocked events) that have hit this\n * listener until a specified number of blocks / successes have occurred.\n */\n@Slf4j\npublic final class OrderedEntryListener implements TransactionOutboxListener {\n  private final CountDownLatch successLatch;\n  private final CountDownLatch blockedLatch;\n\n  @Getter private volatile TransactionOutboxEntry blocked;\n\n  private final CopyOnWriteArrayList<TransactionOutboxEntry> events = new CopyOnWriteArrayList<>();\n  private final CopyOnWriteArrayList<TransactionOutboxEntry> successes =\n      new CopyOnWriteArrayList<>();\n\n  public OrderedEntryListener(CountDownLatch successLatch, CountDownLatch blockedLatch) {\n    this.successLatch = successLatch;\n    this.blockedLatch = blockedLatch;\n  }\n\n  @Override\n  public void scheduled(TransactionOutboxEntry entry) {\n    events.add(from(entry));\n  }\n\n  @Override\n  public void success(TransactionOutboxEntry entry) {\n    var copy = from(entry);\n    events.add(copy);\n    successes.add(copy);\n    log.info(\n        \"Received success #{}. Counting down at {}\", successes.size(), successLatch.getCount());\n    successLatch.countDown();\n  }\n\n  @Override\n  public void failure(TransactionOutboxEntry entry, Throwable cause) {\n    events.add(from(entry));\n  }\n\n  @Override\n  public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n    // due to the implementation of outbox (how it persists updates), it does not make sense to add\n    // the blocked entry to the list for our current testing purposes.\n    blocked = from(entry);\n    blockedLatch.countDown();\n  }\n\n  /**\n   * Retrieve an unmodifiable copy of {@link #events}. Beware, expectation is that this does not/\n   * should not get accessed until the correct number of {@link #success(TransactionOutboxEntry)}\n   * and {@link #blocked(TransactionOutboxEntry, Throwable)}} counts have occurred.\n   *\n   * @return unmodifiable list of ordered outbox entry events.\n   */\n  public List<TransactionOutboxEntry> getEvents() {\n    return List.copyOf(events);\n  }\n\n  public List<TransactionOutboxEntry> getSuccesses() {\n    return List.copyOf(successes);\n  }\n\n  private TransactionOutboxEntry from(TransactionOutboxEntry entry) {\n    return entry.toBuilder().build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/ProcessedEntryListener.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionOutboxEntry;\nimport com.gruelbox.transactionoutbox.TransactionOutboxListener;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.CountDownLatch;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Collects an ordered list of tx outbox entries that have been 'processed' i.e. succeeded or failed\n * in processing.\n */\n@Slf4j\npublic final class ProcessedEntryListener implements TransactionOutboxListener {\n  private final CountDownLatch successLatch;\n\n  private final CopyOnWriteArrayList<TransactionOutboxEntry> successfulEntries =\n      new CopyOnWriteArrayList<>();\n  private final CopyOnWriteArrayList<TransactionOutboxEntry> failingEntries =\n      new CopyOnWriteArrayList<>();\n\n  public ProcessedEntryListener(CountDownLatch successLatch) {\n    this.successLatch = successLatch;\n  }\n\n  @Override\n  public void success(TransactionOutboxEntry entry) {\n    var copy = from(entry);\n    successfulEntries.add(copy);\n    log.info(\n        \"Received success #{}. Counting down at {}\",\n        successfulEntries.size(),\n        successLatch.getCount());\n    successLatch.countDown();\n  }\n\n  @Override\n  public void failure(TransactionOutboxEntry entry, Throwable cause) {\n    failingEntries.add(from(entry));\n  }\n\n  /**\n   * Retrieve an unmodifiable copy of {@link #successfulEntries}. Beware, expectation is that this\n   * does not/ should not get accessed until the correct number of {@link\n   * #success(TransactionOutboxEntry)} and {@link #blocked(TransactionOutboxEntry, Throwable)}}\n   * counts have occurred.\n   *\n   * @return unmodifiable list of ordered outbox entry events.\n   */\n  public List<TransactionOutboxEntry> getSuccessfulEntries() {\n    return List.copyOf(successfulEntries);\n  }\n\n  public List<TransactionOutboxEntry> getFailingEntries() {\n    return List.copyOf(failingEntries);\n  }\n\n  private TransactionOutboxEntry from(TransactionOutboxEntry entry) {\n    return entry.toBuilder().build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/java/com/gruelbox/transactionoutbox/testing/TestUtils.java",
    "content": "package com.gruelbox.transactionoutbox.testing;\n\nimport com.gruelbox.transactionoutbox.TransactionManager;\nimport java.sql.Statement;\n\npublic class TestUtils {\n\n  @SuppressWarnings(\"SameParameterValue\")\n  public static void runSql(TransactionManager transactionManager, String sql) {\n    transactionManager.inTransaction(\n        tx -> {\n          try {\n            try (Statement statement = tx.connection().createStatement()) {\n              statement.execute(sql);\n            }\n          } catch (Exception e) {\n            throw new RuntimeException(e);\n          }\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-testing/src/main/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "transactionoutbox-virtthreads/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<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\">\n  <parent>\n    <artifactId>transactionoutbox-parent</artifactId>\n    <groupId>com.gruelbox</groupId>\n    <version>${revision}</version>\n  </parent>\n  <modelVersion>4.0.0</modelVersion>\n  <name>Transaction Outbox Virtual Threads support</name>\n  <packaging>jar</packaging>\n  <artifactId>transactionoutbox-virtthreads</artifactId>\n  <description>A safe implementation of the transactional outbox pattern for Java (core library)</description>\n  <properties>\n    <maven.compiler.source>25</maven.compiler.source>\n    <maven.compiler.target>25</maven.compiler.target>\n  </properties>\n  <dependencies>\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-core</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <!-- Compile time -->\n    <dependency>\n      <groupId>org.projectlombok</groupId>\n      <artifactId>lombok</artifactId>\n    </dependency>\n    <!-- Test dependencies -->\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-testing</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.gruelbox</groupId>\n      <artifactId>transactionoutbox-jooq</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>testcontainers</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>junit-jupiter</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>postgresql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>oracle-xe</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mysql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.postgresql</groupId>\n      <artifactId>postgresql</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.oracle.database.jdbc</groupId>\n      <artifactId>ojdbc11</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.mysql</groupId>\n      <artifactId>mysql-connector-j</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.h2database</groupId>\n      <artifactId>h2</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mssqlserver</artifactId>\n    </dependency>\n    <dependency>\n      <groupId>com.microsoft.sqlserver</groupId>\n      <artifactId>mssql-jdbc</artifactId>\n    </dependency>\n  </dependencies>\n</project>\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/AbstractVirtualThreadsTest.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static java.util.stream.Collectors.joining;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assertions.fail;\n\nimport com.gruelbox.transactionoutbox.*;\nimport com.gruelbox.transactionoutbox.testing.BaseTest;\nimport com.gruelbox.transactionoutbox.testing.InterfaceProcessor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.sql.SQLException;\nimport java.time.Duration;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.FutureTask;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.stream.IntStream;\nimport jdk.jfr.consumer.RecordedEvent;\nimport jdk.jfr.consumer.RecordingStream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.Assert;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nabstract class AbstractVirtualThreadsTest extends BaseTest {\n\n  private static final String VIRTUAL_THREAD_SCHEDULER_PARALLELISM =\n      \"jdk.virtualThreadScheduler.parallelism\";\n\n  protected RecordingStream stream;\n  private final AtomicBoolean alerted = new AtomicBoolean();\n  private final CountDownLatch pinLatch = new CountDownLatch(1);\n\n  @BeforeEach\n  final void beforeEachAbstractVirtualThreadsTest() throws InterruptedException {\n    stream = new RecordingStream();\n    stream.enable(\"jdk.VirtualThreadPinned\").withThreshold(Duration.ZERO);\n    stream.onEvent(\n        \"jdk.VirtualThreadPinned\",\n        event -> {\n          if (isTransientInitialization(event)) {\n            log.info(\"Ignored classloader issues since these are transient...\");\n            return;\n          }\n          log.info(\n              \"\"\"\n        Pinning Event Captured:\n        Reason: %s\n        Blocking operation: %s\n        Duration: %dms\n        Stack trace:\n        %s\"\"\"\n                  .formatted(\n                      event.getValue(\"pinnedReason\"),\n                      event.getValue(\"blockingOperation\"),\n                      event.getDuration().toMillis(),\n                      event.getStackTrace().getFrames().stream()\n                          .map(\n                              f ->\n                                  \" - %s %s.%s(%s)\"\n                                      .formatted(\n                                          f.getType(),\n                                          f.getMethod().getType().getName(),\n                                          f.getMethod().getName(),\n                                          f.getLineNumber()))\n                          .collect(joining(\"\\n\"))));\n          pinLatch.countDown();\n        });\n    stream.startAsync();\n    Thread.sleep(500);\n  }\n\n  private boolean isTransientInitialization(RecordedEvent event) {\n    var stackTrace = event.getStackTrace();\n    if (stackTrace == null) return false;\n\n    return stackTrace.getFrames().stream()\n        .anyMatch(\n            frame -> {\n              String method = frame.getMethod().getType().getName();\n              return method.contains(\"jdk.internal.loader\")\n                  || method.contains(\"java.lang.ClassLoader\")\n                  || method.contains(\"java.lang.invoke.MethodHandles\");\n            });\n  }\n\n  @AfterEach\n  final void afterEachAbstractVirtualThreadsTest() {\n    try {\n      if (didPin()) {\n        fail(\"Virtual thread was pinned. See earlier messages\");\n      }\n    } finally {\n      stream.close();\n    }\n  }\n\n  protected boolean didPin() {\n    try {\n      if (alerted.getAndSet(true)) {\n        log.info(\"Suppressed pinning alert as already checked\");\n        return false;\n      }\n      if (!pinLatch.await(5, TimeUnit.SECONDS)) {\n        return false;\n      }\n      return true;\n    } catch (InterruptedException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Test\n  final void highVolumeVirtualThreads() throws Exception {\n    var count = 10;\n    var latch = new CountDownLatch(count * 10);\n    var transactionManager = txManager();\n    var results = new ConcurrentHashMap<Integer, Integer>();\n    var duplicates = new ConcurrentHashMap<Integer, Integer>();\n    var persistor = Persistor.forDialect(connectionDetails().dialect());\n    var outbox =\n        TransactionOutbox.builder()\n            .transactionManager(transactionManager)\n            .persistor(persistor)\n            .instantiator(Instantiator.using(clazz -> (InterfaceProcessor) (foo, bar) -> {}))\n            .submitter(\n                Submitter.withExecutor(\n                    r -> Thread.ofVirtual().name(UUID.randomUUID().toString()).start(r)))\n            .attemptFrequency(Duration.ofMillis(500))\n            .flushBatchSize(1000)\n            .listener(\n                new TransactionOutboxListener() {\n                  @Override\n                  public void success(TransactionOutboxEntry entry) {\n                    Integer i = (Integer) entry.getInvocation().getArgs()[0];\n                    if (results.putIfAbsent(i, i) != null) {\n                      duplicates.put(i, i);\n                    }\n                    latch.countDown();\n                  }\n                })\n            .build();\n\n    warmupJdk(transactionManager, persistor);\n\n    var parallelism = System.getProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM);\n    System.setProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM, \"1\");\n    try {\n      withRunningFlusher(\n          outbox,\n          () -> {\n            var futures =\n                IntStream.range(0, count)\n                    .mapToObj(\n                        i ->\n                            new FutureTask<Void>(\n                                () ->\n                                    transactionManager.inTransaction(\n                                        () -> {\n                                          for (int j = 0; j < 10; j++) {\n                                            outbox\n                                                .schedule(InterfaceProcessor.class)\n                                                .process(i * 10 + j, \"Whee\");\n                                          }\n                                        }),\n                                null))\n                    .toList();\n            futures.forEach(Thread::startVirtualThread);\n            for (var future : futures) {\n              future.get(20, TimeUnit.SECONDS);\n            }\n            assertTrue(latch.await(30, TimeUnit.SECONDS), \"Latch not opened in time\");\n          });\n    } finally {\n      if (parallelism == null) {\n        System.clearProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM);\n      } else {\n        System.setProperty(VIRTUAL_THREAD_SCHEDULER_PARALLELISM, parallelism);\n      }\n    }\n\n    assertThat(\n        \"Should never get duplicates running to full completion\", duplicates.keySet(), empty());\n    assertThat(\n        \"Only got: \" + results.keySet(),\n        results.keySet(),\n        containsInAnyOrder(IntStream.range(0, count * 10).boxed().toArray()));\n  }\n\n  private void warmupJdk(TransactionManager transactionManager, DefaultPersistor persistor)\n      throws NoSuchMethodException,\n          IllegalAccessException,\n          InvocationTargetException,\n          SQLException {\n    // Warm up Invocation.invoke so it converts to a MethodHandle now rather than later (which\n    // causes pinning)\n    for (int i = 0; i < 50; i++) {\n      var invocation =\n          new Invocation(\n              InterfaceProcessor.class.getName(),\n              \"process\",\n              new Class<?>[] {int.class, String.class},\n              new Object[] {1, \"\"});\n      InterfaceProcessor warmupTarget =\n          (foo, bar) -> {\n            log.info(\"Warmup\");\n          };\n      var invokeMethod =\n          Invocation.class.getDeclaredMethod(\n              \"invoke\", Object.class, TransactionOutboxListener.class);\n      invokeMethod.setAccessible(true);\n      invokeMethod.invoke(invocation, warmupTarget, new TransactionOutboxListener() {});\n    }\n\n    // And do a bit of database access now to warm up the JDBC driver\n    transactionManager.inTransactionThrows(\n        tx -> {\n          Assert.assertTrue(persistor.checkConnection(tx));\n          persistor.clear(tx);\n        });\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static org.junit.Assert.assertTrue;\n\nimport java.lang.foreign.*;\nimport java.lang.invoke.MethodHandle;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.MethodType;\nimport java.util.Optional;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\npublic class TestVirtualThreadsH2 extends AbstractVirtualThreadsTest {\n\n  private static final String QSORT = \"qsort\";\n\n  /**\n   * Ensures that the logic we use in {@link AbstractVirtualThreadsTest} to detect thread pinning\n   * actually works. In Java 25 we have to turn ourselves inside out to achieve this; even {@code\n   * synchronized} doesn't pin a thread anymore. The only thing that seems to work is to make a\n   * native call that calls back to Java. When that stops working, this test can probably be\n   * removed; there will be very little likelihood of pinning in practice once that last thing is\n   * resolved in the JDK.\n   */\n  @Test\n  void forceTriggerPinningViaUpcall() throws Throwable {\n    simulateThreadPin();\n\n    // Prevents the check on exit from throwing\n    assertTrue(didPin());\n  }\n\n  private void simulateThreadPin() throws NoSuchMethodException, IllegalAccessException {\n    Linker linker = Linker.nativeLinker();\n    SymbolLookup libc =\n        name -> {\n          try {\n            var lookup =\n                System.getProperty(\"os.name\").toLowerCase().contains(\"win\")\n                    ? SymbolLookup.libraryLookup(\"msvcrt.dll\", Arena.global())\n                    : SymbolLookup.libraryLookup(\"libc.so.6\", Arena.global());\n            return lookup.find(name);\n          } catch (Exception e) {\n            return Optional.empty();\n          }\n        };\n    MethodHandle qsort =\n        linker.downcallHandle(\n            libc.find(QSORT)\n                .or(() -> Linker.nativeLinker().defaultLookup().find(QSORT))\n                .or(() -> SymbolLookup.loaderLookup().find(QSORT))\n                .orElseThrow(() -> new RuntimeException(\"Could not find qsort\")),\n            FunctionDescriptor.ofVoid(\n                ValueLayout.ADDRESS,\n                ValueLayout.JAVA_LONG,\n                ValueLayout.JAVA_LONG,\n                ValueLayout.ADDRESS));\n    MethodHandle upcallTarget =\n        MethodHandles.lookup()\n            .findStatic(\n                getClass(),\n                \"javaBlockCallback\",\n                MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class));\n    MemorySegment upcallStub =\n        linker.upcallStub(\n            upcallTarget,\n            FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS),\n            Arena.global());\n    Thread.ofVirtual()\n        .name(\"Pin-Me-Thread\")\n        .start(\n            () -> {\n              try {\n                System.out.println(\"Virtual thread entering native qsort...\");\n                MemorySegment data = Arena.ofConfined().allocate(16); // 2 long elements\n                qsort.invoke(data, 2L, 8L, upcallStub);\n                System.out.println(\"Virtual thread exited native qsort.\");\n              } catch (Throwable e) {\n                throw new RuntimeException(e);\n              }\n            });\n  }\n\n  // This is the Upcall: Java -> Native (qsort) -> Java (here)\n  public static int javaBlockCallback(MemorySegment a, MemorySegment b) {\n    try {\n      // This 'park' operation happens while a native frame (qsort)\n      // is on the stack. Pinning is guaranteed.\n      Thread.sleep(200);\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n    }\n    return 0;\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsH2Jooq.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport com.gruelbox.transactionoutbox.ThreadLocalContextTransactionManager;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionListener;\nimport com.gruelbox.transactionoutbox.jooq.JooqTransactionManager;\nimport org.jooq.SQLDialect;\nimport org.jooq.impl.DSL;\nimport org.jooq.impl.DataSourceConnectionProvider;\nimport org.jooq.impl.DefaultConfiguration;\nimport org.jooq.impl.ThreadLocalTransactionProvider;\nimport org.junit.jupiter.api.BeforeEach;\n\npublic class TestVirtualThreadsH2Jooq extends AbstractVirtualThreadsTest {\n\n  private ThreadLocalContextTransactionManager txm;\n\n  @Override\n  protected final ThreadLocalContextTransactionManager txManager() {\n    return txm;\n  }\n\n  @BeforeEach\n  final void beforeEach() {\n    DataSourceConnectionProvider connectionProvider = new DataSourceConnectionProvider(dataSource);\n    DefaultConfiguration configuration = new DefaultConfiguration();\n    configuration.setConnectionProvider(connectionProvider);\n    configuration.setSQLDialect(SQLDialect.H2);\n    configuration.setTransactionProvider(\n        new ThreadLocalTransactionProvider(connectionProvider, true));\n    JooqTransactionListener listener = JooqTransactionManager.createListener();\n    configuration.set(listener);\n    txm = JooqTransactionManager.create(DSL.using(configuration), listener);\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql5.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.MY_SQL_5;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestVirtualThreadsMySql5 extends AbstractVirtualThreadsTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MySQLContainer<>(\"mysql:5\")\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true)\n          .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(MY_SQL_5)\n        .driverClassName(\"com.mysql.cj.jdbc.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsMySql8.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.MY_SQL_8;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestVirtualThreadsMySql8 extends AbstractVirtualThreadsTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      new MySQLContainer<>(\"mysql:8\")\n          .withStartupTimeout(Duration.ofMinutes(5))\n          .withReuse(true)\n          .withTmpFs(Map.of(\"/var/lib/mysql\", \"rw\"));\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(MY_SQL_8)\n        .driverClassName(\"com.mysql.cj.jdbc.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsOracle21.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.ORACLE;\n\nimport java.time.Duration;\nimport org.junit.jupiter.api.Disabled;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.OracleContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\n@Disabled // Struggling to avoid pinning here at the moment\nclass TestVirtualThreadsOracle21 extends AbstractVirtualThreadsTest {\n\n  @Container\n  @SuppressWarnings(\"rawtypes\")\n  private static final JdbcDatabaseContainer container =\n      new OracleContainer(\"gvenzl/oracle-xe:21-slim-faststart\")\n          .withStartupTimeout(Duration.ofHours(1))\n          .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(ORACLE)\n        .driverClassName(\"oracle.jdbc.OracleDriver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/java/com/gruelbox/transactionoutbox/virtthreads/TestVirtualThreadsPostgres16.java",
    "content": "package com.gruelbox.transactionoutbox.virtthreads;\n\nimport static com.gruelbox.transactionoutbox.Dialect.POSTGRESQL_9;\n\nimport java.time.Duration;\nimport org.testcontainers.containers.JdbcDatabaseContainer;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@SuppressWarnings(\"WeakerAccess\")\n@Testcontainers\nclass TestVirtualThreadsPostgres16 extends AbstractVirtualThreadsTest {\n\n  @Container\n  @SuppressWarnings({\"rawtypes\", \"resource\"})\n  private static final JdbcDatabaseContainer container =\n      (JdbcDatabaseContainer)\n          new PostgreSQLContainer(\"postgres:16\")\n              .withStartupTimeout(Duration.ofHours(1))\n              .withReuse(true);\n\n  @Override\n  protected ConnectionDetails connectionDetails() {\n    return ConnectionDetails.builder()\n        .dialect(POSTGRESQL_9)\n        .driverClassName(\"org.postgresql.Driver\")\n        .url(container.getJdbcUrl())\n        .user(container.getUsername())\n        .password(container.getPassword())\n        .build();\n  }\n}\n"
  },
  {
    "path": "transactionoutbox-virtthreads/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n</pattern>\n    </encoder>\n  </appender>\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "~/settings.xml",
    "content": "<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\"\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd\">\n  <servers>\n    <server>\n      <id>github</id>\n      <username>${env.GITHUB_ACTOR}</username>\n      <password>${env.GITHUB_TOKEN}</password>\n    </server>\n  </servers>\n</settings>"
  },
  {
    "path": "~/toolchains.xml",
    "content": "<?xml version=\"1.0\"?>\n<toolchains xmlns=\"http://maven.apache.org/TOOLCHAINS/1.1.0\"\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/TOOLCHAINS/1.1.0 https://maven.apache.org/xsd/toolchains-1.1.0.xsd\">\n  <toolchain>\n    <type>jdk</type>\n    <provides>\n      <version>25</version>\n      <vendor>zulu</vendor>\n      <id>zulu_25</id>\n    </provides>\n    <configuration>\n      <jdkHome>/opt/hostedtoolcache/Java_Zulu_jdk/25.0.2-10/x64</jdkHome>\n    </configuration>\n  </toolchain>\n</toolchains>"
  }
]