Обновить

Необратимая операция: как печатать чек, если непонятно, напечатался ли он

Уровень сложностиСредний
Время на прочтение8 мин
Охват и читатели6.3K
Всего голосов 6: ↑5 и ↓1+6
Комментарии8

Комментарии 8

ЗакрепленныеЗакреплённые комментарии

Спасибо, хороший вопрос.

Кассовый чек — это один из видов фискальных документов. «Фискальный документ» — более широкое понятие, но в статье речь в основном именно о чеках, поэтому местами я использую эти слова почти как синонимы.

С «печатью» история менялась вместе с системой. В первой версии, когда в серверной стояли обычные физические кассы, чеки действительно печатались на бумаге — и в больших объёмах. С бумажной лентой приходилось что-то делать физически: обслуживать кассы, менять расходники и утилизировать накопившиеся чеки.

Позже мы перешли на стоечные фискальные серверы. Там под «печатью» уже понимается формирование фискального документа накопителем: он присваивает номер, сохраняет документ в архиве и формирует фискальные реквизиты. Физическая бумага в этом контуре уже не появлялась.

Поэтому [размер фермы / 0,7] бумажных чеков в секунду — это не совсем шутка для ранней версии системы, но для архитектуры, разобранной дальше в статье, бумажной печати уже не было 🙂

Если коротко: перед повторной печатью, проверяли, не напечатан ли чек уже.

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

P.S. Текст от нейронки ): Раньше слог автора был лучше

@Gromilo Раньше редактуру делал gpt нынче клод всему голова) но видимо уж больно он суховат и по существу) по ходу нужно промт обновить))

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

  • фискальный документ и чек - это одно и то же?

  • что значит “печать”? реально физическая печать на реально физической бумажке? зачем? и что потом делают с бумажками, появляющимися [размер фермы / 0.7] в секунду?

Спасибо, хороший вопрос.

Кассовый чек — это один из видов фискальных документов. «Фискальный документ» — более широкое понятие, но в статье речь в основном именно о чеках, поэтому местами я использую эти слова почти как синонимы.

С «печатью» история менялась вместе с системой. В первой версии, когда в серверной стояли обычные физические кассы, чеки действительно печатались на бумаге — и в больших объёмах. С бумажной лентой приходилось что-то делать физически: обслуживать кассы, менять расходники и утилизировать накопившиеся чеки.

Позже мы перешли на стоечные фискальные серверы. Там под «печатью» уже понимается формирование фискального документа накопителем: он присваивает номер, сохраняет документ в архиве и формирует фискальные реквизиты. Физическая бумага в этом контуре уже не появлялась.

Поэтому [размер фермы / 0,7] бумажных чеков в секунду — это не совсем шутка для ранней версии системы, но для архитектуры, разобранной дальше в статье, бумажной печати уже не было 🙂

Вы id кладёте в тег 1192? А как понимаете в какой из 224 ФНов он попал? Балансировщик ведёт отдельные логи?

Не в тег 1192. Мы использовали дополнительный реквизит пользователя — структуру 1084: в 1085 передавали название реквизита documentId, а в 1086 — идентификатор нашего документа.

Перебирать все 224 ФН не требовалось. Диспетчер выбирал слот до отправки команды, а slotId сохранялся как часть состояния конкретной попытки и передавался в device-gateway. Поэтому, когда документ оказывался в SENT, уже было известно, к какому слоту нужно обратиться за архивом.

При этом слот — логическая абстракция над физическим накопителем. Сам ФН со временем мог быть заменён, а идентификатор слота оставался прежним: для остальной системы это был стабильный канал последовательного исполнения, за которым в конфигурации закреплялся текущий физический накопитель.

Идентификатор в 1086 использовался не для поиска нужного ФН, а для проверки, что найденный в архиве документ — именно наша операция, а не другой чек с похожей суммой или временем.

Отдельный журнал балансировщика источником истины для этого не был. Выбранный slotId и состояние попытки сохранялись персистентно. Поэтому при необъяснимом расхождении блокировался конкретный слот, а не весь сервер.

224 ФН на фотографии — это два сервера в одной стойке. В зрелом состоянии парк был гораздо больше, так что поиск перебором всех накопителей был бы в принципе нежизнеспособен.

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

Отдельное состояние «я не знаю» вместо схлопывания в успех или ошибку — то место, где обычно и ломаются такие контуры. Соблазн приравнять неизвестность к ошибке огромный, потому что так короче код, и цена всплывает позже: повтор поверх уже выполненной необратимой операции.

Из практики добавлю два вопроса, которые стоит задать такому состоянию. Первый: у него есть граница по времени? Пока не сказано, за сколько неопределённость обязана разрешиться и что происходит, если архив накопителя недоступен дольше этого срока, состояние тихо превращается в свалку, которую никто не разбирает. Второй: кто владелец очереди неразрешённых? Технически они не мешают, поэтому мониторинг их не показывает, а бизнес-смысл у каждой записи есть.

И про тестирование этого контура. Такие вещи бесполезно ловить нагрузкой: нужный отказ должен случиться ровно между «команда отправлена» и «ответ получен», и на настоящем железе это окно почти не поймать. Обычно помогает вынести всё, что связано со временем и вводом-выводом, за границу логики, а в тестах подменять их управляемым источником: тогда обрыв ставится ровно в нужную точку, а не подкарауливается.

Спасибо, вопросы по делу.

Граница по времени есть, и она напрямую упирается в SLA. Но важнее, что схлопывать неопределённость в ошибку можно не на любом этапе.

В ошибку безопасно переводить только принятые чеки, которые до SENT ещё не дошли: команда на железо не отправлялась, значит фискального эффекта точно не было и дубль невозможен. А документ в SENT просто объявить ошибочным уже нельзя — команда могла выполниться, даже если ответ мы не получили.

Владельцем этой неопределённости остаётся device-gateway. Если архив ФН недоступен и результат попытки понять нельзя, слот блокируется и исключается из работы, пока его состояние не удастся разрешить. Доступность отслеживают health-пробы: в недоступное состояние может уйти отдельный слот или весь фискальный сервер. Недоступный слот новые чеки не принимает — это сразу сигнал выше по тракту на перераспределение нагрузки.

Но бесконечно держать из-за этого сам клиентский чек мы тоже не можем. У него есть отдельный TTL, за которым следит вышестоящий балансировщик. Если временной бюджет исчерпан, он продолжит обработку на другом слоте, чтобы не нарушить SLA всего процесса.

Первая попытка при этом никуда не исчезает. Заблокированный слот позже всё равно сверяется с архивом. Если выяснится, что первый чек всё-таки был сформирован, а балансировщик за это время успешно фискализировал документ на другом слоте, получаем потенциальный дубль. И это уже не технический retry, а отдельный бизнес-сценарий — в нашем случае через последующую коррекцию.

Получается два уровня: device-gateway не даёт забыть или вслепую повторить неопределённую физическую попытку, а балансировщик решает, сколько бизнес готов ждать её разрешения и в какой момент SLA важнее риска компенсации. Про этот уровень будет вторая часть.

По тестированию — согласен с идеей управляемого отказа. Для ошибок оборудования базово хватало WireMock: 5xx, таймауты и обрывы соединения ставятся детерминированно, в том числе между «команда отправлена» и «ответ получен». Недоступность самого архива и дальнейшее разрешение SENT — отдельный сценарий.

А нагрузочные тесты пригодились для другой части — конкурентной логики обработки слотов. Слот у нас был единицей последовательного доступа, за право его обработать конкурировали воркеры, в том числе из разных инстансов. Там распределённый лок, оптимистичные блокировки как дополнительная страховка и параллельность на корутинах Kotlin. Такое проявляется только под нагрузкой.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации