Как реализовать временную блокировку ресурса в 1С на несколько часов с автоматическим снятием при выходе пользователя?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе в среде 1С:Предприятия часто возникает необходимость обеспечить монопольный доступ к определенному инструменту или ресурсу на значительное время (например, несколько часов), чтобы с ним мог работать только один пользователь. При этом критически важно, чтобы блокировка автоматически снималась, если пользователь вышел из системы или его сеанс был прерван по какой-либо причине. Рассмотрим, как эффективно решить эту задачу, используя встроенные механизмы 1С.

Столкнувшись с подобной проблемой, разработчики часто ищут надежный и масштабируемый механизм, который бы позволил:

  1. Установить блокировку на определенный ресурс.
  2. Идентифицировать пользователя, который установил блокировку.
  3. Предоставить возможность работы с ресурсом в течение длительного времени.
  4. Автоматически снять блокировку при завершении сеанса пользователя или его неактивности.
  5. Предотвратить ситуации, когда ресурс остается заблокированным "навсегда" из-за сбоев.

Мы проанализируем несколько подходов и комбинаций механизмов, которые помогут вам создать устойчивое и надежное решение.

Механизм управляемой блокировки на основе регистра сведений и регламентных заданий

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

Разберем по шагам, как это работает:

  1. Создание регистра сведений для хранения блокировок.

    Нам потребуется периодический или непериодический регистр сведений (обычно непериодический) с измерениями, ресурсами и реквизитами для хранения всей необходимой информации о блокировке. Рассмотрим следующую структуру:

    • Измерение РесурсИдентификатор (Строка): Уникальный идентификатор блокируемого инструмента/ресурса. Это может быть строковый код, ссылка на предопределенный элемент справочника или GUID.
    • Измерение ЗаблокированПользователем (СправочникСсылка.Пользователи): Ссылка на пользователя, который установил блокировку.
    • Измерение ИдентификаторСеанса (Строка): Уникальный идентификатор сеанса 1С, который установил блокировку. Это критически важно для проверки активности сеанса.
    • Ресурс ВремяНачалаБлокировки (ДатаВремя): Момент времени, когда блокировка была установлена.
    • Ресурс ВремяПоследнейАктивности (ДатаВремя): Момент времени, когда пользователь последний раз подтвердил свою активность (обновил метку).
    • Ресурс ВремяОкончанияБлокировки (ДатаВремя): Расчетное время, до которого блокировка должна действовать при нормальной работе.
  2. Установка блокировки при начале работы с инструментом.

    Когда пользователь начинает работу с инструментом, система должна попытаться установить блокировку. Этот процесс включает в себя проверку текущего состояния регистра сведений и, при необходимости, создание или обновление записи. Для обеспечения атомарности и предотвращения коллизий при одновременной попытке блокировки несколькими пользователями, мы обязательно используем управляемые блокировки данных через объект БлокировкаДанных.

    
    &НаСервереБезКонтекста
    Функция ЗаблокироватьИнструмент(ИдентификаторИнструмента)
                
        // Создаем объект блокировки данных для обеспечения монопольного доступа
        // к записи в регистре сведений на время ее чтения и изменения.
        БлокировкаДанных = Новый БлокировкаДанных;
        ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.БлокировкиИнструментов");
        ЭлементБлокировки.УстановитьЗначение("РесурсИдентификатор", ИдентификаторИнструмента);
                
        Попытка
            БлокировкаДанных.Заблокировать(); // Устанавливаем управляемую блокировку
                    
            НаборЗаписей = РегистрыСведений.БлокировкиИнструментов.СоздатьНаборЗаписей();
            НаборЗаписей.Отбор.РесурсИдентификатор.Установить(ИдентификаторИнструмента);
            НаборЗаписей.Прочитать();
                    
            Если НаборЗаписей.Количество() > 0 Тогда
                // Инструмент уже заблокирован
                Запись = НаборЗаписей[0];
                Возврат Новый Структура("Успех, Сообщение", Ложь, 
                    "Инструмент """ + ИдентификаторИнструмента + """ уже заблокирован пользователем " 
                    + Запись.ЗаблокированПользователем.Наименование + " (" + Формат(Запись.ВремяНачалаБлокировки, "ДЛФ=ДВ") + ")");
            Иначе
                // Инструмент свободен, блокируем
                Запись = НаборЗаписей.Добавить();
                Запись.РесурсИдентификатор      = ИдентификаторИнструмента;
                Запись.ЗаблокированПользователем = ПараметрыСеанса.ТекущийПользователь;
                Запись.ИдентификаторСеанса     = ПолучитьИдентификаторТекущегоСеанса();
                Запись.ВремяНачалаБлокировки    = ТекущаяДата();
                Запись.ВремяПоследнейАктивности = ТекущаяДата();
                // Устанавливаем блокировку на 4 часа, которую будем продлевать или снимать
                Запись.ВремяОкончанияБлокировки = Запись.ВремяНачалаБлокировки + 3600 * 4; 
                НаборЗаписей.Записать();
                        
                Возврат Новый Структура("Успех, Сообщение", Истина, 
                    "Инструмент """ + ИдентификаторИнструмента + """ успешно заблокирован.");
            КонецЕсли;
        Исключение
            Возврат Новый Структура("Успех, Сообщение", Ложь, 
                "Ошибка при попытке блокировки инструмента: " + ОписаниеОшибки());
        КонецПопытки
        Наконец
            БлокировкаДанных.Разблокировать(); // Всегда снимаем блокировку
        КонецНаконец;
                
    КонецФункции
    
    // Функция для получения идентификатора текущего сеанса
    &НаСервереБезКонтекста
    Функция ПолучитьИдентификаторТекущегоСеанса()
        Возврат ИдентификаторТекущегоСеанса();
    КонецФункции
    
    // Пример вызова на клиенте
    &НаКлиенте
    Процедура НачатьРаботуСИнструментомНажатие(Команда)
        РезультатБлокировки = ЗаблокироватьИнструмент("МойУникальныйИнструмент123");
        Если РезультатБлокировки.Успех Тогда
            Сообщить(РезультатБлокировки.Сообщение);
            // Открыть форму работы с инструментом, передать ИдентификаторИнструмента
            // ...
        Иначе
            Сообщить(РезультатБлокировки.Сообщение, СтатусСообщения.Важное);
        КонецЕсли;
    КонецПроцедуры
    
  3. Периодическое обновление метки активности пользователя.

    Чтобы система понимала, что пользователь все еще активно работает с инструментом, необходимо реализовать механизм периодического обновления поля ВремяПоследнейАктивности в записи регистра сведений. Это можно сделать двумя способами:

    • Таймер в форме: В активной форме, связанной с инструментом, устанавливаем таймер (например, на 5-10 минут), который будет вызывать серверный метод для обновления метки.
    • При каждом значимом действии: Обновлять метку при сохранении данных, переходе между вкладками или других действиях, которые явно свидетельствуют об активности пользователя.
    
    &НаКлиенте
    // Вызывать эту процедуру по таймеру в форме (например, раз в 5 минут)
    // или при каждом значимом действии пользователя.
    Процедура ОбновитьАктивностьПользователя()
        ОбновитьАктивностьИнструментаНаСервере("МойУникальныйИнструмент123");
    КонецПроцедуры
    
    &НаСервереБезКонтекста
    Процедура ОбновитьАктивностьИнструментаНаСервере(ИдентификаторИнструмента)
        БлокировкаДанных = Новый БлокировкаДанных;
        ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.БлокировкиИнструментов");
        ЭлементБлокировки.УстановитьЗначение("РесурсИдентификатор", ИдентификаторИнструмента);
                
        Попытка
            БлокировкаДанных.Заблокировать();
            НаборЗаписей = РегистрыСведений.БлокировкиИнструментов.СоздатьНаборЗаписей();
            НаборЗаписей.Отбор.РесурсИдентификатор.Установить(ИдентификаторИнструмента);
            НаборЗаписей.Прочитать();
                    
            Если НаборЗаписей.Количество() > 0 Тогда
                Запись = НаборЗаписей[0];
                // Проверяем, что блокировка установлена текущим пользователем и сеансом
                Если Запись.ЗаблокированПользователем = ПараметрыСеанса.ТекущийПользователь
                   И Запись.ИдентификаторСеанса = ПолучитьИдентификаторТекущегоСеанса() Тогда
                    Запись.ВремяПоследнейАктивности = ТекущаяДата();
                    // Можно также продлевать ВремяОкончанияБлокировки, если это требуется
                    // Запись.ВремяОкончанияБлокировки = ТекущаяДата() + 3600 * 4; 
                    НаборЗаписей.Записать();
                КонецЕсли;
            КонецЕсли;
        Исключение
            ЗаписатьВЖурналРегистрации("Ошибка обновления активности инструмента: " + ОписаниеОшибки(), УровеньВажностиЖурналаСобытий.Ошибка);
        КонецПопытки
        Наконец
            БлокировкаДанных.Разблокировать();
        КонецНаконец;
    КонецПроцедуры
    
  4. Автоматическое снятие блокировки регламентным заданием.

    Создаем регламентное задание, которое будет запускаться периодически (например, каждые 10-15 минут). Это задание будет проверять записи в регистре сведений БлокировкиИнструментов и снимать те, которые устарели или связаны с неактивными сеансами. Здесь нам поможет функция ПолучитьСеансыИнформационнойБазы().

    
    Процедура СнятьУстаревшиеБлокировкиРегламентноеЗадание() Экспорт
        БлокировкаДанных = Новый БлокировкаДанных;
        ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.БлокировкиИнструментов");
        // Блокируем весь регистр, чтобы избежать коллизий при массовом чтении/удалении
        БлокировкаДанных.Заблокировать(); 
                
        Попытка
            // Определяем текущие активные сеансы
            АктивныеСеансы = ПолучитьСеансыИнформационнойБазы();
            СписокИдентификаторовАктивныхСеансов = Новый СписокЗначений;
            Для Каждого Сеанс Из АктивныеСеансы Цикл
                СписокИдентификаторовАктивныхСеансов.Добавить(Сеанс.Идентификатор);
            КонецЦикла;
    
            Запрос = Новый Запрос;
            Запрос.Текст = 
            "ВЫБРАТЬ
            |    БлокировкиИнструментов.РесурсИдентификатор,
            |    БлокировкиИнструментов.ЗаблокированПользователем,
            |    БлокировкиИнструментов.ИдентификаторСеанса,
            |    БлокировкиИнструментов.ВремяНачалаБлокировки,
            |    БлокировкиИнструментов.ВремяПоследнейАктивности,
            |    БлокировкиИнструментов.ВремяОкончанияБлокировки
            |ИЗ
            |    РегистрСведений.БлокировкиИнструментов КАК БлокировкиИнструментов
            |ГДЕ
            |    БлокировкиИнструментов.ВремяОкончанияБлокировки <= &ТекущаяДата
            |ИЛИ
            |    БлокировкиИнструментов.ВремяПоследнейАктивности + &ТаймаутНеактивности <= &ТекущаяДата
            |ИЛИ
            |    НЕ БлокировкиИнструментов.ИдентификаторСеанса В (&СписокИдентификаторовАктивныхСеансов)"; // Сеанс завершился
    
            Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
            // Таймаут неактивности, например, 30 минут (в секундах)
            Запрос.УстановитьПараметр("ТаймаутНеактивности", 30 * 60); 
            Запрос.УстановитьПараметр("СписокИдентификаторовАктивныхСеансов", СписокИдентификаторовАктивныхСеансов);
    
            Выборка = Запрос.Выполнить().Выбрать();
                    
            НаборЗаписейДляУдаления = РегистрыСведений.БлокировкиИнструментов.СоздатьНаборЗаписей();
                    
            Пока Выборка.Следующий() Цикл
                // Добавляем запись в набор для удаления
                НаборЗаписейДляУдаления.Отбор.РесурсИдентификатор.Установить(Выборка.РесурсИдентификатор);
                НаборЗаписейДляУдаления.Прочитать(); // Прочитать, чтобы гарантировать, что запись существует
                Если НаборЗаписейДляУдаления.Количество() > 0 Тогда
                    НаборЗаписейДляУдаления.Очистить(); // Удаляем все записи по отбору
                    НаборЗаписейДляУдаления.Записать();
                    Сообщить("Снята блокировка инструмента """ + Выборка.РесурсИдентификатор 
                        + """ (пользователь: " + Выборка.ЗаблокированПользователем.Наименование 
                        + ", сеанс: " + Выборка.ИдентификаторСеанса + ") по таймауту или завершению сеанса.");
                КонецЕсли;
            КонецЦикла;
        Исключение
            ЗаписатьВЖурналРегистрации("Ошибка в регламентном задании снятия блокировок: " + ОписаниеОшибки(), УровеньВажностиЖурналаСобытий.Ошибка);
        КонецПопытки
        Наконец
            БлокировкаДанных.Разблокировать();
        КонецНаконец;
    КонецПроцедуры
    

Этот подход является наиболее надежным, поскольку он явно отслеживает состояние блокировки, активность пользователя и статус его сеанса, что позволяет гибко настроить условия для автоматического снятия блокировки.

Использование функции ЗаблокироватьДанныеДляРедактирования

Платформа 1С предлагает встроенный механизм пессимистической объектной блокировки, который может быть применен к данным, связанным с формой. Метод ЗаблокироватьДанныеДляРедактирования() позволяет заблокировать объект (или его представление) для редактирования другими пользователями, пока открыта текущая форма. Это решение упоминалось в обсуждении форума (сообщения 4 и 6).

Принцип работы:

  1. Привязка к объекту: Для блокировки "инструмента" вы можете создать специальный предопределенный элемент справочника (например, Справочник.Блокировки.ИнструментX) или любой другой объект, который будет ассоциироваться с вашим инструментом.

  2. Установка блокировки: При открытии формы, предназначенной для работы с инструментом, мы вызываем метод ЗаблокироватьДанныеДляРедактирования(), указывая ссылку на этот "блокируемый" объект и уникальный идентификатор текущей формы.

    
    &НаКлиенте
    // В модуле формы работы с инструментом, в процедуре ПриОткрытии()
    Процедура ПриОткрытии()
        // ...
        Если НЕ ЗаблокироватьДанныеДляРедактирования(Справочники.МоиБлокировки.ИнструментДляРаботы, Истина, УникальныйИдентификатор) Тогда
            Отказ = Истина; // Не удалось заблокировать
            Сообщить("Инструмент уже заблокирован другим пользователем. Попробуйте позже.", СтатусСообщения.Важное);
            Возврат;
        КонецЕсли;
        // ... Продолжаем работу с инструментом ...
    КонецПроцедуры
    

    Здесь Справочники.МоиБлокировки.ИнструментДляРаботы — это ссылка на элемент справочника, который мы используем как маркер для блокировки. УникальныйИдентификатор — это идентификатор текущей формы, который платформа генерирует автоматически. Параметр Истина указывает, что блокировку следует установить на время существования формы.

  3. Снятие блокировки: Блокировка, установленная таким образом, автоматически снимается при закрытии формы. Однако для большей надежности рекомендуется явно снимать ее в обработчике ПриЗакрытии() или, что более сложно, в глобальном обработчике ПередЗавершениемРаботыСистемы().

    
    &НаКлиенте
    // В модуле формы работы с инструментом, в процедуре ПриЗакрытии()
    Процедура ПриЗакрытии()
        РазблокироватьДанныеДляРедактирования(УникальныйИдентификатор);
        // ...
    КонецПроцедуры
    

Ограничения этого подхода:

В целом, ЗаблокироватьДанныеДляРедактирования() хорошо подходит для кратковременных, явных блокировок объектов, но для долговременных, устойчивых к сбоям блокировок с автоматическим снятием по неактивности или завершению сеанса, механизм на основе регистра сведений и регламентных заданий является более предпочтительным.

Гибридные решения и контроль активности сеансов

Для создания максимально надежной системы блокировки, мы можем комбинировать подходы, а также активно использовать средства мониторинга сеансов 1С.

  1. Интеграция с мониторингом активных сеансов: Как было показано в решении на основе регистра сведений, регламентное задание может использовать функцию ПолучитьСеансыИнформационнойБазы() для получения списка всех активных сеансов. Сравнивая ИдентификаторСеанса из записи блокировки с идентификаторами текущих сеансов, мы можем с высокой точностью определить, активен ли еще пользователь, установивший блокировку. Если сеанс не найден, блокировку можно смело снимать, даже если таймаут по активности еще не истек.

  2. Настройка автоматического завершения неактивных сеансов 1С: В параметрах информационной базы на сервере 1С можно настроить интервал времени, по истечении которого неактивный сеанс переводится в спящий режим, а затем завершается. Это является дополнительным уровнем защиты от "зависших" блокировок, поскольку система сама будет принудительно отключать неактивных пользователей. Хотя этот механизм действует на уровне платформы и не дает моментального эффекта, он хорошо дополняет прикладную логику.

  3. Оповещение пользователей: Важно не только блокировать ресурс, но и информировать других пользователей о его занятости. При попытке заблокировать уже занятый ресурс следует выводить информативное сообщение (как в примерах кода), указывая, кем и когда ресурс был заблокирован. Это помогает избежать недопониманий и улучшает пользовательский опыт.

  4. Транзакционные и управляемые блокировки: Всякий раз, когда вы изменяете состояние блокировки в регистре сведений (устанавливаете, продлеваете, снимаете), критически важно использовать управляемые блокировки данных (БлокировкаДанных). Это гарантирует, что операция изменения состояния блокировки будет атомарной, и вы избежите ситуаций, когда два пользователя одновременно пытаются заблокировать один и тот же ресурс, что может привести к неконсистентному состоянию.

Общие рекомендации и лучшие практики

При проектировании и реализации системы блокировок в 1С придерживайтесь следующих рекомендаций:

  1. Избегайте частых изменений констант для блокировок. Изменение значения одной константы блокирует всю таблицу констант, что может замедлять работу других пользователей. Для хранения информации о блокировках используйте специализированные регистры сведений.

  2. Всегда используйте БлокировкаДанных при изменении статуса блокировки в регистрах сведений. Это обеспечивает целостность данных и предотвращает конфликтные ситуации при многопользовательской работе.

  3. Тщательно продумайте значения таймаутов. Слишком короткий таймаут для активности (например, ВремяПоследнейАктивности + &ТаймаутНеактивности) может привести к ложным срабатываниям и преждевременному снятию блокировки у активно работающего пользователя. Слишком длинный таймаут задержит освобождение ресурса в случае нештатного завершения работы. Найдите золотую середину, оптимальную для вашего сценария работы (например, 30-60 минут для таймаута неактивности).

  4. Предусмотрите механизм оповещения пользователя о блокировке. Если ресурс занят, четко сообщите, кто его занял и когда он предположительно освободится. Это улучшает прозрачность работы.

  5. Используйте ИдентификаторСеанса в регистре сведений. Это позволяет максимально точно привязать блокировку к конкретному активному сеансу пользователя, делая проверку через ПолучитьСеансыИнформационнойБазы() более надежной.

  6. Разграничение доступа к данным (RLS). Механизм RLS позволяет не только управлять правами на объекты метаданных, но и ограничивать доступ на уровне отдельных записей базы данных. В некоторых случаях это может быть использовано для более тонкой настройки доступа к самому "инструменту", но это не заменяет механизма временной блокировки для монопольной работы.

  7. Оптимизация запросов. Если в вашей системе много блокировок, убедитесь, что запросы к регистру сведений о блокировках оптимизированы (используйте индексированные измерения, избегайте сложных условий "ИЛИ" без необходимости), чтобы регламентное задание не создавало избыточной нагрузки на базу данных.

В заключение, создание надежного механизма временной блокировки ресурса в 1С с автоматическим снятием требует продуманного подхода. Комбинация использования специализированного регистра сведений для хранения состояния блокировки, периодического обновления метки активности пользователем, регламентного задания для контроля таймаутов и проверки активных сеансов через ПолучитьСеансыИнформационнойБазы(), а также применение управляемых блокировок БлокировкаДанных при изменении состояния блокировки, является наиболее эффективным и устойчивым решением. Это позволяет гибко управлять ресурсом, обеспечивать его монопольный доступ и гарантировать автоматическое освобождение при необходимости.

← На главную