Home > Software engineering >  Getting an IllegalArgumentException: Cannot convert String to AtomicInteger when trying to add data
Getting an IllegalArgumentException: Cannot convert String to AtomicInteger when trying to add data

Time:11-28

I am trying to add a data model to a redis cache. When I take the model out I am getting the exception

org.springframework.core.convert.ConversionFailedException: Failed to convert from type [byte[]] to type [java.util.concurrent.atomic.AtomicInteger] for value '{53}'; nested exception is java.lang.IllegalArgumentException: Cannot convert String [5] to target class [java.util.concurrent.atomic.AtomicInteger]] with root cause

The model is

@RedisHash(value = "ThottleRate", timeToLive = 5)

public class ThottleRate implements Serializable  {
 
       private static final long serialVersionUID = 1L;

       @Id
       private String url;
       private AtomicInteger rate;

       public ThottleRate(String url, int rate) {
             super();
             this.url = url;
             this.rate = new AtomicInteger(rate);
       }

       public boolean isAllowed(){
           if(this.rate.decrementAndGet() <= 0) {
                    return false;
             }
             return true;
       }

The calling code is

try {
                    option = throttleRateRepository.findById(url);
                    ThottleRate rate = option.get();
                    allow = rate.isAllowed();
             } catch(NoSuchElementException e) {
                throttleRateRepository.save(new ThottleRate(url, 5));
                    allow = true;
             }

What I am trying to do is throttling functionality, using redis cache. It has a time to live, and within that a number of times a url can be accessed.

But when the call is

option = throttleRateRepository.findById(url);

This throws the IllegalArgumentException

This seems the most simple way to use redis cache with a time to live and a number of rates

CodePudding user response:

The exception is thrown because Spring Data Redis does not know how to deserialize array of bytes which is stored in Redis to AtomicInteger type when loading the model. You need to register a data type converter for this.

Here's sample data type converter:

@Component
@ReadingConverter
public class BytesToAtomicIntegerConverter implements Converter<byte[], AtomicInteger> {

  @Override
  public AtomicInteger convert(byte[] source) {
    if (ObjectUtils.isEmpty(source)) {
      return null;
    }

    int n = NumberUtils.parseNumber(
        new String(source, StandardCharsets.UTF_8), Integer.class);
    return new AtomicInteger(n);
  }
}

It can be registered with with Spring Data Redis by adding this bean to SpringBoot configuration:

@Bean
public RedisCustomConversions redisCustomConversions(
  BytesToAtomicIntegerConverter converter) {

  return new RedisCustomConversions(List.of(converter));
}

Once the data converter is registered the error will go away.

When implementing such solution you have to take few things in mind though. The AtomicInteger field is used here to keep count of requests for specific person. If there are more than one server that serve requests then different servers will end up having different value stored in this field in local memory. Since communication with Redis takes time (even if only in milliseconds range) there is far from zero chance the value will be updated by another server. This might not be an issue if such deviations are ok for the use case.

I also recommend to look at some existing libraries that could help to implement request throttling. Here're some examples:

  1. Throttling a Rest API in Java
  2. Rate Limiting a Spring API Using Bucket4j
  3. Implementing Throttling in Java (Spring Boot)
  • Related