Skip to content

Commit

Permalink
Feature : Quarkus integration (#320)
Browse files Browse the repository at this point in the history
* feat: cdi implementation

* fix: PR comments

* fix: cdi transaction manager instantiation to be transaction aware

* Update CdiTransactionManager.java

* ref: module name (quarkus)

* ref: module name

Co-authored-by: Romain Wilbert <romain.wilbert-ext@pole-emploi.fr>

(cherry picked from commit 121f27d)
  • Loading branch information
RomainWilbert authored and badgerwithagun committed May 14, 2023
1 parent d5a00d6 commit 4b4891a
Show file tree
Hide file tree
Showing 15 changed files with 616 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ jobs:
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-guice/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-jooq/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-spring/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-quarkus/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-core:\)[^']*_\1${revision}_g" README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-guice:\)[^']*_\1${revision}_g" transactionoutbox-guice/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-jooq:\)[^']*_\1${revision}_g" transactionoutbox-jooq/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-spring:\)[^']*_\1${revision}_g" transactionoutbox-spring/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-quarkus:\)[^']*_\1${revision}_g" transactionoutbox-quarkus/README.md
env:
GITHUB_TOKEN: ${{ github.token }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
Expand All @@ -55,10 +57,12 @@ jobs:
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-guice/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-jooq/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-spring/README.md
sed -i "s_\(<version>\)[^<]*_\1${revision}_g" transactionoutbox-quarkus/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-core:\)[^']*_\1${revision}_g" README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-guice:\)[^']*_\1${revision}_g" transactionoutbox-guice/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-jooq:\)[^']*_\1${revision}_g" transactionoutbox-jooq/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-spring:\)[^']*_\1${revision}_g" transactionoutbox-spring/README.md
sed -i "s_\(implementation 'com.gruelbox:transactionoutbox-quarkus:\)[^']*_\1${revision}_g" transactionoutbox-quarkus/README.md
- name: Create version update pull request
uses: gruelbox/create-pull-request@v2
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
**/.classpath
**/.project
**/.settings
**/.factorypath
/.idea
/*.iml
**/target/**
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<module>transactionoutbox-spring</module>
<module>transactionoutbox-guice</module>
<module>transactionoutbox-jooq</module>
<module>transactionoutbox-quarkus</module>
<module>transactionoutbox-r2dbc</module>
<module>transactionoutbox-testing</module>
<module>transactionoutbox-acceptance</module>
Expand Down
88 changes: 88 additions & 0 deletions transactionoutbox-quarkus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# transactionoutbox-quarkus


Extension for [transaction-outbox-core](../README.md) which integrates CDI's DI and Quarkus transaction management.

Tested with Quarkus implementation (Arc/Agroal)

## Installation

### Stable releases

The latest stable release is available from Maven Central.

#### Maven

```xml
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-quarkus</artifactId>
<version>4.3.281</version>
</dependency>
```

#### Gradle

```groovy
implementation 'com.gruelbox:transactionoutbox-quarkus:4.3.281'
```

### Development snapshots

Maven 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.

#### Maven

```xml
<repositories>
<repository>
<id>github-transaction-outbox</id>
<name>Gruelbox Github Repository</name>
<url>https://maven.pkg.github.com/gruelbox/transaction-outbox</url>
</repository>
</repositories>
```

You will need to authenticate with Github to use Github Package Repository. Create a personal access token in [your GitHub settings](https://github.com/settings/tokens). It only needs **read:package** permissions. Then add something like the following in your Maven `settings.xml`:

```xml
<servers>
<server>
<id>github-transaction-outbox</id>
<username>${env.GITHUB_USERNAME}</username>
<password>${env.GITHUB_TOKEN}</password>
</server>
</servers>
```

The above example uses environment variables, allowing you to keep the credentials out of source control, but you can hard-code them if you know what you're doing.

## Configuration

Create your `TransactionOutbox` as a bean:

```java

@Produces
public TransactionOutbox transactionOutbox(QuarkusTransactionManager transactionManager)
{
return TransactionOutbox.builder().instantiator(CdiInstantiator.create()).transactionManager(transactionManager).persistor(Persistor.forDialect(Dialect.H2)).build();
}
```

## Usage

```java
@Transactional
public void doStuff() {
customerRepository.save(new Customer(1L, "Martin", "Carthy"));
customerRepository.save(new Customer(2L, "Dave", "Pegg"));
outbox.get().schedule(getClass()).publishCustomerCreatedEvent(1L);
outbox.get().schedule(getClass()).publishCustomerCreatedEvent(2L);
}

void publishCustomerCreatedEvent(long id) {
// Remote call here
}
```

88 changes: 88 additions & 0 deletions transactionoutbox-quarkus/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transactionoutbox-parent</artifactId>
<groupId>com.gruelbox</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<name>Transaction Outbox Quarkus</name>
<packaging>jar</packaging>
<artifactId>transactionoutbox-quarkus</artifactId>
<description>A safe implementation of the transactional outbox pattern for Java (Quarkus extension library)
</description>

<properties>
<quarkus.version>2.11.1.Final</quarkus.version>
</properties>

<dependencies>

<!-- Runtime -->
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
</dependency>

<!-- Test -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-agroal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-undertow</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.gruelbox.transactionoutbox;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.CDI;

@ApplicationScoped
public class CdiInstantiator extends AbstractFullyQualifiedNameInstantiator {

public static CdiInstantiator create() {
return new CdiInstantiator();
}

private CdiInstantiator() {}

@Override
protected Object createInstance(Class<?> clazz) {
return CDI.current().select(clazz).get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.gruelbox.transactionoutbox;

import com.gruelbox.transactionoutbox.jdbc.JdbcTransaction;

import javax.sql.DataSource;
import javax.transaction.Synchronization;
import javax.transaction.TransactionSynchronizationRegistry;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;

public final class CdiTransaction implements JdbcTransaction {

private final DataSource datasource;
private final TransactionSynchronizationRegistry tsr;

CdiTransaction(DataSource dataSource, TransactionSynchronizationRegistry tsr) {
this.datasource = dataSource;
this.tsr = tsr;
}

@Override
public Connection connection() {
try {
return datasource.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Override
public PreparedStatement prepareBatchStatement(String sql) {
@SuppressWarnings("resource") BatchCountingStatement preparedStatement =
Utils.uncheckedly(
() -> BatchCountingStatementHandler.countBatches(connection().prepareStatement(sql)));
tsr.registerInterposedSynchronization(
new Synchronization() {
@Override
public void beforeCompletion() {
if (preparedStatement.getBatchCount() != 0) {
Utils.uncheck(preparedStatement::executeBatch);
}
}

@Override
public void afterCompletion(int status) {
Utils.safelyClose(preparedStatement);
}
});
return preparedStatement;
}

@Override
public void addPostCommitHook(Runnable runnable) {
tsr.registerInterposedSynchronization(new Synchronization() {
@Override
public void beforeCompletion() {
}

@Override
public void afterCompletion(int status) {
runnable.run();
}
});
}

@Override
public void addPostCommitHook(Supplier<CompletableFuture<Void>> hook) {
tsr.registerInterposedSynchronization(new Synchronization() {
@Override
public void beforeCompletion() {
}

@Override
public void afterCompletion(int status) {
Utils.join(hook.get());
}
});
}

private interface BatchCountingStatement extends PreparedStatement
{
int getBatchCount();
}

private static final class BatchCountingStatementHandler implements InvocationHandler {

private final PreparedStatement delegate;
private int count = 0;

private BatchCountingStatementHandler(PreparedStatement delegate) {
this.delegate = delegate;
}

static BatchCountingStatement countBatches(PreparedStatement delegate) {
return (BatchCountingStatement)
Proxy.newProxyInstance(
BatchCountingStatementHandler.class.getClassLoader(),
new Class[] {BatchCountingStatement.class},
new BatchCountingStatementHandler(delegate));
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getBatchCount".equals(method.getName())) {
return count;
}
try {
return method.invoke(delegate, args);
} finally {
if ("addBatch".equals(method.getName())) {
++count;
}
}
}
}
}
Loading

0 comments on commit 4b4891a

Please sign in to comment.