• Как оценивать большие задачи
    0
    Раньше на Planing Poker звали менеджера, который общался с клиентом. Иногда оказывалось, что менеджер тоже до конца не знает, чего хочет клиент. Тогда оценку задачи откладывали.

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

    Сейчас за любой историей закрепляются продукт оунер и архитектор, которые выясняют все требования и продумывают решение в общих чертах. Так что команде приходит задача, по которой уже нет вопросов к клиентам. Но все равно остаются нюансы, которые полезно продумать до оценки, и разбиение истории на части помогает их выявить.
  • Как оценивать большие задачи
    0
    Один разработчик может слишком много упустить, разбивая историю на подзадачи. Даже тимлид от этого не застрахован. Команда на оценке может увидеть, что что-то не учтено или планируется сделать неправильно. Командная оценка — это в первую очередь ревью того, как спроектировано решение. Можно сократить этап совместной оценки оценки так: вместо Planning Poker'а команда просто указывает, какие задачи точно нельзя сделать за день и нужно разбить. Оценку каждой задачи уже дает один человек. Но такая оценка будет не качественной. Более самоуверенный программист будет недооценивать, а осторожные — переоценивать. Полноценная оценка всей командой помогает решить эту проблему.

    Для удобства при оценке можно считать, что 1 story point — это 1 час. Важно помнить, что это не реальный час и не путать этим клиентов или менеджеров. Реальный размер story point'а надо вычислять исходя из статистики. У кого-то это будет 2 рабочих часа, а у кого-то и все 4. Мы разбиваем до подзадач, которые по нашей оценке занимают 3-4 часа. Но с учетом рисков, приемки, ревью и отвлечений на общение с менеджерами, один программист делает только 3 story point'а в неделю.
  • Как оценивать большие задачи
    0
    Да, такой подход тоже можно использовать. Так проще разбивать истории на подзадачи и точно не будет возникать эффекта привязки. Но тогда надо будет оценивать каждую подзадачу с помощью Planning Poker и это достаточно долго.

    Важный момент: 1 день, пол дня и 2 часа стоит использовать только для того, чтобы программистам было проще прийти к одинаковому пониманию размер story point'а. Здесь 5 story point'ов — это 1 идеальный день. Чаще всего люди не укладываются в оценку, которую они дали в абсолютных величинах. С относительной оценкой получается лучше. Реальный размер story point'а нужно определять исходя из статистики.

    На скачки графика такой подход не повлияет. График скачет в основном из-за того, что по окончании месяца остаются наполовину сделанные большие истории. Их в скорости мы не учитываем. Учитываем только принятые. Можно по другому учитывать, чтобы график меньше скакал, но это спорный вопрос.
  • Как оценивать большие задачи
    0
    Вместо экспертной оценки рисков, мы разбиваем истории на подзадачи настолько мелко, чтобы нивелировать риски. Они, конечно, все равно остаются, но в среднем мы не учитываем только 20% подзадач.
  • Как оценивать большие задачи
    0
    Ну я так понял детально проработанные истории на следующий спринт — это уже Sprint Backlog (http://www.scrumguides.org/scrum-guide.html#artifacts-sprintbacklog). Но это уже нюансы, конечно.
  • Как оценивать большие задачи
    0
    Посмотрите в Scrum Guide: Product Backlog refinement (http://www.scrumguides.org/scrum-guide.html#artifacts-productbacklog) — в статье вы описываете вашу реализацию этого процесса.
    Не совсем. Наш способ оценки слишком дорогой, чтобы использовать его для оценки бэклога. Хотя и Planning Poker для этого мне кажется слишком затратным. Для оценки задач в бэклоге мы используем экспертную оценку одного-двух человек. Этого достаточно для приоритезации и при этом мы не тратим много времени на поддержку бэклога.

    Делим историю на подзадачи и оцениваем мы непосредственно перед тем как брать их в спринт.
  • Как оценивать большие задачи
    0
    В Planning Poker историям дают относительные оценки. Например, от 0,5 до 40.

    Можно разбивать истории до задач поменьше и оценивать их с помощью Planning Poker.

    Мы же разбиваем истории до подзадач размером 1. Каждую подзадачу мы не оцениваем по какой-то шкале. Просто отвечаем на вопрос — она занимает ровно 1 story point или нет.
  • Как оценивать большие задачи
    +2
    Вы за 30 минут должны весь объём спринта оценить, добрый вечер.

    Когда мы оценивали с помощью Planning Poker на оценку 2-ух недельного спринта уходило не меньше полутора часа. Слишком много возникает вопросов к реализации и требованиям клиента. Пара дней уходит на большие истории, чтобы разобраться в том, что нужно делать и спроектировать решение. Так что это выходит за рамки оценки.
    А сколько тикетов вы успеваете за пол часа оценить? Какого размера? Как часто во время работы над тикетом оказывается, что тикет сильно недооценен?
  • Как мы перестали бояться тикетов на UI
    0
    Насколько я понимаю, html-шаблон у Angular все равно будет не строго типизированным. Эту проблему, к сожалению, никто кроме разработчиков tsx не решил.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    Ну да, с тем, что транзакция могла быть не завершена — это я погорячился)

    Вариант с ROWCOUNT мы уже подробно обсудили веткой выше.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    Мы делали похожим образом (правда там был email, а не FacebookId). Если точнее, мы создавали во время транзакции отдельную транзакцию (в другом connection'е) с уровнем изоляции read uncomitted. В итоге мы получали потребителя по email и, так как он уже есть, вместо добавления пытались его отредактировать, но в другой транзакции оказывалось, что потребителя в базе нет, потому что:
    • либо транзакция, добавившая потребителя, еще не была завершена
    • либо она была откачена
    • либо потребитель успел поменять email

    Если использовать read comitted транзакцию, для проверки есть ли потребитель в БД, то останется только проблема с параллельным редактированием потребителя, но с ней все равно надо как-то бороться.

    Вообще, можно использовать read uncomitted транзакцию, для быстрой предварительной проверки данных, но после нее все равно надо повторно проверить эти данные в транзакции изменяющей их. Тут описан пример, когда такой подход полезен.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    Не уверен, что за счет такого подхода можно ощутимо выиграть в скорости (хотя, это, конечно, зависит от конкретной ситуации). Подход с update блокировкой как-то проще.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    Боюсь, в этом случае больше проблем будет из-за того, что сервисный поток при обработке буферной таблицы будет блокировать вставку в нее потоками отправки. В принципе это все решаемо, конечно, но подход с лишней записью как-то проще.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    У нас сложная гибко настраиваемая логика импорта с выводом подробных валидационных сообщений. Поддерживать ее на процедурах было бы слишком сложно. Поэтому мы используем ORM и serializable транзакции, хотя и жертвуем при этом скоростью в пользу гибкости.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    Хотя, можно выполнить с помощью RAW SQL только запрос
    UPDATE BonusPool 
    SET RemainingCount -= 1
    WHERE BonusID = @BonusID AND RemainingCount > 0
    

    И выполнять дальше с помощью ORM любую логику, если @@ROWCOUNT > 0. Это будет практически полностью аналогично подходу с использованием update блокировки. Разве что, в момент транзакции между наложением update блокировки и изменением RemainingCount, могут выполнятся транзакции, которым нужно только прочитать из таблицы BonusPool, а в случае с update'ом будет наложена эксклюзивная блокировка и любые транзакции, обращающиеся к призу с этим Id, будут ждать.
  • Типичные взаимные блокировки в MS SQL и способы борьбы с ними
    0
    Да, так тоже можно. Но при использовании ORM такой подход не получится использовать, пожалуй (сделать RAW SQL запрос в начале транзакции — это еще приемлемо, а переводить всю транзакцию на RAW SQL — это уже перебор). К тому же у нас более сложная логика и запись о типе приза нам в любом случае нужно вытащить, чтобы проверить другие ограничения на выдачу приза.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Ну SQL мы тоже не считаем 100% надежным. Мы не стремимся обработать запрос пользователя в 100% случаев. Но если одна из операций упала (SQL транзакция или отправка в очередь), то другую операцию мы должны откатить.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Да это вроде рабочий вариант. Хочу только уточнить пару нюансов:
    1. Воркер, переотправляющий сообщения, должен вытаскивать только старые сообщения (например, отправленные более минуты назад), причем скорее всего придется использовать уровень изоляции ReadUncomitted. Иначе SELECT всех сообщений будет блокировать обработку текущих.
    2. Этот подход, как и наш, может добавлять дубли сообщений в очередь.

    На мой взгляд вариант с промежуточной очередью все-таки проще, а также обладает следующими преимуществами:
    1. Можно обойтись без дополнительной таблицы, если в SQL транзакции и так создается сущность с уникальным ключом.
    2. Его можно использовать и для отправки писем о регистрации.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Жертвуем гарантированностью мы на этапе обработки сообщения (отправки email). И это частный случай. При отправке платежей, например, мы можем гарантировать 100% отправку, так как сервисы ОСМП идемпотенты.

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

    То, что мы сейчас обсуждаем выходит за рамки статьи, и в ней про это я и не собирался писать. Статья описывает как сделать распределенную транзакцию, отправляющую сообщение в очередь и сохраняющую информацию об этом в системе-отправителе. Обработка сообщения не является частью этой транзакции.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Хотя второй воркер нам нужен не для поддержания надежности, а из-за других нюансов (не буду в это углубляться).

    В большинстве случаев можно использовать более простые алгоритмы.
    Если мы хотим гарантировать, что email не будет отправлен два раза (и при этом не сможем гарантировать, что он будет отправлен):
    1. Проверяем, что в БД нет записи об email из БД с переданным ключом идемпотентности (если есть — выбрасываем сообщение).
    2. Вставляем запись об email из БД со статусом «Новая».
    3. Коммитим транзакцию.
    4. Отправляем email.
    5. Изменяем статус записи об email на «Отправлено» (это просто атомарная операция без какой либо транзакции).
    6. Ack'аем сообщение.

    Если мы хотим гарантировать, что email будет отправлен хотя бы один раз (и при этом не сможем гарантировать, что email не будет отправлен два раза):
    1. Проверяем, что в БД нет записи об email из БД с переданным ключом идемпотентности (если есть — выбрасываем сообщение).
    2. Вставляем запись об email из БД.
    3. Отправляем email.
    4. Коммитим транзакцию.
    5. Ack'аем сообщение.

    Если использовать последний алгоритм, при падении коммита будут ситуации, когда письмо уже было отправлено, а записи в базе о нем еще нет (но позже она появится). Если мы отслеживаем открытие письма и изменяем при этом статус записи об email в БД, то лучше будет все-таки разделить отправку и выполнение SQL-транзакции по разным воркерам (это гарантирует нам, что email будет отправлен только после поялвения записи в БД).
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    1 и 2 не противоречит друг другу.
    Если email остался в статусе «Отправляется», это может означать два варианта:
    1. Упала отправка email
    2. Упал update статуса

    Т.е. если сообщение повисло в этом статусе мы не знаем наверняка было оно отправлено или нет. Эти сообщения мы автоматически не отправляем, поэтому гарантируется, что один email не будет отправлен два раза.

    В вашем упрощенном подходе email-шлюз должен иметь доступ к БД системы-отправителя. Если вам не нужно выделять email-шлюз в отдельную систему, и вы считаете его частью CRM системы (и у них одна общая БД на двоих), то это вполне валидный подход.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    100% уверенности, что отправление сообщения в MQ не упадет, у нас нет.

    Если сохранять задачу на отправку, например, в SQL, а потом переотправлять, то список этих задач в итоге превратится в очередь. Проблемы с использованием SQL в качестве очереди я описал в конце статьи.

    Подход с промежуточной очередью для отправки работает быстрее и при его использовании чаще всего можно обойтись без дополнительных таблиц, так что он даже проще, чем вариант со списком задач в БД.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Так как мы отсылаем сообщение в очередь до коммита, мы не можем гарантировать, что транзакция была завершена (коммит транзакции может упасть, хоть это и маловероятно). Поэтому нам нужна промежуточная очередь, при обработке сообщения из которой мы проверяем, что запись с идемпотентным ключом есть в БД (а значит транзакция была успешно закоммичена).

    Если отправлять сообщение после коммита SQL транзакции, то может возникнуть ситуация, когда запись в БД об отправке есть, а сама отправка упала.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Немного устал повторять, но статья про другое)

    Проблему с тем, что отправка email на почтовый сервер — не идемпотентная операция, мы решили немного усложнив логику работы второго воркера. Полностью она выглядит так:
    1. Получаем запись об email из БД (если ее там нет, значит транзакция первого воркера не была закомичена, и сообщение можно выбросить).
    2. Проверяем, что статус записи — «Новая» (если нет, значит сообщение уже обрабатывалось и скорее всего email был отправлен, поэтому сообщение выбрасывается).
    3. Изменяем статус записи об email на «Отправляется» и коммитим транзакцию
    4. Отправляем email.
    5. Изменяем статус записи об email на «Отправлено» (это просто атомарная операция без какой либо транзакции).
    6. Ack'аем сообщение.

    При таком подходе мы не гарантируем, что email будет отправлен в 100% случаев, но гарантируем, что один email не будет отправлен два раза. Если упадет отправка email, то запись об email останется в невалидном статусе «Отправляется» (хотя если запись в этом статусе — есть вероятность, что email все таки отправился и упало изменение статуса). У нас настроен мониторинг, который проверяет количество таких записей и шлет алерт если их много. Сейчас сообщения в таком статусе остаются крайне редко — только если возникают проблемы на уровни сети.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    Еще раз поясню, вопрос (да и вся статья) был про транзакцию отправки сообщения в очередь. Вы же описываете транзакцию обработки сообщения из очереди.

    Обработка сообщения у нас устроена примерно так как вы описали, но вы упустили два нюанса:
    1. Так как вы генерируете ключ идемпотентности (guid) при обработке сообщения, вы не сможете его использовать, если транзакция упадет — он просто нигде не сохранится. Ключ идемпотентности должен быть в исходном сообщении и генерировать его должна система-отправитель.
    2. Если транзакция упадет на 5-ом шаге, то она повторится через некоторое время и во второй очереди окажется 2 одинаковых сообщения. Чтобы этого избежать нужно использовать тот же подход, который мы используем при отправке сообщения в очередь — 2ой воркер должен проверить, что запись с guid есть в БД,
  • Распределенные транзакции между RabbitMQ и MS SQL
    +1
    Мы хотим гарантированной доставки сообщений, поэтому используем Ack. Но Ack не гарантирует, что сообщение не будет обработано несколько раз, поэтому обработку сообщений нужно делать идемпотентной.

    При отправке нам необходимо гарантировать, что сообщение будет добавлено в очередь только в случае успешного завершения SQL-транзакции. В зависимости от того в какой момент отправляется сообщение могут быть две разные проблемы:
    • SQL-транзакция выполнилась успешно, но потом отправка сообщения в очередь упала (если мы отправляем сообщение после завершения SQL-транзакции)
    • Сообщение успешно попало в очередь, но потом SQL-транзакция была откачена (если мы отправляем сообщение до завершение SQL-транзакции)

    Как вы предлагаете использовать confrim для того, чтобы избежать обеих проблем?

    Синхронная отправка email нас не устраивает (хотя бы потому, что при этом в случае downtime email-шлюза, не работают все системы, которым нужно отправлять email).
    В течение SQL транзакции мы только добавляем сообщение в промежуточную очередь. Практически сразу после завершения транзакции, мы перекладываем это сообщение в очередь, которую обрабатывает система-получатель. Обработка сообщения (отправка email, если система-получатель — это email-шлюз) полностью асинхронна и система-отправитель не ждет, когда она будет завершена.
  • Распределенные транзакции между RabbitMQ и MS SQL
    0
    NServiceBus работает как надстройка над MSMQ, который умеет интегрироваться с MS DTC.
    MSMQ нас не устраивает, а для RabbitMQ NServiceBus не может сделать распределенную транзакцию.
  • Распределенные транзакции между RabbitMQ и MS SQL
    +1
    Ack актуален только при обработке сообщения, при отправке Ack вызвать нельзя. Если сохранять информацию об успешной обработке сообщения где-либо (в другой очереди или в SQL базе), нет гарантии, что после этого отработает Ack (хотя бы из-за недоступности сети, но в реальных условиях между завершением транзакции и Ack может выполнятся еще код, который также может упасть). В этом случае сообщение через некоторое время обработается еще раз, чего мы не хотим.

    Что делать, если падает именно отправка сообщения, я подробно написал в статье.
  • Распределенные транзакции между RabbitMQ и MS SQL
    +1
    Ack при обработке мы используем, конечно же, но проблема с транзакционностью, которую я описал, относится к транзакции отправки сообщения.

    Но и при обработке сообщения одного только Ack'а не достаточно. Если после публикации данных в очередь с результатами упадет Ack, то это сообщение будет обработано два раза. Поэтому обработку сообщения всегда лучше делать идемпотентной.
  • Слой кэширования поверх Linq to SQL
    0
    Получается сложновато, не хотелось бы столько сложностей добавлять к и так не простой архитектуре кэширования.

    У нас есть места, где кэшируются данные для отдельных потребителей. Этот кэш у нас сбрасывается простым вызовом Redis'а, так как даже если сбросить кэш не удастся — он сам устареет через 5 минут и нас это устраивает. В большинстве случаев это наиболее хороший подход к кэшированию.

    Можно бы было кэш кампаний также сделать устаревающим раз в 5 минут, но тогда у нас было бы значительно больше перезагрузок кэша по сравнению с кэшированием в памяти процесса.
  • Слой кэширования поверх Linq to SQL
    0
    Может упасть timeout, могут быть проблемы с сетью, сервер кэша может не ответить из-за слишком большой нагрузки и т.д. Мы логируем все ошибки и случаи падения запросов к Redis'у имеются. Проблем со сбросом кэша может не быть только при использовании InMemory кэша. Если кэш хранится на другом сервере, полагаться на то, что запрос к нему обработается в 100% случаев, не стоит.
  • Слой кэширования поверх Linq to SQL
    0
    Сброс кэша мы не делали асинхронным. Его можно бы было сделать через очередь, чтобы кэш был сброшен даже в случае падения запроса к Redis (если не удалось сбросить кэш, повторяем пока не получится). В случае использовании очереди будет задержка, но в общем то совсем небольшая — максимум несколько миллисекунд, так что можно было про это и не упоминать, наверное.

    Если мы просто дергаем сброс кэша после редактирования кампании, то в случае если запрос на сброс кэша упадет, у нас останется старая версия кампании до тех пор пока кэш не устареет, что не очень приятно. Причем хотелось бы, чтобы период устаревания кэша был побольше (в текущей реализации кэш вообще не устаревает со временем — он обновляется, только если кампания изменилась).
  • Слой кэширования поверх Linq to SQL
    0
    Необходимо, чтобы кэш гарантированно сбрасывался в случае успешного редактирования кампании. Для этого придется делать что-то вроде распределенной транзакции между Redis'ом и MS-SQL. В принципе можно редактирование кампании сделать так:
    1. считываем кампанию
    2. сбрасываем кэш (если не получилось, откатываем SQL транзакцию)
    3. изменяем кампанию и завершаем транзакцию

    Так как и редактирование кампании и загрузка кэша происходит в транзакции с уровнем изоляции Serializable, то загрузка кэша будет ждать завершения редактировании кампании. Однако такой подход добавляет зависимость админки от кэша (кэш не работает — редактировании кампании не работает) и сервисов от админки (пока редактируется кампания — все запросы сервисов ждут).

    Альтернативный вариант — по окончании редактирования кампании добавлять в очередь Rabbit сообщение с командой на сброс кэша. Затем в windows-сервисе, где у нас обрабатываются сообщения, будет гарантированно сброшен кэш, но с некоторой задержкой, что в принципе приемлемо. При этом надо еще гарантировать, что команда на сброс кэша будет добавлена в очередь (правда распределенные транзакции между SQL и Rabbit мы уже сделали — об этом в следующей статье напишем).
  • Интересные моменты работы LINQ to SQL. Опять
    0
    Контекст у нас свой на каждую транзакцию.
    Тут проблема скорее не в том, что сущности никогда не покинут контекст, а в том, что по коду даже не видно, что они в контекст загружались.

    Например, если выполнении следующего запроса
    var activeActionTemplates = modelContext.Repositories.Get<ActionTemplateRepository>().Items
        .Where(at => at.EffectiveStartDateTimeUtc <= DateTime.UtcNow)
        .Where(at => at.EffectiveEndDateTimeUtc > DateTime.UtcNow)
        .ToArray();
    

    все ActionTemplate'ы загрузятся в память, так как EffectiveStartDateTimeUtc и EffectiveEndDateTimeUtc не мапятся на SQL. Если таких сущностей в базе много, то может даже упасть OutOfMemory, хотя по коду кажется, что мы вычитываем только небольшой массив данных.
  • Триггерные рассылки
    0
    Ну в общем да.

    Если точнее, то у нас есть мини-фреймворк для обработки периодических задач в windows-сервисе. Для каждой задачи есть запись в БД, в которой кроме прочего хранится время последнего выполнения задачи. Поток планировщика каждую минуту проверяет, есть ли задачи, которые пора запустить и запускает каждую в отдельном потоке (а если точнее, то в отдельном System.Threading.Tasks.Task).

    Для каждого триггера мы создаем отдельную периодическую задачу со своим периодом обработки. Минимальный период, который можно выставить в настройках триггерах, — 5 минут. Если нужно, чтобы уведомление (или, например, выдача, баллов) происходило прям сразу и 5 минут — слишком долгий срок, то триггеры мы не используем. Для таких задач у нас есть своя система event'ов, которые могут выполняться как синхронно, так и асинхронно с использованием Rabbit-очередей. Но на event'ах нельзя сделать логику вроде «послать письмо тем, кто не заходил на сайт боле 2-ух недель». Триггеры в общем более гибкие.