Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring RedisConnectionFactory with transaction not returning connection to Pool and then blocks when exhausted

My configuration for creating the connection factory with with a connection pool. I do have a connection pool. Most of this code is copied from Spring's RedisAutoConfiguration which I have disabled for particular reasons.

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class JedisConfiguration implements RedisConfiguration {

    @Bean
    @Scope("prototype")
    @Override
    public RedisConnectionFactory connectionFactory(RedisProperties redisProperties) {
        return createFactory(redisProperties);
    }

    private static JedisConnectionFactory applyProperties(RedisProperties properties, JedisConnectionFactory factory) {
        factory.setHostName(properties.getHost());
        factory.setPort(properties.getPort());
        factory.setDatabase(properties.getDatabase());
        return factory;
    }

    private static JedisPoolConfig jedisPoolConfig(RedisProperties properties) {
        return Optional.ofNullable(properties.getPool())
                       .map(props -> {
                           JedisPoolConfig config = new JedisPoolConfig();
                           config.setMaxTotal(props.getMaxActive());
                           config.setMaxIdle(props.getMaxIdle());
                           config.setMinIdle(props.getMinIdle());
                           config.setMaxWaitMillis(props.getMaxWait());
                           return config;
                       })
                       .orElseGet(JedisPoolConfig::new);
    }

    public static JedisConnectionFactory createFactory(RedisProperties properties) {
        return applyProperties(properties, new JedisConnectionFactory(jedisPoolConfig(properties)));
    }
}

Use Case

I have string keys "A", "B", "C" mapping to hash maps with string hash key and with hash values json serialized from class A, B, and C respectively.

That is "A" -> A::toString -> json(A) and same for B and C.

@Component
public final class UseCase implements InitializingBean {

    private static final String A_KEY = "A";
    private static final String B_KEY = "B";
    private static final String C_KEY = "C";

    private final RedisConnectionFactory factory;
    private final ObjectMapper objectMapper;
    private HashOperations<String, String, A> aMap;
    private HashOperations<String, String, B> bMap;
    private HashOperations<String, String, C> cMap;
    private RedisTemplate<String, ?> template;

    private UseCase(RedisConnectionFactory factory, ObjectMapper objectMapper) {
        this.factory = factory;
        this.objectMapper = objectMapper;
    }

    private <T> RedisTemplate<String, ?> hashMap(Class<T> vClass) {
        RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(stringSerializer());
        redisTemplate.setHashKeySerializer(stringSerializer());
        redisTemplate.setHashValueSerializer(jacksonSerializer(vClass));
        return configure(redisTemplate);
    }


    private <K, V> RedisTemplate<K, V> configure(RedisTemplate<K, V> redisTemplate) {
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    private <T> RedisSerializer<T> jacksonSerializer(Class<T> clazz) {
        Jackson2JsonRedisSerializer<T> serializer = new Jackson2JsonRedisSerializer<>(clazz);
        serializer.setObjectMapper(objectMapper);
        return serializer;
    }

    private RedisSerializer<String> stringSerializer() {
        return new StringRedisSerializer();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        template = hashMap(String.class);
        aMap = hashMap(A.class).opsForHash();
        bMap = hashMap(B.class).opsForHash();
        cMap = hashMap(C.class).opsForHash();
    }

    void put(A a, B b, C c) {
        template.multi();
        aMap.put(A_KEY, a.toString(), a);
        bMap.put(B_KEY, b.toString(), b);
        cMap.put(C_KEY, c.toString(), c);
        template.exec();
    }

    A getA(String aKey) {
        return aMap.get(A_KEY, aKey);
    }

}

Expectations

  1. That the put operation is executed with only one connection and should fail if the connection is lost or corrupted.
  2. That for the put operation, the connection is obtained at the multi call and returned to the Pool after the exec call.
  3. That for the getA operation, the connection is returned to the pool after execution.

I have tests to demonstrate that 1 works, however I am a bit skeptical of it but my problem is with the last two. After debugging, I observed that the connection is not returned to the Pool after either operation and thus the Pool gets blocked when it's exhausted.

The return is attempted but not invoked on the connection because the two branches below fail. Taken from RedisConnectionUtils

// release transactional/read-only and non-transactional/non-bound connections.
// transactional connections for read-only transactions get no synchronizer registered
if (isConnectionTransactional(conn, factory)
        && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
    unbindConnection(factory);
} else if (!isConnectionTransactional(conn, factory)) {
    if (log.isDebugEnabled()) {
        log.debug("Closing Redis Connection");
    }
    conn.close();
}

Questions

  1. What am I doing wrong?
  2. Why is the connection not returned to the Pool?
  3. How can I fix this so that the connection is returned to the Pool?
like image 821
Olayinka Avatar asked Sep 15 '17 11:09

Olayinka


1 Answers

I think the issue is that calling exec() doesn't tell the template that you're actually done with the connection so it can't be returned to the pool.

According to the docs you're supposed to wrap your code in a SessionCallback and execute it with RedisTemplate.execute(SessionCallback<T> callback) which will return the connection to the pool after your callback has executed.

Like this:

template.execute(new SessionCallback<List<Object>>() {
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        operations.multi();
        aMap.put(A_KEY, a.toString(), a);
        bMap.put(B_KEY, b.toString(), b);
        cMap.put(C_KEY, c.toString(), c);
        return operations.exec();
    }
});

Spring Data Redis also has support for @Transactional which will bind/unbind the connection automatically for you, but requires you to implement the method in a bean that can be intercepted (i.e. it can't be final) and transactions will only be started if executed from outside the bean (i.e. not from another method in the same class or a sub-/parent class).

You're already enabling transaction support on the template with redisTemplate.setEnableTransactionSupport(true); so you should be good to go:

@Transactional
public void put(A a, B b, C c) {
    aMap.put(A_KEY, a.toString(), a);
    bMap.put(B_KEY, b.toString(), b);
    cMap.put(C_KEY, c.toString(), c);
}
like image 88
Raniz Avatar answered Nov 01 '22 18:11

Raniz