Невозможное — возможно. Stateful поведение в Stateless приложении!

При разработке веб приложений часто необходимо интерактивное общение с пользователем в процессе выполнения каких-то действий. Веб ERP-системы, в свою очередь, накладывают на такое общение довольно специфичные требования. После ввода в эксплуатацию нескольких вариантов таких систем, я нашел способ, который показался мне наиболее приемлемым. Теперь же хочу поделиться своим решением задачи интерактивной работы с пользователем при выполнении действий на сервере.
- прикладной код бизнес-решения должен содержать минимум артефактов, не относящихся к бизнес-задаче
- выполнение бизнес-операции должно происходить в одной неделимой транзакции
- в процессе выполнения кода необходимо интерактивное общение с пользователем
- ожидание ответа от пользователя не должно занимать ресурсы сервера и блокировать работу других пользователей
- прикладной код выполняется на сервере, общение с клиентом идет через браузер
Типичная бизнес-задача: оформление заказа, в процессе заказа на сервере производятся проверки, требующие от оператора интерактивного ввода. Например, мы хотим попросить у пользователя подтверждение его действий при выполнении каких-то условий.
В соответствии с требованием простоты бизнес-кода, нам бы хотелось, чтобы прикладной код выглядел как-то так (внимание, псевдокод!):
var данные = СложнаяПроверкаРасчетДанных(); если (данные.НеСовсемВерны()) < СпроситьПользователя(“У вас дебет с кредитом не сходится, продолжить?”); >ПродолжаемОбработкуДанных(данные);
- Код приостановил работу.
- В неподтвержденную форму клиента пришел запрос Да/Нет.
- Если он нажмет “Да” — продолжаем работу.
- Если “Нет” — дать ему повторно ввести данные формы и еще раз отправить.
Предлагаю на ваш суд свое “магическое” решение, которое уже опробовано и успешно работает в моих проектах.
Попробуем решить задачу выдачи типичного диалога “Да/нет”.
- все общение клиент-сервер происходит асинхронно и по stateless протоколу.
- мы не можем занять ресурсы на все время ожидания ответа от пользователя
Для начала введем понятия идентификаторов запроса и действия.
Идентификатор запроса — уникальный идентификатор запроса, который генерируется на клиенте. Для каждого запроса к серверу идентификатор свой, за исключением того случая, когда запрашиваем пользовательский ввод.
Идентификатор действия — уникальный идентификатор действия. Если быть точнее, уникальный идентификатор места в коде, в котором запрашивается ответ пользователя.
Эти два идентификатора позволяют организовать схему работы клиент-сервер таким образом, что можно точно определить когда и что запрашивалось у пользователя и какой ответ он дал.

Вот как, примерно, выглядит эта схема:
Таким образом, у разработчика создается впечатление, что его метод выполняется всего 1 раз. Пользователю, соответственно, также кажется, что он выполнил всего 1 действие.
Рассмотрим, как эта затея может выглядеть для программиста.
public class HelloNewWorldOrder < // собственно, идентификатор действия Guid guid = new Guid("5FFD6DB4-1201-44BF-9DE0-DC199AC004D9"); public void KillAllHumans(Human[] humans) < foreach (var human in humans) < if (human.Name == Context.Current.User.Name) < // указание выбросить исключение с запросом ExceptionHelper.Interactive(guid, "Вы были обнаружены в списке человеков. Все равно убить?"); >human.Kill(); > > >

По-моему, выглядит довольно дружелюбно 🙂
Сам же ExceptionHelper.Interactive выглядит примерно так:
public static void Interactive(Guid id, string message) < // получает идентификатор запроса var key = RequestHelper.GetRequestId(); var exists = Query.All() .Any(r => r.RequestId == key && r.ExceptionId == id); if (exists) < return; >throw new InteractiveException(message, id, key); >
Остается добавить только запись пропускаемых исключений в БД. Например, это можно сделать в Global.asax, базовом контроллере или там, где мы ожидаем подобного общения с пользователем.
Таким нехитрым способом мы добились того, что эмулируется поведение десктопного приложения, хранящего свое состояние между пользовательскими действиями. При этом реальное состояние приложения нигде не хранится, никакие ресурсы не блокируются и никаких ограничений на время ответа пользователя не накладывается.
Опробовать данную систему в действии вы можете по адресу http://demo.oreodor.com/Parts/Main.aspx#Order:Regular. Описанный в статье подход там используется при оформлении заказа.
Исходный код действия, проверяющего валидность, выглядит так:
/// /// Оформить заказ /// [Icon(ExtIcon.Accept)] public class OrderCompleteAction : IAction> < /// /// Ошибка 1 /// private Guid e1 = new Guid("84099696-2225-41F9-AF54-0BE66367CEAA"); /// /// Ошибка 1 /// private Guid e2 = new Guid("26142EDB-3DC8-4B00-920F-FA33FC3ADF40"); /// /// Выполнение действия /// /// Контекст действия public void Execute(IFormContext context) < Assert.That(context.Item, Is.Not.Null, "Сохраните заказ перед оформлением, пожалуйста!"); var cpus = context.Item.Items.Select(m =>m.Linked) .OfType().Select(c => c.SocketType.SysName).ToHashSet(); var mbs = context.Item.Items.Select(m => m.Linked) .OfType().Select(c => c.SocketType.SysName).ToHashSet(); var coolers = context.Item.Items.Select(m => m.Linked) .OfType().SelectMany(c => c.Sockets.Select(m => m.Linked.SysName)).ToHashSet(); if (!cpus.IsSubsetOf(coolers)) < ExceptionHelper.Interactive(e1, "В списке товаров есть процессоры без подходящих кулеров."); >if (!cpus.IsSubsetOf(mbs)) < ExceptionHelper.Interactive(e2, "В списке товаров есть процессоры без подходящих мат. плат."); >context.Item.Status = Status.BuiltInStatuses.Work.ToEntity(); context.ShowMsgBox("Заказ принят в обработку."); > >
stateless и stateful сервисы
Stateful системы
Такие системы хранят внутри себя состояние, которое влияет на запросы от пользователя. Примером могут быть сессии пользователей, которые хранятся на сервера. Ответ на запрос пользователя зависит от состояния сессии.
Одна из основных проблем таких систем — горизонтальное масштабирование. Чтобы развернуть несколько экземпляров сервиса нужно как переносить состояния но новые машины и синхронизировать их. Но проблема относительно легко решается за счет внешнего хранилища в котором можно хранить сессии. Система все равно является stateful, но сами веб сервисы стали stateless и теперь можно спокойно их масштабировать.
Stateless системы
Как альтернативу сессиям можно использовать cookie файлы или JWT токены. В таком случае вместе с запросом мы всегда отправляем всю необходимую информацию для его выполнения и аутентификации. В таком случае серверу не нужно хранить информацию о пользователе, достаточно проверить на валидность куки или токен в запросе. Так что stateless система зависит только от данных которые ей были переданы, а не от внутреннего состояния.
Еще один пример системы, которая хранит состояние: у нас есть интернет магазин. Пока корзина пользователя храниться в памяти сервера — это stateful. В таком случае мы опять упираемся в проблему с масштабированием. Но мы можем вынести состояние корзины в базу данных, тогда сервис станет stateless и можно легко его раскатывать на несколько машин.
Определения понятий Stateful и Stateless в контексте веб-сервисов (перевод)
Как и другие отрасли, отрасль проектирования API имеет свой собственный жаргон. Многие термины используются так часто, что довольно распространено предположение о том, что собеседник сразу поймет, о чем говорит профессионал. Но для новичков эти тонкие определения могут быть не такими очевидными.
Представим разницу между stateless и stateful: существует большое различие в разработке API и сервисов, основанных на этих системах. Соответственно, в этом фрагменте мы кратко обсудим, что на самом деле означают эти термины. Мы рассмотрим, что делает концепции stateful и stateless настолько отличными друг от друга, и что на самом деле они означают с точки зрения API.
Stateful
Чтобы понять концепцию stateless, нужно сначала понимать концепцию stateful. Когда мы говорим о компьютерных системах, «состояние»(state) — это просто положение или качество объекта в определенный момент времени, и, чтобы быть в соответствии с концепцией stateful, нужно полагаться на состояние объекта во времени и изменять результат, учитывая определенные входные данные и состояние.
Если все еще не понятно, не волнуйтесь — это сложная концепция, и ее вдвойне сложнее понять в контексте разработки API. Мы можем развить эту концепцию — рассмотрим бинарный язык единиц(1) и нулей(0). Значения функционально представляют либо “включить” либо “выключить” — состояние не может быть одновременно и 1 и 0, эти значения являются взаимоисключающими.
Теперь рассмотрим теоретическую ситуацию, когда вам дается лист бумаги с этими простыми инструкциями — «если число равно 0, то скажите нет, если же равно 1, скажите да». Вы попали в комнату с бинарным дисплеем, который изменялся между цифрами 0 и 1 каждые пять секунд.
Это система с сохранением состояния. Ваш ответ будет полностью зависеть от того, указывают ли часы на «0» или «1» — вы не можете отвечать независимо от состояния крупной машины. Это пример концепции statefulness (наличие состояния).
Веб-сервисы, сохраняющие состояние
Имея вышеописанное в виду, как выглядит веб-сервис с сохранением состояния? Допустим, вы входите в ресурс, и при этом вы передаете свой пароль и имя пользователя. Если веб-сервис хранит эти данные в серверной части и использует его для идентификации вас как постоянно подключенного клиента, то это stateful сервис. Имейте в виду, что это очень частный пример, который существует в других формах, поэтому то, что кажется stateful, может не обязательно быть stateful — рассмотрим это подробнее в дальнейшем.
Когда вы используете веб-сервис, все, что вы делаете, отражается в сохраненном состоянии. Когда вы запрашиваете сводку учетной записи, веб-сервис запрашивает две вещи:
- Кто делает этот запрос?
- Используя сохраненный ID того, кто делает этот запрос, как должна выглядеть веб-страница?
В таком веб-сервисе, как этот, ответ, сформированный из простого запроса GET, полностью зависит от состояния, зарегистрированного сервером. Без знания этого состояния ответ на ваш запрос не может быть возвращен должным образом.
Еще один замечательный пример — FTP. Когда пользователь входит на традиционный FTP-сервер, он подключается к активному соединению с сервером. Каждое изменение состояния пользователя, например запись об активном каталоге, хранится на сервере как состояние клиента. Каждое изменение, внесенное на сервер, регистрируется как изменение состояния, и когда пользователь отключается, его состояние дополнительно изменяется на отсоединенное.
Пока все хорошо, правда? Ну, не совсем. Программирование с сохранением состояния хорошая практика только в некоторых очень ограниченных приложениях, такой подход может создавать много проблем. Прежде всего, когда вам нужно ссылаться на состояние, у вас появляется множество незавершенных сессий и транзакций. Предположим, вы отправляете запрос, чтобы получить определенную часть данных. Как долго сервис должен оставлять ваше соединение открытым, в системе, где состояние определяется клиентом? Как проверить, был ли клиент «разбит» или отключен? Как мы отслеживаем действия пользователя, сохраняя при этом возможность документировать изменения и откатываться, когда это необходимо?
Хотя существуют обходные пути для всех этих вопросов, чаще всего, сохранение состояния реально полезно, только если сами функции зависят от состояния. Большинство пользователей могут взаимодействовать с веб-сервисом различными способами, и поэтому сохранение состояния сервера не зависит от приложения-клиента, так как если клиент не может реализовать функционал сервиса, то и в сохранении состояния нет никакой необходимости.
Stateless
Ответ на эти вопросы — отсутствие состояния. Stateless — это противоположность stateful, в которой любой ответ сервера не зависит от какого-либо состояния.
Вернемся к этой бинарной теореме. Вам даются те же двоичные часы, только на этот раз на бумаге просто написано имя — «Джек» — и инструкция велит произносить это имя, когда кто-то говорит пароль — «рыба». Вы сидите, наблюдая, как часы медленно изменяются, и каждый раз, когда кто-то говорит специальный пароль, вы произносите имя «Джек».
Это пример отсутствия сохранения состояния (statelessness) — нет необходимости даже ссылаться на часы, потому что информация хранится локально таким образом, чтобы запросы были автономными — это зависит только от данных, которые вы предоставляете. Спикер мог легко сказать секретное слово, сказать вам изменить имя, а затем уйти. Он может вернуться через час, сказать секретный пароль и получить новое имя — все содержится в запросе и обрабатывается в двух отдельных этапах, с «запросом» и с «ответом».
Это система без сохранения состояния. Ваш ответ не зависит от «0» или «1», и каждый запрос является автономным.
Веб-сервисы без сохранения состояния
Statelessness является фундаментальным аспектом современного Интернета настолько, что каждый день вы используете целый перечень различных служб и приложений без сохранения состояния. Когда вы читаете новости, вы используете протокол HTTP для подключения в манере отсутствия состояния, используя сообщения, которые могут быть проанализированы и обработаны изолированно друг от друга и вашего состояния.
Если у вас есть Twitter на телефоне, вы постоянно используете stateless сервис. Когда сервис запрашивает список последних личных сообщений, используя Twitter REST API, он формирует следующий запрос:
Ответ, который вы получите, полностью независим от любого хранилища состояний на сервере, и все хранится на стороне клиента в виде кеша.
Давайте посмотрим на другой пример. В приведенном ниже примере мы формируем POST запрос, создавая запись в HypotheticalService:
В этом примере мы создаем запись, но однако она не зависит от какого-либо состояния. Имейте в виду, что это простой вариант использования, поскольку он не передает никаких данных авторизации/аутентификации, а сам POST запрос содержит только очень простые данные.
Даже имея в виду все это, вы можете ясно видеть, что выполнение POST без учета состояния означает, что вам не нужно ждать синхронизации сервера, чтобы обеспечить надлежащее завершение процесса, как с FTP или другими службами с сохранением состояния. Вы получаете подтверждение запроса от сервера, но это подтверждение — просто положительный ответ, а не взаимно разделяемое состояние.
Вкратце можно сказать, что REST специально разработан как функционально не имеющий состояния. Вся концепция «передачи репрезентативного состояния» (из которой стиль REST получил свое название) зависит от идеи передачи всех данных для обработки запроса таким образом, чтобы передавать необходимые данные в самом запросе. Таким образом, REST следует считать stateless (и, на самом деле, это одно из основных соображений относительно того, является ли что-то RESTful или нет в соответствии с оригинальной диссертацией Роя Филдинга, в которой подробно изложена концепция).
Обман и неразбериха
Мы должны быть несколько осторожны, когда мы говорим о веб-сервисах как о примерах stateful или stateless, поскольку то, что, по-видимому, относится к одной категории, на самом деле может быть не так. Это в значительной степени связано с тем, что stateless сервисы сумели скопировать поведение, присущее stateful сервисам без, технически, «пересечения линии».
Statelessness, как и наш пример выше, относится к внутреннему состоянию клиента и ссылке, а не к внешнему хранению состояния. Разница между этой концепцией и ее противоположностью statefulness — это то, где хранится состояние. Когда мы читаем что-нибудь в Интернете или проверяем свою почту, мы генерируем состояние, и это состояние будет куда-то отсылаться.
Когда состояние хранится на сервере, оно генерирует сессию. Это — stateful. Когда состояние хранится клиентом, оно генерирует какие-то данные, которые будут использоваться для различных систем — в то время как, технически, это «stateful» в том, что существует состояние, однако состояние хранится клиентом, поэтому мы называет это stateless.
Это кажется запутанным, но на самом деле это лучший способ обойти ограничения концепции stateless. В чистой stateless системе мы, по существу, взаимодействуем с ограниченной системой — когда мы планировали бы заказать что-нибудь онлайн, система не хранила бы наш адрес, наши способы оплаты, даже запись нашего заказа, она просто обрабатывала бы наши платежи, и мы бы не думали, насколько это касается сервера.
Это, очевидно, не лучший сценарий, и поэтому мы сделали некоторые уступки. В клиентском куки(cookie) мы храним некоторые базовые данные аутентификации. На стороне сервера мы создаем временные данные клиента или сохраняем в базе данных и ссылаемся на такой внешний фрагмент данных. Когда мы возвращаемся, чтобы сделать еще один платеж, для идентификации состояния используется наш файл cookie, а не несуществующая сессия.
Что плохого в сессиях?
Что касается веб-сервисов, общепринятая парадигма заключается в том, чтобы избегать сессий любой ценой. Хотя это, безусловно, не относится ко всем вариантам использования, использование сессий как метода для передачи состояния обычно является тем, чего вы хотите избежать.
Начнем с того, что сессии добавляют большое количество сложности, принося очень небольшую выгоду. Сессии затрудняют воспроизведение и исправление ошибок. В системе с использованием сессий нельзя добавить некоторые страницы в закладки, так как все хранится на стороне сервера. Все это важные проблемы, но они блекнут по сравнению с простым фактом, что сессии не масштабируемы.
Грегор Риглер в BeABetterDeveloper дал прекрасное объяснение, почему так в его этюде «Sessions, a Pitfall»:
Допустим, вы профессиональный игрок в шахматы, и вы хотели бы сыграть одновременно против нескольких людей. Если вы попытаетесь запомнить каждую игру и свою стратегию на ней, вы довольно быстро достигните своего порога запоминания информации. Теперь представьте, что вы не помнили ничего из этих игр, и вы просто перечитывали шахматную доску на каждом шагу. В то же время вы могли играть буквально против 1.000.000 человек, и это не имело бы для вас никакого значения.
Теперь нарисуйте аналогию с вашим сервером. Если ваше приложение перегружено, вам, возможно, придется распространять его на разные серверы. Если вы использовали сессии, вам вдруг пришлось реплицировать все сессии на все серверы. Система станет еще более сложной и подверженной ошибкам.
Проще говоря, сессии не делают то, для чего они предназначены, без тонны накладных расходов(overhead), и их функциональность может быть легко реплицирована с помощью файлов cookie, кэширования клиентов и других подобных решений. Есть, конечно, ситуации, в которых сессии имеют смысл, особенно когда серверы хотели сохранить состояние, не имея даже незначительной возможности изменения данных клиента во время выполнения.
Например, FTP является stateful по очень веской причине, поскольку он реплицирует изменения как на стороне клиента, так и на стороне сервера, обеспечивая повышенную безопасность из-за характера запрашиваемого доступа. Это выполнимо, потому что одному человеку требуется доступ к одному серверу для одного заявленного переноса данных, даже если передача включает в себя несколько папок, файлов и каталогов.
Это не так с чем-то вроде группового Dropbox, в котором stateful сессии могут вызвать дополнительную сложность без добавления какого-либо преимущества. В этом случае, концепция stateless будет гораздо лучшим выбором.
Заключение
Мы надеемся, что это прояснило разницу между концепциями stateful и stateless, когда дело доходит до API. Понимание этой простой концепции — фундамент, на котором основано большинство архитектур и конструкций. Такая концепция, как RESTful design, основана на этих идеях, поэтому наличие концептуального понимания данных идей чрезвычайно важно для современного разработчика.
Что такое Stateless и Statefull?
И я запутался. Например, в основном рекомендуется делать приложения Stateless. Но как в этой концепции обработать ту же корзину товаров? Потому что хранить её в куках не очень разумно, а если её не хранить в сессии (потому что сессии в stateless исключаются), то что остаётся — хранение в БД?
Я понимаю, что для REST stateless подходит идеально, потому что там единичные запросы и проще не создавать сессию, чем создавать. Ну а как быть с приложениями с UI? Где мне хранить корзину товаров? Может быть понятия stateless и statefull в принципе применимы только к REST API?
Таким образом вопрос у меня в том, что такое stateless и statefull, очень желательно с примерами, что будет считаться stateless, а что нет.
Отслеживать
задан 11 фев 2021 в 20:15
2,074 2 2 золотых знака 13 13 серебряных знаков 45 45 бронзовых знаков
Для решения вашей задачи можно воспользоваться хранением корзины в localStorage для неавторизованного пользователя и в базе данных для авторизованного пользователя. Если вы хотите обеспечить сохранность корзины клиента, вам конечно же стоит писать в базу данных связь с пользователем и выбранным товаром и выводить/изменять/удалять корзину. Хранить в сессии корзину нет смысла, когда есть такие замечательные два варианта.
11 фев 2021 в 21:34
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Мне кажется, у Вас в общем правильные представления и о том, что такое стейтлесс, и о том, что такое стейтфул, и том, где и что хранится.
Могу немного расшифровать, почему стейтлесс является модным.
Дело — в горизонтальном масштабировании.
Когад у вас миллион клиентов, и все они бесперывно что то покупают — то один веб — сервер не справляется. Ставят целуб «батарею» веб серверов, а перед ними — так называемый «распределитель нагрузки», который будет «кидать» пользовательский коннект на тот или иной сервер. Или раунд-робином (грубо говоря, по очереди), или на наименее загруженный в данный момент сервер.
Но вот беда. При этом один и тот же пользователь при кадом запросе «чего нибудь» с сервера может попадать на разные сервера. И ему пришлось бы или как «таскать за собой» сессию или. или хранить всё у себя, и каждый раз передавать на сервер.
Вторая причина стетлесса — простота тестирования. Понимаете, состояние — это такая штука, которую сложно воспроизвести во время автоматизированных тестов. А если всё «состояние» — это заранее оперделенная структура (например, json с определенными полями, о значении котрых ВСЕ известно) — то задача становится гораздо проще.
Теперь — у вас есть аргументы в пользу стетлесса.
И нам осталось одно: помирить эти два прекрасных мира.
Сделать это нетрудно.
Во первых, во многих серверных фремворках (и я говорю «фремворки», включая сюда как чистый PHP, так и что то понавороченее), есть возможность storage session to redis — ну, короче, в базе данных, только в очень простой, очень быстрой и, скорее всего, лежащей прямо в памяти.
Тогда можно не «гонять» туда-сюда весь стейт — можно гонять только идентификатор сессии. Тогда та нода, на которую «упал» клиентский запрос, быстренько «поднимет» сессию из редиса, и потом как ни в чем не бывало ответит Вам. А потом пойдет отвечать другому клиенту, сохранив Вашу сессию в базе.
Ну и наконец, хранить «корзину» на клиенте при современном развитии JS и вообще всего-всего — это фигня-вопрос. Фактически, Вам достаточно посылать тот же самый json-чик каждый раз, когда Вы что то делаете на сайте. И в это json-чике описано все-все-все. Добавили товар — изменился json. Удалили — опять изменился. Попросили скидку — этот факт отражен в Json’е. То есть, есть взаимно — однозанчное соответствие «что вижу на экране — то описано в состоянии».
Ну, а отсюда Вам прямая дорога в реакт. Реакт — это штука, которая предназначена для работы с состоянием.
На этом я заканчиваю свой краткий ответ, и надеюсь, что чем то смог Вам помочь