Repository navigation
Description
Affected: 3.4.2 onwards (since #3072), including 4.0.x, 4.1.x and main. On 4.1+ the pool leak needs the legacy client path (setUseUnifiedJedis(false), the deprecated JedisPoolConfig constructors, or a Jedis version without RedisClient).
start() sets the state to STARTING, creates the pool, then calls pool.preparePool(). With minIdle > 0 and a Redis that is down or stalling, preparePool() throws and start() rethrows it as PoolException. After that:
- the state stays
STARTING.stop()only acts onSTARTED, so neitherstop()nordestroy()closes the pool (destroy()only setsDESTROYED); - a second
start()silently does nothing andgetConnection()fails, so the factory can never be used or retried; - the
poolfield is private, so the caller cannot close it either; - in a Spring container, a bean whose
afterPropertiesSet()threw is never destroyed, so the only place the pool can be released is insidestart()itself.
The abandoned GenericObjectPool stays registered in JMX (enabled by default), which keeps it reachable, and its evictor keeps running (JedisPoolConfig sets a 30 s interval). While Redis is down, every eviction run tries to create the missing idle connections and fails; once Redis is reachable again, every orphaned pool keeps reopening about minIdle connections (idle ones are evicted after 60 s and recreated) for the life of the JVM. With sentinels configured and the master unreachable, the same happens to the JedisSentinelPool, whose MasterListener threads also keep running.
An application that builds a new factory after a failure (for example one factory per Redis server, created on demand) leaks one pool per failed attempt, so an outage of a few minutes can leave enough orphaned pools behind to keep the server at maxclients after it has recovered.
The "cannot be retried" part is not limited to the legacy path: if creating the unified client throws (for example a cluster that is unreachable at startup), the factory also stays in STARTING.
Reproducer:
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMinIdle(1);
JedisConnectionFactory factory = new JedisConnectionFactory(
new RedisStandaloneConfiguration("127.0.0.1", 6390), // nothing listening
JedisClientConfiguration.builder().usePooling().poolConfig(poolConfig).build());
factory.setUseUnifiedJedis(false); // 4.1+ only
factory.afterPropertiesSet(); // PoolException: Could not prepare the pool
factory.destroy(); // sets DESTROYED, releases nothing
// org.apache.commons.pool2:type=GenericObjectPool,* is still registered, and the
// commons-pool-evictor thread keeps trying to create minIdle connections every 30 s.Expected: a start() that fails releases what it created and returns the factory to a state from which start() can be retried.
(Not covered here: if no sentinel answers, the JedisSentinelPool constructor itself throws after its pool was registered, so that pool never reaches Spring Data.)
I'm happy to send a PR: run the body of start() in a try/catch, dispose the created resources the same way stop() does, restore the previous state and rethrow.
Activity
Add tests for failed JedisConnectionFactory.start().
Release resources when JedisConnectionFactory.start() fails.
mp911de commentedon Oct 29, 2026
This issue is essentially a copy of #3484 with a high volume of prose and neither of these is helpful. Batch-creating pull requests and issues is missing the point of discussing a proposal entirely.
Please either create an issue to discuss aspects first or a proper pull request that could have a chance of being merged.