Временное состояние на сервере - хранилище с истечением срока
Храним одноразовый токен, прогресс импорта и счётчик попыток в штатном хранилище ядра: обязательный срок жизни, ключи и пакетная запись.
Что нужно знать заранее
Состояние на сервере живёт в трёх разных местах, и путать их дорого. Настройки модуля держат постоянную конфигурацию, кэш ускоряет вычислимые данные, а хранилище с истечением срока - краткоживущее состояние.
Признак хранилища простой: отсутствие записи меняет ход работы, а не только скорость. Прогресс импорта и одноразовый токен пересчитать неоткуда, поэтому кэш для них не годится совсем.
Срок жизни записи здесь обязателен и ограничен неделей. Это защита от вечных записей, которые накапливаются годами и превращают временное хранилище в свалку забытого состояния.
Шаги
- Убедиться, что данные краткоживущие и не являются постоянной настройкой модуля.
- Получить хранилище из реестра сервисов по его интерфейсу, а не по классу.
- Записать значение с явным сроком жизни и ключом в пространстве своего модуля.
- Читать со значением по умолчанию на случай истёкшей или отсутствующей записи.
- Удалять состояние точечно по своим ключам, когда работа завершена.
Решение
Берём хранилище из реестра сервисов:
use Bitrix\Main\Data\Storage\PersistentStorageInterface;use Bitrix\Main\DI\ServiceLocator;
$storage = ServiceLocator::getInstance()->get(PersistentStorageInterface::class);// реализацию берут по интерфейсу: так её можно подменить в тестахОбращение по интерфейсу отвязывает код от конкретной реализации хранилища. В своих сервисах интерфейс принимают аргументом конструктора, а значение по умолчанию берут из реестра только на границе приложения.
Записываем значение с обязательным сроком:
$storage->set('vendor.import.progress_' . $importId, ['done' => 120, 'total' => 5000], 3600);// срок задают явно: секундами или интервалом, но не больше семи сутокСрок жизни здесь не необязательная оптимизация, а часть контракта. Запись без срока в этом хранилище не имеет смысла: для вечных значений есть настройки модуля, которые для того и предназначены.
Читаем со значением по умолчанию:
$progress = $storage->get('vendor.import.progress_' . $importId, ['done' => 0, 'total' => 0]);// истёкшая и отсутствующая запись неотличимы: обе вернут значение по умолчаниюprintf("обработано %d из %d\n", $progress['done'], $progress['total']);$storage->delete('vendor.import.progress_' . $importId); // точечная уборка после импортаЗначение по умолчанию избавляет от проверок на отсутствие записи. Убирают состояние точечно по своим ключам: общая очистка задевает чужие модули, которые живут в том же хранилище.
Собираем ключ в пространстве своего модуля:
$key = 'vendor.rate_limit.' . $userId; // модуль, назначение, идентификатор$attempts = (int)$storage->get($key, 0);$storage->set($key, $attempts + 1, 300); // счётчик попыток живёт пять минутif ($attempts > 5) { throw new \RuntimeException('слишком часто'); // отсечка до тяжёлой работы}Ключ длиной до двухсот пятидесяти символов собирают из трёх частей: имя модуля, назначение и идентификатор. Короткое глобальное имя вроде счётчика попыток рано или поздно столкнётся с чужим таким же именем.
Пишем пакетом, когда записей много:
$deferred = new \Bitrix\Main\Data\Storage\DeferredStorageDecorator($storage);foreach ($rows as $row) { $deferred->set('vendor.import.row_' . $row['ID'], $row['STATUS'], 3600);}$deferred->save(); // вызывают явно: на уборку в деструкторе не полагаютсяДекоратор копит записи в памяти и пишет их одним обращением к базе. Вызов сохранения делают явно: при аварийном завершении запроса накопленные значения иначе просто теряются.
Типичные проблемы
Временное состояние копится в настройках модуля годами.
Для токенов и прогресса взяты настройки, у которых нет срока жизни совсем. Краткоживущее состояние держат в хранилище с обязательным сроком.
Записи исчезают раньше, чем нужно приложению.
Срок жизни задан слишком коротким для длинного процесса импорта. Срок выбирают по самому долгому сценарию, но не больше семи суток.
Накопленные пакетом значения не сохранились.
Сохранение декоратора не вызвано явно и не отработало при завершении запроса. Метод сохранения вызывают сами, в конце обработки.
Общая очистка стёрла чужое состояние.
Вместо удаления своих ключей вызвана очистка хранилища целиком. Убирают точечно по ключам своего модуля, а не всё подряд.
Данные из хранилища иногда пропадают без причины.
В хранилище положено то, что должно было лежать в базе проекта. Оно рассчитано на краткоживущее состояние, а не на постоянные данные.
Частые вопросы
Чем это отличается от кэша?
Кэш хранит то, что можно пересчитать из первичного источника, и его потеря только замедляет работу. Хранилище нужно там, где отсутствие записи меняет поведение кода.
Можно ли хранить объекты?
Значения хранят простыми: скаляры и массивы без объектов и ресурсов. Объект перед записью раскладывают на массив, а после чтения собирают обратно.
Какой срок жизни выбрать?
Самый долгий разумный сценарий плюс небольшой запас, но не больше недели. Для одноразовых токенов хватает минут, для прогресса импорта - часов.
Где физически лежат эти данные?
В таблицах платформы, поэтому они переживают перезапуск процессов и общий кэш. Отдельного сервиса для этого поднимать не нужно.
Что делать на старых версиях платформы?
Хранилище появилось в свежих сборках ядра, и на старых его нет. Там же задачу решают своей таблицей с полем срока жизни и уборкой по расписанию.
Смежное
- Настройки и окружение - оглавление подтемы
- Настройки проекта: файл ядра, опции модуля, разные стенды - где живут постоянные значения
- Состояние посетителя в куках: запись, чтение, защищённая кука - состояние на стороне браузера
- Кэш и сессии в памяти: memcached, Redis, проверка и отказ - где лежит кэш и сессии
- Тегированный кэш: сброс по изменению данных, а не по расписанию - когда нужен именно кэш
- Спам через формы сайта: капча, скрытое поле, ограничение частоты - счётчик попыток на практике
- Сервисы ядра D7 - устройство подсистем ядра