⚔️ Что делать, если два инженера одновременно изменяют один проект? Это частный случай одного большого вопроса, который я обещал осветить в прошлых постах. Как обрабатывать конкурентные команды в многопользовательском приложении?

➡️ Самый простой вариант - применять команды в порядке поступления. Клиенты читают одно состояние, формируют независимые команды изменения и отправляют их. Отсутствие какой-либо защиты приводит к тому, что команда, которая обработалась позже, перезатрет результат команды, которая обработалась раньше (так называемый Lost Update).

🔢 Более сложным механизмом является оптимистичная блокировка. Например, сущность хранит свою версию, которая инкрементируется с каждым изменением. Каждая команда содержит "ожидаемую версию", и сервер возвращает ошибку, если ожидаемая версия не совпадает с фактической, например, потому что уже была применена команда другого клиента. Существует альтернативная реализация с временными метками, но она менее надежна, хотя бы потому, что две правки могут прийти в одну миллисекунду.

⚠️ Но что, если сущность имеет множество атрибутов? Например, проект в IDE имеет название и комментарий. Очевидно, что эти атрибуты не связаны общим инвариантом. И, реализуя оптимистичную блокировку через одну ожидаемую версию всей сущности, мы фактически говорим пользователям - твоя команда изменения комментария не прошла, потому что другой пользователь поменял имя этого проекта. Звучит абсурдно, и никто вам спасибо за такую реализацию не скажет.

🔍 Поэтому для команд обновления атрибутов можно придумать более сложные правила проверки, сделать обработку конкурентных действий пользователя частью бизнес-логики приложения. Например, добавить в команду обновления атрибута новый аргумент - ожидаемое предыдущее значение (Compare And Swap, CAS). Тогда множество независимых параллельных команд могут быть обработаны без возникновения мнимых конфликтов. И ошибка будет возникать только у пользователей, которые меняют одно поле одновременно.

🔗 Также атрибуты могут быть зависимыми. Причем не всегда взаимно зависимыми, и характер этой зависимости будет определяться бизнес-правилами. Например, изменение типа данных может влиять на начальное значение переменной, но изменение начального значения на тип не влияет.

🔄 Представим ситуацию, когда пользователь меняет тип данных переменной INT → UINT → INT. При переходе к UINT отрицательное начальное значение переменной -10 сбрасывается в 0 и при возврате к INT таким и остается. Параллельная команда другого пользователя INT → DINT все равно применится, но начальное значение уже не -10, а 0, и программа будет работать некорректно.

🛡️ Этот случай называется проблемой ABA (A → B → A). Проверка видит, что последнее значение совпадает с ожидаемым, но не видит, что происходило между. Если промежуточные изменения имеют побочные эффекты, то требуется более сложная проверка. Можно усложнить команду смены типа данных, чтобы она содержала ожидаемое значение связанного атрибута (начального значения), но, проектируя такие сложные проверки, вы рискуете что-то упустить. Более простой вариант - независимое версионирование для каждого атрибута, но это накладные расходы по памяти.

💡 В общем случае команда должна содержать минимально необходимый набор условий, соблюдение которых позволит обеспечить ожидаемый пользовательский опыт. Это основа обработки конкурентных команд на BackEnd. Про обработку на FrontEnd и другие менее тривиальные Edge кейсы как-нибудь в другой раз.

⚔️ Что делать, если два инженера одновременно изменяют один проект? Это частный случай одного большого вопроса, который я обещал осветить в прошлых постах | Сетка — социальная сеть от hh.ru