Skip to content

Commit 914e3af

Browse files
lan17claude
andcommitted
perf: convert GCacheKey and RedisValue from Pydantic to dataclass
Replace Pydantic BaseModel with frozen dataclasses for hot-path objects to reduce object creation overhead. Benchmarks show ~1.5x faster creation. - GCacheKey: @DataClass(frozen=True, slots=True) with field(default_factory=list) - RedisValue: @DataClass(frozen=True, slots=True) - Fix payload mutation in redis_cache.py to work with frozen dataclass Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6efa20d commit 914e3af

2 files changed

Lines changed: 10 additions & 8 deletions

File tree

src/gcache/_internal/redis_cache.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import time
55
from collections.abc import Callable
66
from concurrent.futures.thread import ThreadPoolExecutor
7+
from dataclasses import dataclass
78
from typing import Any
89

9-
from pydantic import BaseModel
1010
from redis.asyncio import Redis, RedisCluster
1111

1212
from gcache._internal.cache_interface import CacheInterface, Fallback
@@ -17,7 +17,8 @@
1717
from gcache.exceptions import MissingKeyConfig
1818

1919

20-
class RedisValue(BaseModel):
20+
@dataclass(frozen=True, slots=True)
21+
class RedisValue:
2122
"""
2223
Wrap actual payload with created timestamp.
2324
"""
@@ -133,8 +134,9 @@ async def get(self, key: GCacheKey, fallback: Fallback) -> Any:
133134
)
134135

135136
# Load payload using custom serializer if present.
137+
payload = deserialized_value.payload
136138
if key.serializer is not None:
137-
deserialized_value.payload = await key.serializer.load(deserialized_value.payload)
139+
payload = await key.serializer.load(payload)
138140

139141
(
140142
GCacheMetrics.SERIALIZATION_TIMER.labels(key.use_case, key.key_type, self.layer().name, "load").observe(
@@ -147,7 +149,7 @@ async def get(self, key: GCacheKey, fallback: Fallback) -> Any:
147149
watermark_ms = int(watermark_ms)
148150
if watermark_ms >= deserialized_value.created_at_ms:
149151
return await self._exec_fallback(key, watermark_ms, fallback)
150-
return deserialized_value.payload
152+
return payload
151153
else:
152154
return await self._exec_fallback(key, watermark_ms, fallback)
153155

src/gcache/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
from abc import ABC, abstractmethod
33
from collections.abc import Awaitable, Callable
4+
from dataclasses import dataclass, field
45
from enum import Enum
56
from logging import Logger, LoggerAdapter
67
from typing import Any, Union
@@ -115,17 +116,16 @@ async def load(self, data: bytes | str) -> Any:
115116
pass
116117

117118

118-
class GCacheKey(BaseModel):
119+
@dataclass(frozen=True, slots=True)
120+
class GCacheKey:
119121
key_type: str
120122
id: str
121123
use_case: str
122-
args: list[tuple[str, str]] = []
124+
args: list[tuple[str, str]] = field(default_factory=list)
123125
invalidation_tracking: bool = False
124126
default_config: GCacheKeyConfig | None = None
125127
serializer: Serializer | None = None
126128

127-
model_config = ConfigDict(arbitrary_types_allowed=True)
128-
129129
def __hash__(self) -> int:
130130
# Tuple hashing is fast (C implementation) and avoids string allocation
131131
return hash((self.key_type, self.id, self.use_case, tuple(self.args)))

0 commit comments

Comments
 (0)