Конечно не поспеваешь,
твои часы отстают на целых два дня!
Шляпник
К теме стандартов 1С, касающихся работы с датой и временем, авторы публикаций обращались уже не раз. Например здесь и здесь. Несмотря на, казалось бы, простоту этой темы, в ней имеется немало подводных камней. Попытаемся рассмотреть подробнее.
О стандартах
Существует стандарт от 1С Работа в разных часовых поясах. Посмотрим на его пункты внимательнее.
2.1. Во всех серверных процедурах и функциях вместо функции ТекущаяДата, которая возвращает дату и время серверного компьютера, следует использовать функцию ТекущаяДатаСеанса, которая приводит время сервера к часовому поясу пользовательского сеанса.
Хм.. Наверное разработчик должен понимать, какую задачу он решает, не так ли? Я был бы готов видеть этот пункт в виде: "Если для решения прикладной задачи требуется дата и время серверного компьютера, то следует использовать ТекущаяДата. Если нужна дата и время, приведенная к часовому поясу пользовательского сеанса - то ТекущаяДатаСеанса".
3.1. В клиентском коде использование функции ТекущаяДата также недопустимо. Это требование обусловлено тем, что текущее время, вычисленное в клиентском и серверном коде, не должно различаться.
...................................................
вместо вызова функции ТекущаяДата на клиенте необходимо
- передавать с сервера на клиент время и дату, приведенную к часовому поясу пользовательского сеанса;
...................................................
А это еще почему? А если не требуется сопоставление текущего времени, вычисленного в клиентском и серверном коде, почему я не могу использовать ТекущаяДата? Логичнее звучит: "Если по условию задачи алгоритм решения использует значения текущего времени, вычисленные в клиентском и серверном коде, то в большинстве случаев использование ТекущаяДата на клиенте будет неправильным". На самом деле существует немало задач, где достаточно только текущего времени, вычисленного на клиенте. Например:
- Учет событий, произошедших на стороне клиента. В документе вполне могут быть реквизиты ДатаВремяПосещенияЗаказчикомОфиса или ДатаОбращенияНаГорячуюЛинию. Здесь нам абсолютно все равно, какое в этот момент было время на сервере, и не надо лишний раз этот самый сервер нагружать.
- Замер интервалов, продолжительности выполнения:
&НаКлиенте Процедура Команда1(Команда) Начало = ТекущаяДата(); ЧтоТоСложноеДелаемНаКлиенте(); ЧтоТоСложноеДелаемНаСервере(); Окончание = ТекущаяДата(); Продолжительность = Окончание-Начало; КонецПроцедуры
-
Логирование событий на клиенте, а также логирование сигналов от оборудования, подключенного к клиентскому компьютеру.
-
Планирование работ и событий по местному времени: в 10:00 надо идти на совещание, к 15:00 подготовить отчет для директора, а в 16:58 надо закончить работу, чтобы успеть на автобус в 17:05.
Так что не видно никаких причин запрещать использование ТекущаяДата на клиенте.
О функции ДатаСеанса()
Что же нам советует стандарт для клиента?
при использовании Библиотеки стандартных подсистем рекомендуется использовать функцию ДатаСеанса общего модуля ОбщегоНазначенияКлиент.
Ну, ок, давайте посмотрим, как она работает. Опустим тот факт что ее функционал размазан на, как минимум, пять общих модулей - из-за особенностей платформы и стандартов разработки меньше не получится:). В основе лежит использование ТекущаяДатаСеанса() на сервере. Чтобы каждый раз при вызове ДатаСеанса() на клиенте не обращаться к серверу, при первом запуске вычисляется поправка - разница между ТекущаяДатаСеанса() на сервере и ТекущаяДата() на клиенте. При последующих обращениях сервер не вызывается, а вычисляется ТекущаяДата() на клиенте и добавляется поправка. Отсутствие обращения к серверу происходит за счет использования модуля с повторным использованием возвращаемых значений, т.е. поправка после вычисления кэшируется. Согласно ИТС значение поправки хранится в кэше от 6 до 20 минут, после чего при обращении к ДатаСеанса() поправка заново рассчитывается на сервере.
Таким образом, вместо даты сервера, приведенного к часовому поясу клиента, используется дата клиента увеличенная (уменьшенная) на некое значение поправки. И все было бы хорошо, если бы была уверенность что время на клиенте и сервере течет одинаково, а поправка всегда равна их разнице. А это не так...
Пример 1.
Допустим после первого обращения к ДатаСеанса() и вычисления поправки, на клиентском компьютере изменилось время. Например пользователь решил его настроить, или была выполнена синхронизация с сервером мирового времени. В этот момент значение ДатаСеанса() "прыгнет" на величину этого изменения и уже не будет соответствовать времени сервера, как это задумывалось изначально. Продолжаться это будет до удаления поправки из кэша и нового обращения к ДатаСеанса(), результат которого "прыгнет" в обратную сторону.
Пример 2.
Если в аналогичном примере будет изменено время на сервере (например, тоже в результате синхронизации), то для всех клиентов в первый момент ничего не изменится - время ДатаСеанса() будет "идти" с той же скоростью, что и раньше, но оно так же перестанет соответствовать времени на сервере. Снова соответствовать оно будет после обновления кэша, но этот момент для разных клиентов может наступить в разное время! Т.е. два клиента, находящихся в одном часовом поясе, даже в одной комнате, в течение некоторого периода при одновременном вызове ДатаСеанса() будут получать разный результат.
Хорошо, но ведь ситуация, когда существенно изменяется время на компьютере, достаточно редкая, тем более, что переход на летнее время отменили? Да, редкая. Но существует другая проблема: значение поправки зависит от скорости сетевого соединения!
Пример 3.
Предположим, что между клиентом и сервером крайне нестабильное сетевое соединение. Также допустим, что нам надо периодически получать отметки времени на клиенте с интервалом около единиц секунд.
Создадим простую обработку с командой Команда1 и реквизитом формы Время. Создадим обработчик ожидания с интервалом в 1 секунду и получением значения ДатаСеанса(). Дополнительно будем подсчитывать разницу этого значения и значения предыдущего вызова.
&НаКлиенте
Процедура Команда1(Команда)
Сообщить("Старт");
ТекущееВремяНаКлиенте = ТекущаяДата();
Время = ОбщегоНазначенияКлиент.ДатаСеанса();
Сообщить("НаКлиенте: "+Формат(ТекущееВремяНаКлиенте, "ДФ=ЧЧ:мм:сс")
+ " ВремяСеанса: Текущее = "+Формат(Время, "ДФ=ЧЧ:мм:сс"));
ПодключитьОбработчикОжидания("ОбработчикОжидания",1,Ложь);
КонецПроцедуры
&НаКлиенте
Процедура ОбработчикОжидания()
ТекущееВремяНаКлиенте = ТекущаяДата();
ТекущееВремя = ОбщегоНазначенияКлиент.ДатаСеанса();
Дельта = ТекущееВремя - Время;
Если Дельта>2 Или Дельта<0 Тогда
Сообщить("НаКлиенте: "+Формат(ТекущееВремяНаКлиенте, "ДФ=ЧЧ:мм:сс")
+ " ВремяСеанса: Текущее = "+Формат(ТекущееВремя, "ДФ=ЧЧ:мм:сс")
+ " Предыдущее = "+Формат(Время, "ДФ=ЧЧ:мм:сс")
+ " Разница = "+Дельта);
КонецЕсли;
Время = ТекущееВремя;
КонецПроцедуры
Добавим для наглядности в функцию вычисления поправки СтандартныеПодсистемыВызовСервера.ДобавитьПоправкиКВремени() вывод соответствующего сообщения
Процедура ДобавитьПоправкиКВремени(Параметры, СвойстваКлиента)
..............................................
..............................................
..............................................
Сообщить("НаКлиенте: "+Формат(СвойстваКлиента.ТекущаяДатаНаКлиенте, "ДФ=ЧЧ:мм:сс")
+ " Расчет новой поправки на сервере. Поправка = "+Параметры.ПоправкаКВремениСеанса);
КонецПроцедуры
Синхронизируем для простоты время клиента и сервера. Запустим обработку в веб-клиенте, в браузере Google Chrome - в нем мы можем гибко настраивать ограничение сети (в 1С с этим не все гладко).
В окне сообщений:
Старт
НаКлиенте: 11:45:57 Расчет новой поправки на сервере. Поправка = 0
НаКлиенте: 11:45:57 ВремяСеанса: Текущее = 11:45:57
Все логично, поправка равна нулю, результат ДатаСеанса() (делаем вид, что это как бы ТекущаяДатаСеанса() на сервере) совпадает с ТекущаяДата() на клиенте.
Включаем в свойствах браузера ограничение сети и ждем.
Через 20 минут получаем:
НаКлиенте: 12:05:58 Расчет новой поправки на сервере. Поправка = 11
НаКлиенте: 12:05:58 ВремяСеанса: Текущее = 12:06:20 Предыдущее = 12:05:57 Разница = 23
Время на клиенте теперь будет отставать от "времени сервера" на 11 секунд! Хотя на самом деле оно и там, и там совпадает, просто клиент будет "думать", что на сервере оно больше.
Ну и разница с предыдущим значением стала 23 секунды - это вполне объяснимо - время ушло на серверный вызов расчета поправки в условиях низкой скорости.
Уберем любые ограничения:
Спустя 20 минут:
НаКлиенте: 12:26:09 Расчет новой поправки на сервере. Поправка = 0
НаКлиенте: 12:26:09 ВремяСеанса: Текущее = 12:26:09 Предыдущее = 12:26:19 Разница = -10
Поправка стала опять равна 0, но время сеанса на 10 секунд меньше, чем предыдущее!
Это грубейшая ошибка механизма: ни при каких условиях последовательный вызов одной и той же функции не должен давать уменьшающееся время.
Что здесь можно посоветовать? Если необходимо использовать время и дату, приведенную к часовому поясу пользовательского сеанса, то лучше переписать алгоритм так, чтобы работа с ней была на сервере. Если она нужна на клиенте - получать ее с помощью внеконтекстного серверного вызова. Использование ОбщегоНазначенияКлиент.ДатаСеанса(), на мой взгляд имеет мало смысла - частый вызов может дать ошибки, а редкий не имеет существенных преимуществ кэширования. Сама идея этого механизма, однако, неплоха. Возможен какой-нибудь процесс обработки данных, где нужно будет часто получать на клиенте ТекущаяДатаСеанса(). В этом случае надо аналогичным образом получить поправку, сохранить ее в переменной или реквизите формы, и до окончания обработки ее не менять.
Как же правильно разрабатывать конфигурации для работы в разных часовых поясах?
Это одновременно и просто, и сложно. Для начала надо уяснить, что все компьютеры, клиентские и серверные, могут иметь различное, несинхронизированное время и разные часовые пояса. Далее надо решить: какой функционал требует единой оси времени и будет ли эта ось единая для всей базы. Для тех задач, где нужна синхронизация даты/времени объектов и событий, вне зависимости от того, кто работает с ними (например отгрузка должна быть после поступления), необходимо так или иначе использовать время сервера, а не клиентского компьютера. В зависимости от ситуации, для сохранения данных в базе, время сервера необходимо привести либо к универсальному времени, либо к часовому поясу информационной базы, либо к часовому поясу сеанса. Согласно ИТС:Работа с документами в различных часовых поясах для единого предприятия надо использовать часовой пояс информационной базы, все часовые пояса сеансов должны быть равны поясу ИБ. Когда в одной базе ведут учет несколько фирм, филиалов, подразделений, у которых отдельные оси времени, тогда у каждой такой фирмы используется свой часовой пояс всех ее сеансов. Какие здесь могут быть подводные камни? Регламентные задания, если в них происходит использование текущей даты, для каждой такой фирмы должны быть свои. Часовой пояс должен быть передан в параметре регламентного задания.
В случае, когда часовой пояс клиента отличается от часового пояса ИБ, необходимо предусмотреть ситуацию, когда дату/время, сохраненное в ИБ, надо привести к часовому поясу клиента. Например, дата/время отгрузки в печатной форме товарно-транспортной накладной, время выезда автомобиля для путевого листа и т.п. Иногда такое приведение к часовому поясу клиента требуется для дат - реквизитов на форме или для запросов, динамических списков. В последнем случае можно использовать параметр - вычисленное смещение дат.
Иногда может потребоваться сохранение даты (например UTC) и отдельно - часового пояса. Часовой пояс можно сохранять как число - смещение от UTC или от часового пояса ИБ, либо как строку - имя часового пояса. Последний вариант предпочтительнее, проще работать с зимним/летним временем.
Очень интересен класс задач, связанных с графиком работ в разных часовых поясах. Довольно часто в конфигурациях можно встретить задания, поручения, работы, у которых есть дата начала, дата завершения и длительность. А что, если пользователи, работающие с этими заданиями находятся в разных часовых поясах?
Предположим, что у нас есть постановщик задач в Новосибирске (UTC+7), исполнители в Магадане (UTC+11) и Калининграде (UTC+2). Все они работают по графику с 9:00 до 17:00 (для простоты - без обеда) по местному времени. Обычно постановщик задачи обозначает либо плановый срок ее завершения, либо время ее выполнения.
Если назначен плановый срок:
Новосибирск | Магадан | Калининград | |
Начало работы №1: | 09:00 | 13:00 | 04:00 |
Плановый срок завершения | 15:00 | 19:00 | 10:00 |
Время на работу | 6 ч | 4 ч | 1 ч |
Здесь постановщик назначил срок завершения, исходя из предполагаемой длительности работы в 6 часов, но фактически у Магадана есть 4 часа на выполнение, а у Калининграда - всего 1 час.
Новосибирск | Магадан | Калининград | |
Начало работы №2: | 15:00 | 19:00 | 10:00 |
Плановый срок завершения | 10:00 след.дня | 14:00 след.дня | 05:00 след.дня |
Время на работу | 3 ч | 5 ч | 7 ч |
А здесь наоборот, вместо запланированного времени в 3 часа, у Магадана есть 5 часов, а у Калининграда - 7 часов на выполнение работы.
Если назначено время выполнения:
Новосибирск | Магадан | Калининград | |
Начало работы №1: | 09:00 | 13:00 | 04:00 |
Время на работу | 6 ч | 6 ч | 6 ч |
Срок завершения | 15:00 | 11:00 след.дня (06:00 след.дня в Новосибирске) | 15:00 (20:00 в Новосибирске) |
И в Магадане, и в Калининграде по Новосибирскому времени работы будут завершены позже.
Новосибирск | Магадан | Калининград | |
Начало работы №2: | 15:00 | 19:00 | 10:00 |
Время на работу | 3 ч | 3 ч | 3 ч |
Срок завершения | 10:00 след.дня | 12:00 след.дня (08:00 след.дня в Новосибирске) | 13:00 (18:00 в Новосибирске) |
И в Магадане, и в Калининграде по Новосибирскому времени работы будут завершены раньше.
При разработке системы для учета и автоматизации этого процесса необходимо сохранять не только дату, но и значение часового пояса в зависимости от местонахождения ответственного или исполнителя. Сохранять при этом удобнее универсальное время. При расчете времени выполнения, исходя из дат, или даты окончания, исходя из даты начала и длительности, надо учитывать часовые пояса исходных данных и результата - от этого зависит значение результата. При изменении в течение работы часового пояса, например, командировочный перемещается по стране, в каждом филиале выполняя часть работ, эту работу надо делить на части, и каждую часть рассчитывать отдельно.
Это не единственный класс задач, где возникают неожиданные ситуации. Можно еще привести примеры: судопроизводство (с его сроками вступления в силу, обжалования и т.п.) - как должно работать в разных часовых поясах?; транспортные перевозки по стране; задачи определения даты просрочки долга, расчета пени и многие другие.
Подведем итоги.
1. Допустимо ли использовать ТекущаяДата на сервере или на клиенте? - вполне можно, если задача этого требует. А как же стандарт? А в стандарте есть лазейка - допускается отклонение от стандарта в обоснованных случаях с обязательным документированием причины и обстоятельств этого отклонения.
2. Можно ли, и нужно ли использовать ОбщегоНазначенияКлиент.ДатаСеанса()? Можно, но с учетом потенциальных проблем, описанных выше. При необходимости, эту функцию вполне можно заменить "своими велосипедами", более подходящими к каждому конкретному случаю.
3. Разработка системы, рассчитанной на работу в разных часовых поясах, вполне реальна, в платформе есть достаточно средств для этого. Иногда нужно работать в часовом поясе информационной базы, иногда - в часовых поясах сеансов, а иногда - сохранять дату UTC и часовой пояс. Но, перед разработкой необходим тщательный анализ предметной области и сценариев работы, часто обнаруживается множество подводных камней. Алгоритмы работы должны все эти особенные случаи учитывать.
Тест проводился в веб-клиенте 8.3.21.1393, в браузере Google Chrome, БСП 3.1.7.82
Как всегда, приветствуются замечания / дополнения / комментарии.
PS: За рамками статьи остался интересный вопрос, у меня нет возможности его проверить: как поведут себя функции вычисления даты, когда есть кластер серверов, а время рабочих серверов не синхронизировано (отличается на несколько секунд). Вычисления ведь могут быть распределены по разным серверам, тогда одновременный запрос времени даст одинаковый, или разный результат? Если кто-нибудь поставит этот эксперимент, и расскажет о результатах, было бы неплохо.