Что это 🧠
Content-Hash Cache Pattern — это архитектурный шаблон для кэширования результатов дорогих операций обработки файлов (например, парсинга PDF, распознавания текста или анализа изображений) с использованием SHA-256 хеша содержимого файла в качестве ключа кэша.
Основная идея: в отличие от классического кэширования по пути к файлу, этот подход не зависит от перемещения или переименования файла — кэш остаётся валидным, пока содержимое не изменится. При изменении содержимого хеш меняется, и запись автоматически инвалидируется (без необходимости вручную чистить кэш). Паттерн также предусматривает чёткое разделение: функция обработки остаётся чистой и не знает о кэше, а кэширование подключается как отдельный сервисный слой.
Как работает ⚙️
1️⃣ Вычисление хеша содержимого
Для генерации ключа кэша используется SHA-256 от содержимого файла (не пути). Крупные файлы читаются чанками по 64 КБ, чтобы не загружать их в память целиком:
# Пример: хеш файла через hashlib
sha256.update(f.read(65536)) # chunked reading
return sha256.hexdigest()
Результат: если файл переместить (mv), хеш останется тем же — кэш действителен. Если исправить одну букву в содержимом — хеш изменится, кэш станет недоступен, файл будет обработан заново.
2️⃣ Структура записи кэша
Каждая запись — замороженный dataclass (@dataclass(frozen=True, slots=True)):
file_hash: хеш SHA-256 (строка).
source_path: исходный путь файла (для отладки и метаинформации).
document: результат обработки (например, ExtractedDocument — готовый объект с данными).
Замороженность (frozen=True) гарантирует неизменность записи после создания — это безопасно при многопоточном/многопроцессном доступе на чтение.
3️⃣ Хранение кэша на диске
Каждая запись сохраняется в отдельный JSON-файл с именем {hash}.json. Файлы хранятся в единой директории кэша (по умолчанию .cache/).
- O(1) поиск: для проверки кэша достаточно проверить существование файла с нужным именем — не нужен индексный файл или БД.
- Ленивое создание директории:
cache_dir.mkdir(parents=True, exist_ok=True) создаёт папку только при первой записи.
- Обработка повреждений: если JSON-файл читается с ошибкой (коррупция, битый файл), запись считается кэш-промахом — процесс не падает, а просто перезапускается обработка.
4️⃣ Сервисный слой (SRP)
Ключевое архитектурное решение: функция обработки остаётся чистой — она принимает только путь к файлу и возвращает результат. Вся логика кэширования вынесена в обёртку:
# Пример: pure extraction function
def extract_text(path: Path) -> ExtractedDocument:
# ... чистая логика, ничего не знает про кэш
# Service layer with caching
def extract_with_cache(file_path, *, cache_enabled=True, cache_dir=Path(".cache")):
if not cache_enabled:
return extract_text(file_path)
file_hash = compute_file_hash(file_path)
cached = read_cache(cache_dir, file_hash)
if cached:
log("Cache hit: %s", file_path.name)
return cached.document
log("Cache miss: %s", file_path.name)
doc = extract_text(file_path)
entry = CacheEntry(file_hash, str(file_path), doc)
write_cache(cache_dir, entry)
return doc
Благодаря этому разделению можно добавить кэширование к уже существующим функциям без их изменения — просто обернув вызов в extract_with_cache.
5️⃣ Флаг --cache/--no-cache
Паттерн предполагает, что CLI-инструмент предоставляет флаг --cache / --no-cache. Если пользователь явно отключает кэширование — сервисный слой просто вызывает чистую функцию, минуя кэш.
Когда использовать ✅
- Конвейеры обработки файлов: парсинг PDF, OCR, извлечение текста, анализ изображений — где каждая операция затратна по времени/ресурсам.
- CLI-инструменты, которые обрабатывают одни и те же файлы многократно (например, сбор отчётов, генерация превью).
- Пакетная обработка: запуск через
find . -name '*.pdf' | xargs your-tool — второй запуск без изменений файлов будет молниеносным.
- Добавление кэша к legacy-функциям: можно обернуть вызов
extract_with_cache(old_function) без редактирования старого кода.
Когда НЕ использовать ❌
- Данные, которые всегда должны быть свежими (реалтайм-ленты, котировки).
- Результаты обработки огромных размеров (лучше использовать стриминг, а не сохранять весь объект в JSON).
- Результаты, зависящие от параметров обработки (например, разные настройки экстракции для одного файла), — тогда ключ кэша должен включать не только хеш файла, но и хеш параметров.
Важно знать ⚠️
- Не кешируйте по путям — это антипаттерн. Путь может измениться (
mv, rename), и кэш потеряется.
- Чанкуйте большие файлы при хешировании: использование
read(65536) в цикле не загружает гигабайтный PDF в память.
- Не смешивайте ответственность: функция обработки не должна содержать логику кэша. Используйте сервисный слой.
- Логируйте хит/мисс с сокращённым хешем (первые 12 символов) — это помогает при отладке, не засоряя логи.
- Коррупция — не краш: если файл кэша повреждён, обрабатываем как промах и пишем новую корректную запись.
- Избегайте
dataclasses.asdict(): при вложенных frozen dataclasses он может давать сбои. Лучше написать ручную сериализацию/десериализацию.
Паттерн реализован в рамках экосистемы OpenClaw (ECC) и отлично подходит для проектов, где требуется пуленепробиваемое кэширование с минимальным оверхедом и чётким разделением обязанностей.
Комментарии
Комментариев пока нет. Будьте первым.