/Jdempotent

Make your consumer, API, etc. idempotent easily.

Primary LanguageJavaMIT LicenseMIT

Jdempotent

Release Jdempotent

Goal of this Jdempotent-spring-boot-starter

Make your endpoints idempotent easily

Usage

  1. First of all, you need to add a dependency to pom.xml

For Redis:

<dependency>
    <groupId>com.trendyol</groupId>
    <artifactId>Jdempotent-spring-boot-redis-starter</artifactId>
    <version>1.0.11</version>
</dependency>

For Couchbase:

<dependency>
    <groupId>com.trendyol</groupId>
    <artifactId>Jdempotent-spring-boot-couchbase-starter</artifactId>
    <version>1.0.11</version>
</dependency>
  1. You should add @IdempotentResource annotation to the method that you want to make idempotent resource, listener etc.
@IdempotentResource(cachePrefix = "WelcomingListener")
@KafkaListener(topics = "trendyol.mail.welcome", groupId = "group_id")
public void consumeMessage(@IdempotentRequestPayload String emailAdress) {
    SendEmailRequest request = SendEmailRequest.builder()
            .email(message)
            .subject(subject)
            .build();

    try {
        mailSenderService.sendMail(request);
    } catch (MessagingException e) {
        logger.error("MailSenderService.sendEmail() throw exception {} event: {} ", e, emailAdress);

        // Throwing any exception is enough to delete from redis. When successful, it will not be deleted from redis and will be idempotent.
        throw new RetryIdempotentRequestException(e);
    }
}

If want that idempotencyId in your payload. Put @JdempotentId annotation that places the generated idempotency identifier into annotated field. Can be thought of as @Id annotation in jpa

For example:

public class IdempotentPaylaod {
 @JdempotentId
 private String jdempotentId;
 private Object data;
}
  1. If you want to handle a custom error case, you need to implement ErrorConditionalCallback like the following example:
@Component
public class AspectConditionalCallback implements ErrorConditionalCallback {

    @Override
    public boolean onErrorCondition(Object response) {
        return response == IdempotentStateEnum.ERROR;
    }
    
    public RuntimeException onErrorCustomException() {
        return new RuntimeException("Status cannot be error");
    }

}
  1. Let's make the configuration:

For redis configuration:

jdempotent:
  enable: true
  cache:
    redis:
      database: 1
      password: "password"
      sentinelHostList: 192.168.0.1,192.168.0.2,192.168.0.3
      sentinelPort: "26379"
      sentinelMasterName: "admin"
      expirationTimeHour: 2
      dialTimeoutSecond: 3
      readTimeoutSecond: 3
      writeTimeoutSecond: 3
      maxRetryCount: 3
      expireTimeoutHour: 3

For couchbase configuration:

jdempotent:
  enable: true
  cryptography:
    algorithm: MD5
  cache:
    couchbase:
      connection-string: XXXXXXXX
      password: XXXXXXXX
      username: XXXXXXXX
      bucket-name: XXXXXXXX
      connect-timeout: 100000
      query-timeout: 20000
      kv-timeout: 3000

Please note that you can disable Jdempotent easily if you need to. For example, assume that you don't have a circut breaker and your Redis is down. In that case, you can disable Jdempotent with the following configuration:

  enable: false
@SpringBootApplication(
      exclude = { RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class }
)

Performance

As it is shown in the following image, the most cpu consuming part of Jdempotent is getting a Redis connection so we don't need to worry performance related issues.

Docs

Jdempotent Medium Article
Jdempotent-core Javadoc
Jdempotent-spring-boot-redis-starter Javadoc

Support

memojja's twitter

Licence

MIT Licence

Contributing

  1. Fork it ( https://github.com/Trendyol/Jdempotent/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

  • memojja Mehmet ARI - creator, maintainer