Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : Quarkus integration #320

Merged
merged 6 commits into from
Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -46,6 +46,7 @@
<module>transactionoutbox-spring</module>
<module>transactionoutbox-guice</module>
<module>transactionoutbox-jooq</module>
<module>transactionoutbox-cdi</module>
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
</modules>

<dependencyManagement>
Expand Down
93 changes: 93 additions & 0 deletions transactionoutbox-cdi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# transaction-outbox-cdi
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved


Extension for [transaction-outbox-core](../README.md) which integrates CDI's DI and/or 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-cdi</artifactId>
<version>4.3.281</version>
</dependency>
```

#### Gradle

```groovy
implementation 'com.gruelbox:transactionoutbox-cdi: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 TransactionManager cdiTransactionManager(DataSource datasource, TransactionSynchronizationRegistry tsr) throws SQLException
{
return new CdiTransactionManager(datasource, tsr);
}

@Produces
public TransactionOutbox transactionOutbox(TransactionManager transactionManager, CdiInstanciator instanciator)
{
return TransactionOutbox.builder().instantiator(instanciator).blockAfterAttempts(1).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-cdi/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 CDI</name>
<packaging>jar</packaging>
<artifactId>transactionoutbox-cdi</artifactId>
<description>A safe implementation of the transactional outbox pattern for Java (CDI 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,15 @@
package com.gruelbox.transactionoutbox;

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

@ApplicationScoped
public class CdiInstanciator extends AbstractFullyQualifiedNameInstantiator
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
{

@Override
protected Object createInstance(Class<?> clazz)
{
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
return CDI.current().select(clazz).get();
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package com.gruelbox.transactionoutbox;

import static com.gruelbox.transactionoutbox.Utils.uncheck;

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 javax.sql.DataSource;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;


public class CdiTransactionManager implements ThreadLocalContextTransactionManager
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
{
private final CdiTransaction transactionInstance;

private TransactionSynchronizationRegistry tsr;
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved

public CdiTransactionManager(DataSource datasource, TransactionSynchronizationRegistry tsr) throws SQLException
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
{
transactionInstance = new CdiTransaction(datasource);
this.tsr = tsr;
}

@Override
@Transactional(value = TxType.REQUIRES_NEW)
public void inTransaction(Runnable runnable)
{
uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromRunnable(runnable)));
}

@Override
@Transactional(value = TxType.REQUIRES_NEW)
public void inTransaction(TransactionalWork work)
{
uncheck(() -> inTransactionReturnsThrows(ThrowingTransactionalSupplier.fromWork(work)));
}

@Override
@Transactional(value = TxType.REQUIRES_NEW)
public <T, E extends Exception> T inTransactionReturnsThrows(ThrowingTransactionalSupplier<T, E> work) throws E
{
return work.doWork(transactionInstance);
}

@Override
public <T, E extends Exception> T requireTransactionReturns(ThrowingTransactionalSupplier<T, E> work) throws E, NoTransactionActiveException
{
if (tsr.getTransactionStatus() != Status.STATUS_ACTIVE)
{
throw new NoTransactionActiveException();
}

return work.doWork(transactionInstance);
}

private final class CdiTransaction implements Transaction
{

private DataSource datasource;

public CdiTransaction(DataSource datasource) throws SQLException
{
this.datasource = datasource;
}

@Override
public final Connection connection()
{
try
{
return datasource.getConnection();
RomainWilbert marked this conversation as resolved.
Show resolved Hide resolved
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}

@Override
public PreparedStatement prepareBatchStatement(String sql)
{
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();
}
});
}
}

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