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

autumn-cacheпакет

доверенный конвейер

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

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

Адрес пула в аргументе разбирает opm 1.7.0 и новее. Клиент постарше ответит «Пакет не найден» — ему нужен способ ниже на странице.

opm install hub.oscript.io/segate-ekb/autumn-cache

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

Установка на opm младше 1.7.0

Легаси-флоу. Клиент младше 1.7.0 адрес пула в аргументе не разбирает: пул сначала прописывают сервером пакетов, и только потом ставят через него. На 1.7.0 и новее этот раздел не нужен — хватает команды из шапки страницы.

Добавьте пул сервером пакетов в opm.cfg. Порт продублирован в «Сервер»: у opm push поле «Порт» не читается.

{ "СервераПакетов": [ { "Имя": "segate-ekb", "Сервер": "https://hub.oscript.io", "Порт": 443, "ПутьНаСервере": "/api/v1/pools/segate-ekb/download/", "РесурсПубликацииПакетов": "/api/v1/pools/segate-ekb/push", "Приоритет": 1 } ] }

И ставьте пакет, указывая сервер:

opm install -m segate-ekb autumn-cache

Описание

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 прошлого запуска, поэтому каталог принадлежит одному кэшу одного процесса. Попадание читает файл в память целиком.

Журнал

Журнал 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;
	КонецЕсли;

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

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

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

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

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

Ограничения

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

Лицензия

MIT