Когда-то я придумал для своего багтракинга фичу, которая мне казалась уникальной для багтракингов. Фича связана с тем, как обрабатываются изменения, которые человек сохраняет в тикет, в то время, как тикет уже успел поменяться кем-то другим за время с момента открытия того в окне браузера.
И вот на днях, в процессе переписывания TaCo на Джанго (да, я таки занялся) я обнаружил в джанговских формах фичу, про которую как-то нигде "официально" не рассказывали.
Задачка
Пусть у нас есть Маша и Саша, которые работают в багтракинге над одним проектом. Маша открывает баг про падение программы, и, словив озарение, начинает его тут же фиксить, потому что работы там на 10 минут. Через часок она понимает, что починить все так просто не получилось и решает отложить работу. Перед тем, как пойти налить кофейку, она переключается обратно в окно браузера с багом и переводить его в статус "In progress".
Беда в том, что в это же время Саша, который взял на себя обязанности пастуха багтракинга, тоже нашел этот баг и, справедливо подумав, что падение программы — проблема серьезная, адекватно поменял поле severity на "Major".
Так вот, если сабмит формы тикета обрабатывать стандартным образом, то Машин перевод тикета в "In Progress" помимо этого вернет ему severity в "Normal", потому что именно в таком виде она открыла эту форму час назад.
Решение
TaCo решал эту проблему. Вместо того, чтобы сохранять в тикет данные из формы целиком, он определял, какие именно поля в этой форме пользователь поменял, и применял только эти изменения. То есть, в нашем случае от Маши пришло бы только указание "поменять статус", и severity остался бы нетронутым.
Реализовано это самым очевидным способом: в форме вместе с видимыми юзеру полями рисуются их скрытые дубликаты с именами типа "OldSeverity", "OldOwner" и т.д. При сабмите из формы вынимаются поля и их старые значения, по которым выясняется, что поменялось.
Уголок буквоеда. Да, это все равно не устраняет race condition целиком. Но это и не цель. Мне нужно решить задачу правильного поведения в большинстве практических случаев. В остальных случаях люди просто вручную обновят тикет еще раз как надо.
Django
Я был очень рад и немало удивлен, когда увидел, что в Джанге такой функционал просто встроен в стандартные формы. Оказывается, совсем не идиотский подход я тогда "изобрел", а другие так тоже делают.
Включается это для отдельных полей указанием атрибута show_hidden_initial
:
class TaskForm(forms.Form):
severity = forms.ChoiceField(..., show_hidden_initial=True)
owner = forms.ChoiceField(..., show_hidden_initial=True)
# ...
После валидации у формы будет доступен form.changed_data
— список с названиями измененных полей, который уже можно использовать по своему усмотрению. Например:
if form.is_valid():
for fieldname in form.changed_data:
setattr(obj, fieldname, form.cleaned_data[fieldname])
obj.save()
Кажется, это хорошая иллюстрация полезности фреймворков. Такая фича — нудная мелочь, которую никто не будет реализовывать прямо сразу, а скорее всего оставит "на потом". А во фреймворке она просто есть, потому что кому-то в мире она уже понадобилась и он ее оформил патчем.
P.S. Я заодно подпилил TagsField, чтобы он поддерживал этот параметр. Правда не опубликовал еще.
Комментарии: 27 (особо ценных: 1)
А не проще запоминать номер ревизии бага (инкрементировать после каждого изменения), и если во время коммита обнаружилось, что у тебя не updated состояние, то вывести предупреждение и предложить решить конфликт? Потому что делать это потихому мне кажется не правильно.
Это наверное было бы проще программировать, но юзеру с этим точно не проще работать. Фактически его заставляют просто нажать рефреш и сделать точно те же изменения, что он только что делал. Это бесит :-)
Делать это по-тихому ничем, в общем-то, не грозит. Юзер не думает в терминах "я заменяю состояние модели данными из формы", он думает "я меняю статус". Поэтому другие изменения его обычно просто не беспокоят. На крайний случай (который я себе пока не представляю), все можно восстановить по логу всех действий над тикетом.
Кошмарное решение. :)
Такие вещи IMHO делаются стратегией блокировок (оптимистическая либо пессимистическая соответственно). Предупреждаем Сашу о том, что баг обрабатывается кем-то, либо предупреждаем Машу о том, данные в базе изменены.
Сливать параллельные изменения - очень неправильно.
Занятно, но к примеру в продукте ServiceDesk от компании HP сделано не менее продумано:
Каждое изменение любого значения любой формы вносится в лог: время, пользователь, изменение какого поля с какого на какое значение.
если после редактирования пользователь нажимает "Save", то эти изменения применятся и сохранятся в логе, если нажмёт "Отмена", то записи из лога исчезнут не сохранившись. Разумеется, реальное значение данных изменяется только в момент сохранения.
Здравствуй, Паша!
Ну и кому от этого будет хорошо? Саша должен ждать час, пока Маша отдаст ему баг? Маша должна помнить, что простой клик на ссылку тикета обязывает ее помнить нажать кнопку "Разблокировать"? Или Маша, как я описал, должна делать заново пляски с контролами?
Это же веб. Тут сессиями работать неудобно, и никто так не делает.
Чем?
Действительно приятная фича.
Имхо, как раз таки во всех нормальных многопользовательских системах (не только багтрекинговых) именно так и сделано (или должно быть сделано).
Правда, логика в общем случае может быть несколько сложнее: тихое обновление может быть отменено в случае, если какие-то другие поля, которые считаются "критическими", поменяли своё значение. Сейчас лень придумывать правильный пример "критических" полей, попробую "притянуть за уши" к рассматриваемому случаю.
Пофантазируем, что описание бага считается "критическим" полем, т.к. оно якобы может полностью обессмыслить все другие изменения, которые с ним делались.
Так вот, в такой ситуации если Саша изменил описание бага, то Маша не сможет изменить уже совсем ничего, пока не перезагрузит всю информацию по нему.
Я надеюсь, Вы, Иван, потом все же вспомнили, что эта фича в trac имеется с версии 0.10? :)
Я считаю, что достаточно того, что каждый из пользователей будет потом очень долго хлопать глазами, глядя на дважды измененный тикет, пытаясь сообразить, а что собственно произошло?
Вот в траке и не сливают, а предупреждают, что изменилось всё. При этом твои данные (комментарий там) уходит в топку обычно, и ты испытываешь ненависть к этой дряни, которая обычно очень даже удобна.
Надо либо сливать сразу, либо, как в DVCS, давать смержить - сохраняя мои данные, хотя бы в сессии. Дать возможность применить их выборочно или полностью, но не просто говорить, что "всё плохо!".
У нас в одном из продуктов при изменении данных в подобной структуре форма просто не сохраняется с ошибкой, гласящей о появлении новых изменений. Соответственно все изменения человека до сих пор живут в перерисованной с ошибкой форме и он либо может отправить форму ещё раз сразу, либо поравив её, ли бо её вообще не отправлять.
Что практически всегда и происходит, да?
Это просто неверно выбранное умолчание. В юзабилити есть принцип, что чем выдавать бесполезные алерты в большинстве нормальных ситуаций, лучше давать возможность исправить редкие ошибки. Это как раз тот случай.
разве, он мне постоянно превью показывает, если что-то изменилось и предлагает ввести опять?
Особо ценный комментарий
Что-то меня помутило, и я написал, что
changed_data
— это dict. На самом деле это список с названиями измененных полей. Сути это особо не меняет, пример кода в посте поправил.В именно нашем случае — верное: новое письмо в треде хелпдеска, пришедшее во время написания ответа, имеет смысл прочитать до отправки ответа.
Я к тому, что если необходимо вмешательство пользователя, то у для этого у джанги есть красивый механизм вываливания ошибки формы с сохранением всех полей этой формы.
Есть менее зубодробительная альтернатива хранению старых значение — следить за изменениями полей. Минуса собственно два:
Зато просто и без дупликации потенциально больших данных. ;-)
Неправильное решение.
Вдруг, если бы девушка увидела изменение молодого человека, она бы не стала делать свои изменения или сделала бы другие изменения.
Объединение изменений приведёт к куче проблем и непониманий.
Да, он работает по такому принципу. Я под "такая фича" подразумевал толковую обработку ситуации конкурентного редактирования одной сущности, а не конкретную реализацию, предложенную Иваном:
На практике это "вдруг" случается довольно редко. И когда случается, еще раз повторюсь, это очень просто исправить. Я описал конкретную ситуацию. Маша полезла отмечать тикет "In Progress" после того, как что-то физически сделала с кодом. Никакие Сашины изменения тикета этого не отменят все равно. И это не просто так случайно подобрано. Багтракинг — не средство срочного оповещения. Если бы Саше срочно надо было прервать Машу, он бы пришел/позвонил/написал в мессенджер.
Многими системами сложно пользоваться только потому, что архитекторы не понимают этого баланса: у ошибок есть разная практическая цена как допущения, так и избегания.
Кстати, я не мог этого вспомнить, потому что оригинальный код TaCo на полгода постарше Trac 0.10 :-)
По-моему, описанный вами кейс (параллельные изменения одного общего объекта) - штатная ситуация в любой groupware-программе. В "классических" gw-клиентах - тот же MS Outlook, например, при работе с Exchange - пользователю просто всегда показывается текущее состояние объекта, а не то, которое было на момент его подключения. Аналогично ведут себя IMAP-клиенты при работе с общими папками на IMAP4-сервере - сразу показывают изменения флагов, какие делают другие пользователи. Для этого есть полная поддержка в IMAP-протоколе и достаточно визуальных средств в клиентах.
Да собственно в groupware и конкретно баг-трекер - в лице task-трекера - штатная фишка.
И в вебе можно имитировать подобный реалтайм - на любой странице, какую видит пользователь, можно по comet принимать её изменения, отслеживаемые сервером. Если уж есть онлайновые текстовые редакторы, которые разрешают одновременное редактирование одной и той же страницы (текст других пользователей сразу "сам" вставляется тут же, обычно другим цветом), то уж в баг-трекере это тем более можно, т.к. манёвр у пользователя более ограничен.
To Andrey Cherezov:
Нафиг бы такой гемморой использовать в багтрекере? Багтрекер должен быть самой надёжной софтиной после компилятора(интерпретатора) и VCS, а излишнее усложнение этому не способствует!
Согласен. Однако исходный пост как раз о том, как бы его так усложнить, чтобы описанных коллизий не бывало.