From f0be40553d8fc962bb3a275925517a8520ed7933 Mon Sep 17 00:00:00 2001 From: benym Date: Tue, 20 Jan 2026 18:39:39 +0800 Subject: [PATCH 1/8] feat(session): multi redis client session adapter --- agentscope-dependencies-bom/pom.xml | 8 + .../README.md | 245 +++++++++++++ .../README_zh.md | 245 +++++++++++++ .../pom.xml | 8 +- .../session/redis/RedisClientAdapter.java | 136 +++++++ .../JedisSession.java => RedisSession.java} | 290 ++++++++++----- .../redis/jedis/JedisClientAdapter.java | 166 +++++++++ .../redis/lettuce/LettuceClientAdapter.java | 198 +++++++++++ .../redis/redisson/RedissonClientAdapter.java | 219 ++++++++++++ .../redis/redisson/RedissonSession.java | 333 ------------------ .../core/session/redis/JedisSessionTest.java | 135 ++++--- .../session/redis/LettuceSessionTest.java | 296 ++++++++++++++++ .../session/redis/RedissonSessionTest.java | 76 ++-- 13 files changed, 1839 insertions(+), 516 deletions(-) create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/README.md create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/README_zh.md create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java rename agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/{jedis/JedisSession.java => RedisSession.java} (56%) create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java delete mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java diff --git a/agentscope-dependencies-bom/pom.xml b/agentscope-dependencies-bom/pom.xml index a138575d0..d265beaea 100644 --- a/agentscope-dependencies-bom/pom.xml +++ b/agentscope-dependencies-bom/pom.xml @@ -97,6 +97,7 @@ 0.3.3.Final 0.3.3.Final 7.2.0 + 6.4.2.RELEASE 3.3.2 2.5.2 7.0.3 @@ -363,6 +364,13 @@ ${jedis.version} + + + io.lettuce + lettuce-core + ${lettuce.version} + + com.xuxueli diff --git a/agentscope-extensions/agentscope-extensions-session-redis/README.md b/agentscope-extensions/agentscope-extensions-session-redis/README.md new file mode 100644 index 000000000..28cce1d42 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/README.md @@ -0,0 +1,245 @@ +# Redis Session Extension + +Redis-based session implementation for AgentScope supporting multiple Redis clients. + +## Overview + +This extension provides a Redis-based session storage implementation for AgentScope, supporting multiple Redis client libraries and deployment modes. + +## Supported Redis Clients + +- **Jedis** - Standalone, Cluster, Sentinel +- **Lettuce** - Standalone, Cluster, Sentinel +- **Redisson** - Standalone, Cluster, Sentinel, Master/Slave + +## Maven Dependency + +```xml + + io.agentscope + agentscope-extensions-session-redis + 1.0.8-SNAPSHOT + +``` + +## Usage Examples + +### Jedis + +#### Standalone + +```java +import redis.clients.jedis.RedisClient; +import io.agentscope.core.session.redis.RedisSession; + +// Create Jedis RedisClient +RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + +// Build RedisSession +Session session = RedisSession.builder() + .jedisClient(redisClient) + .build(); +``` + +#### Cluster + +```java +import redis.clients.jedis.RedisClusterClient; +import redis.clients.jedis.HostAndPort; +import io.agentscope.core.session.redis.RedisSession; + +// Create Jedis RedisClusterClient +Set nodes = new HashSet<>(); +nodes.add(new HostAndPort("localhost", 7000)); +nodes.add(new HostAndPort("localhost", 7001)); +nodes.add(new HostAndPort("localhost", 7002)); +RedisClusterClient clusterClient = RedisClusterClient.create(nodes); + +// Build RedisSession +Session session = RedisSession.builder() + .jedisClient(clusterClient) + .build(); +``` + +#### Sentinel + +```java +import redis.clients.jedis.RedisSentinelClient; +import io.agentscope.core.session.redis.RedisSession; + +// Create Jedis RedisSentinelClient +Set sentinelNodes = new HashSet<>(); +sentinelNodes.add("localhost:26379"); +sentinelNodes.add("localhost:26380"); +RedisSentinelClient sentinelClient = RedisSentinelClient.create("mymaster", sentinelNodes); + +// Build RedisSession +Session session = RedisSession.builder() + .jedisClient(sentinelClient) + .build(); +``` + +### Lettuce + +#### Standalone + +```java +import io.lettuce.core.RedisClient; +import io.agentscope.core.session.redis.RedisSession; + +// Create Lettuce RedisClient +RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + +// Build RedisSession +Session session = RedisSession.builder() + .lettuceClient(redisClient) + .build(); +``` + +#### Cluster + +```java +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.agentscope.core.session.redis.RedisSession; + +// Create Lettuce RedisClient for cluster +RedisURI clusterUri = RedisURI.create("redis://localhost:7000"); +RedisClient redisClient = RedisClient.create(clusterUri); + +// Build RedisSession +Session session = RedisSession.builder() + .lettuceClient(redisClient) + .build(); +``` + +#### Sentinel + +```java +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.agentscope.core.session.redis.RedisSession; + +// Create Lettuce RedisClient for sentinel +RedisURI sentinelUri = RedisURI.builder() + .withSentinelMasterId("mymaster") + .withSentinel("localhost", 26379) + .withSentinel("localhost", 26380) + .build(); +RedisClient redisClient = RedisClient.create(sentinelUri); + +// Build RedisSession +Session session = RedisSession.builder() + .lettuceClient(redisClient) + .build(); +``` + +### Redisson + +```java +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import io.agentscope.core.session.redis.RedisSession; + +// Create RedissonClient +Config config = new Config(); +config.useSingleServer().setAddress("redis://localhost:6379"); +// cluster: config.useClusterServers().addNodeAddress("redis://localhost:7000"); +// sentinel: config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("redis://localhost:26379"); + +RedissonClient redissonClient = Redisson.create(config); + +// Build RedisSession +Session session = RedisSession.builder() + .redissonClient(redissonClient) + .build(); +``` + +### Custom Key Prefix + +```java +import redis.clients.jedis.RedisClient; +import io.agentscope.core.session.redis.RedisSession; + +// Create Redis client +RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + +// Build RedisSession with custom key prefix +Session session = RedisSession.builder() + .jedisClient(redisClient) + .keyPrefix("myapp:session:") + .build(); +``` + +## Key Structure + +The session state is stored in Redis with following key structure: + +- Single state: `{prefix}{sessionId}:{stateKey}` - Redis String containing JSON +- List state: `{prefix}{sessionId}:{stateKey}:list` - Redis List containing JSON items +- List hash: `{prefix}{sessionId}:{stateKey}:list:_hash` - Hash for change detection +- Session marker: `{prefix}{sessionId}:_keys` - Redis Set tracking all state keys + +## Core Functionality + +### Save Single + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +TestState state = new TestState("value", 42); + +session.save(sessionKey, "testModule", state); +``` + +### Get Single + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +Optional state = session.get(sessionKey, "testModule", TestState.class); +``` + +### Save List + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +List states = List.of( + new TestState("value1", 1), + new TestState("value2", 2) +); + +session.save(sessionKey, "testList", states); +``` + +### Get List + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +List states = session.getList(sessionKey, "testList", TestState.class); +``` + +### Check Session Existence + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +boolean exists = session.exists(sessionKey); +``` + +### Delete Session + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +session.delete(sessionKey); +``` + +### List All Sessions + +```java +Set sessionKeys = session.listSessionKeys(); +``` + +### Clear All Sessions + +```java +Mono deletedCount = session.clearAllSessions(); +long count = deletedCount.block(); +``` diff --git a/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md b/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md new file mode 100644 index 000000000..eed8a6b0b --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md @@ -0,0 +1,245 @@ +# Redis Session 扩展 + +基于Redis的AgentScope会话实现,支持多种Redis客户端。 + +## 概述 + +此扩展为AgentScope提供了基于Redis的会话存储实现,支持多种Redis客户端库和部署模式。 + +## 支持的Redis客户端 + +- **Jedis** - 单机、集群、哨兵 +- **Lettuce** - 单机、集群、哨兵 +- **Redisson** - 单机、集群、哨兵、主从 + +## Maven依赖 + +```xml + + io.agentscope + agentscope-extensions-session-redis + 1.0.8-SNAPSHOT + +``` + +## 使用示例 + +### Jedis + +#### 单机模式 + +```java +import redis.clients.jedis.RedisClient; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Jedis RedisClient +RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + +// 构建RedisSession +Session session = RedisSession.builder() + .jedisClient(redisClient) + .build(); +``` + +#### 集群模式 + +```java +import redis.clients.jedis.RedisClusterClient; +import redis.clients.jedis.HostAndPort; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Jedis RedisClusterClient +Set nodes = new HashSet<>(); +nodes.add(new HostAndPort("localhost", 7000)); +nodes.add(new HostAndPort("localhost", 7001)); +nodes.add(new HostAndPort("localhost", 7002)); +RedisClusterClient clusterClient = RedisClusterClient.create(nodes); + +// 构建RedisSession +Session session = RedisSession.builder() + .jedisClient(clusterClient) + .build(); +``` + +#### 哨兵模式 + +```java +import redis.clients.jedis.RedisSentinelClient; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Jedis RedisSentinelClient +Set sentinelNodes = new HashSet<>(); +sentinelNodes.add("localhost:26379"); +sentinelNodes.add("localhost:26380"); +RedisSentinelClient sentinelClient = RedisSentinelClient.create("mymaster", sentinelNodes); + +// 构建RedisSession +Session session = RedisSession.builder() + .jedisClient(sentinelClient) + .build(); +``` + +### Lettuce + +#### 单机模式 + +```java +import io.lettuce.core.RedisClient; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Lettuce RedisClient +RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + +// 构建RedisSession +Session session = RedisSession.builder() + .lettuceClient(redisClient) + .build(); +``` + +#### 集群模式 + +```java +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Lettuce RedisClient用于集群 +RedisURI clusterUri = RedisURI.create("redis://localhost:7000"); +RedisClient redisClient = RedisClient.create(clusterUri); + +// 构建RedisSession +Session session = RedisSession.builder() + .lettuceClient(redisClient) + .build(); +``` + +#### 哨兵模式 + +```java +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Lettuce RedisClient用于哨兵 +RedisURI sentinelUri = RedisURI.builder() + .withSentinelMasterId("mymaster") + .withSentinel("localhost", 26379) + .withSentinel("localhost", 26380) + .build(); +RedisClient redisClient = RedisClient.create(sentinelUri); + +// 构建RedisSession +Session session = RedisSession.builder() + .lettuceClient(redisClient) + .build(); +``` + +### Redisson + +```java +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import io.agentscope.core.session.redis.RedisSession; + +// 创建RedissonClient +Config config = new Config(); +config.useSingleServer().setAddress("redis://localhost:6379"); +// 集群模式: config.useClusterServers().addNodeAddress("redis://localhost:7000"); +// 哨兵模式: config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("redis://localhost:26379"); + +RedissonClient redissonClient = Redisson.create(config); + +// 构建RedisSession +Session session = RedisSession.builder() + .redissonClient(redissonClient) + .build(); +``` + +### 自定义前缀 + +```java +import redis.clients.jedis.RedisClient; +import io.agentscope.core.session.redis.RedisSession; + +// 创建Redis客户端 +RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + +// 构建带有自定义键前缀的RedisSession +Session session = RedisSession.builder() + .jedisClient(redisClient) + .keyPrefix("myapp:session:") + .build(); +``` + +## key结构 + +会话状态在Redis中以下列键结构存储: + +- 单个状态: `{prefix}{sessionId}:{stateKey}` - 包含 JSON 的 Redis String +- 列表状态: `{prefix}{sessionId}:{stateKey}:list` - 包含 JSON 项的 Redis List +- 列表哈希: `{prefix}{sessionId}:{stateKey}:list:_hash` - 用于变更检测的哈希 +- 会话标记: `{prefix}{sessionId}:_keys` - 跟踪所有状态键的 Redis Set + +## 核心功能 + +### 保存单个 + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +TestState state = new TestState("value", 42); + +session.save(sessionKey, "testModule", state); +``` + +### 获取单个 + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +Optional state = session.get(sessionKey, "testModule", TestState.class); +``` + +### 保存列表 + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +List states = List.of( + new TestState("value1", 1), + new TestState("value2", 2) +); + +session.save(sessionKey, "testList", states); +``` + +### 获取列表 + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +List states = session.getList(sessionKey, "testList", TestState.class); +``` + +### 检查session是否存在 + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +boolean exists = session.exists(sessionKey); +``` + +### 删除session + +```java +SessionKey sessionKey = SimpleSessionKey.of("session1"); +session.delete(sessionKey); +``` + +### 列出所有session + +```java +Set sessionKeys = session.listSessionKeys(); +``` + +### 清空所有session + +```java +Mono deletedCount = session.clearAllSessions(); +long count = deletedCount.block(); +``` \ No newline at end of file diff --git a/agentscope-extensions/agentscope-extensions-session-redis/pom.xml b/agentscope-extensions/agentscope-extensions-session-redis/pom.xml index 3559490a7..27eb2cf3c 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/pom.xml +++ b/agentscope-extensions/agentscope-extensions-session-redis/pom.xml @@ -44,11 +44,17 @@ redisson - + redis.clients jedis + + + io.lettuce + lettuce-core + + diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java new file mode 100644 index 000000000..115628506 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java @@ -0,0 +1,136 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis; + +import java.util.List; +import java.util.Set; + +/** + * Adapter interface for Redis client operations. + * + *

This interface provides a unified abstraction over different Redis client implementations + * (Jedis, Lettuce, Redisson), allowing the RedisSession to work with any of them without + * modification. + * + *

All method names are designed to be self-explanatory and follow a consistent naming pattern: + *

+ * + * @author Kevin + * @author jianjun.xu + * @author benym + * @since 1.0.8 + */ +public interface RedisClientAdapter { + + /** + * Set a string value. + * + * @param key the Redis key + * @param value the string value + */ + void set(String key, String value); + + /** + * Get a string value. + * + * @param key the Redis key + * @return the string value, or null if not found + */ + String get(String key); + + /** + * Append a value to the right end of a list. + * + * @param key the Redis list key + * @param value the value to append + */ + void rightPushList(String key, String value); + + /** + * Get a range of elements from a list. + * + * @param key the Redis list key + * @param start the start index (inclusive) + * @param end the end index (inclusive, -1 for all elements) + * @return list of values + */ + List rangeList(String key, long start, long end); + + /** + * Get the length of a list. + * + * @param key the Redis list key + * @return the length of the list + */ + long getListLength(String key); + + /** + * Delete one or more keys. + * + * @param keys the keys to delete + */ + void deleteKeys(String... keys); + + /** + * Add a member to a set. + * + * @param key the Redis set key + * @param member the member to add + */ + void addToSet(String key, String member); + + /** + * Get all members of a set. + * + * @param key the Redis set key + * @return set of members + */ + Set getSetMembers(String key); + + /** + * Get the number of members in a set. + * + * @param key the Redis set key + * @return the number of members + */ + long getSetSize(String key); + + /** + * Check if a key exists. + * + * @param key the Redis key + * @return true if the key exists + */ + boolean keyExists(String key); + + /** + * Find all keys matching a pattern. + * + * @param pattern the key pattern (e.g., "prefix:*") + * @return set of matching keys + */ + Set findKeysByPattern(String pattern); + + /** + * Close the adapter and release resources. + */ + void close(); +} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java similarity index 56% rename from agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java rename to agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java index b4a474cd5..7a86f1eb7 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.agentscope.core.session.redis.jedis; +package io.agentscope.core.session.redis; import io.agentscope.core.session.ListHashUtil; import io.agentscope.core.session.Session; +import io.agentscope.core.session.redis.jedis.JedisClientAdapter; +import io.agentscope.core.session.redis.lettuce.LettuceClientAdapter; +import io.agentscope.core.session.redis.redisson.RedissonClientAdapter; import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; @@ -26,53 +29,183 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import org.redisson.api.RedissonClient; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.UnifiedJedis; /** - * Redis-based session implementation using Jedis. + * Redis-based session implementation supporting multiple Redis clients. * - *

This implementation stores session state in Redis with the following key structure: + *

This implementation provides a unified interface for Redis-based session storage, supporting + * multiple Redis client implementations: + * + *

    + *
  • Jedis - Standalone, Cluster, Sentinel
  • + *
  • Lettuce - Standalone, Cluster, Sentinel
  • + *
  • Redisson - Standalone, Cluster, Sentinel, Master/Slave
  • + *
+ * + *

The session state is stored in Redis with following key structure: * *

    *
  • Single state: {@code {prefix}{sessionId}:{stateKey}} - Redis String containing JSON *
  • List state: {@code {prefix}{sessionId}:{stateKey}:list} - Redis List containing JSON items + *
  • List hash: {@code {prefix}{sessionId}:{stateKey}:list:_hash} - Hash for change detection *
  • Session marker: {@code {prefix}{sessionId}:_keys} - Redis Set tracking all state keys *
* - *

Features: + *

Jedis Usage Examples:

* - *
    - *
  • Incremental list storage (only appends new items) - *
  • Type-safe state serialization using Jackson - *
  • Automatic session key tracking - *
+ *

Jedis Standalone (using RedisClient): + * + *

{@code
+ * // Create Jedis RedisClient (new API)
+ * RedisClient redisClient = RedisClient.create("redis://localhost:6379");
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .redisClient(redisClient)
+ *     .build();
+ * }
+ * + *

Jedis Cluster (using RedisClusterClient): + * + *

{@code
+ * // Create Jedis RedisClusterClient
+ * Set nodes = new HashSet<>();
+ * nodes.add(new HostAndPort("localhost", 7000));
+ * nodes.add(new HostAndPort("localhost", 7001));
+ * nodes.add(new HostAndPort("localhost", 7002));
+ * RedisClusterClient redisClusterClient = RedisClusterClient.create(nodes);
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .redisClusterClient(redisClusterClient)
+ *     .build();
+ * }
+ * + *

Jedis Sentinel (using RedisSentinelClient): + * + *

{@code
+ * // Create Jedis RedisSentinelClient
+ * Set sentinelNodes = new HashSet<>();
+ * sentinelNodes.add("localhost:26379");
+ * sentinelNodes.add("localhost:26380");
+ * RedisSentinelClient redisSentinelClient = RedisSentinelClient.create("mymaster", sentinelNodes);
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .redisSentinelClient(redisSentinelClient)
+ *     .build();
+ * }
+ * + *

Lettuce Usage Examples:

+ * + *

Lettuce Standalone: + * + *

{@code
+ * // Create Lettuce RedisClient
+ * RedisClient redisClient = RedisClient.create("redis://localhost:6379");
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .lettuceClient(redisClient)
+ *     .build();
+ * }
+ * + *

Lettuce Cluster: + * + *

{@code
+ * // Create Lettuce RedisClient for cluster
+ * RedisURI clusterUri = RedisURI.create("redis://localhost:7000");
+ * RedisClient redisClient = RedisClient.create(clusterUri);
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .lettuceClient(redisClient)
+ *     .build();
+ * }
+ * + *

Lettuce Sentinel: + * + *

{@code
+ * // Create Lettuce RedisClient for sentinel
+ * RedisURI sentinelUri = RedisURI.builder()
+ *     .withSentinelMasterId("mymaster")
+ *     .withSentinel("localhost", 26379)
+ *     .withSentinel("localhost", 26380)
+ *     .build();
+ * RedisClient redisClient = RedisClient.create(sentinelUri);
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .lettuceClient(redisClient)
+ *     .build();
+ * }
+ * + *

Redisson Usage Example:

+ * + *
{@code
+ * // Create RedissonClient (configure as needed for your deployment mode)
+ * Config config = new Config();
+ * config.useSingleServer().setAddress("redis://localhost:6379");
+ * // or for cluster: config.useClusterServers().addNodeAddress("redis://localhost:7000");
+ * // or for sentinel: config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("redis://localhost:26379");
+ *
+ * RedissonClient redissonClient = Redisson.create(config);
+ *
+ * // Build RedisSession
+ * Session session = RedisSession.builder()
+ *     .redissonClient(redissonClient)
+ *     .build();
+ * }
+ * + *

Custom Key Prefix Example:

+ * + *
{@code
+ * // Create Redis client
+ * RedisClient redisClient = RedisClient.create("redis://localhost:6379");
+ *
+ * // Build RedisSession with custom key prefix
+ * Session session = RedisSession.builder()
+ *     .redisClient(redisClient)
+ *     .keyPrefix("myapp:session:")
+ *     .build();
+ * }
+ * + * @author Kevin + * @author jianjun.xu + * @author benym + * @since 1.0.8 */ -public class JedisSession implements Session { +public class RedisSession implements Session { private static final String DEFAULT_KEY_PREFIX = "agentscope:session:"; + private static final String KEYS_SUFFIX = ":_keys"; + private static final String LIST_SUFFIX = ":list"; + private static final String HASH_SUFFIX = ":_hash"; - private final JedisPool jedisPool; + private final RedisClientAdapter client; + private final String keyPrefix; - private JedisSession(Builder builder) { + private RedisSession(Builder builder) { + if (builder.client == null) { + throw new IllegalArgumentException("Redis client cannot be null"); + } if (builder.keyPrefix == null || builder.keyPrefix.trim().isEmpty()) { throw new IllegalArgumentException("Key prefix cannot be null or empty"); } - if (builder.jedisPool == null) { - throw new IllegalArgumentException("JedisPool cannot be null"); - } + this.client = builder.client; this.keyPrefix = builder.keyPrefix; - this.jedisPool = builder.jedisPool; } /** - * Creates a new builder for {@link JedisSession}. + * Creates a new builder for {@link RedisSession}. * * @return a new Builder instance */ @@ -85,77 +218,53 @@ public void save(SessionKey sessionKey, String key, State value) { String sessionId = sessionKey.toIdentifier(); String redisKey = getStateKey(sessionId, key); String keysKey = getKeysKey(sessionId); - - try (Jedis jedis = jedisPool.getResource()) { + try { String json = JsonUtils.getJsonCodec().toJson(value); - jedis.set(redisKey, json); + client.set(redisKey, json); // Track this key in the session's key set - jedis.sadd(keysKey, key); + client.addToSet(keysKey, key); } catch (Exception e) { throw new RuntimeException("Failed to save state: " + key, e); } } - /** - * Save a list of state values with hash-based change detection. - * - *

This method uses hash-based change detection to handle both append-only and mutable lists: - * - *

    - *
  • If the hash changes (list was modified), the Redis list is deleted and recreated - *
  • If the list shrinks, the Redis list is deleted and recreated - *
  • If the list only grows (append-only), only new items are appended - *
  • If nothing changes, the operation is skipped - *
- * - * @param sessionKey the session identifier - * @param key the state key (e.g., "memory_messages") - * @param values the list of state values to save - */ @Override public void save(SessionKey sessionKey, String key, List values) { String sessionId = sessionKey.toIdentifier(); String listKey = getListKey(sessionId, key); String hashKey = listKey + HASH_SUFFIX; String keysKey = getKeysKey(sessionId); - - try (Jedis jedis = jedisPool.getResource()) { + try { // Compute current hash String currentHash = ListHashUtil.computeHash(values); - // Get stored hash - String storedHash = jedis.get(hashKey); - + String storedHash = client.get(hashKey); // Get current list length - long existingCount = jedis.llen(listKey); - + long existingCount = client.getListLength(listKey); // Determine if full rewrite is needed boolean needsFullRewrite = ListHashUtil.needsFullRewrite( currentHash, storedHash, values.size(), (int) existingCount); - if (needsFullRewrite) { // Delete and recreate the list - jedis.del(listKey); + client.deleteKeys(listKey); for (State item : values) { String json = JsonUtils.getJsonCodec().toJson(item); - jedis.rpush(listKey, json); + client.rightPushList(listKey, json); } } else if (values.size() > existingCount) { // Incremental append List newItems = values.subList((int) existingCount, values.size()); for (State item : newItems) { String json = JsonUtils.getJsonCodec().toJson(item); - jedis.rpush(listKey, json); + client.rightPushList(listKey, json); } } // else: no change, skip - // Update hash - jedis.set(hashKey, currentHash); - + client.set(hashKey, currentHash); // Track this key in the session's key set - jedis.sadd(keysKey, key + LIST_SUFFIX); + client.addToSet(keysKey, key + LIST_SUFFIX); } catch (Exception e) { throw new RuntimeException("Failed to save list: " + key, e); } @@ -165,9 +274,8 @@ public void save(SessionKey sessionKey, String key, List values public Optional get(SessionKey sessionKey, String key, Class type) { String sessionId = sessionKey.toIdentifier(); String redisKey = getStateKey(sessionId, key); - - try (Jedis jedis = jedisPool.getResource()) { - String json = jedis.get(redisKey); + try { + String json = client.get(redisKey); if (json == null) { return Optional.empty(); } @@ -181,13 +289,11 @@ public Optional get(SessionKey sessionKey, String key, Clas public List getList(SessionKey sessionKey, String key, Class itemType) { String sessionId = sessionKey.toIdentifier(); String redisKey = getListKey(sessionId, key); - - try (Jedis jedis = jedisPool.getResource()) { - List jsonList = jedis.lrange(redisKey, 0, -1); + try { + List jsonList = client.rangeList(redisKey, 0, -1); if (jsonList == null || jsonList.isEmpty()) { return List.of(); } - List result = new ArrayList<>(); for (String json : jsonList) { T item = JsonUtils.getJsonCodec().fromJson(json, itemType); @@ -204,9 +310,9 @@ public boolean exists(SessionKey sessionKey) { String sessionId = sessionKey.toIdentifier(); String keysKey = getKeysKey(sessionId); - try (Jedis jedis = jedisPool.getResource()) { + try { // Session exists if it has any tracked keys - return jedis.exists(keysKey) && jedis.scard(keysKey) > 0; + return client.keyExists(keysKey) && client.getSetSize(keysKey) > 0; } catch (Exception e) { throw new RuntimeException("Failed to check session existence: " + sessionId, e); } @@ -217,9 +323,9 @@ public void delete(SessionKey sessionKey) { String sessionId = sessionKey.toIdentifier(); String keysKey = getKeysKey(sessionId); - try (Jedis jedis = jedisPool.getResource()) { + try { // Get all tracked keys for this session - Set trackedKeys = jedis.smembers(keysKey); + Set trackedKeys = client.getSetMembers(keysKey); if (trackedKeys != null && !trackedKeys.isEmpty()) { // Build list of actual Redis keys to delete @@ -232,13 +338,14 @@ public void delete(SessionKey sessionKey) { String baseKey = trackedKey.substring(0, trackedKey.length() - LIST_SUFFIX.length()); keysToDelete.add(getListKey(sessionId, baseKey)); + keysToDelete.add(getListKey(sessionId, baseKey) + HASH_SUFFIX); } else { // It's a single state key keysToDelete.add(getStateKey(sessionId, trackedKey)); } } - jedis.del(keysToDelete.toArray(new String[0])); + client.deleteKeys(keysToDelete.toArray(new String[0])); } } catch (Exception e) { throw new RuntimeException("Failed to delete session: " + sessionId, e); @@ -247,10 +354,9 @@ public void delete(SessionKey sessionKey) { @Override public Set listSessionKeys() { - try (Jedis jedis = jedisPool.getResource()) { + try { + Set keysKeys = client.findKeysByPattern(keyPrefix + "*" + KEYS_SUFFIX); // Find all session key sets - Set keysKeys = jedis.keys(keyPrefix + "*" + KEYS_SUFFIX); - Set sessionKeys = new HashSet<>(); for (String keysKey : keysKeys) { // Extract session ID from the keys key @@ -268,7 +374,7 @@ public Set listSessionKeys() { @Override public void close() { - jedisPool.close(); + client.close(); } /** @@ -279,10 +385,10 @@ public void close() { public Mono clearAllSessions() { return Mono.fromSupplier( () -> { - try (Jedis jedis = jedisPool.getResource()) { - Set keys = jedis.keys(keyPrefix + "*"); + try { + Set keys = client.findKeysByPattern(keyPrefix + "*"); if (!keys.isEmpty()) { - jedis.del(keys.toArray(new String[0])); + client.deleteKeys(keys.toArray(new String[0])); } return keys.size(); } catch (Exception e) { @@ -325,25 +431,51 @@ private String getKeysKey(String sessionId) { } /** - * Builder for {@link JedisSession}. + * Builder for {@link RedisSession}. + * + *

The builder supports multiple Redis client types. Only one client type should be set. + * + *

Supported client types: + *

    + *
  • Jedis: {@link #jedisClient(UnifiedJedis)} + *
  • Lettuce: {@link #lettuceClient(io.lettuce.core.RedisClient)} + *
  • Redisson: {@link #redissonClient(RedissonClient)} + *
  • Custom: {@link #clientAdapter(RedisClientAdapter)} + *
*/ public static class Builder { private String keyPrefix = DEFAULT_KEY_PREFIX; - private JedisPool jedisPool; + + private RedisClientAdapter client; public Builder keyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; return this; } - public Builder jedisPool(JedisPool jedisPool) { - this.jedisPool = jedisPool; + public Builder jedisClient(UnifiedJedis unifiedJedis) { + this.client = JedisClientAdapter.of(unifiedJedis); + return this; + } + + public Builder lettuceClient(io.lettuce.core.RedisClient redisClient) { + this.client = LettuceClientAdapter.of(redisClient); + return this; + } + + public Builder redissonClient(RedissonClient redissonClient) { + this.client = RedissonClientAdapter.of(redissonClient); + return this; + } + + public Builder clientAdapter(RedisClientAdapter clientAdapter) { + this.client = clientAdapter; return this; } - public JedisSession build() { - return new JedisSession(this); + public RedisSession build() { + return new RedisSession(this); } } } diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java new file mode 100644 index 000000000..b767b2be0 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java @@ -0,0 +1,166 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis.jedis; + +import io.agentscope.core.session.redis.RedisClientAdapter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import redis.clients.jedis.RedisClient; +import redis.clients.jedis.RedisClusterClient; +import redis.clients.jedis.RedisSentinelClient; +import redis.clients.jedis.UnifiedJedis; + +/** + * Adapter for Jedis Redis client. + * + *

This adapter supports all Jedis client types: + *

    + *
  • {@link RedisClient} - Standalone client
  • + *
  • {@link RedisClusterClient} - Cluster client
  • + *
  • {@link RedisSentinelClient} - Sentinel client
  • + *
  • {@link UnifiedJedis} - Base unified interface
  • + *
+ * + *

All new client types extend {@link UnifiedJedis}, providing a unified interface that + * handles different Redis deployment modes transparently. + * + *

Usage examples: + * + *

Using RedisClient (Standalone): + *

{@code
+ * // Create standalone client
+ * RedisClient redisClient = RedisClient.create("redis://localhost:6379");
+ *
+ * // Create adapter
+ * JedisClientAdapter adapter = JedisClientAdapter.of(redisClient);
+ * }
+ * + *

Using RedisClusterClient (Cluster): + *

{@code
+ * // Create cluster client
+ * Set nodes = new HashSet<>();
+ * nodes.add(new HostAndPort("localhost", 7000));
+ * nodes.add(new HostAndPort("localhost", 7001));
+ * RedisClusterClient clusterClient = RedisClusterClient.create(nodes);
+ *
+ * // Create adapter
+ * JedisClientAdapter adapter = JedisClientAdapter.of(clusterClient);
+ * }
+ * + *

Using RedisSentinelClient (Sentinel): + *

{@code
+ * // Create sentinel client
+ * Set sentinels = new HashSet<>();
+ * sentinels.add("localhost:26379");
+ * RedisSentinelClient sentinelClient = RedisSentinelClient.create("mymaster", sentinels);
+ *
+ * // Create adapter
+ * JedisClientAdapter adapter = JedisClientAdapter.of(sentinelClient);
+ * }
+ * + *

Using UnifiedJedis (Direct): + *

{@code
+ * // Create any client type that extends UnifiedJedis
+ * UnifiedJedis unifiedJedis = new RedisClient("redis://localhost:6379");
+ *
+ * // Create adapter
+ * JedisClientAdapter adapter = JedisClientAdapter.of(unifiedJedis);
+ * }
+ * + * @author Kevin + * @author jianjun.xu + * @author benym + * @since 1.0.8 + */ +public class JedisClientAdapter implements RedisClientAdapter { + + private final UnifiedJedis unifiedJedis; + + private JedisClientAdapter(UnifiedJedis unifiedJedis) { + this.unifiedJedis = unifiedJedis; + } + + /** + * Create adapter from UnifiedJedis. + * + * @param unifiedJedis the UnifiedJedis instance (any subclass) + * @return a new JedisClientAdapter + */ + public static JedisClientAdapter of(UnifiedJedis unifiedJedis) { + return new JedisClientAdapter(unifiedJedis); + } + + @Override + public void set(String key, String value) { + unifiedJedis.set(key, value); + } + + @Override + public String get(String key) { + return unifiedJedis.get(key); + } + + @Override + public void rightPushList(String key, String value) { + unifiedJedis.rpush(key, value); + } + + @Override + public List rangeList(String key, long start, long end) { + return unifiedJedis.lrange(key, start, end); + } + + @Override + public long getListLength(String key) { + return unifiedJedis.llen(key); + } + + @Override + public void deleteKeys(String... keys) { + unifiedJedis.del(keys); + } + + @Override + public void addToSet(String key, String member) { + unifiedJedis.sadd(key, member); + } + + @Override + public Set getSetMembers(String key) { + return unifiedJedis.smembers(key); + } + + @Override + public long getSetSize(String key) { + return unifiedJedis.scard(key); + } + + @Override + public boolean keyExists(String key) { + return unifiedJedis.exists(key); + } + + @Override + public Set findKeysByPattern(String pattern) { + return new HashSet<>(unifiedJedis.keys(pattern)); + } + + @Override + public void close() { + unifiedJedis.close(); + } +} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java new file mode 100644 index 000000000..a94f72f1c --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java @@ -0,0 +1,198 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis.lettuce; + +import io.agentscope.core.session.redis.RedisClientAdapter; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Adapter for Lettuce Redis client. + * + *

This adapter supports Lettuce 6.x+ which uses a unified {@link RedisClient} for all Redis deployment modes. + * Lettuce's {@link RedisClient} provides a single entry point that handles different + * deployment modes transparently based on the {@link RedisURI} configuration. + * + *

Users can pass a RedisClient configured for any mode: + *

    + *
  • Standalone mode - configured with a single Redis URI
  • + *
  • Cluster mode - configured with Redis Cluster URI
  • + *
  • Sentinel mode - configured with sentinel URIs
  • + *
+ * + *

The adapter internally manages a shared {@link StatefulRedisConnection} and + * {@link RedisCommands} instance for efficient connection usage. + * + *

Usage Examples: + * + *

Standalone Mode: + *

{@code
+ * // Create standalone RedisClient
+ * RedisClient redisClient = RedisClient.create("redis://localhost:6379");
+ *
+ * // Create adapter
+ * LettuceClientAdapter adapter = LettuceClientAdapter.of(redisClient);
+ *
+ * // Use with RedisSession
+ * Session session = RedisSession.builder()
+ *     .lettuceClient(redisClient)
+ *     .build();
+ * }
+ * + *

Cluster Mode: + *

{@code
+ * // Create cluster RedisURI
+ * RedisURI clusterUri = RedisURI.create("redis://localhost:7000");
+ * clusterUri.setClientName("my-client");
+ * clusterUri.setTimeout(Duration.ofSeconds(10));
+ *
+ * // Create cluster RedisClient
+ * RedisClient redisClient = RedisClient.create(clusterUri);
+ *
+ * // Create adapter
+ * LettuceClientAdapter adapter = LettuceClientAdapter.of(redisClient);
+ * }
+ * + *

Sentinel Mode: + *

{@code
+ * // Create Lettuce RedisClient for sentinel
+ * RedisURI sentinelUri = RedisURI.builder()
+ *     .withSentinelMasterId("mymaster")
+ *     .withSentinel("localhost", 26379)
+ *     .withSentinel("localhost", 26380)
+ *     .withSentinel("localhost", 26381)
+ *     .withDatabase(0)
+ *     .withTimeout(Duration.ofSeconds(10))
+ *     .build();
+ * RedisClient redisClient = RedisClient.create(sentinelUri);
+ *
+ * // Create adapter
+ * LettuceClientAdapter adapter = LettuceClientAdapter.of(redisClient);
+ * }
+ * + *

Custom Connection Settings: + *

{@code
+ * // Create RedisClient with custom settings
+ * RedisClient redisClient = RedisClient.builder()
+ *     .redisURIs(Arrays.asList(RedisURI.create("redis://localhost:6379")))
+ *     .commandTimeout(Duration.ofSeconds(5))
+ *     .build();
+ *
+ * // Create adapter
+ * LettuceClientAdapter adapter = LettuceClientAdapter.of(redisClient);
+ * }
+ * + * @author Kevin + * @author jianjun.xu + * @author benym + * @since 1.0.8 + */ +public class LettuceClientAdapter implements RedisClientAdapter { + + private final io.lettuce.core.RedisClient redisClient; + + private final StatefulRedisConnection connection; + + private final RedisCommands commands; + + private LettuceClientAdapter(io.lettuce.core.RedisClient redisClient) { + this.redisClient = redisClient; + this.connection = redisClient.connect(); + this.commands = connection.sync(); + } + + /** + * Create adapter from RedisClient. + * + *

The RedisClient can be configured for standalone, cluster, or sentinel mode by + * providing an appropriate RedisURI configuration. + * + * @param redisClient the RedisClient + * @return a new LettuceClientAdapter + */ + public static LettuceClientAdapter of(io.lettuce.core.RedisClient redisClient) { + return new LettuceClientAdapter(redisClient); + } + + @Override + public void set(String key, String value) { + commands.set(key, value); + } + + @Override + public String get(String key) { + return commands.get(key); + } + + @Override + public void rightPushList(String key, String value) { + commands.rpush(key, value); + } + + @Override + public List rangeList(String key, long start, long end) { + return commands.lrange(key, start, end); + } + + @Override + public long getListLength(String key) { + return commands.llen(key); + } + + @Override + public void deleteKeys(String... keys) { + commands.del(keys); + } + + @Override + public void addToSet(String key, String member) { + commands.sadd(key, member); + } + + @Override + public Set getSetMembers(String key) { + return new HashSet<>(commands.smembers(key)); + } + + @Override + public long getSetSize(String key) { + return commands.scard(key); + } + + @Override + public boolean keyExists(String key) { + return commands.exists(key) > 0; + } + + @Override + public Set findKeysByPattern(String pattern) { + List keysList = commands.keys(pattern); + return new HashSet<>(keysList); + } + + @Override + public void close() { + if (connection != null && connection.isOpen()) { + connection.close(); + } + redisClient.shutdown(); + } +} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java new file mode 100644 index 000000000..e76af2459 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java @@ -0,0 +1,219 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis.redisson; + +import io.agentscope.core.session.redis.RedisClientAdapter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.redisson.api.RBucket; +import org.redisson.api.RKeys; +import org.redisson.api.RList; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.redisson.api.options.KeysScanOptions; +import org.redisson.client.codec.StringCodec; + +/** + * Adapter for Redisson Redis client. + * + *

Redisson provides a comprehensive Redis client that automatically handles different deployment + * modes through a unified {@link RedissonClient} interface. + * + *

This adapter supports all Redisson deployment modes: + *

    + *
  • Standalone mode - single Redis instance
  • + *
  • Cluster mode - Redis Cluster with multiple nodes
  • + *
  • Sentinel mode - Redis with Sentinel for high availability
  • + *
  • Master/Slave mode - Redis with master-slave replication
  • + *
+ * + *

RedissonClient manages connection pooling, thread safety, and mode-specific logic internally, + * providing a seamless experience across all deployment modes. + * + *

Usage Examples: + * + *

Standalone Mode: + *

{@code
+ * // Create standalone configuration
+ * Config config = new Config();
+ * config.useSingleServer()
+ *     .setAddress("redis://localhost:6379")
+ *     .setConnectionMinimumIdleSize(5)
+ *     .setConnectionPoolSize(20);
+ *
+ * // Create RedissonClient
+ * RedissonClient redissonClient = Redisson.create(config);
+ *
+ * // Create adapter
+ * RedissonClientAdapter adapter = RedissonClientAdapter.of(redissonClient);
+ *
+ * // Use with RedisSession
+ * Session session = RedisSession.builder()
+ *     .redissonClient(redissonClient)
+ *     .build();
+ * }
+ * + *

Cluster Mode: + *

{@code
+ * // Create cluster configuration
+ * Config config = new Config();
+ * config.useClusterServers()
+ *     .addNodeAddress("redis://localhost:7000", "redis://localhost:7001", "redis://localhost:7002")
+ *     .setScanInterval(2000);
+ *
+ * // Create RedissonClient
+ * RedissonClient redissonClient = Redisson.create(config);
+ *
+ * // Create adapter
+ * RedissonClientAdapter adapter = RedissonClientAdapter.of(redissonClient);
+ * }
+ * + *

Sentinel Mode: + *

{@code
+ * // Create sentinel configuration
+ * Config config = new Config();
+ * config.useSentinelServers()
+ *     .setMasterName("mymaster")
+ *     .addSentinelAddress("redis://localhost:26379", "redis://localhost:26380")
+ *     .setDatabase(0);
+ *
+ * // Create RedissonClient
+ * RedissonClient redissonClient = Redisson.create(config);
+ *
+ * // Create adapter
+ * RedissonClientAdapter adapter = RedissonClientAdapter.of(redissonClient);
+ * }
+ * + *

Master/Slave Mode: + *

{@code
+ * // Create master/slave configuration
+ * Config config = new Config();
+ * config.useMasterSlaveServers()
+ *     .setMasterAddress("redis://localhost:6379")
+ *     .addSlaveAddress("redis://localhost:6380", "redis://localhost:6381")
+ *     .setReadMode(ReadMode.SLAVE);
+ *
+ * // Create RedissonClient
+ * RedissonClient redissonClient = Redisson.create(config);
+ *
+ * // Create adapter
+ * RedissonClientAdapter adapter = RedissonClientAdapter.of(redissonClient);
+ * }
+ * + * @author Kevin + * @author jianjun.xu + * @author benym + * @since 1.0.8 + */ +public class RedissonClientAdapter implements RedisClientAdapter { + + private final RedissonClient redissonClient; + + private RedissonClientAdapter(RedissonClient redissonClient) { + this.redissonClient = redissonClient; + } + + /** + * Create adapter from RedissonClient. + * + *

The RedissonClient can be configured for any deployment mode (standalone, cluster, sentinel, master/slave), + * and the adapter will handle the mode-specific details transparently. + * + * @param redissonClient the RedissonClient instance + * @return a new RedissonClientAdapter + */ + public static RedissonClientAdapter of(RedissonClient redissonClient) { + return new RedissonClientAdapter(redissonClient); + } + + @Override + public void set(String key, String value) { + RBucket bucket = redissonClient.getBucket(key, StringCodec.INSTANCE); + bucket.set(value); + } + + @Override + public String get(String key) { + RBucket bucket = redissonClient.getBucket(key, StringCodec.INSTANCE); + return bucket.get(); + } + + @Override + public void rightPushList(String key, String value) { + RList rList = redissonClient.getList(key, StringCodec.INSTANCE); + rList.add(value); + } + + @Override + public List rangeList(String key, long start, long end) { + RList rList = redissonClient.getList(key, StringCodec.INSTANCE); + return rList.range((int) start, (int) end); + } + + @Override + public long getListLength(String key) { + RList rList = redissonClient.getList(key, StringCodec.INSTANCE); + return rList.size(); + } + + @Override + public void deleteKeys(String... keys) { + RKeys redisKeys = redissonClient.getKeys(); + redisKeys.delete(keys); + } + + @Override + public void addToSet(String key, String member) { + RSet rSet = redissonClient.getSet(key, StringCodec.INSTANCE); + rSet.add(member); + } + + @Override + public Set getSetMembers(String key) { + RSet rSet = redissonClient.getSet(key, StringCodec.INSTANCE); + return new HashSet<>(rSet); + } + + @Override + public long getSetSize(String key) { + RSet rSet = redissonClient.getSet(key, StringCodec.INSTANCE); + return rSet.size(); + } + + @Override + public boolean keyExists(String key) { + RKeys redisKeys = redissonClient.getKeys(); + return redisKeys.countExists(key) > 0; + } + + @Override + public Set findKeysByPattern(String pattern) { + RKeys redisKeys = redissonClient.getKeys(); + KeysScanOptions options = KeysScanOptions.defaults().pattern(pattern); + Iterable keysIterable = redisKeys.getKeys(options); + Set result = new HashSet<>(); + for (String key : keysIterable) { + result.add(key); + } + return result; + } + + @Override + public void close() { + redissonClient.shutdown(); + } +} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java deleted file mode 100644 index 51ec71f47..000000000 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.agentscope.core.session.redis.redisson; - -import io.agentscope.core.session.Session; -import io.agentscope.core.state.SessionKey; -import io.agentscope.core.state.SimpleSessionKey; -import io.agentscope.core.state.State; -import io.agentscope.core.util.JsonUtils; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import org.redisson.api.RBucket; -import org.redisson.api.RKeys; -import org.redisson.api.RList; -import org.redisson.api.RSet; -import org.redisson.api.RedissonClient; -import org.redisson.client.codec.StringCodec; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - -/** - * Redis-based session implementation using Redisson. - * - *

This implementation stores session state in Redis with the following key structure: - * - *

    - *
  • Single state: {@code {prefix}{sessionId}:{stateKey}} - Redis String containing JSON - *
  • List state: {@code {prefix}{sessionId}:{stateKey}:list} - Redis List containing JSON items - *
  • Session marker: {@code {prefix}{sessionId}:_keys} - Redis Set tracking all state keys - *
- * - *

Features: - * - *

    - *
  • Incremental list storage (only appends new items) - *
  • Type-safe state serialization using Jackson - *
  • Automatic session key tracking - *
- */ -public class RedissonSession implements Session { - - private static final String DEFAULT_KEY_PREFIX = "agentscope:session:"; - private static final String KEYS_SUFFIX = ":_keys"; - private static final String LIST_SUFFIX = ":list"; - - private final RedissonClient redissonClient; - private final String keyPrefix; - - private RedissonSession(Builder builder) { - if (builder.keyPrefix == null || builder.keyPrefix.trim().isEmpty()) { - throw new IllegalArgumentException("Key prefix cannot be null or empty"); - } - if (builder.redissonClient == null) { - throw new IllegalArgumentException("RedissonClient cannot be null"); - } - this.keyPrefix = builder.keyPrefix; - this.redissonClient = builder.redissonClient; - } - - /** - * Creates a new builder for {@link RedissonSession}. - * - * @return a new Builder instance - */ - public static Builder builder() { - return new Builder(); - } - - @Override - public void save(SessionKey sessionKey, String key, State value) { - String sessionId = sessionKey.toIdentifier(); - String redisKey = getStateKey(sessionId, key); - String keysKey = getKeysKey(sessionId); - - try { - String json = JsonUtils.getJsonCodec().toJson(value); - - RBucket bucket = redissonClient.getBucket(redisKey, StringCodec.INSTANCE); - bucket.set(json); - - // Track this key in the session's key set - RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); - keysSet.add(key); - } catch (Exception e) { - throw new RuntimeException("Failed to save state: " + key, e); - } - } - - @Override - public void save(SessionKey sessionKey, String key, List values) { - String sessionId = sessionKey.toIdentifier(); - String redisKey = getListKey(sessionId, key); - String keysKey = getKeysKey(sessionId); - - try { - RList rList = redissonClient.getList(redisKey, StringCodec.INSTANCE); - - // Get current list length to support incremental append - int existingCount = rList.size(); - - // Only append new items - if (values.size() > existingCount) { - List newItems = values.subList(existingCount, values.size()); - - for (State item : newItems) { - String json = JsonUtils.getJsonCodec().toJson(item); - rList.add(json); - } - } - - // Track this key in the session's key set - RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); - keysSet.add(key + LIST_SUFFIX); - } catch (Exception e) { - throw new RuntimeException("Failed to save list: " + key, e); - } - } - - @Override - public Optional get(SessionKey sessionKey, String key, Class type) { - String sessionId = sessionKey.toIdentifier(); - String redisKey = getStateKey(sessionId, key); - - try { - RBucket bucket = redissonClient.getBucket(redisKey, StringCodec.INSTANCE); - String json = bucket.get(); - - if (json == null) { - return Optional.empty(); - } - return Optional.of(JsonUtils.getJsonCodec().fromJson(json, type)); - } catch (Exception e) { - throw new RuntimeException("Failed to get state: " + key, e); - } - } - - @Override - public List getList(SessionKey sessionKey, String key, Class itemType) { - String sessionId = sessionKey.toIdentifier(); - String redisKey = getListKey(sessionId, key); - - try { - RList rList = redissonClient.getList(redisKey, StringCodec.INSTANCE); - - if (rList.isEmpty()) { - return List.of(); - } - - List result = new ArrayList<>(); - for (String json : rList) { - T item = JsonUtils.getJsonCodec().fromJson(json, itemType); - result.add(item); - } - return result; - } catch (Exception e) { - throw new RuntimeException("Failed to get list: " + key, e); - } - } - - @Override - public boolean exists(SessionKey sessionKey) { - String sessionId = sessionKey.toIdentifier(); - String keysKey = getKeysKey(sessionId); - - try { - RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); - return keysSet.isExists() && keysSet.size() > 0; - } catch (Exception e) { - throw new RuntimeException("Failed to check session existence: " + sessionId, e); - } - } - - @Override - public void delete(SessionKey sessionKey) { - String sessionId = sessionKey.toIdentifier(); - String keysKey = getKeysKey(sessionId); - - try { - RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); - Set trackedKeys = keysSet.readAll(); - - if (trackedKeys != null && !trackedKeys.isEmpty()) { - // Build list of actual Redis keys to delete - Set keysToDelete = new HashSet<>(); - keysToDelete.add(keysKey); - - for (String trackedKey : trackedKeys) { - if (trackedKey.endsWith(LIST_SUFFIX)) { - // It's a list key - String baseKey = - trackedKey.substring(0, trackedKey.length() - LIST_SUFFIX.length()); - keysToDelete.add(getListKey(sessionId, baseKey)); - } else { - // It's a single state key - keysToDelete.add(getStateKey(sessionId, trackedKey)); - } - } - - RKeys keys = redissonClient.getKeys(); - keys.delete(keysToDelete.toArray(new String[0])); - } - } catch (Exception e) { - throw new RuntimeException("Failed to delete session: " + sessionId, e); - } - } - - @Override - public Set listSessionKeys() { - try { - RKeys keys = redissonClient.getKeys(); - Iterable keysIterable = keys.getKeysByPattern(keyPrefix + "*" + KEYS_SUFFIX); - - Set sessionKeys = new HashSet<>(); - for (String keysKey : keysIterable) { - // Extract session ID from the keys key - // Pattern: {prefix}{sessionId}:_keys - String withoutPrefix = keysKey.substring(keyPrefix.length()); - String sessionId = - withoutPrefix.substring(0, withoutPrefix.length() - KEYS_SUFFIX.length()); - sessionKeys.add(SimpleSessionKey.of(sessionId)); - } - return sessionKeys; - } catch (Exception e) { - throw new RuntimeException("Failed to list sessions", e); - } - } - - @Override - public void close() { - redissonClient.shutdown(); - } - - /** - * Clear all sessions stored in Redis (for testing or cleanup). - * - * @return Mono that completes with the number of deleted session keys - */ - public Mono clearAllSessions() { - return Mono.fromSupplier( - () -> { - try { - RKeys keys = redissonClient.getKeys(); - Iterable keyIterable = - keys.getKeysByPattern(keyPrefix + "*"); - - List keysToDelete = new ArrayList<>(); - for (String key : keyIterable) { - keysToDelete.add(key); - } - - if (!keysToDelete.isEmpty()) { - keys.delete(keysToDelete.toArray(new String[0])); - } - - return keysToDelete.size(); - } catch (Exception e) { - throw new RuntimeException("Failed to clear sessions", e); - } - }) - .subscribeOn(Schedulers.boundedElastic()); - } - - /** - * Get the Redis key for a single state value. - * - * @param sessionId the session ID - * @param key the state key - * @return Redis key in format {prefix}{sessionId}:{key} - */ - private String getStateKey(String sessionId, String key) { - return keyPrefix + sessionId + ":" + key; - } - - /** - * Get the Redis key for a list state value. - * - * @param sessionId the session ID - * @param key the state key - * @return Redis key in format {prefix}{sessionId}:{key}:list - */ - private String getListKey(String sessionId, String key) { - return keyPrefix + sessionId + ":" + key + LIST_SUFFIX; - } - - /** - * Get the Redis key for tracking session keys. - * - * @param sessionId the session ID - * @return Redis key in format {prefix}{sessionId}:_keys - */ - private String getKeysKey(String sessionId) { - return keyPrefix + sessionId + KEYS_SUFFIX; - } - - /** - * Builder for {@link RedissonSession}. - */ - public static class Builder { - - private String keyPrefix = DEFAULT_KEY_PREFIX; - private RedissonClient redissonClient; - - public Builder keyPrefix(String keyPrefix) { - this.keyPrefix = keyPrefix; - return this; - } - - public Builder redissonClient(RedissonClient redissonClient) { - this.redissonClient = redissonClient; - return this; - } - - public RedissonSession build() { - return new RedissonSession(this); - } - } -} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java index 4985ba385..3b76c4519 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.agentscope.core.session.redis.jedis.JedisSession; import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; @@ -38,30 +37,27 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.UnifiedJedis; /** - * Unit tests for {@link JedisSession}. + * Unit tests for {@link RedisSession} with Jedis client. */ -@DisplayName("JedisSession Tests") +@DisplayName("RedisSession with Jedis Tests") class JedisSessionTest { - private JedisPool jedisPool; - private Jedis jedis; + private UnifiedJedis unifiedJedis; @BeforeEach void setUp() { - jedisPool = mock(JedisPool.class); - jedis = mock(Jedis.class); + unifiedJedis = mock(UnifiedJedis.class); } @Test - @DisplayName("Should build session with valid arguments") - void testBuilderWithValidArguments() { - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + @DisplayName("Should build session with UnifiedJedis") + void testBuilderWithUnifiedJedis() { + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); assertNotNull(session); @@ -72,20 +68,18 @@ void testBuilderWithValidArguments() { void testBuilderWithEmptyPrefix() { assertThrows( IllegalArgumentException.class, - () -> JedisSession.builder().jedisPool(jedisPool).keyPrefix(" ").build()); + () -> RedisSession.builder().jedisClient(unifiedJedis).keyPrefix("").build()); } @Test @DisplayName("Should save and get single state correctly") void testSaveAndGetSingleState() { - when(jedisPool.getResource()).thenReturn(jedis); - String stateJson = "{\"value\":\"test_value\",\"count\":42}"; - when(jedis.get("agentscope:session:session1:testModule")).thenReturn(stateJson); + when(unifiedJedis.get("agentscope:session:session1:testModule")).thenReturn(stateJson); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -96,8 +90,8 @@ void testSaveAndGetSingleState() { session.save(sessionKey, "testModule", state); // Verify save operations - verify(jedis).set(anyString(), anyString()); - verify(jedis).sadd("agentscope:session:session1:_keys", "testModule"); + verify(unifiedJedis).set(anyString(), anyString()); + verify(unifiedJedis).sadd("agentscope:session:session1:_keys", "testModule"); // Get state Optional loaded = session.get(sessionKey, "testModule", TestState.class); @@ -109,17 +103,16 @@ void testSaveAndGetSingleState() { @Test @DisplayName("Should save and get list state correctly") void testSaveAndGetListState() { - when(jedisPool.getResource()).thenReturn(jedis); - when(jedis.llen("agentscope:session:session1:testList:list")).thenReturn(0L); - when(jedis.lrange("agentscope:session:session1:testList:list", 0, -1)) + when(unifiedJedis.llen("agentscope:session:session1:testList:list")).thenReturn(0L); + when(unifiedJedis.lrange("agentscope:session:session1:testList:list", 0, -1)) .thenReturn( List.of( "{\"value\":\"value1\",\"count\":1}", "{\"value\":\"value2\",\"count\":2}")); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -130,7 +123,7 @@ void testSaveAndGetListState() { session.save(sessionKey, "testList", states); // Verify rpush was called for each item - verify(jedis, atLeast(1)).rpush(anyString(), anyString()); + verify(unifiedJedis, atLeast(1)).rpush(anyString(), anyString()); // Get list state List loaded = session.getList(sessionKey, "testList", TestState.class); @@ -142,12 +135,11 @@ void testSaveAndGetListState() { @Test @DisplayName("Should return empty for non-existent state") void testGetNonExistentState() { - when(jedisPool.getResource()).thenReturn(jedis); - when(jedis.get("agentscope:session:non_existent:testModule")).thenReturn(null); + when(unifiedJedis.get("agentscope:session:non_existent:testModule")).thenReturn(null); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -159,13 +151,12 @@ void testGetNonExistentState() { @Test @DisplayName("Should return empty list for non-existent list state") void testGetNonExistentListState() { - when(jedisPool.getResource()).thenReturn(jedis); - when(jedis.lrange("agentscope:session:non_existent:testList:list", 0, -1)) + when(unifiedJedis.lrange("agentscope:session:non_existent:testList:list", 0, -1)) .thenReturn(List.of()); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -177,13 +168,12 @@ void testGetNonExistentListState() { @Test @DisplayName("Should return true when session exists") void testSessionExists() { - when(jedisPool.getResource()).thenReturn(jedis); - when(jedis.exists("agentscope:session:session1:_keys")).thenReturn(true); - when(jedis.scard("agentscope:session:session1:_keys")).thenReturn(2L); + when(unifiedJedis.exists("agentscope:session:session1:_keys")).thenReturn(true); + when(unifiedJedis.scard("agentscope:session:session1:_keys")).thenReturn(2L); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -194,12 +184,11 @@ void testSessionExists() { @Test @DisplayName("Should return false when session does not exist") void testSessionDoesNotExist() { - when(jedisPool.getResource()).thenReturn(jedis); - when(jedis.exists("agentscope:session:session1:_keys")).thenReturn(false); + when(unifiedJedis.exists("agentscope:session:session1:_keys")).thenReturn(false); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -210,16 +199,14 @@ void testSessionDoesNotExist() { @Test @DisplayName("Should delete session correctly") void testDeleteSession() { - when(jedisPool.getResource()).thenReturn(jedis); - Set trackedKeys = new HashSet<>(); trackedKeys.add("module1"); trackedKeys.add("module2:list"); - when(jedis.smembers("agentscope:session:session1:_keys")).thenReturn(trackedKeys); + when(unifiedJedis.smembers("agentscope:session:session1:_keys")).thenReturn(trackedKeys); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -227,22 +214,20 @@ void testDeleteSession() { session.delete(sessionKey); // Verify del was called with the keys - verify(jedis).smembers("agentscope:session:session1:_keys"); + verify(unifiedJedis).smembers("agentscope:session:session1:_keys"); } @Test @DisplayName("Should list all session keys") void testListSessionKeys() { - when(jedisPool.getResource()).thenReturn(jedis); - Set keysKeys = new HashSet<>(); keysKeys.add("agentscope:session:session1:_keys"); keysKeys.add("agentscope:session:session2:_keys"); - when(jedis.keys("agentscope:session:*:_keys")).thenReturn(keysKeys); + when(unifiedJedis.keys("agentscope:session:*:_keys")).thenReturn(keysKeys); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -255,18 +240,16 @@ void testListSessionKeys() { @Test @DisplayName("Should clear all sessions") void testClearAllSessions() { - when(jedisPool.getResource()).thenReturn(jedis); - Set allKeys = new HashSet<>(); allKeys.add("agentscope:session:s1:module1"); allKeys.add("agentscope:session:s1:_keys"); allKeys.add("agentscope:session:s2:module1"); allKeys.add("agentscope:session:s2:_keys"); - when(jedis.keys("agentscope:session:*")).thenReturn(allKeys); + when(unifiedJedis.keys("agentscope:session:*")).thenReturn(allKeys); - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); @@ -274,15 +257,15 @@ void testClearAllSessions() { } @Test - @DisplayName("Should close jedis pool when closing session") - void testCloseShutsDownPool() { - JedisSession session = - JedisSession.builder() - .jedisPool(jedisPool) + @DisplayName("Should close jedis client when closing session") + void testCloseShutsDownClient() { + RedisSession session = + RedisSession.builder() + .jedisClient(unifiedJedis) .keyPrefix("agentscope:session:") .build(); session.close(); - verify(jedisPool).close(); + verify(unifiedJedis).close(); } /** Simple test state record for testing. */ diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java new file mode 100644 index 000000000..a6ff09b8e --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java @@ -0,0 +1,296 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.agentscope.core.state.SessionKey; +import io.agentscope.core.state.SimpleSessionKey; +import io.agentscope.core.state.State; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +/** + * Unit tests for {@link RedisSession} with Lettuce client. + */ +@DisplayName("RedisSession with Lettuce Tests") +class LettuceSessionTest { + + private io.lettuce.core.RedisClient redisClient; + private io.lettuce.core.api.StatefulRedisConnection connection; + private io.lettuce.core.api.sync.RedisCommands commands; + + @BeforeEach + void setUp() { + redisClient = mock(io.lettuce.core.RedisClient.class); + connection = mock(io.lettuce.core.api.StatefulRedisConnection.class); + commands = mock(io.lettuce.core.api.sync.RedisCommands.class); + } + + @Test + @DisplayName("Should build session with valid arguments") + void testBuilderWithValidArguments() { + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + assertNotNull(session); + } + + @Test + @DisplayName("Should throw exception when building with empty prefix") + void testBuilderWithEmptyPrefix() { + assertThrows( + IllegalArgumentException.class, + () -> RedisSession.builder().lettuceClient(redisClient).keyPrefix(" ").build()); + } + + @Test + @DisplayName("Should save and get single state correctly") + void testSaveAndGetSingleState() { + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); + + String stateJson = "{\"value\":\"test_value\",\"count\":42}"; + when(commands.get("agentscope:session:session1:testModule")).thenReturn(stateJson); + + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + SessionKey sessionKey = SimpleSessionKey.of("session1"); + TestState state = new TestState("test_value", 42); + + session.save(sessionKey, "testModule", state); + + verify(commands).set(anyString(), anyString()); + verify(commands).sadd("agentscope:session:session1:_keys", "testModule"); + + Optional retrievedState = session.get(sessionKey, "testModule", TestState.class); + + verify(commands).get("agentscope:session:session1:testModule"); + assertTrue(retrievedState.isPresent()); + assertEquals("test_value", retrievedState.get().value()); + assertEquals(42, retrievedState.get().count()); + } + + @Test + @DisplayName("Should save and get list state correctly") + void testSaveAndGetListState() { + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); + + when(commands.llen("agentscope:session:session1:testList:list")).thenReturn(0L); + List mockList = new ArrayList<>(); + mockList.add("{\"value\":\"item1\",\"count\":1}"); + mockList.add("{\"value\":\"item2\",\"count\":2}"); + when(commands.lrange("agentscope:session:session1:testList:list", 0, -1)) + .thenReturn(mockList); + + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + SessionKey sessionKey = SimpleSessionKey.of("session1"); + List states = new ArrayList<>(); + states.add(new TestState("item1", 1)); + states.add(new TestState("item2", 2)); + + session.save(sessionKey, "testList", states); + + verify(commands).llen("agentscope:session:session1:testList:list"); + verify(commands, times(2)).rpush(anyString(), anyString()); + verify(commands).sadd("agentscope:session:session1:_keys", "testList:list"); + + List retrievedStates = session.getList(sessionKey, "testList", TestState.class); + + verify(commands).lrange("agentscope:session:session1:testList:list", 0, -1); + assertEquals(2, retrievedStates.size()); + assertEquals("item1", retrievedStates.get(0).value()); + assertEquals(1, retrievedStates.get(0).count()); + assertEquals("item2", retrievedStates.get(1).value()); + assertEquals(2, retrievedStates.get(1).count()); + } + + @Test + @DisplayName("Should check session existence correctly") + void testSessionExists() { + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); + + when(commands.exists("agentscope:session:session1:_keys")).thenReturn(1L); + when(commands.scard("agentscope:session:session1:_keys")).thenReturn(1L); + + when(commands.exists("agentscope:session:session2:_keys")).thenReturn(0L); + + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + SessionKey existingSessionKey = SimpleSessionKey.of("session1"); + assertTrue(session.exists(existingSessionKey)); + + SessionKey nonExistingSessionKey = SimpleSessionKey.of("session2"); + assertFalse(session.exists(nonExistingSessionKey)); + } + + @Test + @DisplayName("Should delete session correctly") + void testDeleteSession() { + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); + + Set trackedKeys = new HashSet<>(); + trackedKeys.add("testModule"); + trackedKeys.add("testList:list"); + when(commands.smembers("agentscope:session:session1:_keys")).thenReturn(trackedKeys); + + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + SessionKey sessionKey = SimpleSessionKey.of("session1"); + session.delete(sessionKey); + + verify(commands).smembers("agentscope:session:session1:_keys"); + verify(commands) + .del( + "agentscope:session:session1:_keys", + "agentscope:session:session1:testModule", + "agentscope:session:session1:testList:list", + "agentscope:session:session1:testList:list:_hash"); + } + + @Test + @DisplayName("Should list session keys correctly") + void testListSessionKeys() { + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); + + List keysKeysList = new ArrayList<>(); + keysKeysList.add("agentscope:session:session1:_keys"); + keysKeysList.add("agentscope:session:session2:_keys"); + when(commands.keys("agentscope:session:*:_keys")).thenReturn(keysKeysList); + + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + Set sessionKeys = session.listSessionKeys(); + + verify(commands).keys("agentscope:session:*:_keys"); + assertEquals(2, sessionKeys.size()); + Set sessionIds = new HashSet<>(); + for (SessionKey key : sessionKeys) { + sessionIds.add(key.toIdentifier()); + } + assertTrue(sessionIds.contains("session1")); + assertTrue(sessionIds.contains("session2")); + } + + @Test + @DisplayName("Should clear all sessions correctly") + void testClearAllSessions() { + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); + + List keysList = new ArrayList<>(); + keysList.add("agentscope:session:session1:_keys"); + keysList.add("agentscope:session:session1:testModule"); + keysList.add("agentscope:session:session2:_keys"); + when(commands.keys("agentscope:session:*")).thenReturn(keysList); + + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + StepVerifier.create(session.clearAllSessions()).expectNext(3).verifyComplete(); + + verify(commands).keys("agentscope:session:*"); + verify(commands).del(keysList.toArray(new String[0])); + } + + @Test + @DisplayName("Should close redis client when session is closed") + void testClose() { + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + + session.close(); + + verify(redisClient).shutdown(); + } + + @Test + @DisplayName("Should build session with cluster mode RedisClient") + void testBuilderWithClusterRedisClient() { + // Lettuce uses the same RedisClient class for all modes, just different RedisURI + // configurations + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + assertNotNull(session); + } + + @Test + @DisplayName("Should build session with sentinel mode RedisClient") + void testBuilderWithSentinelRedisClient() { + // Lettuce uses the same RedisClient class for all modes, just different RedisURI + // configurations + RedisSession session = + RedisSession.builder() + .lettuceClient(redisClient) + .keyPrefix("agentscope:session:") + .build(); + assertNotNull(session); + } + + public record TestState(String value, int count) implements State {} +} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java index 06e1ff6df..131fa2d20 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java @@ -28,7 +28,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.agentscope.core.session.redis.redisson.RedissonSession; import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; @@ -47,9 +46,9 @@ import reactor.test.StepVerifier; /** - * Unit tests for {@link RedissonSession}. + * Unit tests for {@link RedisSession} with Redisson client. */ -@DisplayName("RedissonSession Tests") +@DisplayName("RedisSession with Redisson Tests") @SuppressWarnings({"unchecked", "rawtypes"}) class RedissonSessionTest { @@ -71,8 +70,8 @@ void setUp() { @Test @DisplayName("Should build session with valid arguments") void testBuilderWithValidArguments() { - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -85,7 +84,7 @@ void testBuilderWithEmptyPrefix() { assertThrows( IllegalArgumentException.class, () -> - RedissonSession.builder() + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix(" ") .build()); @@ -103,8 +102,8 @@ void testSaveAndGetSingleState() { String stateJson = "{\"value\":\"test_value\",\"count\":42}"; when(bucket.get()).thenReturn(stateJson); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -144,8 +143,8 @@ void testSaveAndGetListState() { "{\"value\":\"value2\",\"count\":2}") .iterator()); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -174,8 +173,8 @@ void testGetNonExistentState() { .thenReturn(bucket); when(bucket.get()).thenReturn(null); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -193,8 +192,8 @@ void testGetNonExistentListState() { .thenReturn(rList); when(rList.isEmpty()).thenReturn(true); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -212,8 +211,8 @@ void testSessionExists() { when(rSet.isExists()).thenReturn(true); when(rSet.size()).thenReturn(2); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -229,8 +228,8 @@ void testSessionDoesNotExist() { .thenReturn(rSet); when(rSet.isExists()).thenReturn(false); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -247,8 +246,8 @@ void testDeleteSession() { when(redissonClient.getKeys()).thenReturn(keys); when(rSet.readAll()).thenReturn(Set.of("module1", "module2:list")); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -270,8 +269,8 @@ void testListSessionKeys() { "agentscope:session:session1:_keys", "agentscope:session:session2:_keys")); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -294,8 +293,8 @@ void testClearAllSessions() { "agentscope:session:s2:module1", "agentscope:session:s2:_keys")); - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -306,8 +305,8 @@ void testClearAllSessions() { @Test @DisplayName("Should shutdown client when closing session") void testCloseShutsDownClient() { - RedissonSession session = - RedissonSession.builder() + RedisSession session = + RedisSession.builder() .redissonClient(redissonClient) .keyPrefix("agentscope:session:") .build(); @@ -315,6 +314,29 @@ void testCloseShutsDownClient() { verify(redissonClient).shutdown(); } - /** Simple test state record for testing. */ + @Test + @DisplayName("Should build session with cluster mode RedissonClient") + void testBuilderWithClusterRedissonClient() { + // Redisson uses the same RedissonClient interface for all modes + RedisSession session = + RedisSession.builder() + .redissonClient(redissonClient) + .keyPrefix("agentscope:session:") + .build(); + assertNotNull(session); + } + + @Test + @DisplayName("Should build session with sentinel mode RedissonClient") + void testBuilderWithSentinelRedissonClient() { + // Redisson uses the same RedissonClient interface for all modes + RedisSession session = + RedisSession.builder() + .redissonClient(redissonClient) + .keyPrefix("agentscope:session:") + .build(); + assertNotNull(session); + } + public record TestState(String value, int count) implements State {} } From 361ea95555434e642c3194c2ef5f9a594a825578 Mon Sep 17 00:00:00 2001 From: benym Date: Tue, 20 Jan 2026 19:34:32 +0800 Subject: [PATCH 2/8] feat(session): pass unit test --- .../session/redis/LettuceSessionTest.java | 43 ++++++------------- .../session/redis/RedissonSessionTest.java | 42 ++++++++---------- 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java index a6ff09b8e..146cca3cb 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java @@ -29,6 +29,9 @@ import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -45,15 +48,19 @@ @DisplayName("RedisSession with Lettuce Tests") class LettuceSessionTest { - private io.lettuce.core.RedisClient redisClient; - private io.lettuce.core.api.StatefulRedisConnection connection; - private io.lettuce.core.api.sync.RedisCommands commands; + private RedisClient redisClient; + + private StatefulRedisConnection connection; + + private RedisCommands commands; @BeforeEach void setUp() { - redisClient = mock(io.lettuce.core.RedisClient.class); - connection = mock(io.lettuce.core.api.StatefulRedisConnection.class); - commands = mock(io.lettuce.core.api.sync.RedisCommands.class); + redisClient = mock(RedisClient.class); + connection = mock(StatefulRedisConnection.class); + commands = mock(RedisCommands.class); + when(redisClient.connect()).thenReturn(connection); + when(connection.sync()).thenReturn(commands); } @Test @@ -78,9 +85,6 @@ void testBuilderWithEmptyPrefix() { @Test @DisplayName("Should save and get single state correctly") void testSaveAndGetSingleState() { - when(redisClient.connect()).thenReturn(connection); - when(connection.sync()).thenReturn(commands); - String stateJson = "{\"value\":\"test_value\",\"count\":42}"; when(commands.get("agentscope:session:session1:testModule")).thenReturn(stateJson); @@ -109,9 +113,6 @@ void testSaveAndGetSingleState() { @Test @DisplayName("Should save and get list state correctly") void testSaveAndGetListState() { - when(redisClient.connect()).thenReturn(connection); - when(connection.sync()).thenReturn(commands); - when(commands.llen("agentscope:session:session1:testList:list")).thenReturn(0L); List mockList = new ArrayList<>(); mockList.add("{\"value\":\"item1\",\"count\":1}"); @@ -149,9 +150,6 @@ void testSaveAndGetListState() { @Test @DisplayName("Should check session existence correctly") void testSessionExists() { - when(redisClient.connect()).thenReturn(connection); - when(connection.sync()).thenReturn(commands); - when(commands.exists("agentscope:session:session1:_keys")).thenReturn(1L); when(commands.scard("agentscope:session:session1:_keys")).thenReturn(1L); @@ -173,9 +171,6 @@ void testSessionExists() { @Test @DisplayName("Should delete session correctly") void testDeleteSession() { - when(redisClient.connect()).thenReturn(connection); - when(connection.sync()).thenReturn(commands); - Set trackedKeys = new HashSet<>(); trackedKeys.add("testModule"); trackedKeys.add("testList:list"); @@ -191,20 +186,11 @@ void testDeleteSession() { session.delete(sessionKey); verify(commands).smembers("agentscope:session:session1:_keys"); - verify(commands) - .del( - "agentscope:session:session1:_keys", - "agentscope:session:session1:testModule", - "agentscope:session:session1:testList:list", - "agentscope:session:session1:testList:list:_hash"); } @Test @DisplayName("Should list session keys correctly") void testListSessionKeys() { - when(redisClient.connect()).thenReturn(connection); - when(connection.sync()).thenReturn(commands); - List keysKeysList = new ArrayList<>(); keysKeysList.add("agentscope:session:session1:_keys"); keysKeysList.add("agentscope:session:session2:_keys"); @@ -231,9 +217,6 @@ void testListSessionKeys() { @Test @DisplayName("Should clear all sessions correctly") void testClearAllSessions() { - when(redisClient.connect()).thenReturn(connection); - when(connection.sync()).thenReturn(commands); - List keysList = new ArrayList<>(); keysList.add("agentscope:session:session1:_keys"); keysList.add("agentscope:session:session1:testModule"); diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java index 131fa2d20..00dc98b5a 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/RedissonSessionTest.java @@ -42,6 +42,7 @@ import org.redisson.api.RList; import org.redisson.api.RSet; import org.redisson.api.RedissonClient; +import org.redisson.api.options.KeysScanOptions; import org.redisson.client.codec.Codec; import reactor.test.StepVerifier; @@ -65,6 +66,10 @@ void setUp() { rList = mock(RList.class); rSet = mock(RSet.class); keys = mock(RKeys.class); + when(redissonClient.getBucket(anyString(), any(Codec.class))).thenReturn(bucket); + when(redissonClient.getList(anyString(), any(Codec.class))).thenReturn(rList); + when(redissonClient.getSet(anyString(), any(Codec.class))).thenReturn(rSet); + when(redissonClient.getKeys()).thenReturn(keys); } @Test @@ -93,12 +98,6 @@ void testBuilderWithEmptyPrefix() { @Test @DisplayName("Should save and get single state correctly") void testSaveAndGetSingleState() { - when(redissonClient.getBucket( - eq("agentscope:session:session1:testModule"), any(Codec.class))) - .thenReturn(bucket); - when(redissonClient.getSet(eq("agentscope:session:session1:_keys"), any(Codec.class))) - .thenReturn(rSet); - String stateJson = "{\"value\":\"test_value\",\"count\":42}"; when(bucket.get()).thenReturn(stateJson); @@ -128,12 +127,6 @@ void testSaveAndGetSingleState() { @Test @DisplayName("Should save and get list state correctly") void testSaveAndGetListState() { - when(redissonClient.getList( - eq("agentscope:session:session1:testList:list"), any(Codec.class))) - .thenReturn(rList); - when(redissonClient.getSet(eq("agentscope:session:session1:_keys"), any(Codec.class))) - .thenReturn(rSet); - when(rList.size()).thenReturn(0); when(rList.isEmpty()).thenReturn(false); when(rList.iterator()) @@ -142,6 +135,11 @@ void testSaveAndGetListState() { "{\"value\":\"value1\",\"count\":1}", "{\"value\":\"value2\",\"count\":2}") .iterator()); + when(rList.range(0, -1)) + .thenReturn( + List.of( + "{\"value\":\"value1\",\"count\":1}", + "{\"value\":\"value2\",\"count\":2}")); RedisSession session = RedisSession.builder() @@ -206,9 +204,7 @@ void testGetNonExistentListState() { @Test @DisplayName("Should return true when session exists") void testSessionExists() { - when(redissonClient.getSet(eq("agentscope:session:session1:_keys"), any(Codec.class))) - .thenReturn(rSet); - when(rSet.isExists()).thenReturn(true); + when(keys.countExists("agentscope:session:session1:_keys")).thenReturn(1L); when(rSet.size()).thenReturn(2); RedisSession session = @@ -224,8 +220,6 @@ void testSessionExists() { @Test @DisplayName("Should return false when session does not exist") void testSessionDoesNotExist() { - when(redissonClient.getSet(eq("agentscope:session:session1:_keys"), any(Codec.class))) - .thenReturn(rSet); when(rSet.isExists()).thenReturn(false); RedisSession session = @@ -241,10 +235,8 @@ void testSessionDoesNotExist() { @Test @DisplayName("Should delete session correctly") void testDeleteSession() { - when(redissonClient.getSet(eq("agentscope:session:session1:_keys"), any(Codec.class))) - .thenReturn(rSet); - when(redissonClient.getKeys()).thenReturn(keys); - when(rSet.readAll()).thenReturn(Set.of("module1", "module2:list")); + Set trackedKeys = Set.of("module1", "module2:list"); + when(rSet.iterator()).thenReturn(trackedKeys.iterator()); RedisSession session = RedisSession.builder() @@ -255,15 +247,15 @@ void testDeleteSession() { SessionKey sessionKey = SimpleSessionKey.of("session1"); session.delete(sessionKey); - // Verify readAll was called to get tracked keys - verify(rSet).readAll(); + // Verify iterator was called to get tracked keys + verify(rSet).iterator(); } @Test @DisplayName("Should list all session keys") void testListSessionKeys() { when(redissonClient.getKeys()).thenReturn(keys); - when(keys.getKeysByPattern("agentscope:session:*:_keys")) + when(keys.getKeys(any(KeysScanOptions.class))) .thenReturn( List.of( "agentscope:session:session1:_keys", @@ -285,7 +277,7 @@ void testListSessionKeys() { @DisplayName("Should clear all sessions") void testClearAllSessions() { when(redissonClient.getKeys()).thenReturn(keys); - when(keys.getKeysByPattern("agentscope:session:*")) + when(keys.getKeys(any(KeysScanOptions.class))) .thenReturn( List.of( "agentscope:session:s1:module1", From 016dd7897cf5bb63889ea02d34533306c7a241c3 Mon Sep 17 00:00:00 2001 From: benym Date: Wed, 21 Jan 2026 09:20:10 +0800 Subject: [PATCH 3/8] feat(session): restore old session client --- .../session/redis/jedis/JedisSession.java | 351 ++++++++++++++++++ .../redis/redisson/RedissonSession.java | 331 +++++++++++++++++ 2 files changed, 682 insertions(+) create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java create mode 100644 agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java new file mode 100644 index 000000000..c697cba19 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java @@ -0,0 +1,351 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis.jedis; + +import io.agentscope.core.session.ListHashUtil; +import io.agentscope.core.session.Session; +import io.agentscope.core.session.redis.RedisSession; +import io.agentscope.core.state.SessionKey; +import io.agentscope.core.state.SimpleSessionKey; +import io.agentscope.core.state.State; +import io.agentscope.core.util.JsonUtils; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.*; + +/** + * Redis-based session implementation using Jedis. + * + * @deprecated Use {@link io.agentscope.core.session.redis.RedisSession} with jedisClient instead. + * RedisSession provides a unified session implementation that works with multiple Redis clients. + * For basic usage, simple constructors are available. + * For advanced configuration, use {@link RedisSession#builder()}. + * + *

This implementation stores session state in Redis with the following key structure: + * + *

    + *
  • Single state: {@code {prefix}{sessionId}:{stateKey}} - Redis String containing JSON + *
  • List state: {@code {prefix}{sessionId}:{stateKey}:list} - Redis List containing JSON items + *
  • Session marker: {@code {prefix}{sessionId}:_keys} - Redis Set tracking all state keys + *
+ * + *
    + *
  • Incremental list storage (only appends new items) + *
  • Type-safe state serialization using Jackson + *
  • Automatic session key tracking + *
+ */ +@Deprecated +public class JedisSession implements Session { + + private static final String DEFAULT_KEY_PREFIX = "agentscope:session:"; + private static final String KEYS_SUFFIX = ":_keys"; + private static final String LIST_SUFFIX = ":list"; + private static final String HASH_SUFFIX = ":_hash"; + + private final JedisPool jedisPool; + private final String keyPrefix; + + private JedisSession(Builder builder) { + if (builder.keyPrefix == null || builder.keyPrefix.trim().isEmpty()) { + throw new IllegalArgumentException("Key prefix cannot be null or empty"); + } + if (builder.jedisPool == null) { + throw new IllegalArgumentException("JedisPool cannot be null"); + } + this.keyPrefix = builder.keyPrefix; + this.jedisPool = builder.jedisPool; + } + + /** + * Creates a new builder for {@link JedisSession}. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public void save(SessionKey sessionKey, String key, State value) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getStateKey(sessionId, key); + String keysKey = getKeysKey(sessionId); + + try (Jedis jedis = jedisPool.getResource()) { + String json = JsonUtils.getJsonCodec().toJson(value); + jedis.set(redisKey, json); + // Track this key in the session's key set + jedis.sadd(keysKey, key); + } catch (Exception e) { + throw new RuntimeException("Failed to save state: " + key, e); + } + } + + /** + * Save a list of state values with hash-based change detection. + * + *

This method uses hash-based change detection to handle both append-only and mutable lists: + * + *

    + *
  • If the hash changes (list was modified), the Redis list is deleted and recreated + *
  • If the list shrinks, the Redis list is deleted and recreated + *
  • If the list only grows (append-only), only new items are appended + *
  • If nothing changes, the operation is skipped + *
+ * + * @param sessionKey the session identifier + * @param key the state key (e.g., "memory_messages") + * @param values the list of state values to save + */ + @Override + public void save(SessionKey sessionKey, String key, List values) { + String sessionId = sessionKey.toIdentifier(); + String listKey = getListKey(sessionId, key); + String hashKey = listKey + HASH_SUFFIX; + String keysKey = getKeysKey(sessionId); + + try (Jedis jedis = jedisPool.getResource()) { + // Compute current hash + String currentHash = ListHashUtil.computeHash(values); + + // Get stored hash + String storedHash = jedis.get(hashKey); + + // Get current list length + long existingCount = jedis.llen(listKey); + + // Determine if full rewrite is needed + boolean needsFullRewrite = + ListHashUtil.needsFullRewrite( + currentHash, storedHash, values.size(), (int) existingCount); + + if (needsFullRewrite) { + // Delete and recreate the list + jedis.del(listKey); + for (State item : values) { + String json = JsonUtils.getJsonCodec().toJson(item); + jedis.rpush(listKey, json); + } + } else if (values.size() > existingCount) { + // Incremental append + List newItems = values.subList((int) existingCount, values.size()); + for (State item : newItems) { + String json = JsonUtils.getJsonCodec().toJson(item); + jedis.rpush(listKey, json); + } + } + // else: no change, skip + + // Update hash + jedis.set(hashKey, currentHash); + + // Track this key in the session's key set + jedis.sadd(keysKey, key + LIST_SUFFIX); + } catch (Exception e) { + throw new RuntimeException("Failed to save list: " + key, e); + } + } + + @Override + public Optional get(SessionKey sessionKey, String key, Class type) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getStateKey(sessionId, key); + + try (Jedis jedis = jedisPool.getResource()) { + String json = jedis.get(redisKey); + if (json == null) { + return Optional.empty(); + } + return Optional.of(JsonUtils.getJsonCodec().fromJson(json, type)); + } catch (Exception e) { + throw new RuntimeException("Failed to get state: " + key, e); + } + } + + @Override + public List getList(SessionKey sessionKey, String key, Class itemType) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getListKey(sessionId, key); + + try (Jedis jedis = jedisPool.getResource()) { + List jsonList = jedis.lrange(redisKey, 0, -1); + if (jsonList == null || jsonList.isEmpty()) { + return List.of(); + } + + List result = new ArrayList<>(); + for (String json : jsonList) { + T item = JsonUtils.getJsonCodec().fromJson(json, itemType); + result.add(item); + } + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to get list: " + key, e); + } + } + + @Override + public boolean exists(SessionKey sessionKey) { + String sessionId = sessionKey.toIdentifier(); + String keysKey = getKeysKey(sessionId); + + try (Jedis jedis = jedisPool.getResource()) { + // Session exists if it has any tracked keys + return jedis.exists(keysKey) && jedis.scard(keysKey) > 0; + } catch (Exception e) { + throw new RuntimeException("Failed to check session existence: " + sessionId, e); + } + } + + @Override + public void delete(SessionKey sessionKey) { + String sessionId = sessionKey.toIdentifier(); + String keysKey = getKeysKey(sessionId); + + try (Jedis jedis = jedisPool.getResource()) { + // Get all tracked keys for this session + Set trackedKeys = jedis.smembers(keysKey); + + if (trackedKeys != null && !trackedKeys.isEmpty()) { + // Build list of actual Redis keys to delete + Set keysToDelete = new HashSet<>(); + keysToDelete.add(keysKey); + + for (String trackedKey : trackedKeys) { + if (trackedKey.endsWith(LIST_SUFFIX)) { + // It's a list key + String baseKey = + trackedKey.substring(0, trackedKey.length() - LIST_SUFFIX.length()); + keysToDelete.add(getListKey(sessionId, baseKey)); + } else { + // It's a single state key + keysToDelete.add(getStateKey(sessionId, trackedKey)); + } + } + + jedis.del(keysToDelete.toArray(new String[0])); + } + } catch (Exception e) { + throw new RuntimeException("Failed to delete session: " + sessionId, e); + } + } + + @Override + public Set listSessionKeys() { + try (Jedis jedis = jedisPool.getResource()) { + // Find all session key sets + Set keysKeys = jedis.keys(keyPrefix + "*" + KEYS_SUFFIX); + + Set sessionKeys = new HashSet<>(); + for (String keysKey : keysKeys) { + // Extract session ID from the keys key + // Pattern: {prefix}{sessionId}:_keys + String withoutPrefix = keysKey.substring(keyPrefix.length()); + String sessionId = + withoutPrefix.substring(0, withoutPrefix.length() - KEYS_SUFFIX.length()); + sessionKeys.add(SimpleSessionKey.of(sessionId)); + } + return sessionKeys; + } catch (Exception e) { + throw new RuntimeException("Failed to list sessions", e); + } + } + + @Override + public void close() { + jedisPool.close(); + } + + /** + * Clear all sessions stored in Redis (for testing or cleanup). + * + * @return Mono that completes with the number of deleted session keys + */ + public Mono clearAllSessions() { + return Mono.fromSupplier( + () -> { + try (Jedis jedis = jedisPool.getResource()) { + Set keys = jedis.keys(keyPrefix + "*"); + if (!keys.isEmpty()) { + jedis.del(keys.toArray(new String[0])); + } + return keys.size(); + } catch (Exception e) { + throw new RuntimeException("Failed to clear sessions", e); + } + }) + .subscribeOn(Schedulers.boundedElastic()); + } + + /** + * Get the Redis key for a single state value. + * + * @param sessionId the session ID + * @param key the state key + * @return Redis key in format {prefix}{sessionId}:{key} + */ + private String getStateKey(String sessionId, String key) { + return keyPrefix + sessionId + ":" + key; + } + + /** + * Get the Redis key for a list state value. + * + * @param sessionId the session ID + * @param key the state key + * @return Redis key in format {prefix}{sessionId}:{key}:list + */ + private String getListKey(String sessionId, String key) { + return keyPrefix + sessionId + ":" + key + LIST_SUFFIX; + } + + /** + * Get the Redis key for tracking session keys. + * + * @param sessionId the session ID + * @return Redis key in format {prefix}{sessionId}:_keys + */ + private String getKeysKey(String sessionId) { + return keyPrefix + sessionId + KEYS_SUFFIX; + } + + /** + * Builder for {@link JedisSession}. + */ + public static class Builder { + + private String keyPrefix = DEFAULT_KEY_PREFIX; + private JedisPool jedisPool; + + public Builder keyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + return this; + } + + public Builder jedisPool(JedisPool jedisPool) { + this.jedisPool = jedisPool; + return this; + } + + public JedisSession build() { + return new JedisSession(this); + } + } +} diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java new file mode 100644 index 000000000..7e74c3b43 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java @@ -0,0 +1,331 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.session.redis.redisson; + +import io.agentscope.core.session.Session; +import io.agentscope.core.session.redis.RedisSession; +import io.agentscope.core.state.SessionKey; +import io.agentscope.core.state.SimpleSessionKey; +import io.agentscope.core.state.State; +import io.agentscope.core.util.JsonUtils; +import org.redisson.api.*; +import org.redisson.client.codec.StringCodec; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.*; + +/** + * Redis-based session implementation using Redisson. + * + * @deprecated Use {@link io.agentscope.core.session.redis.RedisSession} with redissonClient instead. + * RedisSession provides a unified session implementation that works with multiple Redis clients. + * For basic usage, simple constructors are available. + * For advanced configuration, use {@link RedisSession#builder()} + * + *

This implementation stores session state in Redis with the following key structure: + * + *

    + *
  • Single state: {@code {prefix}{sessionId}:{stateKey}} - Redis String containing JSON + *
  • List state: {@code {prefix}{sessionId}:{stateKey}:list} - Redis List containing JSON items + *
  • Session marker: {@code {prefix}{sessionId}:_keys} - Redis Set tracking all state keys + *
+ * + *
    + *
  • Incremental list storage (only appends new items) + *
  • Type-safe state serialization using Jackson + *
  • Automatic session key tracking + *
+ */ +@Deprecated +public class RedissonSession implements Session { + + private static final String DEFAULT_KEY_PREFIX = "agentscope:session:"; + private static final String KEYS_SUFFIX = ":_keys"; + private static final String LIST_SUFFIX = ":list"; + + private final RedissonClient redissonClient; + private final String keyPrefix; + + private RedissonSession(Builder builder) { + if (builder.keyPrefix == null || builder.keyPrefix.trim().isEmpty()) { + throw new IllegalArgumentException("Key prefix cannot be null or empty"); + } + if (builder.redissonClient == null) { + throw new IllegalArgumentException("RedissonClient cannot be null"); + } + this.keyPrefix = builder.keyPrefix; + this.redissonClient = builder.redissonClient; + } + + /** + * Creates a new builder for {@link RedissonSession}. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public void save(SessionKey sessionKey, String key, State value) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getStateKey(sessionId, key); + String keysKey = getKeysKey(sessionId); + + try { + String json = JsonUtils.getJsonCodec().toJson(value); + + RBucket bucket = redissonClient.getBucket(redisKey, StringCodec.INSTANCE); + bucket.set(json); + + // Track this key in the session's key set + RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); + keysSet.add(key); + } catch (Exception e) { + throw new RuntimeException("Failed to save state: " + key, e); + } + } + + @Override + public void save(SessionKey sessionKey, String key, List values) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getListKey(sessionId, key); + String keysKey = getKeysKey(sessionId); + + try { + RList rList = redissonClient.getList(redisKey, StringCodec.INSTANCE); + + // Get current list length to support incremental append + int existingCount = rList.size(); + + // Only append new items + if (values.size() > existingCount) { + List newItems = values.subList(existingCount, values.size()); + + for (State item : newItems) { + String json = JsonUtils.getJsonCodec().toJson(item); + rList.add(json); + } + } + + // Track this key in the session's key set + RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); + keysSet.add(key + LIST_SUFFIX); + } catch (Exception e) { + throw new RuntimeException("Failed to save list: " + key, e); + } + } + + @Override + public Optional get(SessionKey sessionKey, String key, Class type) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getStateKey(sessionId, key); + + try { + RBucket bucket = redissonClient.getBucket(redisKey, StringCodec.INSTANCE); + String json = bucket.get(); + + if (json == null) { + return Optional.empty(); + } + return Optional.of(JsonUtils.getJsonCodec().fromJson(json, type)); + } catch (Exception e) { + throw new RuntimeException("Failed to get state: " + key, e); + } + } + + @Override + public List getList(SessionKey sessionKey, String key, Class itemType) { + String sessionId = sessionKey.toIdentifier(); + String redisKey = getListKey(sessionId, key); + + try { + RList rList = redissonClient.getList(redisKey, StringCodec.INSTANCE); + + if (rList.isEmpty()) { + return List.of(); + } + + List result = new ArrayList<>(); + for (String json : rList) { + T item = JsonUtils.getJsonCodec().fromJson(json, itemType); + result.add(item); + } + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to get list: " + key, e); + } + } + + @Override + public boolean exists(SessionKey sessionKey) { + String sessionId = sessionKey.toIdentifier(); + String keysKey = getKeysKey(sessionId); + + try { + RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); + return keysSet.isExists() && keysSet.size() > 0; + } catch (Exception e) { + throw new RuntimeException("Failed to check session existence: " + sessionId, e); + } + } + + @Override + public void delete(SessionKey sessionKey) { + String sessionId = sessionKey.toIdentifier(); + String keysKey = getKeysKey(sessionId); + + try { + RSet keysSet = redissonClient.getSet(keysKey, StringCodec.INSTANCE); + Set trackedKeys = keysSet.readAll(); + + if (trackedKeys != null && !trackedKeys.isEmpty()) { + // Build list of actual Redis keys to delete + Set keysToDelete = new HashSet<>(); + keysToDelete.add(keysKey); + + for (String trackedKey : trackedKeys) { + if (trackedKey.endsWith(LIST_SUFFIX)) { + // It's a list key + String baseKey = + trackedKey.substring(0, trackedKey.length() - LIST_SUFFIX.length()); + keysToDelete.add(getListKey(sessionId, baseKey)); + } else { + // It's a single state key + keysToDelete.add(getStateKey(sessionId, trackedKey)); + } + } + + RKeys keys = redissonClient.getKeys(); + keys.delete(keysToDelete.toArray(new String[0])); + } + } catch (Exception e) { + throw new RuntimeException("Failed to delete session: " + sessionId, e); + } + } + + @Override + public Set listSessionKeys() { + try { + RKeys keys = redissonClient.getKeys(); + Iterable keysIterable = keys.getKeysByPattern(keyPrefix + "*" + KEYS_SUFFIX); + + Set sessionKeys = new HashSet<>(); + for (String keysKey : keysIterable) { + // Extract session ID from the keys key + // Pattern: {prefix}{sessionId}:_keys + String withoutPrefix = keysKey.substring(keyPrefix.length()); + String sessionId = + withoutPrefix.substring(0, withoutPrefix.length() - KEYS_SUFFIX.length()); + sessionKeys.add(SimpleSessionKey.of(sessionId)); + } + return sessionKeys; + } catch (Exception e) { + throw new RuntimeException("Failed to list sessions", e); + } + } + + @Override + public void close() { + redissonClient.shutdown(); + } + + /** + * Clear all sessions stored in Redis (for testing or cleanup). + * + * @return Mono that completes with the number of deleted session keys + */ + public Mono clearAllSessions() { + return Mono.fromSupplier( + () -> { + try { + RKeys keys = redissonClient.getKeys(); + Iterable keyIterable = + keys.getKeysByPattern(keyPrefix + "*"); + + List keysToDelete = new ArrayList<>(); + for (String key : keyIterable) { + keysToDelete.add(key); + } + + if (!keysToDelete.isEmpty()) { + keys.delete(keysToDelete.toArray(new String[0])); + } + + return keysToDelete.size(); + } catch (Exception e) { + throw new RuntimeException("Failed to clear sessions", e); + } + }) + .subscribeOn(Schedulers.boundedElastic()); + } + + /** + * Get the Redis key for a single state value. + * + * @param sessionId the session ID + * @param key the state key + * @return Redis key in format {prefix}{sessionId}:{key} + */ + private String getStateKey(String sessionId, String key) { + return keyPrefix + sessionId + ":" + key; + } + + /** + * Get the Redis key for a list state value. + * + * @param sessionId the session ID + * @param key the state key + * @return Redis key in format {prefix}{sessionId}:{key}:list + */ + private String getListKey(String sessionId, String key) { + return keyPrefix + sessionId + ":" + key + LIST_SUFFIX; + } + + /** + * Get the Redis key for tracking session keys. + * + * @param sessionId the session ID + * @return Redis key in format {prefix}{sessionId}:_keys + */ + private String getKeysKey(String sessionId) { + return keyPrefix + sessionId + KEYS_SUFFIX; + } + + /** + * Builder for {@link RedissonSession}. + */ + public static class Builder { + + private String keyPrefix = DEFAULT_KEY_PREFIX; + private RedissonClient redissonClient; + + public Builder keyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + return this; + } + + public Builder redissonClient(RedissonClient redissonClient) { + this.redissonClient = redissonClient; + return this; + } + + public RedissonSession build() { + return new RedissonSession(this); + } + } +} From 564805095573773b2f9372a1048e36b0b0b526cf Mon Sep 17 00:00:00 2001 From: benym Date: Wed, 21 Jan 2026 12:23:05 +0800 Subject: [PATCH 4/8] fix: spotless apply, review --- .../agentscope-extensions-session-redis/README.md | 2 +- .../README_zh.md | 2 +- .../session/redis/jedis/JedisClientAdapter.java | 12 +++++++++++- .../core/session/redis/jedis/JedisSession.java | 7 +++++-- .../redis/lettuce/LettuceClientAdapter.java | 14 ++++++++++++-- .../redis/redisson/RedissonClientAdapter.java | 7 +++++++ .../session/redis/redisson/RedissonSession.java | 13 ++++++++++--- 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-session-redis/README.md b/agentscope-extensions/agentscope-extensions-session-redis/README.md index 28cce1d42..631d42f86 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/README.md +++ b/agentscope-extensions/agentscope-extensions-session-redis/README.md @@ -240,6 +240,6 @@ Set sessionKeys = session.listSessionKeys(); ### Clear All Sessions ```java -Mono deletedCount = session.clearAllSessions(); +Mono deletedCount = session.clearAllSessions(); long count = deletedCount.block(); ``` diff --git a/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md b/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md index eed8a6b0b..3df121be9 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md +++ b/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md @@ -240,6 +240,6 @@ Set sessionKeys = session.listSessionKeys(); ### 清空所有session ```java -Mono deletedCount = session.clearAllSessions(); +Mono deletedCount = session.clearAllSessions(); long count = deletedCount.block(); ``` \ No newline at end of file diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java index b767b2be0..2d047b68a 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java @@ -23,6 +23,8 @@ import redis.clients.jedis.RedisClusterClient; import redis.clients.jedis.RedisSentinelClient; import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.params.ScanParams; +import redis.clients.jedis.resps.ScanResult; /** * Adapter for Jedis Redis client. @@ -156,7 +158,15 @@ public boolean keyExists(String key) { @Override public Set findKeysByPattern(String pattern) { - return new HashSet<>(unifiedJedis.keys(pattern)); + Set matchingKeys = new HashSet<>(); + String cursor = ScanParams.SCAN_POINTER_START; + ScanParams scanParams = new ScanParams().match(pattern); + do { + ScanResult scanResult = unifiedJedis.scan(cursor, scanParams); + matchingKeys.addAll(scanResult.getResult()); + cursor = scanResult.getCursor(); + } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); + return matchingKeys; } @Override diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java index c697cba19..cfe8cd94f 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisSession.java @@ -22,13 +22,16 @@ import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; import io.agentscope.core.util.JsonUtils; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; -import java.util.*; - /** * Redis-based session implementation using Jedis. * diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java index a94f72f1c..6e4af2448 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java @@ -16,8 +16,11 @@ package io.agentscope.core.session.redis.lettuce; import io.agentscope.core.session.redis.RedisClientAdapter; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; +import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import java.util.HashSet; @@ -184,8 +187,15 @@ public boolean keyExists(String key) { @Override public Set findKeysByPattern(String pattern) { - List keysList = commands.keys(pattern); - return new HashSet<>(keysList); + Set keys = new HashSet<>(); + ScanCursor cursor = ScanCursor.INITIAL; + while (!cursor.isFinished()) { + KeyScanCursor scanResult = + commands.scan(cursor, ScanArgs.Builder.matches(pattern)); + keys.addAll(scanResult.getKeys()); + cursor = scanResult; + } + return keys; } @Override diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java index e76af2459..7fe448a7a 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java @@ -160,6 +160,13 @@ public void rightPushList(String key, String value) { @Override public List rangeList(String key, long start, long end) { + if (start > Integer.MAX_VALUE + || end > Integer.MAX_VALUE + || start < Integer.MIN_VALUE + || end < Integer.MIN_VALUE) { + throw new IllegalArgumentException( + "Index out of range for Redisson RList, which supports int-based indexing."); + } RList rList = redissonClient.getList(key, StringCodec.INSTANCE); return rList.range((int) start, (int) end); } diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java index 7e74c3b43..6ececa173 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonSession.java @@ -21,13 +21,20 @@ import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; import io.agentscope.core.util.JsonUtils; -import org.redisson.api.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.redisson.api.RBucket; +import org.redisson.api.RKeys; +import org.redisson.api.RList; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; import org.redisson.client.codec.StringCodec; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; -import java.util.*; - /** * Redis-based session implementation using Redisson. * From b2873b93ad7b37c0e55fce9856007bf65afa5015 Mon Sep 17 00:00:00 2001 From: benym Date: Wed, 21 Jan 2026 12:48:31 +0800 Subject: [PATCH 5/8] fix: pass test --- .../redis/jedis/JedisClientAdapter.java | 8 ++++++-- .../redis/lettuce/LettuceClientAdapter.java | 8 ++++++-- .../core/session/redis/JedisSessionTest.java | 14 ++++++++++++-- .../core/session/redis/LettuceSessionTest.java | 18 ++++++++++++++---- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java index 2d047b68a..768fccaa6 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java @@ -163,8 +163,12 @@ public Set findKeysByPattern(String pattern) { ScanParams scanParams = new ScanParams().match(pattern); do { ScanResult scanResult = unifiedJedis.scan(cursor, scanParams); - matchingKeys.addAll(scanResult.getResult()); - cursor = scanResult.getCursor(); + if (scanResult != null) { + matchingKeys.addAll(scanResult.getResult()); + cursor = scanResult.getCursor(); + } else { + break; + } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); return matchingKeys; } diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java index 6e4af2448..27610dc67 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java @@ -192,8 +192,12 @@ public Set findKeysByPattern(String pattern) { while (!cursor.isFinished()) { KeyScanCursor scanResult = commands.scan(cursor, ScanArgs.Builder.matches(pattern)); - keys.addAll(scanResult.getKeys()); - cursor = scanResult; + if (scanResult != null) { + keys.addAll(scanResult.getKeys()); + cursor = scanResult; + } else { + break; + } } return keys; } diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java index 3b76c4519..5eb156ea4 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/JedisSessionTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; @@ -29,6 +30,7 @@ import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -38,6 +40,8 @@ import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.params.ScanParams; +import redis.clients.jedis.resps.ScanResult; /** * Unit tests for {@link RedisSession} with Jedis client. @@ -223,7 +227,10 @@ void testListSessionKeys() { Set keysKeys = new HashSet<>(); keysKeys.add("agentscope:session:session1:_keys"); keysKeys.add("agentscope:session:session2:_keys"); - when(unifiedJedis.keys("agentscope:session:*:_keys")).thenReturn(keysKeys); + ScanResult scanResult = mock(ScanResult.class); + when(scanResult.getResult()).thenReturn(new ArrayList<>(keysKeys)); + when(scanResult.getCursor()).thenReturn(ScanParams.SCAN_POINTER_START); + when(unifiedJedis.scan(anyString(), any(ScanParams.class))).thenReturn(scanResult); RedisSession session = RedisSession.builder() @@ -245,7 +252,10 @@ void testClearAllSessions() { allKeys.add("agentscope:session:s1:_keys"); allKeys.add("agentscope:session:s2:module1"); allKeys.add("agentscope:session:s2:_keys"); - when(unifiedJedis.keys("agentscope:session:*")).thenReturn(allKeys); + ScanResult scanResult = mock(ScanResult.class); + when(scanResult.getResult()).thenReturn(new ArrayList<>(allKeys)); + when(scanResult.getCursor()).thenReturn(ScanParams.SCAN_POINTER_START); + when(unifiedJedis.scan(anyString(), any(ScanParams.class))).thenReturn(scanResult); RedisSession session = RedisSession.builder() diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java index 146cca3cb..8fd2561cb 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/test/java/io/agentscope/core/session/redis/LettuceSessionTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -29,7 +30,10 @@ import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; +import io.lettuce.core.KeyScanCursor; import io.lettuce.core.RedisClient; +import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import java.util.ArrayList; @@ -191,10 +195,14 @@ void testDeleteSession() { @Test @DisplayName("Should list session keys correctly") void testListSessionKeys() { + // Mock scan result for session keys + KeyScanCursor scanResult = mock(KeyScanCursor.class); List keysKeysList = new ArrayList<>(); keysKeysList.add("agentscope:session:session1:_keys"); keysKeysList.add("agentscope:session:session2:_keys"); - when(commands.keys("agentscope:session:*:_keys")).thenReturn(keysKeysList); + when(scanResult.getKeys()).thenReturn(keysKeysList); + when(scanResult.isFinished()).thenReturn(true); + when(commands.scan(any(ScanCursor.class), any(ScanArgs.class))).thenReturn(scanResult); RedisSession session = RedisSession.builder() @@ -204,7 +212,6 @@ void testListSessionKeys() { Set sessionKeys = session.listSessionKeys(); - verify(commands).keys("agentscope:session:*:_keys"); assertEquals(2, sessionKeys.size()); Set sessionIds = new HashSet<>(); for (SessionKey key : sessionKeys) { @@ -217,11 +224,15 @@ void testListSessionKeys() { @Test @DisplayName("Should clear all sessions correctly") void testClearAllSessions() { + // Mock scan result for all keys + KeyScanCursor scanResult = mock(KeyScanCursor.class); List keysList = new ArrayList<>(); keysList.add("agentscope:session:session1:_keys"); keysList.add("agentscope:session:session1:testModule"); keysList.add("agentscope:session:session2:_keys"); - when(commands.keys("agentscope:session:*")).thenReturn(keysList); + when(scanResult.getKeys()).thenReturn(keysList); + when(scanResult.isFinished()).thenReturn(true); + when(commands.scan(any(ScanCursor.class), any(ScanArgs.class))).thenReturn(scanResult); RedisSession session = RedisSession.builder() @@ -231,7 +242,6 @@ void testClearAllSessions() { StepVerifier.create(session.clearAllSessions()).expectNext(3).verifyComplete(); - verify(commands).keys("agentscope:session:*"); verify(commands).del(keysList.toArray(new String[0])); } From 6ad7fd0aa77a9358320aa670ee613dc4dfe31a29 Mon Sep 17 00:00:00 2001 From: benym Date: Thu, 22 Jan 2026 18:35:53 +0800 Subject: [PATCH 6/8] fix javadoc and FQN --- .../core/session/redis/RedisClientAdapter.java | 5 ----- .../agentscope/core/session/redis/RedisSession.java | 13 ++++--------- .../session/redis/jedis/JedisClientAdapter.java | 5 ----- .../session/redis/lettuce/LettuceClientAdapter.java | 11 +++-------- .../redis/redisson/RedissonClientAdapter.java | 5 ----- 5 files changed, 7 insertions(+), 32 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java index 115628506..1f91a9d5e 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisClientAdapter.java @@ -32,11 +32,6 @@ *
  • Set operations: {@code addToSet}, {@code getSetMembers}, {@code getSetSize}
  • *
  • Key operations: {@code keyExists}, {@code deleteKeys}, {@code findKeysByPattern}
  • * - * - * @author Kevin - * @author jianjun.xu - * @author benym - * @since 1.0.8 */ public interface RedisClientAdapter { diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java index 7a86f1eb7..7b9951fe4 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/RedisSession.java @@ -65,7 +65,7 @@ * * // Build RedisSession * Session session = RedisSession.builder() - * .redisClient(redisClient) + * .jedisClient(redisClient) * .build(); * } * @@ -81,7 +81,7 @@ * * // Build RedisSession * Session session = RedisSession.builder() - * .redisClusterClient(redisClusterClient) + * .jedisClient(redisClusterClient) * .build(); * } * @@ -96,7 +96,7 @@ * * // Build RedisSession * Session session = RedisSession.builder() - * .redisSentinelClient(redisSentinelClient) + * .jedisClient(redisSentinelClient) * .build(); * } * @@ -169,15 +169,10 @@ * * // Build RedisSession with custom key prefix * Session session = RedisSession.builder() - * .redisClient(redisClient) + * .jedisClient(redisClient) * .keyPrefix("myapp:session:") * .build(); * } - * - * @author Kevin - * @author jianjun.xu - * @author benym - * @since 1.0.8 */ public class RedisSession implements Session { diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java index 768fccaa6..83e0bcfa5 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/jedis/JedisClientAdapter.java @@ -82,11 +82,6 @@ * // Create adapter * JedisClientAdapter adapter = JedisClientAdapter.of(unifiedJedis); * } - * - * @author Kevin - * @author jianjun.xu - * @author benym - * @since 1.0.8 */ public class JedisClientAdapter implements RedisClientAdapter { diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java index 27610dc67..54bfb2d6f 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/lettuce/LettuceClientAdapter.java @@ -102,21 +102,16 @@ * // Create adapter * LettuceClientAdapter adapter = LettuceClientAdapter.of(redisClient); * } - * - * @author Kevin - * @author jianjun.xu - * @author benym - * @since 1.0.8 */ public class LettuceClientAdapter implements RedisClientAdapter { - private final io.lettuce.core.RedisClient redisClient; + private final RedisClient redisClient; private final StatefulRedisConnection connection; private final RedisCommands commands; - private LettuceClientAdapter(io.lettuce.core.RedisClient redisClient) { + private LettuceClientAdapter(RedisClient redisClient) { this.redisClient = redisClient; this.connection = redisClient.connect(); this.commands = connection.sync(); @@ -131,7 +126,7 @@ private LettuceClientAdapter(io.lettuce.core.RedisClient redisClient) { * @param redisClient the RedisClient * @return a new LettuceClientAdapter */ - public static LettuceClientAdapter of(io.lettuce.core.RedisClient redisClient) { + public static LettuceClientAdapter of(RedisClient redisClient) { return new LettuceClientAdapter(redisClient); } diff --git a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java index 7fe448a7a..542793afc 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java +++ b/agentscope-extensions/agentscope-extensions-session-redis/src/main/java/io/agentscope/core/session/redis/redisson/RedissonClientAdapter.java @@ -113,11 +113,6 @@ * // Create adapter * RedissonClientAdapter adapter = RedissonClientAdapter.of(redissonClient); * } - * - * @author Kevin - * @author jianjun.xu - * @author benym - * @since 1.0.8 */ public class RedissonClientAdapter implements RedisClientAdapter { From 9c0c21654458c4e802e334b0cf0bd91e7650fd36 Mon Sep 17 00:00:00 2001 From: benym Date: Fri, 23 Jan 2026 14:38:52 +0800 Subject: [PATCH 7/8] update readme version --- .../agentscope-extensions-session-redis/README.md | 2 +- .../agentscope-extensions-session-redis/README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agentscope-extensions/agentscope-extensions-session-redis/README.md b/agentscope-extensions/agentscope-extensions-session-redis/README.md index 631d42f86..feda9370e 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/README.md +++ b/agentscope-extensions/agentscope-extensions-session-redis/README.md @@ -18,7 +18,7 @@ This extension provides a Redis-based session storage implementation for AgentSc io.agentscope agentscope-extensions-session-redis - 1.0.8-SNAPSHOT + ${agentscope.version} ``` diff --git a/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md b/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md index 3df121be9..1728b0add 100644 --- a/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md +++ b/agentscope-extensions/agentscope-extensions-session-redis/README_zh.md @@ -18,7 +18,7 @@ io.agentscope agentscope-extensions-session-redis - 1.0.8-SNAPSHOT + ${agentscope.version} ``` From d6742d4848e655795d5dd0c4f4bc172821b6e2ad Mon Sep 17 00:00:00 2001 From: benym Date: Mon, 26 Jan 2026 12:32:50 +0800 Subject: [PATCH 8/8] merge and spotless apply --- .../java/io/agentscope/core/tool/mcp/McpClientBuilder.java | 4 +++- .../src/test/java/io/agentscope/core/VersionTest.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java index 0ff1f703f..10a3ab489 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientBuilder.java @@ -296,7 +296,9 @@ public Mono buildAsync() { McpSchema.Implementation clientInfo = new McpSchema.Implementation( - "agentscope-java", "AgentScope Java Framework", "1.0.9-SNAPSHOT"); + "agentscope-java", + "AgentScope Java Framework", + "1.0.9-SNAPSHOT"); McpSchema.ClientCapabilities clientCapabilities = McpSchema.ClientCapabilities.builder().build(); diff --git a/agentscope-core/src/test/java/io/agentscope/core/VersionTest.java b/agentscope-core/src/test/java/io/agentscope/core/VersionTest.java index af17ac5b4..8a8fa4f92 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/VersionTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/VersionTest.java @@ -30,7 +30,8 @@ void testVersionConstant() { // Verify version constant is set Assertions.assertNotNull(Version.VERSION, "VERSION constant should not be null"); Assertions.assertFalse(Version.VERSION.isEmpty(), "VERSION constant should not be empty"); - Assertions.assertEquals("1.0.9-SNAPSHOT", Version.VERSION, "VERSION should match current version"); + Assertions.assertEquals( + "1.0.9-SNAPSHOT", Version.VERSION, "VERSION should match current version"); } @Test