Skip to content

dgrandemange/idempotence-receiver

Repository files navigation

Build Status

Maven Central

Javadocs

This project aims to help idempotence mechanism integration into a Spring MVC RESTful service.

  • provides a way to make relevant controller handler method idempotent through a simple annotation,
  • provides a dedicated Spring Boot Starter module that takes care of all dependencies declaration and Spring plumbing, while providing a set of dedicated Spring Boot properties to configure the idempotence mechanism.

Idempotence : what, why, when

Generally speaking, idempotence is the ability of an operation to produce the same effect regardless of how many times it is invoked.

In a distributed service oriented architecture (like say a micro-service architecture), where the services communicate with each other through the network, we should not consider the network as fully reliable. Meaning our services should be designed in order to be resilient to network micro-failures :

  • from a consumer service point of view, this means, for instance, being able to do some request retries when no response has been received from the invoked producer service within the allocated time (i.e. request timeout),
  • from a producer service point of view, that means being able to handle those successive retries, meaning some times (and depending on the nature of operation) guaranteeing that the repeatedly requested operation is processed only one time

Please consider reading resources below for detailed explanations, and to actually determine if you need idempotence, how you can achieve it, and if you need this library for that (which may not be the case actually) :

Integration recipe

Here below are the steps to follow to integrate manual idempotence into an existing Spring MVC RESTful API.

See also a concrete integration example with the provided webapp sample.

Add idempotence receiver spring boot starter dependency to your project

Maven Central

Annotate service methods that need manual idempotence handling

Some methods doesn't need manual idempotent management, because there are already idempotent by nature.
For instance, methods that only read resources, without affecting their state, are natively idempotent and do not require a special manual idempotence handling. It may even reveal counter-productive to manually handle idempotence for such methods.
On the other hand, methods that affect resources state, especially resource creation (usually through HTTP verb POST in a RESTful API) are good candidates to manual idempotence handling.

To enable manual idempotence management of a method, simply annotate it with the @Idempotent annotation.

As an example, look at method com.github.dgrandemange.idempotencereceiver.examples.webapp.controller.BookResource.create(Book).

Configure idempotence management

Idempotence configuration is made through dedicated Spring Boot configuration properties.

Common configuration

NB : for a standard usage, default values provided for those common properties don't need to be overridden.

Look at IdempotentReceiverCommonConfiguration Javadoc to get an exhaustive list of available properties.

Idempotence repository configuration

Idempotence mechanism relies on a repository where idempotent method results are cached for a certain amount of time.

As there are different repositories implementations available, one of the available repository implementations must be chosen and specified via the idempotence-receiver.repository.type property (if not set, idempotence mechanism won't be enabled at all).
Each repository implementation requires its own set of configuration properties to be set. This will be detailed in the next sections.

Repository failure resiliency common configuration

The idempotence-receiver.repository.resiliency.* properties configure the retry and circuit breaker policies.
These policies aim to offer resilience to idempotence repository potential failures.

Look at ResiliencyConfiguration Javadoc to get an exhaustive list of available properties configurable in Spring Boot app configuration application.yaml.

NB : these properties are common to every repository implementation.

Internal memory repository configuration

Select this implementation by setting the idempotence-receiver.repository.type property to internal-memory.

It configures an in-memory idempotence repository, internally using a WeakHashMap as storage facility, meaning idempotent method results stored in this map are cleared every time a garbage collector is triggered (such GC can be manually triggered thanks to the Java Visual VM tooling).

NB : this implementation is provided for test purpose only, and is quite not acceptable in production environments.

Excerpt of Spring Boot config application.yaml :

spring :
  application :
    name : my-rest-api

idempotence-receiver :
  repository :
    type : internal-memory

    resiliency :
      retry :
        delay-ms : 250
        max-retries : 2
      circuit-breaker:
        failure-threshold : 5
        success-threshold : 3
        delay-ms : 60000

Infinispan cache repository configuration

Select this implementation by setting the idempotence-receiver.repository.type property to infinispan-cache.

It configures an idempotence repository relying on a remote infinispan cache (or cluster of) that will be accessed via a Hot Rod Java client.

For instance, an infinispan server instance can be easily mounted via a docker environment (make sure to expose port 11222) :

docker run --rm -p 11222:11222 -it jboss/infinispan-server

NB : in case docker host is a VM running on a Windows OS, you'll need to create a SSH tunnel and map target port 11222 to source localhost:11222.

Look at IdempotentReceiverInfinispanHotrodConfiguration Javadoc to get an exhaustive list of available properties configurable in Spring Boot app configuration application.yaml.

Here below is an configuration sample to configure a repository that will connect to an infinispan server providing a default cache on localhost:11222 :

spring :
  application :
    name : my-rest-api

idempotence-receiver :
  repository :
    type : infinispan-cache

    resiliency :
      retry :
        delay-ms : 250
        max-retries : 2
      circuit-breaker:
        failure-threshold : 5
        success-threshold : 3
        delay-ms : 60000

    infinispan-cache :
      hotrod-client-configpath : classpath:/hotrod-client-config.properties
      cache-name : default
      ttl-ms : 120000

having a Hotrod client configuration hotrod-client-config.properties that should look like this :

# =============================================================================
# Connection properties
# =============================================================================
infinispan.client.hotrod.server_list=127.0.0.1:11222
infinispan.client.hotrod.tcp_no_delay=true
infinispan.client.hotrod.tcp_keep_alive=false
infinispan.client.hotrod.client_intelligence=BASIC
infinispan.client.hotrod.request_balancing_strategy=org.infinispan.client.hotrod.impl.transport.tcp.RoundRobinBalancingStrategy
infinispan.client.hotrod.socket_timeout=2000
infinispan.client.hotrod.connect_timeout=250
# NB : max_retries is forced to 0 as retry mechanism is already handled at a higher level
# (see 'idempotence-receiver.repository.resiliency.retry.*' properties in application.yaml)
infinispan.client.hotrod.max_retries=0
infinispan.client.hotrod.batch_size=10000
#infinispan.client.hotrod.protocol_version=

# =============================================================================
# Connection pool properties
# =============================================================================
infinispan.client.hotrod.connection_pool.max_active=10
infinispan.client.hotrod.connection_pool.exhausted_action=WAIT
infinispan.client.hotrod.connection_pool.max_wait=500
infinispan.client.hotrod.connection_pool.min_idle=5
infinispan.client.hotrod.connection_pool.min_evictable_idle_time=60000
infinispan.client.hotrod.connection_pool.max_pending_requests=-1

# =============================================================================
# Thread pool properties
# =============================================================================
infinispan.client.hotrod.async_executor_factory=org.infinispan.client.hotrod.impl.async.DefaultAsyncExecutorFactory
infinispan.client.hotrod.default_executor_factory.pool_size=20
infinispan.client.hotrod.default_executor_factory.threadname_prefix=HotRod-client-async-pool
#infinispan.client.hotrod.default_executor_factory.threadname_suffix=

NB : for a list of available configuration properties (hotrod client v9.4.5), see :

Update your RESTful API documentation and communicate it to consumers

All methods marked @Idempotent now require consumer services to provide a specific Idempotency-Key HTTP header in their requests in order to be able to consume your API.
This header should stand as a request unique identifier and therefore must vary from one request to another, except in case of request re-presentation (i.e. retries) where it MUST remain the same as the one initially set on request first presentation.
Consumers should use something like a UUID/GUID generator for that purpose. UUID v4 is a generally a good choice, even if entropy source must be considered as UUID v4 is random based.

Here is a list of some available UUID implementations your consumers can use :

NB : not providing the Idempotency-Key HTTP header in requests that requires it will lead to a 400 - Bad Request response, unless configuring property idempotence-receiver.idempotencyKeyHeaderMandatory to false in your RESTful application configuration. In this case, the idempotence mechanism will identify incoming requests by a hash dynamically computed using incoming request body contents, and other meta-informations (consumer IP address, URI, session id and principal identity when available). Nonetheless, this practice is discouraged.

About

This project aims to help idempotence mechanism integration into a Spring MVC RESTful service

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages