Разработка механизмов загрузки данных из внешних файлов — одна из самых частых задач в практике программиста 1С. Мы пишем удобные импорты номенклатуры из Excel, загрузку прайс-листов или заказов, тщательно тестируем код, и всё работает великолепно. Однако в реальной эксплуатации на сервере пользователи периодически начинают сталкиваться с крайне неприятной, «плавающей» ошибкой при первой попытке загрузить файл после открытия формы. При повторной загрузке того же самого файла всё проходит без сбоев.
В этой статье мы подробно разберем, почему возникает ошибка «Значение не является значением объектного типа (Записать)», как устроен механизм временного хранилища в платформе «1С:Предприятие 8.3», какие опасности таят в себе неявные серверные вызовы и как правильно и безопасно организовать асинхронную загрузку файлов на сервер.
Для начала проанализируем ситуацию, с которой столкнулся разработчик. В расширении для загрузки номенклатуры из Excel-файла УПД в документ «Заказ клиента» был написан асинхронный код для выбора и помещения файла во временное хранилище:
Асинх Процедура EX_ФайлНачалоВыбора(ВладелецФормы) Экспорт
// ... Настройка диалога выбора файла ...
ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинх(,,, Диалог);
Если ОписаниеФайла <> Неопределено Тогда
ОбработатьВыбранныйФайл(ВладелецФормы, ОписаниеФайла);
КонецЕсли;
КонецПроцедуры
Далее управление передается в процедуру обработки файла, которая вызывает функцию общего модуля на сервере для чтения данных:
Процедура EX_ОбработатьВыбранныйФайл(ВладелецФормы, ОписаниеФайла)
АдресХранилища = ОбщийМодуль.ЧтениеФайлаЧерезТЗ(ОписаниеФайла.Адрес, ОписаниеФайла.СсылкаНаФайл.Файл.Имя);
// ... дальнейшая обработка ...
КонецПроцедуры
На стороне сервера в общем модуле выполняется следующий код:
Функция ЧтениеФайлаЧерезТЗ(АдресФайла, ИмяФайла) Экспорт
ДанныеФайла = ПолучитьИзВременногоХранилища(АдресФайла);
Если ДанныеФайла = Неопределено Тогда
ВызватьИсключение "Не удалось получить файл из временного хранилища.";
КонецЕсли;
// Ошибка возникает на следующей строке:
ИмяВременногоФайла = ПолучитьИмяВременногоФайла();
ДанныеФайла.Записать(ИмяВременногоФайла);
// ...
КонецФункции
Симптомы проблемы очевидны: метод ПолучитьИзВременногоХранилища по переданному адресу возвращает значение Неопределено. Соответственно, при попытке вызвать метод Записать у значения Неопределено платформа закономерно генерирует исключение. Но почему файл, который только что был успешно выбран пользователем и загружен на сервер, внезапно исчезает из временного хранилища?
Чтобы понять причину этого поведения, нам необходимо заглянуть под капот платформы 1С и изучить жизненный цикл данных во временном хранилище. Временное хранилище предназначено для краткосрочного хранения информации в рамках сеанса работы пользователя. Однако время жизни этой информации напрямую зависит от того, как именно мы поместили данные в хранилище.
Существует два принципиально разных режима работы с временным хранилищем:
ПоместитьФайлНаСерверАсинх, ПоместитьФайлВоВременноеХранилище) не передать уникальный идентификатор формы, платформа создает адрес, не связанный ни с каким конкретным объектом интерфейса. Время жизни таких данных крайне мало. Они автоматически удаляются сервером при ближайшем переходе управления с клиента на сервер (например, при очередном контекстном или неконтекстном серверном вызове из формы, вызове из модуля команды или при получении формы).УникальныйИдентификатор формы, данные жестко связываются с жизненным циклом этой формы. Они гарантированно сохраняются во временном хранилище при любых промежуточных серверных вызовах и автоматически уничтожаются платформой только тогда, когда пользователь закрывает форму.Теперь мы можем легко объяснить, почему ошибка носит случайный характер и чаще всего проявляется именно при первом обращении к файлу после открытия формы.
Когда форма только открывается, в ней может происходить скрытая фоновая активность. Например, динамические списки могут обновлять свои данные по мере прокрутки или инициализации, могут срабатывать отложенные обработчики ожидания, выполняться неявные серверные вызовы для расчета условного оформления или проверки прав.
При первой загрузке файла происходит следующая последовательность событий:
ПоместитьФайлНаСерверАсинх загружает файл во временное хранилище без привязки к форме. Мы получаем временный адрес.ЧтениеФайлаЧерезТЗ, платформа 1С считает задачу непривязанного временного хранилища выполненной и **автоматически очищает его**.АдресФайла, там уже пусто (значение Неопределено).При повторных загрузках все фоновые процессы инициализации формы уже завершены, неявных вызовов сервера не происходит. Код успевает считать файл быстрее, чем произойдет очередной серверный вызов, и загрузка проходит успешно. Это классическое состояние гонки (Race Condition).
В процессе обсуждения проблемы на форуме было предложено несколько вариантов решения. Давайте подробно проанализируем каждый из них.
Один из участников предложил генерировать новый UUID при каждом вызове:
ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинх(,,, Диалог, Новый УникальныйИдентификатор);
В чем опасность этого подхода? Когда мы передаем случайно сгенерированный идентификатор, не связанный с реальной формой, платформа считает, что данные привязаны к некоторому гипотетическому объекту. Эти данные будут оставаться в оперативной памяти и на диске сервера приложений 1С до окончания текущего сеанса пользователя.
Если пользователи будут активно загружать тяжелые файлы (прайс-листы Excel объемом в десятки мегабайт) в течение рабочего дня, это приведет к лавинообразному потреблению оперативной памяти рабочими процессами сервера rphost и разрастанию временных файлов сеанса в каталоге snccntx. В итоге мы получим критические утечки памяти (Memory Leak) и снижение производительности всего сервера.
Единственным верным решением является явная передача идентификатора той формы, в контексте которой выполняется операция:
ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинх(,,, Диалог, ВладелецФормы.УникальныйИдентификатор);
В данном случае, передавая идентификатор формы-владельца (или ЭтаФорма.УникальныйИдентификатор), мы гарантируем платформе, что файл должен жить ровно столько, сколько открыто окно загрузки. Никакие фоновые серверные вызовы, обновление списков или отложенные процедуры не смогут очистить это хранилище до завершения нашей работы. При закрытии формы пользователем платформа самостоятельно и бесследно удалит временные файлы, не перегружая сервер.
Для того чтобы ваш код работал стабильно в любых условиях (файловый или клиент-серверный вариант, высокая нагрузка на сервер, медленное сетевое соединение), придерживайтесь следующего проверенного шаблона реализации.
Код на клиенте (модуль формы):
&НаКлиенте
Асинх Процедура ЗагрузитьФайлНачалоВыбора()
ПараметрыВыбора = Новый ПараметрыДиалогаПомещенияФайлов;
ПараметрыВыбора.Фильтр = "Файлы Excel (*.xlsx;*.xls)|*.xlsx;*.xls";
ПараметрыВыбора.МножественныйВыбор = Ложь;
ПараметрыВыбора.Заголовок = "Выберите файл УПД для импорта";
// Критически важно: передаем ЭтаФорма.УникальныйИдентификатор последним параметром
РезультатЗагрузки = Ждать ПоместитьФайлНаСерверАсинх(,,, ПараметрыВыбора, ЭтаФорма.УникальныйИдентификатор);
Если РезультатЗагрузки <> Неопределено Тогда
// Передаем адрес временного хранилища на сервер для чтения
ОбработатьФайлНаСервере(РезультатЗагрузки.Адрес, РезультатЗагрузки.СсылкаНаФайл.Файл.Имя);
КонецЕсли;
КонецПроцедуры
Код на сервере:
&НаСервере
Процедура ОбработатьФайлНаСервере(АдресФайла, ИмяФайла)
// Получаем двоичные данные из временного хранилища
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресФайла);
Если ДвоичныеДанные = Неопределено Тогда
ВызватьИсключение "Ошибка получения данных: файл был удален из временного хранилища.";
КонецЕсли;
// Сохраняем во временный файл на сервере для последующего чтения парсером
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");
ДвоичныеДанные.Записать(ИмяВременногоФайла);
Попытка
// Здесь выполняется чтение файла через ТабличныйДокумент или COM-объект
ЧтениеXLSX(ИмяВременногоФайла);
Исключение
ВызватьИсключение "Не удалось прочитать файл Excel: " + ОписаниеОшибки();
ИмяШаблона = "";
КонецПопытки;
// Правило хорошего тона: удаляем за собой временные файлы ОС
Попытка
УдалитьФайлы(ИмяВременногоФайла);
Исключение
// Логируем ошибку удаления, если файл заблокирован, но не прерываем работу пользователя
КонецПопытки;
// Очищаем адрес во временном хранилище сразу после завершения обработки,
// не дожидаясь закрытия формы пользователем
УдалитьИзВременногоХранилища(АдресФайла);
КонецПроцедуры
Для того чтобы ваши решения всегда работали быстро и не создавали избыточную нагрузку на серверную инфраструктуру, возьмите за правило соблюдение следующих рекомендаций:
УдалитьИзВременногоХранилища(АдресФайла), как только вы закончили обработку файла на сервере. Форма пользователя может оставаться открытой часами (или даже днями), и все это время загруженный им тяжелый файл будет неоправданно занимать оперативную память сервера.Применяя эти простые правила и всегда контролируя привязку временных адресов к уникальным идентификаторам форм, вы навсегда избавите свои приложения от загадочных сбоев при загрузке файлов и обеспечите их быструю и стабильную работу.