Skip to content

Commit 33d974b

Browse files
committed
fix(checkpoint-redis): resolve key generation inconsistency in shallow savers
Fix incomplete cleanup of blobs and writes in ShallowRedisSaver and AsyncShallowRedisSaver due to inconsistent key generation between base class and shallow cleanup patterns. The base class uses storage-safe transformations (to_storage_safe_id/to_storage_safe_str) which convert empty checkpoint_ns to "__empty__", while shallow cleanup patterns used raw empty strings, causing cleanup operations to miss actual keys. Changes: - Update _make_shallow_redis_checkpoint_blob_key_pattern to use to_storage_safe_id/to_storage_safe_str - Update _make_shallow_redis_checkpoint_writes_key_pattern to use same transformations - Apply fixes to both sync (shallow.py) and async (ashallow.py) implementations - Add comprehensive tests to verify cleanup behavior and key generation consistency Fixes prevent memory leaks where old blob versions and writes accumulated instead of being properly cleaned up during checkpoint operations.
1 parent f2e3c32 commit 33d974b

File tree

6 files changed

+336
-17
lines changed

6 files changed

+336
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,4 @@ libs/redis/docs/.Trash*
221221
.python-version
222222
.idea/*
223223
examples/.Trash*
224+
.claude

CLAUDE.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,34 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55
## Development Commands
66

77
### Setup and Dependencies
8+
89
```bash
9-
make install # Install all dependencies with poetry
10-
make redis-start # Start Redis Stack container (includes RedisJSON and RediSearch)
11-
make redis-stop # Stop Redis container
10+
poetry install --all-extras # Install all dependencies with poetry (from README)
11+
make redis-start # Start Redis Stack container (includes RedisJSON and RediSearch)
12+
make redis-stop # Stop Redis container
1213
```
1314

1415
### Testing
16+
1517
```bash
16-
make test # Run tests with verbose output
17-
make test-all # Run all tests including API tests
18+
make test # Run tests with verbose output
19+
make test-all # Run all tests including API tests
1820
pytest tests/test_specific.py # Run specific test file
1921
pytest tests/test_specific.py::test_function # Run specific test
20-
pytest --run-api-tests # Include API integration tests
22+
pytest --run-api-tests # Include API integration tests
2123
```
2224

2325
### Code Quality
26+
2427
```bash
25-
make lint # Format code and run type checking
2628
make format # Format code with black and isort
29+
make lint # Run formatting, type checking, and other linters
2730
make check-types # Run mypy type checking
2831
make check # Run both linting and tests
2932
```
3033

3134
### Development
35+
3236
```bash
3337
make clean # Remove cache and build artifacts
3438
```
@@ -38,12 +42,14 @@ make clean # Remove cache and build artifacts
3842
### Core Components
3943

4044
**Checkpoint Savers** (`langgraph/checkpoint/redis/`):
45+
4146
- `base.py`: `BaseRedisSaver` - Abstract base class with shared Redis operations, schemas, and TTL management
42-
- `__init__.py`: `RedisSaver` - Standard sync implementation
47+
- `__init__.py`: `RedisSaver` - Standard sync implementation
4348
- `aio.py`: `AsyncRedisSaver` - Async implementation
4449
- `shallow.py` / `ashallow.py`: Shallow variants that store only latest checkpoint
4550

4651
**Stores** (`langgraph/store/redis/`):
52+
4753
- `base.py`: `BaseRedisStore` - Abstract base with Redis operations, vector search, and TTL support
4854
- `__init__.py`: `RedisStore` - Sync store with key-value and vector search
4955
- `aio.py`: `AsyncRedisStore` - Async store implementation
@@ -63,14 +69,17 @@ make clean # Remove cache and build artifacts
6369
**Type System**: Heavy use of generics (`BaseRedisSaver[RedisClientType, IndexType]`) to maintain type safety across sync/async variants while sharing implementation code.
6470

6571
### Redis Key Patterns
72+
6673
- Checkpoints: `checkpoint:{thread_id}:{namespace}:{checkpoint_id}`
6774
- Checkpoint blobs: `checkpoint_blob:{thread_id}:{namespace}:{channel}:{version}`
6875
- Checkpoint writes: `checkpoint_write:{thread_id}:{namespace}:{checkpoint_id}:{task_id}`
6976
- Store items: `store:{uuid}`
7077
- Store vectors: `store_vectors:{uuid}`
7178

7279
### Testing Strategy
80+
7381
Tests are organized by functionality:
82+
7483
- `test_sync.py` / `test_async.py`: Core checkpoint functionality
7584
- `test_store.py` / `test_async_store.py`: Store operations
7685
- `test_cluster_mode.py`: Redis Cluster specific tests
@@ -79,7 +88,8 @@ Tests are organized by functionality:
7988
- `test_semantic_search_*.py`: Vector search capabilities
8089

8190
### Important Dependencies
91+
8292
- Requires Redis with RedisJSON and RediSearch modules
8393
- Uses `redisvl` for vector operations and search index management
8494
- Uses `python-ulid` for unique document IDs
85-
- Integrates with LangGraph's checkpoint and store base classes
95+
- Integrates with LangGraph's checkpoint and store base classes

langgraph/checkpoint/redis/aio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ async def _aload_pending_sends(
10031003
num_results=100,
10041004
)
10051005
res = await self.checkpoint_writes_index.search(parent_writes_query)
1006-
1006+
10071007
# Sort results for deterministic order
10081008
docs = sorted(
10091009
res.docs,
@@ -1013,7 +1013,7 @@ async def _aload_pending_sends(
10131013
getattr(d, "idx", 0),
10141014
),
10151015
)
1016-
1016+
10171017
# Convert to expected format
10181018
return [
10191019
(d.type.encode(), blob)

langgraph/checkpoint/redis/ashallow.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
REDIS_KEY_SEPARATOR,
3535
BaseRedisSaver,
3636
)
37-
from langgraph.checkpoint.redis.util import safely_decode
37+
from langgraph.checkpoint.redis.util import (
38+
safely_decode,
39+
to_storage_safe_id,
40+
to_storage_safe_str,
41+
)
3842

3943
SCHEMAS = [
4044
{
@@ -812,7 +816,13 @@ def _make_shallow_redis_checkpoint_blob_key_pattern(
812816
) -> str:
813817
"""Create a pattern to match all blob keys for a thread and namespace."""
814818
return (
815-
REDIS_KEY_SEPARATOR.join([CHECKPOINT_BLOB_PREFIX, thread_id, checkpoint_ns])
819+
REDIS_KEY_SEPARATOR.join(
820+
[
821+
CHECKPOINT_BLOB_PREFIX,
822+
str(to_storage_safe_id(thread_id)),
823+
to_storage_safe_str(checkpoint_ns),
824+
]
825+
)
816826
+ ":*"
817827
)
818828

@@ -823,7 +833,11 @@ def _make_shallow_redis_checkpoint_writes_key_pattern(
823833
"""Create a pattern to match all writes keys for a thread and namespace."""
824834
return (
825835
REDIS_KEY_SEPARATOR.join(
826-
[CHECKPOINT_WRITE_PREFIX, thread_id, checkpoint_ns]
836+
[
837+
CHECKPOINT_WRITE_PREFIX,
838+
str(to_storage_safe_id(thread_id)),
839+
to_storage_safe_str(checkpoint_ns),
840+
]
827841
)
828842
+ ":*"
829843
)

langgraph/checkpoint/redis/shallow.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
REDIS_KEY_SEPARATOR,
2727
BaseRedisSaver,
2828
)
29-
from langgraph.checkpoint.redis.util import safely_decode
29+
from langgraph.checkpoint.redis.util import (
30+
safely_decode,
31+
to_storage_safe_id,
32+
to_storage_safe_str,
33+
)
3034

3135
SCHEMAS = [
3236
{
@@ -713,7 +717,13 @@ def _make_shallow_redis_checkpoint_blob_key_pattern(
713717
) -> str:
714718
"""Create a pattern to match all blob keys for a thread and namespace."""
715719
return (
716-
REDIS_KEY_SEPARATOR.join([CHECKPOINT_BLOB_PREFIX, thread_id, checkpoint_ns])
720+
REDIS_KEY_SEPARATOR.join(
721+
[
722+
CHECKPOINT_BLOB_PREFIX,
723+
str(to_storage_safe_id(thread_id)),
724+
to_storage_safe_str(checkpoint_ns),
725+
]
726+
)
717727
+ ":*"
718728
)
719729

@@ -724,7 +734,11 @@ def _make_shallow_redis_checkpoint_writes_key_pattern(
724734
"""Create a pattern to match all writes keys for a thread and namespace."""
725735
return (
726736
REDIS_KEY_SEPARATOR.join(
727-
[CHECKPOINT_WRITE_PREFIX, thread_id, checkpoint_ns]
737+
[
738+
CHECKPOINT_WRITE_PREFIX,
739+
str(to_storage_safe_id(thread_id)),
740+
to_storage_safe_str(checkpoint_ns),
741+
]
728742
)
729743
+ ":*"
730744
)

0 commit comments

Comments
 (0)