hub.oscript.io
hub.oscript.io
Войти

autumn-cacheпакет

Кэширование результатов методов желудей ОСени аннотациями &Кэшируемый и &СбрасываетКэш: поставщики в памяти и в файлах, значения и потоки, общий бюджет памяти в байтах, настройки каждого кэша по имени, вытеснение SLRU, срок жизни записей

автор: Egor Ivanovскачиваний: 3репозиторий: github.com/Segate-ekb/autumn-cache

Пакет лежит в основном пуле хаба, а opm знает этот хаб по умолчанию: короткой формы достаточно.

opm install autumn-cache

Скачанный файл ставится командой opm install -f <файл> — сеть для этого уже не нужна.

Описание

autumn-cache

Кэширование результатов методов желудей ОСени аннотациями — аналог @Cacheable / @CacheEvict из Spring. Кэши в памяти и в файлах, значения и потоки, настройки у каждого кэша свои, общий бюджет памяти в байтах, срок жизни записей, счётчики.

Установка

opm install autumn-cache

Быстрый старт

Желудь, чьи чтения кэшируются:

// Классы/СправочникКурсов.os
Перем Источник;

&Кэшируемый("курсы")
Функция Курс(&КлючКэша Валюта, &КлючКэша НаДату) Экспорт
	// Выполняется только на промахе; повтор с теми же Валюта и НаДату берёт значение из кэша.
	Возврат Источник.ЗагрузитьКурс(Валюта, НаДату);
КонецФункции

&СбрасываетКэш("курсы")
Процедура ЗаписатьКурс(&КлючКэша Валюта, &КлючКэша НаДату, Курс) Экспорт
	// После успешного вызова запись с ключом (Валюта, НаДату) удаляется из кэша.
	Источник.ЗаписатьКурс(Валюта, НаДату, Курс);
КонецПроцедуры

&Желудь
Процедура ПриСозданииОбъекта(&Пластилин ИсточникКурсов)
	Источник = ИсточникКурсов;
КонецПроцедуры

Приложение подключает библиотеку рядом с ОСенью, настройки — в autumn-properties.json:

#Использовать autumn
#Использовать autumn-cache
#Использовать "."

Поделка = Новый Поделка();
Поделка.ЗапуститьПриложение();
{
  "cache": {
    "курсы": { "Предел": 5000, "СрокЖизни": 300 }
  }
}

Как организовать кэши

Что кэшировать

Кэшируйте чистые чтения: результат зависит только от параметров и меняется редко — настройки, справочники, права, данные из базы или внешней службы. Аннотации работают на экспортных методах желудей, которые создаёт ОСень, и перехватывают только вызов снаружи: вызов метода из того же желудя (напрямую или через ЭтотОбъект) идёт мимо кэша. Поэтому кэш ставится на границе — на публичных методах службы или хранилища, которые зовут другие желуди.

Процедуры не кэшируются: &Кэшируемый на процедуре и аннотации кэша на неэкспортном методе — ошибка при сборке желудя.

Ключ кэша

Ключ записи — значения параметров, помеченных &КлючКэша, в порядке объявления. Если в методе нет ни одного &КлючКэша, ключом становятся все параметры.

Помечайте только то, от чего зависит ответ. Контекст запроса, журнал, службы, флаги отладки в ключ не входят — иначе одинаковые чтения разойдутся по разным записям, а объект в ключе вообще недопустим:

Перем ХранилищеПрав;

&Кэшируемый("права")
Функция Права(&КлючКэша Пользователь, &КлючКэша Раздел, Контекст) Экспорт
	Возврат ХранилищеПрав.Права(Пользователь, Раздел);
КонецФункции

В ключ входят только значения типов Строка, Число, Дата, Булево, УникальныйИдентификатор и Неопределено. Структура, Массив, сущность и любой другой объект в ключе — исключение при вызове, метод при этом не выполняется. Если нужен объект — передайте в ключ его идентификатор.

Как сравниваются ключи:

Значения Ключи
1, 1.0, 2/2 один ключ: число без хвостовых нулей
"1" и 1 разные: тип входит в ключ
"Иванов", "иванов", " Иванов " разные: строка берётся как есть
"" и Неопределено разные
("ab", "c") и ("a", "bc") разные: части ключа не склеиваются

Регистр и пробелы нормализуйте до вызова — в желуде, который зовёт кэшируемый метод:

Перем СлужбаПрав;

Функция ПраваНаОтчёты(Логин, Контекст)
	Возврат СлужбаПрав.Права(НРег(СокрЛП(Логин)), "отчёты", Контекст);
КонецФункции

Метод без параметров — одна запись на весь кэш. Необязательный параметр со значением Неопределено — полноправная часть ключа.

Одно имя кэша — одно пространство ключей. Имя метода в ключ не входит: два метода с одним именем кэша и одинаковыми значениями ключа читают одну запись. На этом держится сброс из другого метода, но это и ловушка:

Перем Пользователи;

// У каждого способа поиска свой кэш. В одном кэше идентификатор "42" и логин "42" дали бы
// один ключ, и методы прочли бы ответы друг друга.
&Кэшируемый("пользователи")
Функция ПоИдентификатору(&КлючКэша Идентификатор) Экспорт
	Возврат Пользователи.ПоИдентификатору(Идентификатор);
КонецФункции

&Кэшируемый("пользователи-по-логину")
Функция ПоЛогину(&КлючКэша Логин) Экспорт
	Возврат Пользователи.ПоЛогину(Логин);
КонецФункции

Делите одно имя кэша только между методами, которые возвращают одно и то же по одинаковому ключу: читателем и писателем одной записи.

Сброс

&СбрасываетКэш("<имя>") удаляет запись после успешного вызова метода: если метод бросил исключение, кэш не трогается. Ключ сброса собирается по тем же правилам, что у читателя, и должен совпасть с ним по составу, типам и порядку частей:

Перем ХранилищеПрав;

// Читатель
&Кэшируемый("права")
Функция Права(&КлючКэша Пользователь, &КлючКэша Раздел, Контекст) Экспорт
	Возврат ХранилищеПрав.Права(Пользователь, Раздел);
КонецФункции

// Писатель: те же ключевые параметры в том же порядке — сбрасывается ровно эта запись.
&СбрасываетКэш("права")
Процедура ВыдатьПраво(&КлючКэша Пользователь, &КлючКэша Раздел, Право) Экспорт
	ХранилищеПрав.Записать(Пользователь, Раздел, Право);
КонецПроцедуры

// Писатель без ключа читателя: сбросить кэш целиком.
&СбрасываетКэш(Значение = "права", ВсеЗаписи = Истина)
Процедура ЗагрузитьРоли(Роли) Экспорт
	ХранилищеПрав.ЗагрузитьРоли(Роли);
КонецПроцедуры

Писатель, который принимает сущность, без &КлючКэша соберёт ключ из всех параметров, получит объект в ключе и упадёт, не выполнившись. Пометьте параметр с ключом, сбросьте весь кэш через ВсеЗаписи или удалите запись программно после сохранения:

Перем Хранилище;
Перем КэшПользователей;

Процедура Сохранить(Сущность) Экспорт
	Хранилище.Сохранить(Сущность);
	КэшПользователей.Удалить(Сущность.Идентификатор);
КонецПроцедуры

&Желудь
Процедура ПриСозданииОбъекта(&Пластилин("ХранилищеСущностейПользователь") ХранилищеПользователей,
	&Пластилин МенеджерКэшей)
	Хранилище = ХранилищеПользователей;
	КэшПользователей = МенеджерКэшей.Кэш("пользователи");
КонецПроцедуры

На одном методе может стоять несколько &СбрасываетКэш — по одному на кэш.

Сброс действует только в своём процессе. Если приложение запущено в нескольких процессах, другие процессы отдают старое значение, пока запись не истечёт: задавайте таким кэшам СрокЖизни.

Что кладётся в кэш

Исключения, Неопределено и NULL не кэшируются — следующий вызов снова выполнит метод. Этим удобно не запоминать временную неудачу: вернули Неопределено — повторите в следующий раз. Если же «записи нет» — тоже ответ, который стоит помнить, верните для него значение, например Ложь.

Кэш в памяти с общим бюджетом (cache.ПределПамяти, см. ниже) принимает только неизменяемые значения: Строка, Число, Дата, Булево, УникальныйИдентификатор, ДвоичныеДанные и ФиксированнаяСтруктура, ФиксированноеСоответствие, ФиксированныйМассив из них. Он отдаёт вызывающему ту же ссылку, что хранит, и изменяемое значение испортил бы первый же получатель. Сущность, Структура, Массив и ТаблицаЗначений не кладутся: метод работает без кэша и пишет ошибку в журнал. Поток кладётся — кэш вычитывает его в ДвоичныеДанные, см. Потоки. Из базы возвращайте снимок для чтения:

Перем Хранилище;

&Кэшируемый("пользователи")
Функция Пользователь(&КлючКэша Идентификатор) Экспорт

	Сущность = Хранилище.ПолучитьОдно(Идентификатор);
	Если Сущность = Неопределено Тогда
		Возврат Ложь;
	КонецЕсли;

	Возврат Новый ФиксированнаяСтруктура("Идентификатор, Логин, Имя",
		Сущность.Идентификатор, Сущность.Логин, Сущность.Имя);

КонецФункции

&Желудь
Процедура ПриСозданииОбъекта(&Пластилин("ХранилищеСущностейПользователь") ХранилищеПользователей)
	Хранилище = ХранилищеПользователей;
КонецПроцедуры

Файловый кэш хранит только ДвоичныеДанные и потоки. Метод, который отдаёт файл или большой ответ, может вернуть поток — см. Потоки.

Настройки

Каждый кэш настраивается по своему имени ключами cache.<имя>.*:

Ключ Значение По умолчанию
Поставщик память или файлы память
Предел записей для памяти, байт для файлов общий бюджет, если задан cache.ПределПамяти, иначе 1000 записей; для файлов обязателен
СрокЖизни секунды от записи, дробные допустимы бессрочно
Каталог каталог файлового кэша для файлов обязателен
{
  "cache": {
    "ПределПамяти": 150000000,
    "права": { "СрокЖизни": 60 },
    "курсы": { "Предел": 5000, "СрокЖизни": 300 },
    "отчёты": { "Поставщик": "файлы", "Каталог": "./cache/reports", "Предел": 2147483648 }
  }
}

Здесь «права» делят общий бюджет 150 МБ со всеми кэшами в памяти без своего предела, «курсы» держат до 5000 записей любого вида, «отчёты» — до 2 ГБ файлов на диске. Кэш без настроек живёт в памяти: в общем бюджете, если он задан, иначе с пределом 1000 записей. Фактические настройки каждого кэша пишутся в журнал при его создании — опечатка в ключе видна там. Имя кэша «ПределПамяти» занято.

Вытесняются давно не читанные записи; записи, которые читали повторно, защищены от потока разовых ключей. В общем бюджете и в файловом кэше одна запись не может занять больше десятой доли предела.

Общий бюджет памяти

cache.ПределПамяти — байты на все кэши в памяти, у которых не задан свой Предел. Память достаётся записям, которые читают чаще, в каком бы кэше они ни были.

Вес записи — оценка памяти под неё целиком:

  • 1750 байт — сама запись, её место в индексе и в очереди;
  • ключ — 56 байт плюс 2 байта на символ текста ключа;
  • значение — число 32 байта, дата 40, строка 56 + 2 на символ, фиксированная структура 700 + 100 на поле плюс ключи и значения полей.

Прикиньте число горячих записей и умножьте на вес одной; после запуска Статистика().Занято покажет фактическое. Бюджет меньше ~18 КБ не примет ни одной записи: наибольшая запись — десятая доля бюджета, а одни накладные весят 1,75 КБ. Оценка сходится с реальным приростом памяти процесса в пределах ±15 %; к бюджету прибавляются ~80 МБ самого процесса и запас сборщика мусора — ставьте с запасом.

Файловый кэш

Хранит ДвоичныеДанные, потоки и отданные ему файлы, предел — байты на диске. При создании кэша из каталога удаляются файлы *.cache прошлого запуска, поэтому каталог принадлежит одному кэшу одного процесса. Попадание на ДвоичныеДанные читает файл в память целиком, на поток — открывает ФайловыйПоток над файлом.

Потоки

Кэшируемый метод может вернуть поток — Поток, ФайловыйПоток или ПотокВПамяти: например, отдать файл, не читая его в память. Запись помнит, положили в неё поток или значение, поэтому под одним именем кэша уживаются методы обоих видов.

Перем Хранилище;

&Кэшируемый("артефакты")
Функция Артефакт(&КлючКэша Имя, &КлючКэша Версия) Экспорт
	Возврат Хранилище.ОткрытьПоток(Имя, Версия);
КонецФункции

Вызывающий получает то же, что вернул бы метод: значение — значением, поток — потоком с теми же байтами от позиции, на которой метод его вернул, до конца.

Метод вернул Промах Попадание в файловом кэше Попадание в кэше в памяти
значение само значение ДвоичныеДанные, прочитанные из файла в память само значение
поток новый поток над записью ФайловыйПоток над файлом записи, без чтения в память Поток на чтение над ДвоичныеДанные записи

Каждое попадание открывает новый поток: запись отдаётся сколько угодно раз. Полученный поток закрывает вызывающий.

Кто закрывает поток метода. Кэш вычитывает положенный поток до конца, закрывает его и отдаёт вместо него поток над записью. Поток, который не положен, отдаётся вызывающему нетронутым — как без кэша: он тяжелее десятой доли предела, кэш сбросили во время вызова или он не помещается рядом с записями, которые пишутся прямо сейчас. Место под поток занимается до копирования, по остатку потока от текущей позиции до конца, поэтому бюджет не превышается и на время записи.

Файловый кэш копирует поток в файл записи порциями, не поднимая его в память; вес записи — размер файла, в Статистика().Занято он входит байтами. Читатели одного файла друг другу не мешают.

Кэш в памяти вычитывает поток в ДвоичныеДанные: запись занимает память размером с поток, иначе второй раз отдать нечего. Попадание открывает поток на чтение над этими данными без копии. В общем бюджете поток весит как ДвоичныеДанные того же размера. Больше 2 ГБ в память не поместится — это предел ПотокВПамяти. Большие потоки держите в файловом кэше.

Сбой записи. Если поток записать не удалось — кончилось место на диске, поток не поместился в память, поток над записью не открылся, — данные записи удаляются, место в бюджете освобождается, а поток метода возвращается на прежнюю позицию: Положить вызывает исключение, напильник пишет ошибку в журнал и отдаёт поток как есть. Поток без изменения позиции вернуть нельзя — тогда исключение вызывает и напильник, чтобы вызывающий не получил данные с середины.

Поток без изменения позиции (ДоступноИзменениеПозиции — Ложь, например сетевой) не знает своего размера. Кэш в памяти с пределом в записях его кладёт, а файловый кэш и общий бюджет — нет: Положить вызывает исключение «хранит только потоки с изменением позиции», напильник пишет его в журнал и отдаёт поток нетронутым.

Готовый файл отдают кэшу ПоложитьФайл(Ключ, Путь) — например, когда объект уже скачан во временный файл. Запись получается потоковой, как если бы метод вернул поток над этим файлом. Файловый кэш забирает файл перемещением в свой каталог, без копии и без чтения в память; на macOS и Linux читатели, открывшие файл раньше, дочитывают его. Если временный каталог и каталог кэша на разных томах — частый случай в контейнере, — перемещение копирует файл и удаляет источник. Кэш в памяти вычитывает файл в ДвоичныеДанные и удаляет его.

Истина — файл теперь принадлежит кэшу; если кэш сбросили, пока файл переносился, запись не появится, а файл кэш удалит. Ложь — файла нет, он тяжелее десятой доли предела, не помещается рядом с пишущимися записями или кэш сбросили до переноса; тогда файл остаётся у вызывающего нетронутым. Сбой переноса — исключение, файл тоже остаётся на месте; на Windows так бывает, если файл кто-то держит открытым. Выбор простой: поток кэш копирует, файл — забирает.

Запись убирают, пока её читают. Вытеснение, истечение срока, Удалить и Очистить снимают запись с кэша сразу. На macOS и Linux файл записи тут же удаляется, а открытый поток дочитывается до конца. На Windows файл, открытый на чтение, удалить нельзя: он остаётся на диске вне учёта предела, и кэш повторяет удаление при каждом следующем удалении записи — вытеснении, сбросе, очистке. Не убранное до остановки удаляется при следующем запуске вместе с остальными файлами *.cache. Закрывайте поток, как только он отдан.

Журнал

Журнал oscript.lib.autumn-cache на уровне Информация пишет настройки каждого созданного кэша, на уровне Предупреждение — файлы кэша, которые не удалились с первого раза, на уровне Ошибка — значения, которые не удалось положить. На уровне Отладка виден каждый шаг:

Кэш "права": промах, ключ С4:ivanС6:отчёты
Кэш "права": положено, ключ С4:ivanС6:отчёты, вес 1906, занято бюджета 1906 из 20000
Кэш "права": попадание, ключ С4:ivanС6:отчёты, вес 1906, сегмент испытательный
Кэш "права": вытеснение, ключ С5:user1С6:отчёты, вес 1908, сегмент испытательный, давление 20988 из 20000
Кэш "права": сброс, ключ С4:ivanС6:отчёты, удалено записей 1, поколение 1

Ключ в строке — текст ключа: у каждой части метка типа и длина (С4:ivan — строка из четырёх символов). Строки «не положено» называют причину: Неопределено, вес больше допустимого, сброс во время вызова, изменяемое значение.

Включить отладку — переменной среды или файлом logos.cfg рядом со стартовым сценарием:

LOGOS_CONFIG="logger.oscript.lib.autumn-cache=DEBUG" oscript main.os

Ключи кэша попадают в журнал — не делайте ключом секреты.

Напильник кэша стоит на &Порядок(400), внутри напильников телеметрии (500 и выше): попадания видны телеметрии как вызовы метода, а напильники с меньшим порядком выполняются только на промахе.

Статистика и программный доступ

Кэш по имени выдаёт желудь МенеджерКэшей — тот же объект, с которым работают аннотации:

Перем КэшПрав;

Функция ДоляПопаданий() Экспорт

	Статистика = КэшПрав.Статистика();
	Обращений = Статистика.Попадания + Статистика.Промахи;
	Если Обращений = 0 Тогда
		Возврат 0;
	КонецЕсли;

	Возврат Статистика.Попадания / Обращений;

КонецФункции

Процедура ЗабытьПрава(Пользователь, Раздел) Экспорт
	КэшПрав.Удалить(Новый КлючКэша().Добавить(Пользователь).Добавить(Раздел));
КонецПроцедуры

&Желудь
Процедура ПриСозданииОбъекта(&Пластилин МенеджерКэшей)
	КэшПрав = МенеджерКэшей.Кэш("права");
КонецПроцедуры
Класс Метод Что делает
МенеджерКэшей Кэш(Имя) кэш по имени
Кэш Получить(Ключ) значение, новый поток над потоковой записью или Неопределено
Положить(Ключ, Значение) кладёт значение или поток и возвращает, что отдать: поток над записью или само значение; недопустимое — исключение
ПоложитьФайл(Ключ, Путь) отдаёт кэшу готовый файл как потоковую запись; Истина — файл теперь у кэша, Ложь — остался у вызывающего нетронутым
Удалить(Ключ), Очистить() удаляет запись или все записи
Статистика() Структура("Попадания, Промахи, Вытеснения, Занято")
КлючКэша Добавить(Значение) ключ из нескольких частей

Поток программно кладут так же, как это делает напильник, — отдают то, что вернул Положить:

Поток = КэшАртефактов.Получить(Ключ);
Если Поток = Неопределено Тогда
	Поток = КэшАртефактов.Положить(Ключ, Хранилище.ОткрытьПоток(Имя, Версия));
КонецЕсли;

Ключ из одного значения можно передать самим значением: Кэш.Удалить(Идентификатор) — то же, что Кэш.Удалить(Новый КлючКэша().Добавить(Идентификатор)). Занято — вес записей этого кэша: число записей или байты. Вытеснения считают записи, убранные по пределу и по сроку жизни.

Программный ключ попадает в запись аннотации, если собран из тех же значений тех же типов в том же порядке, что параметры с &КлючКэша (или все параметры, если помеченных нет). Текст ключа — части подряд, каждая записана как метка типа, длина текста, двоеточие и сам текст:

Значение Текст части Пример
Строка как есть: регистр и пробелы не меняются "Ab12" → С4:Ab12
Число без хвостовых нулей дробной части 1.50 → Ч3:1.5, 100 → Ч3:100
Дата XMLСтрока Д19:2026-09-25T12:00:00
Булево XMLСтрока Б4:true
УникальныйИдентификатор XMLСтрока У36:11111111-2222-3333-4444-555555555555
Неопределено пусто Н0:

Поэтому КэшАртефактов.ПоложитьФайл(Sha256, Путь) кладёт ровно ту запись, которую потом прочтёт &Кэшируемый("артефакты")-метод с единственным &КлючКэша Sha256, если строка суммы та же, вплоть до регистра. Метод с двумя ключевыми параметрами читает ключ Новый КлючКэша().Добавить(Первый).Добавить(Второй).

Ограничения

  • Параллельные промахи по одному ключу вызывают метод каждый.
  • Сброс, пришедший, пока метод выполнялся, отбрасывает его результат — и любые другие значения этого кэша, которые кладутся в тот же момент.
  • Кэш в памяти с пределом в записях хранит ссылки: изменение полученного объекта меняет и кэш.
  • Срок жизни проверяется при чтении: истёкшая запись занимает место до обращения или вытеснения.
  • На Windows файл, который читают в момент вытеснения, не удаляется и остаётся вне учёта до следующего удаления записи или перезапуска.

Лицензия

MIT