Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib-jedis-pool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Add this dependency to your `build.gradle`:

```gradle
dependencies {
implementation 'io.seqera:lib-jedis-pool:1.2.0'
implementation 'io.seqera:lib-jedis-pool:1.3.0'
}
```

Expand All @@ -32,8 +32,13 @@ redis:
maxIdle: 10 # Default: 10
maxTotal: 50 # Default: 50
testOnBorrow: false # Default: false — PING-validate connections on borrow
maxWait: -1 # Default: -1 (block indefinitely on an exhausted pool), millis
client:
timeout: 5000 # Default: 5000ms
timeout: 5000 # Default: 5000ms — connection and socket timeout
blockingTimeout: -1 # Default: -1 (inherit `timeout`), millis. Socket timeout for
# blocking reads (pub/sub subscribe, BLPOP/BRPOP, XREAD BLOCK).
# Use 0 for no timeout, required by long-lived subscribers that
# sit idle on the socket between messages.
```

## Usage
Expand Down
2 changes: 1 addition & 1 deletion lib-jedis-pool/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.0
1.3.0
9 changes: 9 additions & 0 deletions lib-jedis-pool/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# lib-jedis-pool changelog

1.3.0 - 4 Aug 2026
- Close the pool on bean disposal via @Bean(preDestroy='close'); previously the pool was
never disposed, letting background threads borrow from a pool whose connections were gone
- Add redis.client.blockingTimeout option for the blocking-read socket timeout (pub/sub
subscribe, BLPOP/BRPOP, XREAD BLOCK); default -1 inherits redis.client.timeout, 0 disables
the timeout as long-lived subscribers require
- Treat a blank redis.password override as absent instead of sending AUTH "" and failing
every connection against a password-less Redis

1.2.0 - 2 Aug 2026
- Add redis.pool.maxWait option bounding a borrow against an exhausted pool (millis);
default -1 preserves the pre-existing unbounded blocking behavior
Expand Down
35 changes: 26 additions & 9 deletions lib-jedis-pool/src/main/java/io/seqera/jedis/JedisPoolFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.time.Duration;

import io.micrometer.core.instrument.MeterRegistry;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
Expand Down Expand Up @@ -56,7 +57,13 @@ public class JedisPoolFactory {
@Inject
private MeterRegistry meterRegistry;

// preDestroy = 'close' makes Micronaut close the pool during bean disposal (reverse
// dependency-injection order), so beans that injected this pool are guaranteed to have
// run their @PreDestroy first. Without it the pool is never closed — JedisPool is
// Closeable but not a Micronaut LifeCycle, so nothing disposes it — and background
// consumer threads can call getResource() on a pool whose Redis connections are gone.
@Singleton
@Bean(preDestroy = "close")
public JedisPool createRedisPool(
@Value("${redis.uri}") String connection,
@Value("${redis.pool.minIdle:0}") int minIdle,
Expand All @@ -65,6 +72,7 @@ public JedisPool createRedisPool(
@Value("${redis.pool.testOnBorrow:false}") boolean testOnBorrow,
@Value("${redis.pool.maxWait:-1}") long maxWait,
@Value("${redis.client.timeout:5000}") int timeout,
@Value("${redis.client.blockingTimeout:-1}") int blockingTimeout,
@Nullable @Value("${redis.password}") String password
) {
final URI uri = URI.create(connection);
Expand All @@ -73,8 +81,8 @@ public JedisPool createRedisPool(
}
final int database = JedisURIHelper.getDBIndex(uri);

log.info("Creating Redis pool - uri={}; database={}; minIdle={}; maxIdle={}; maxTotal={}; testOnBorrow={}; maxWait={}; timeout={}",
maskPassword(connection), database, minIdle, maxIdle, maxTotal, testOnBorrow, maxWait, timeout);
log.info("Creating Redis pool - uri={}; database={}; minIdle={}; maxIdle={}; maxTotal={}; testOnBorrow={}; maxWait={}; timeout={}; blockingTimeout={}",
maskPassword(connection), database, minIdle, maxIdle, maxTotal, testOnBorrow, maxWait, timeout, blockingTimeout);

// Pool config
final JedisPoolConfig config = new JedisPoolConfig();
Expand All @@ -93,7 +101,7 @@ public JedisPool createRedisPool(
config.setMaxWait(Duration.ofMillis(maxWait));

// Client config with database support
final JedisClientConfig clientConfig = clientConfig(uri, password, timeout);
final JedisClientConfig clientConfig = clientConfig(uri, password, timeout, blockingTimeout);

// Create the Jedis pool
final JedisPool pool = new JedisPool(config, JedisURIHelper.getHostAndPort(uri), clientConfig);
Expand All @@ -109,22 +117,31 @@ public JedisPool createRedisPool(
/**
* Creates the Jedis client configuration from the URI.
*
* @param uri the Redis URI
* @param password optional password override (if null, extracted from URI)
* @param timeout connection timeout in milliseconds
* @param uri the Redis URI
* @param password optional password override (blank or null → extracted from URI)
* @param timeout connection and socket timeout in milliseconds
* @param blockingTimeout socket timeout applied to blocking reads (pub/sub subscribe,
* BLPOP/BRPOP, XREAD BLOCK) in milliseconds; 0 means no timeout
* and any negative value inherits {@code timeout}
* @return the configured JedisClientConfig
*/
protected JedisClientConfig clientConfig(URI uri, String password, int timeout) {
protected JedisClientConfig clientConfig(URI uri, String password, int timeout, int blockingTimeout) {
if (!JedisURIHelper.isValid(uri)) {
throw new InvalidURIException("Invalid Redis connection URI: " + uri);
}

return DefaultJedisClientConfig.builder()
.connectionTimeoutMillis(timeout)
.socketTimeoutMillis(timeout)
.blockingSocketTimeoutMillis(timeout)
// Jedis applies this timeout only while a blocking read is in flight, and treats
// 0 as "no timeout" — which a long-lived pub/sub subscriber needs, since it sits
// idle on the socket between messages and would otherwise be torn down every
// `timeout` ms. Negative inherits `timeout` to keep the previous behavior.
.blockingSocketTimeoutMillis(blockingTimeout < 0 ? timeout : blockingTimeout)
.user(JedisURIHelper.getUser(uri))
.password(password != null ? password : JedisURIHelper.getPassword(uri))
// an empty override means "no password configured" — passing it through would make
// Jedis send AUTH "" and fail every connection against a password-less Redis
.password(password != null && !password.isBlank() ? password : JedisURIHelper.getPassword(uri))
.database(JedisURIHelper.getDBIndex(uri))
.protocol(JedisURIHelper.getRedisProtocol(uri))
.ssl(JedisURIHelper.isRedisSSLScheme(uri))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class JedisPoolFactoryTest extends Specification {
def factory = new JedisPoolFactory(meterRegistry: Mock(MeterRegistry))

when:
def pool = factory.createRedisPool(URI_STRING, MIN_IDLE, MAX_IDLE, MAX_TOTAL, false, -1, TIMEOUT, 'password')
def pool = factory.createRedisPool(URI_STRING, MIN_IDLE, MAX_IDLE, MAX_TOTAL, false, -1, TIMEOUT, -1, 'password')

then:
pool != null
Expand All @@ -56,7 +56,7 @@ class JedisPoolFactoryTest extends Specification {
def factory = new JedisPoolFactory()

when:
def clientConfig = factory.clientConfig(URI.create(URI_STRING), null, 5000)
def clientConfig = factory.clientConfig(URI.create(URI_STRING), null, 5000, -1)

then:
clientConfig.database == EXPECTED_DB
Expand All @@ -75,7 +75,7 @@ class JedisPoolFactoryTest extends Specification {
def factory = new JedisPoolFactory(meterRegistry: Mock(MeterRegistry))

when:
factory.createRedisPool(URI_STRING, 0, 10, 50, false, -1, 5000, null)
factory.createRedisPool(URI_STRING, 0, 10, 50, false, -1, 5000, -1, null)

then:
def e = thrown(InvalidURIException)
Expand All @@ -92,7 +92,7 @@ class JedisPoolFactoryTest extends Specification {
def factory = new JedisPoolFactory()

when:
def pool = factory.createRedisPool('redis://localhost:6379', 0, 10, 50, ON_BORROW, -1, 5000, null)
def pool = factory.createRedisPool('redis://localhost:6379', 0, 10, 50, ON_BORROW, -1, 5000, -1, null)

then:
pool.testOnBorrow == ON_BORROW
Expand All @@ -109,7 +109,7 @@ class JedisPoolFactoryTest extends Specification {
def factory = new JedisPoolFactory()

when:
def pool = factory.createRedisPool('redis://localhost:6379', 0, 10, 50, false, MAX_WAIT, 5000, null)
def pool = factory.createRedisPool('redis://localhost:6379', 0, 10, 50, false, MAX_WAIT, 5000, -1, null)

then:
pool.maxWaitDuration == java.time.Duration.ofMillis(EXPECTED)
Expand All @@ -123,12 +123,51 @@ class JedisPoolFactoryTest extends Specification {
-1 | -1 // default: block indefinitely - the pre-existing commons-pool2 behavior, unchanged
}

def 'should apply the blocking socket timeout'() {
given:
def factory = new JedisPoolFactory()

when:
def clientConfig = factory.clientConfig(URI.create('redis://localhost:6379'), null, 5000, BLOCKING)

then:
clientConfig.blockingSocketTimeoutMillis == EXPECTED
and: 'the non-blocking timeout is unaffected'
clientConfig.socketTimeoutMillis == 5000

where:
BLOCKING | EXPECTED
0 | 0 // no timeout — an idle pub/sub subscriber is not torn down between messages
1000 | 1000
-1 | 5000 // inherit `timeout`, the pre-existing behavior
}

def 'should ignore a blank password override'() {
given:
def factory = new JedisPoolFactory()

when:
def clientConfig = factory.clientConfig(URI.create(URI_STRING), PASSWORD, 5000, -1)

then:
clientConfig.password == EXPECTED

where:
URI_STRING | PASSWORD | EXPECTED
'redis://localhost:6379' | '' | null // must not become AUTH ""
'redis://localhost:6379' | ' ' | null
'redis://localhost:6379' | null | null
'redis://localhost:6379' | 'secret' | 'secret'
'redis://:from-uri@localhost:6379' | '' | 'from-uri' // blank override falls back to the URI
'redis://:from-uri@localhost:6379' | 'secret' | 'secret'
}

def 'should create pool without meter registry'() {
given:
def factory = new JedisPoolFactory()

when:
def pool = factory.createRedisPool('redis://localhost:6379', 0, 10, 50, false, -1, 5000, null)
def pool = factory.createRedisPool('redis://localhost:6379', 0, 10, 50, false, -1, 5000, -1, null)

then:
pool != null
Expand Down
Loading