Skip to content

Commit

Permalink
feat: transaction tagging (#977)
Browse files Browse the repository at this point in the history
* feat: transaction tagging

Adds support for transaction tags using an interceptor that
traverses the call stack up to the method that actually
started the transaction. The tag is then determined by:
1. Looking for an @TransactionTag annotation on the method.
2. Or, if enabled, automatically generating a tag from the
   class name and method name.

The option to automatically add tags based on class and method
name can be enabled through a system property. This makes it
possible to enable this feature only during debugging, and
disable it again when it is no longer needed.

* chore: cleanup
  • Loading branch information
olavloite authored Mar 28, 2024
1 parent da7fda2 commit da41b42
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 0 deletions.
61 changes: 61 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,67 @@ List<Singer> findByActive(boolean active);
This https://github.com/GoogleCloudPlatform/google-cloud-spanner-hibernate/blob/-/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample[working sample application]
shows how to use the above hint.

==== Transaction Tags

Spanner supports adding
https://cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags[transaction tags]
for troubleshooting queries and transactions. You can add transaction tags to your Hibernate or
Spring Data JPA application by adding the
`com.google.cloud.spanner.hibernate.TransactionTagInterceptor` to your Hibernate configuration, and
then adding the `com.google.cloud.spanner.hibernate.TransactionTag` annotation to the method that
starts the transaction.

Example for adding the `TransactionTagInterceptor`:

[source,java]
----
package com.google.cloud.spanner.sample;
import com.google.cloud.spanner.hibernate.TransactionTagInterceptor;
import com.google.common.collect.ImmutableSet;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.stereotype.Component;
/** This component adds the TransactionTagInterceptor to the Hibernate configuration. */
@Component
public class TaggingHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer {
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put(AvailableSettings.INTERCEPTOR, new TransactionTagInterceptor(
ImmutableSet.of(MyApplication.class.getPackageName()), false));
}
}
----

Then add the `@TransactionTag` to the methods that should be tagged:

[source,java]
----
@Service
public class VenueService {
private final VenueRepository repository;
public VenueService(VenueRepository repository) {
this.repository = repository;
}
/**
* Deletes all Venue records in the database.
*/
@Transactional
@TransactionTag("delete_all_venues")
public void deleteAllVenues() {
repository.deleteAll();
}
}
----

This https://github.com/GoogleCloudPlatform/google-cloud-spanner-hibernate/blob/-/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample[working sample application]
shows how to use transaction tags.

==== Custom Spanner Column Types

This project offers the following Hibernate type mappings for specific Spanner column types:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
<artifactId>spring-data-jpa-full-sample</artifactId>

<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>
<spring-boot-version>3.1.2</spring-boot-version>
</properties>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2019-2024 Google LLC
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/

package com.google.cloud.spanner.sample;

import com.google.cloud.spanner.hibernate.TransactionTagInterceptor;
import com.google.common.collect.ImmutableSet;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.stereotype.Component;

/** This component adds the {@link TransactionTagInterceptor} to the sample application. */
@Component
public class TaggingHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer {
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put(AvailableSettings.INTERCEPTOR, new TransactionTagInterceptor(
ImmutableSet.of(SampleApplication.class.getPackageName()), false));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package com.google.cloud.spanner.sample.service;

import com.google.cloud.spanner.hibernate.TransactionTag;
import com.google.cloud.spanner.sample.entities.Venue;
import com.google.cloud.spanner.sample.entities.Venue.VenueDescription;
import com.google.cloud.spanner.sample.repository.VenueRepository;
Expand Down Expand Up @@ -57,6 +58,7 @@ public void deleteAllVenues() {
* Generates the specified number of random Venue records.
*/
@Transactional
@TransactionTag("generate_random_venues")
public List<Venue> generateRandomVenues(int count) {
Random random = new Random();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.connection.AbstractMockServerTest;
import com.google.common.base.Strings;
import com.google.longrunning.Operation;
import com.google.protobuf.Any;
import com.google.protobuf.Empty;
Expand Down Expand Up @@ -394,12 +395,33 @@ public void testRunApplication() {
System.setProperty("spanner.emulator", "false");
System.setProperty("spanner.host", "//localhost:" + getPort());
System.setProperty("spanner.connectionProperties", ";usePlainText=true");
// Enable automatic tagging of transactions that do not already have a tag.
System.setProperty("spanner.auto_tag_transactions", "true");
SpringApplication.run(SampleApplication.class).close();

assertEquals(31, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> !request.getSql().equals("SELECT 1")).count());
assertEquals(6, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
assertEquals(9, mockSpanner.countRequestsOfType(CommitRequest.class));

// Verify that we receive a transaction tag for the generateRandomVenues() method.
assertEquals(1, mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).stream()
.filter(request -> request.getRequestOptions().getTransactionTag().equals("generate_random_venues")).count());
assertEquals(1, mockSpanner.getRequestsOfType(CommitRequest.class).stream()
.filter(request -> request.getRequestOptions().getTransactionTag().equals("generate_random_venues")).count());
// Also verify that we get the auto-generated transaction tags.
assertEquals(6, mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).stream()
.filter(request ->
!Strings.isNullOrEmpty(request.getRequestOptions().getTransactionTag())).count());
assertEquals(1, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getRequestOptions().getTransactionTag()
.equals("service_SingerService_deleteAllSingers")).count());
assertEquals(1, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getRequestOptions().getTransactionTag()
.equals("service_AlbumService_deleteAllAlbums")).count());
assertEquals(1, mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).stream()
.filter(request -> request.getRequestOptions().getTransactionTag()
.equals("service_SingerService_generateRandomSingers")).count());
}

private static void addDdlResponseToSpannerAdmin() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2019-2024 Google LLC
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/

package com.google.cloud.spanner.hibernate;

import java.lang.reflect.Field;
import javax.annotation.Nullable;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.transaction.internal.TransactionImpl;
import org.hibernate.internal.SessionFactoryImpl;

/** Base class for interceptors that add transaction tags. */
public abstract class AbstractTransactionTagInterceptor implements Interceptor {
private final Field sessionField;

private Boolean dialectIsPostgres;

protected AbstractTransactionTagInterceptor() {
try {
sessionField = TransactionImpl.class.getDeclaredField("session");
sessionField.setAccessible(true);
} catch (NoSuchFieldException noSuchFieldException) {
throw new HibernateException("Could not get 'session' field of TransactionImpl");
}
}

@Override
public void afterTransactionBegin(Transaction tx) {
String tag = getTag();
if (tag != null) {
Session session = getSession(tx);
if (session != null) {
session.doWork(connection -> {
if (!(connection.isReadOnly() || connection.getAutoCommit())) {
connection.createStatement().execute(generateSetTransactionTagStatement(session, tag));
}
});
}
}
}

private String generateSetTransactionTagStatement(Session session, String tag) {
if (dialectIsPostgres(session)) {
return "set spanner.transaction_tag='" + tag + "'";
}
return "set transaction_tag='" + tag + "'";
}

private boolean dialectIsPostgres(Session session) {
if (this.dialectIsPostgres == null) {
synchronized (this) {
if (this.dialectIsPostgres == null) {
SessionFactory factory = session.getSessionFactory();
if (factory instanceof SessionFactoryImpl) {
Dialect dialect = ((SessionFactoryImpl) factory).getJdbcServices().getDialect();
this.dialectIsPostgres = dialect.openQuote() == '"';
} else {
this.dialectIsPostgres = false;
}
}
}
}
return this.dialectIsPostgres;
}

/** Returns the tag that should be added to the transaction that is being started. */
protected abstract String getTag();

/**
* Gets the session from the transaction.
* Unfortunately, there is no public API to do so, so we have to use reflection.
*/
private @Nullable Session getSession(Transaction tx) {
if (tx instanceof TransactionImpl && sessionField != null) {
try {
TransactionImpl transaction = (TransactionImpl) tx;
return (Session) sessionField.get(transaction);
} catch (IllegalAccessException illegalAccessException) {
throw new HibernateException(
"Failed to get session from transaction", illegalAccessException);
}
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2019-2024 Google LLC
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/

package com.google.cloud.spanner.hibernate;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation for adding transaction tags to Hibernate transactions.
*
* <p>Usage:
* <ol>
* <li>Add the {@link TransactionTagInterceptor} to your Hibernate configuration.</li>
* <li>Add the {@link TransactionTag} annotation to a method that is also tagged with
* {@link jakarta.transaction.Transactional}.</li>
* </ol>
*
* <p>Example:
*
* <pre>{@code
* // Add TransactionTagInterceptor to the Hibernate configuration.
* @Component
* public class TaggingHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer {
* @Override
* public void customize(Map<String, Object> hibernateProperties) {
* hibernateProperties.put(AvailableSettings.INTERCEPTOR, new TransactionTagInterceptor(
* ImmutableSet.of(SampleApplication.class.getPackageName()), false));
* }
* }
*
* @Service
* public class VenueService {
* @Transactional
* @TransactionTag("generate_random_venues")
* public List<Venue> generateRandomVenues(int count) {
* // Code that is executed in a transaction...
* }
* }
* }</pre>
*
* <p>See <a href="https://github.com/GoogleCloudPlatform/google-cloud-spanner-hibernate/blob/-/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample">
* Spring Data JPA Full Sample</a> for a working sample application.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TransactionTag {

/**
* The transaction tag value. Max length is 50 characters.
* See <a href="https://cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags#limitations">
* Limitations</a> for all limitations on transaction tag values.
*/
String value();
}
Loading

0 comments on commit da41b42

Please sign in to comment.