Comments 8
Фикс
Ну и странный какой-то фикс. Почему мы юзера удаляем мягко, а запись из листа ожидания - жёстко?
У каждого компонента было своё определение «удалённый пользователь». У некоторых его не было вообще.
Ну бардак он и есть бардак.
Здесь код promoteFromWaitlist написан правильно — он делает именно то для чего предназначен: находит первого в очереди без активного appointment и продвигает его.
А если продолжить "... плюя на всё" - то уже как-то возникают сомнения в правильности. Своё сделал, а там хоть трава не расти.
PS. Stored procedures.
Спасибо за комментарий.
Согласна, что показанный фикс решает только конкретный сценарий с waitlist и сам по себе не отвечает на вопрос, что делать с остальными связанными сущностями.
Наверное, моя формулировка про то, что “promoteFromWaitlist написан правильно” была слишком категоричной. Точнее было бы сказать, что функция была написана в соответствии со своими локальными правилами и не учитывала состояние deletedAt. Именно это расхождение между локальной логикой компонентов и привело к появлению ghost appointment.
Собственно, это и было основным наблюдением из инцидента: soft delete существовал на уровне users, но не был частью общего контракта системы.
Мы тоже используем у себя мягкое удаление. И тоже, то что казалось идеальным решением стало небольшим геморроем, только у нас в этой же таблице есть еще и “архивные” записи с точно такой же логикой. Там где мы используем ORM еще можно использовать глобальные фильтры… но часть логики у нас сознательно протекла в хранимые процедуры SQL Server… ради скорости, и там приходится обмазываться фильтрами в каждом запросе.
Корневая проблема – не в том, что фильтров много, а в том, что в одной таблице живут три разных типа сущности: активная, мягко удалённая и архивная. ORM-фильтры и хранимые процедуры – это ручная компенсация отсутствующего типа. Выход: view `active_users` + `archived_users` поверх одной таблицы со status-колонкой enum – и SQL-сторона перестаёт «протекать».
Корневая проблема – не в том, что фильтров много
Корневая проблема в том, что наш продукт родился из эксельки с макросами… где зафигачить дополнительную колонку в таблицу оказалось быстрым и приемлимым решением. Так далеко никто не загадывал.
Выход
Я надеюсь что когда-нибудь наш стек будет помечен как “неприемлимо устаревший”, а сейчас он просто устаревший… и придется все переписать на Postgres с нормальными архитектурными решениями. Вопрос только в том не закончится ли жизненный цикл продукта раньше, чем это случится ))
Порядок важен: если сначала выставить deletedAt, а потом чистить вейтлист — в промежутке может сработать ...
А ещё существуют транзакции...
Если при выполнении подобных действий вы их не используете, то вас ждёт ещё много сюрпризов.
Согласна. В статье я сфокусировалась на семантике soft delete и недостаточно раскрыла вопрос атомарности операции.
В реальной реализации такие изменения действительно должны выполняться в транзакции. Моя мысль была скорее о том, что даже при корректной технической реализации остаётся вопрос бизнес-семантики: что именно означает “удалить пользователя” для waitlist, appointment и других связанных компонентов.
GHOST-01: soft delete — это не delete