Spring Annotations for CockroachDB

A guide to using annotations and aspects in Spring to leverage CockroachDB features and bring clarity to transaction management

Overview

Spring and Spring Boot provides a rich ecosystem for building modern applications and services. It's a powerful platform that provides easy-to-use infrastructure abstractions and seamless integration with most databases, messaging systems, cloud infrastructure and what not else.

In this tutorial, we will explore how annotations and AOP aspects can be used to leverage certain features in CockroachDB, such as follower reads and time travel queries. The same mechanism will also be used to implement a transparent transaction retry strategy.

The goal with this approach is to avoid obscuring business logic with data access logic and error handling, which can become quite verbose otherwise. Following the AOP concept, these cross-cutting concerns can be concentrated to one single place and then weaved in where needed by annotations and pointcut expressions. Aspects in Spring are very useful for weaving in specific advices either around, before or after method calls.

Screenshot 2021-03-23 at 20.48.13.png

Spring Annotations

Let's take a look at how this can be applied for the following use cases:

  • Time travel queries used for reading from a timestamp in the past.
  • Follower reads used for reading from a follower replica, slightly in the past (~5s).
  • Adopting a design pattern for transactional robustness and clarity.
  • Transaction retries with exponential backoff.
  • Transaction priorities and other attributes.

Time Travel

To learn about time travel queries in CockroachDB, see as of system time. To use it in our Spring application, let's first create a method level annotation for declaring the intention.

@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RUNTIME)
public @interface TimeTravel {
    String interval() default "-30s";
}

Then we create a before advice that sets the time interval to read from the past (can also be an around advice).

@Aspect
@Order(AdvisorOrder.WITHIN_CONTEXT)
public class TimeTravelAspect {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Before(value = "@annotation(timeTravel)", argNames = "timeTravel")
    public void beforeTimeTravelOperation(TimeTravel timeTravel) {
        Assert.isTrue(TransactionSynchronizationManager.isActualTransactionActive(),
                "TX not active - explicit transaction required");
        jdbcTemplate.update("SET TRANSACTION AS OF SYSTEM TIME INTERVAL '" + timeTravel.interval() + "'");
    }
}

The @Order annotation is used to control in what order the AOP proxies are invoked, which is important when using multiple advices are used at the same time. Typically, we want the retry proxy to be called first, acting as outer boundary, then the transaction proxy, then the transaction attributes proxy. Setting transaction attributes requires explicit transactions to work (disable auto-commit) so the inverse order wouldn't work.

Now, lets' see how this is used in a web (API) controller:

    @GetMapping(value = "/{id}")
    @TransactionBoundary
    @FollowerRead
    public HttpEntity<AccountModel> getAccount(@PathVariable("id") Long accountId) {
        return new ResponseEntity<>(accountResourceAssembler
                .toModel(accountRepository.getOne(accountId)), HttpStatus.OK);
    }

All you do is add the @FollowerRead annotation, and that's it. After a transaction is created but before the service method gets called, the advice kicks in and sets the transaction attribute.

@TransactionBoundary is another meta-annotation that wrap Spring's @Transactional annotation with a fixed propagation attribute of REQUIRES_NEW which is what is expected from a transaction boundary. The motivation for that will become clear in the next section.

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Transactional(propagation = Propagation.REQUIRES_NEW)
public @interface TransactionBoundary {
..
}

Follower Reads

To learn more about follower reads in CockroachDB, see here. A follower read pretty much follows the same pattern. Technically, it is a time travel query.

First create the marker annotation:

@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RUNTIME)
public @interface FollowerRead {
}

Then create a before advice that sets the follower read expression.

@Aspect
@Order(AdvisorOrder.WITHIN_CONTEXT)
public class FollowerReadAspect {
    @Autowired
    private JdbcTemplate jdbcTemplate;

   @Before(value = "@annotation(followerRead)", argNames = "followerRead")
   public void beforeFollowerReadOperation(FollowerRead followerRead) {
        Assert.isTrue(TransactionSynchronizationManager.isActualTransactionActive(),
                "TX not active - explicit transaction required");
        jdbcTemplate.execute("SET TRANSACTION AS OF SYSTEM TIME experimental_follower_read_timestamp()");
    }
}

Transaction Retries

Now let's look at something a bit more interesting - transaction retries. Any database that runs in serializable isolation is subject to serialization errors in contending workloads.

To handle this gracefully when using explicit transactions (begin+commit), it's recommended to catch them server side and retry the transactions after an exponential backoff period. In Spring, there's an entire exception class hierarchy dedicated to classify transient exceptions.

There is one classic stability pattern called Entity-Control-Boundary (ECB) that fits quite well for this purpose. This pattern brings clarity and robustness to transaction management in a typical Spring Boot application.

It works by using meta-annotations to assign ECB architectural roles to different elements, and then use aspects to apply transaction behaviour for these roles.

In a nutshell:

  • Use meta-annotations to drive transaction demarcation and propagation
  • Enable Spring's annotation driven transaction manager
  • Use aspects to implement transparent transaction retries on transient errors

This annotation is optional and you can achieve the same thing by just using Springs @Transactional annotation, but it doesn't help to enforce architectural roles. Boundaries have a tendency to get blurry in organically growing code bases.

Elements

  • Boundary - Typically a web controller, service facade or JMS/Kafka service activator method that exposes the functionality of a service and interacts with clients.
  • Control - Typically a fine-grained service behind a boundary web that implements business logic.
  • Entity - Refers to a persistent domain object, typically mapped to a JPA entity.

Annotations

These are the (meta-)annotations applied to the above elements.

  • @TransactionBoundary - Annotation marking a transaction boundary. A boundary is allowed to start new transactions and suspend existing ones, hence it uses REQUIRES_NEW propagation.

      @Inherited
      @Documented
      @Retention(RetentionPolicy.RUNTIME)
      @Target({ElementType.TYPE, ElementType.METHOD})
      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public @interface TransactionBoundary {
          ...
      }
    
  • @TransactionControlService - Annotation marking a control service method that is NOT allowed to start new transactions and must be invoked from a transactional context. Hence it uses MANDATORY propagation.

      @Inherited
      @Documented
      @Retention(RetentionPolicy.RUNTIME)
      @Target({ElementType.TYPE, ElementType.METHOD})
      @Transactional(propagation = Propagation.MANDATORY)
      public @interface TransactionService {
      }
    
  • @javax.persistence.Entity - Standard JPA entity annotation marking a managed persistent entity. Not strictly needed.

Aspects

So far we have just defined the semantic descriptors and transaction demarcations through annotations. Let's move on to the actual retry aspects. There are two flavours of retries, one executing the entire atomic / logical transaction again and the second rolling back to a savepoint and retrying from there.

Notice that savepoints are not supported by JPA/Hibernate.

The order in which these advices are weaved in between the source and target have significance. The ordering must be relative to Spring's transactional advice activated with @EnableTransactionManagement.

The typical call chain need to be something like this:

   source (controller/service facade/service activator)
        |--> retryableAdvice (no transaction context allowed)  
        |--> transactionAdvice (Spring advice that starts a transaction)  
        |--> transactionHintsAdvice (within a transaction context)  
    target (business service/repository expecting a transaction)

Usage Example

Using transparent retries in a web controller, acting as the transaction boundary:

    @PostMapping(value = "/transfer")
    @TransactionBoundary(retryAttempts = 20, maxBackoff = 45000)
    public HttpEntity<Void> transfer(@RequestBody TransferRequest request) {
        BigDecimal totalBalance = accountRepository.getBalance(request.getName());
        accountRepository.updateBalance(request.getName(), request.getAccountType(), request.getAmount());
        return new ResponseEntity<>(HttpStatus.OK);
    }

This particular use case, reading and writing to the same account concurrently is subject to the write skew anomaly that serializable isolation protects against. CockroachDB only runs in serializable. The demo shows how that manifests in transient retry errors.

The repository called using the @TransactionControlService marker annotation just to denote its role:

@Repository
@TransactionControlService
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Modifying
    @Query("update Account a set a.balance = a.balance + ?3 where a.name = ?1 and a.type=?2")
    void updateBalance(String name, AccountType type, BigDecimal balance);
}

Conclusions

In this article, we learned how to leverage annotations and aspects in Spring to inject cross-cutting concerns like transaction retries or transaction attributes for time travel.

Annotation and aspect examples are available on GitHub here with a demo here .

No Comments Yet