Каков наилучший способ добавить механизм повтора/отката для задач sync/async в С#?
Представьте себе приложение WebForms, где есть основной метод с именем CreateAll(). Я могу описать процесс задач метода шаг за шагом следующим образом:
1) Магазины в базу данных (обновить/создать элементы Db 3-4 раза)
2) Начинает новый поток
3) Result1 = Вызывает услугу мыла и с помощью порога тайм-аута проверяет состояние и после x минут. Он продолжает (статус теперь в порядке, и это не означает отказ)
4) Магазины в базу данных (обновить/создать элементы Db в 3-4 раза)
5) result2 = Вызывает услугу мыла (в режиме огня и забывания)
6) Обновляет файл конфигурации (который фактически берется из result1)
7) Используя обратные вызовы, он проверяет каждый х секунд на передней части состояние результата2, и пользовательский интерфейс показывает индикатор выполнения. Если процесс завершен (100%), это означает успех
Я рассматриваю, что все они являются задачами, которые могут быть сгруппированы по их типу. Как правило, несколько типов действий:
- Тип1: транзакция DB
- Тип2: служебная связь/транзакция
- Тип3: операции ввода-вывода файла конфигурации
Я хочу добавить механизм отката/повтора в существующую реализацию и использовать целевую архитектуру и реорганизовать существующий унаследованный код.
Я обнаружил, что для этой цели может помочь что-то вроде шаблона проектирования Memento Design ИЛИ Command в С#. Я также нашел шаблон msdn Retry Pattern описание интересно. Я не знаю, и я хочу, чтобы кто-то привел меня к самому безопасному и лучшему решению...
Можете ли вы предложить мне лучший способ для этого случая сохранить существующую реализацию и поток, но обернуть его в общую и абстрактную реализацию retry/rollback/tasklist?
Окончательная реализация должна иметь возможность повторять в каждом случае (независимо от задачи или общего сбоя, например, таймаута и т.д. в течение всего процесса createAll), а также будет список решений отката, в котором приложение должно иметь возможность откатывать все задачи это было выполнено.
Я хочу несколько примеров, как разбить этот связанный код.
PseudoCode, который может быть полезен:
class something
{
static result CreateAll(object1 obj1, object2 obj2 ...)
{
//Save to database obj1
//...
//Update to database obj1
//
//NEW THREAD
//Start a new thread with obj1, obj2 ...CreateAll
//...
}
void CreateAllAsync()
{
//Type1 Save to database obj1
//...
//Type1 Update to database obj2
//Type2 Call Web Service to create obj1 on the service (not async)
while (state != null && now < times)
{
if (status == "OK")
break;
else
//Wait for X seconds
}
//Check status continue or general failure
//Type1 Update to database obj2 and obj1
//Type2 Call Web Service to create obj2 on the service (fire and forget)
//Type3 Update Configuration File
//Type1 Update to database obj2 and obj1
//..
return;
}
//Then the UI takes the responsibility to check the status of result2
Ответы
Ответ 1
Посмотрите на использование Polly для сценариев повтора, которые, похоже, хорошо согласуются с вашим псевдокодом. В конце этого ответа приведен образец из документации. Вы можете выполнять все виды повторных сценариев, повторять попытку и ждать и т.д. Например, вы могли бы повторно выполнить полную транзакцию несколько раз или, альтернативно, повторить набор идемпотентных действий несколько раз, а затем написать логику компенсации, если/когда повторная попытка политика, наконец, не удалась.
Мембранные шаблоны больше подходят для логики отмены, которые вы найдете в текстовом процессоре (Ctrl-Z и Ctrl-Y).
Другими полезными шаблонами для просмотра являются простая очередь, постоянная очередь или даже служебная шина, чтобы дать вам возможную последовательность, не требуя, чтобы пользователь подождал, пока все будет успешно завершено.
// Retry three times, calling an action on each retry
// with the current exception and retry count
Policy
.Handle<DivideByZeroException>()
.Retry(3, (exception, retryCount) =>
{
// do something
});
Образец, основанный на вашем Псевдокоде, может выглядеть следующим образом:
static bool CreateAll(object1 obj1, object2 obj2)
{
// Policy to retry 3 times, waiting 5 seconds between retries.
var policy =
Policy
.Handle<SqlException>()
.WaitAndRetry(3, count =>
{
return TimeSpan.FromSeconds(5);
});
policy.Execute(() => UpdateDatabase1(obj1));
policy.Execute(() => UpdateDatabase2(obj2));
}
Ответ 2
Вы можете выбрать шаблон команды, в котором каждая команда содержит всю необходимую информацию, такую как строка подключения, URL-адрес службы, счетчик повторов и т.д. В верхней части этого раздела вы можете рассмотреть rx, блоки потока данных для выполнения сантехники.
Изображение высокого уровня
:
Обновление: намерение состоит в том, чтобы разделить беспокойство. Логика повторения ограничивается одним классом, который является оболочкой существующей команды.
Вы можете сделать больше анализа и создать подходящие команды, объекты-вызовы и получатели и добавить функции отката.
public abstract class BaseCommand
{
public abstract RxObservables Execute();
}
public class DBCommand : BaseCommand
{
public override RxObservables Execute()
{
return new RxObservables();
}
}
public class WebServiceCommand : BaseCommand
{
public override RxObservables Execute()
{
return new RxObservables();
}
}
public class ReTryCommand : BaseCommand // Decorator to existing db/web command
{
private readonly BaseCommand _baseCommand;
public RetryCommand(BaseCommand baseCommand)
{
_baseCommand = baseCommand
}
public override RxObservables Execute()
{
try
{
//retry using Polly or Custom
return _baseCommand.Execute();
}
catch (Exception)
{
throw;
}
}
}
public class TaskDispatcher
{
private readonly BaseCommand _baseCommand;
public TaskDispatcher(BaseCommand baseCommand)
{
_baseCommand = baseCommand;
}
public RxObservables ExecuteTask()
{
return _baseCommand.Execute();
}
}
public class Orchestrator
{
public void Orchestrate()
{
var taskDispatcherForDb = new TaskDispatcher(new ReTryCommand(new DBCommand));
var taskDispatcherForWeb = new TaskDispatcher(new ReTryCommand(new WebCommand));
var dbResultStream = taskDispatcherForDb.ExecuteTask();
var WebResultStream = taskDispatcherForDb.ExecuteTask();
}
}
Ответ 3
Для меня это звучит как "Распределенные транзакции", так как у вас разные ресурсы (база данных, служебная связь, файловый ввод/вывод) и вы хотите сделать транзакцию, которая включает все из них.
В С# вы можете решить эту проблему с помощью Microsoft Distributed Transaction Coordinator. Для каждого ресурса вам нужен менеджер ресурсов. Насколько я знаю, для баз данных, таких как сервер sql и файл ввода/вывода, он уже доступен. Для других вы можете развивать свои собственные.
В качестве примера для выполнения этих транзакций вы можете использовать класс TransactionScope
следующим образом:
using (TransactionScope ts = new TransactionScope())
{
//all db code here
// if an error occurs jump out of the using block and it will dispose and rollback
ts.Complete();
}
(Пример из здесь)
Чтобы разработать собственный менеджер ресурсов, вам нужно реализовать IEnlistmentNotification
, и это может быть довольно сложной задачей. Вот короткий пример.
Ответ 4
Некоторый код, который может помочь вам в достижении вашей цели.
public static class Retry
{
public static void Do(
Action action,
TimeSpan retryInterval,
int retryCount = 3)
{
Do<object>(() =>
{
action();
return null;
}, retryInterval, retryCount);
}
public static T Do<T>(
Func<T> action,
TimeSpan retryInterval,
int retryCount = 3)
{
var exceptions = new List<Exception>();
for (int retry = 0; retry < retryCount; retry++)
{
try
{
if (retry > 0)
Thread.Sleep(retryInterval);
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
throw new AggregateException(exceptions);
}
}
Вызовите и повторите попытку, как показано ниже:
int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);
Ссылка: http://gist.github.com/KennyBu/ac56371b1666a949daf8
Ответ 5
Ну... звучит как действительно, очень неприятная ситуация. Вы не можете открыть транзакцию, написать что-нибудь в базу данных и пойти гулять с собакой в парк. Потому что у транзакций есть эта неприятная привычка блокировать ресурсы для всех. Это устраняет ваш лучший вариант: распределенные транзакции.
Я бы выполнил все операции и подготовил обратный script, когда я иду. Если операция успешна, я бы очистил script. В противном случае я его запустил. Но это открыто для потенциальных ловушек, и script должен быть готов к их обработке. Например - что, если в середине времени кто-то уже обновил записи, которые вы добавили; или рассчитывается совокупность на основе ваших значений?
Тем не менее: создание обратного script - это простое решение, без ракеты. Просто
List<Command> reverseScript;
а затем, если вам нужно откат:
using (TransactionScope tx= new TransactionScope()) {
foreach(Command cmd in reverseScript) cmd.Execute();
tx.Complete();
}