Как стать автором
Обновить

PostSharp. Менеджмент транзакций

Время на прочтение6 мин
Количество просмотров1.1K
Автор оригинала: Matthew D. Groves
imageКогда в последний раз я консультировал одну компанию, мы обсуждали внутренний SOA фреймворк, который должен взаимодействовать с базами данных предприятия. Этот фреймворк был SOA только разве что по названию, полностью доморощенный, и что самое грустное, он был «любимым проектом» начальника отдела ИТ. Он был не безопасным и построен на сомнительных технологиях и решениях. В общем он был сделан для решения некоторой проблемы, которой либо не существует, либо она не могла была разрешима боле простыми средствами. Моя команда была крайне разочарована структурой этого фреймворка. Но, в качестве консультанта, очень часто приходится сначала продумать пути решения проблемы, нельзя порсто сказать заказчику что его продукт «плохой», необходимо сначала построить его доверие к себе, и только потом решать более серъезные проблемы. Так или иначе такой процесс может занять годы, и все зависит от фирмы, в которой будут происходить такого рода изменения. Тогда я и задумался над тем чтобы начать использовать аспектно-ориентированный фреймворк чтобы решить эту проблему

Все исходные тексты приведенных программ доступны по ссылке: GitHub

В тоже самое время мы были обречены на провал с этим фреймворком, поскольку он абсолютно не предназначен для многопоточных приложений (например, для web-сервера). Из-за этого постоянно случаются аварии, а иногда он может просто ни работать ни запускаться. Мы должны были построить рабочие и хорошо отлаженные приложения вокруг этого беспорядка и хаоса в первую очередь потому что мы хотим построить к себе определенный уровень доверия со стороны фирмы-заказчика. То что мы уже сделали для этой фирмы, можно посмотреть в предыдущих статьях: это и логгирование и перехват ошибок. Это работало хорошо, и скрывало все «гадости» от конечного пользователя. Однако для того чтобы пользоваться этой системой, необходимо защищать каждый участок кода, который с ней соприкасается. И, делая это, я довольно быстро устал. Ведь представьте: для каждого метода, который вызывали во фреймворке или считывали значения свойств и полей классов, было необходимо провесети одинаковые проверки, и при этом не переборщить: иначе мы теряем в производительности.
Однако, хватит разглагольствовать о старых проектах, поговорим о деле: вы можете использовать PostSharp для управления транзакциями, независимо от того, строите вы свое приложение на доморощенных фреймворках или нет. Вот простой пример OnMethodBoudaryAspect, который даст вам пример основ типичной поддержки транзакций:
[Serializable]
public class TransactionScopeAttribute : OnMethodBoundaryAspect
{
        [NonSerialized] private ICharityService _charityService;
        private string _methodName;
        private string _className;

        public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
        {
                _className = method.DeclaringType.Name;
                _methodName = method.Name;
        }

        public override void RuntimeInitialize(System.Reflection.MethodBase method)
        {
                // in practice, the begin/rollback/commit might be in a more general service
                // but for convenience in this demo, they reside in CharityService alongside
                // the normal repository methods
                _charityService = new CharityService();
        }

        public override void OnEntry(MethodExecutionArgs args)
        {
                _charityService.BeginTransaction(); 
        }

        public override void OnException(MethodExecutionArgs args)
        {
                _charityService.RollbackTransaction();
                var logMsg = string.Format("exception in {0}.{1}", _className, _methodName);
                // do some logging
        }

        public override void OnSuccess(MethodExecutionArgs args)
        {
                _charityService.CommitTransaction();
        }
}


Все действительно просто и здесь нет абсолютно ничего нового относительно моих более старых статей. Заметьте, что здесь было бы здорово перегенерировать исключение в случае отката транзакции, и именно это и делается по умолчанию (в коде, который вызывает onException). Все что вам надо сделать – пометить методы, в которых необходимо поддержать транзакции атрибутом [TransactionScope], и вы убережете себя от написания try/catch begin/commit/rollback кода все снова и снова одним простым аспектом. Также мы вынесли общий код в одно место, и можем таким же образом добавить логгирование или какой-либо специальный метод перехвата исключений.
Один из случаев, кода код выше не будет работать должным образом, это вложенные транзакции. В моем коде, изложенном выше, методы Begin/Commit/Rollback всего лишь пустышки, которые на самом деле ничего не делают. В конечном приложении, которое вы будете разрабатывать, менеджмент транзакий сильно зависит от метода доступа к данным. Если вы используете ADO.NET соединения, такие как SqlConnection, ODBC, Oracle, и проч., тогда вы скорее всего будете использовать TransactionScope (не перепутайте с названием аспекта) чтобы сделать внедрение транзакций простым и поддерживаемым. Однако если вы используете некоторую самостоятельную технологию, вы будете использовать совершенно иной API и вам потребуются дополнительные решения.
Теперь, когда у нас есть базовый аспект поддержки транзакций, давайте вернемся к моему изначальному примеру. Сервис, который я использую, постоянно ломается. Потому я напишу некий цикл, который будет пытаться сделать операцию до тех пор, пока операция не пройдет успешно. И, конечно, я не хочу чтобы попытки были постоянными и переросли в бесконечный цикл. Поэтому, введем ограничения на количество попыток. Также пусть мы точно знаем что признаком неудачной попытки служит исключение DataException, а все остальные исключения говорят нам о том что все дальнейшие попытки провести операцию предпринимать бесполезно. Поэтому мы залоггируем такие исключения и перестаним предпринимать попытки:
[Serializable]
public class TransactionScopeAttribute : MethodInterceptionAspect
{
        [NonSerialized] private ICharityService _charityService;
        [NonSerialized] private ILogService _logService;

        private int _maxRetries;
        private string _methodName;
        private string _className;

        public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
        {
                _methodName = method.Name;
                _className = method.DeclaringType.Name;
        }

        public override void RuntimeInitialize(System.Reflection.MethodBase method)
        {
                _charityService = new CharityService();
                _logService = new LogService();
                _maxRetries = 4;            // you could load this from XML instead
        }

        public override void OnInvoke(MethodInterceptionArgs args)
        {
                var retries = 1;
                while (retries <= _maxRetries)
                {
                        try
                        {
                                _charityService.BeginTransaction();
                                args.Proceed();
                                _charityService.CommitTransaction();
                                break;
                        }
                        catch (DataException)
                        {
                                _charityService.RollbackTransaction(); 
                                if (retries <= _maxRetries)
                                {
                                        _logService.AddLogMessage(string.Format(
                                                "[{3}] Retry #{2} in {0}.{1}",
                                                _methodName, _className,
                                                retries, DateTime.Now));
                                        retries++;
                                }
                                else
                                {
                                        _logService.AddLogMessage(string.Format(
                                                "[{2}] Max retries exceeded in {0}.{1}",
                                                _methodName, _className, DateTime.Now));
                                        _logService.AddLogMessage("-------------------");
                                        throw;
                                }
                        }
                        catch (Exception ex)
                        {
                                _charityService.RollbackTransaction();
                                _logService.AddLogMessage(string.Format(
                                        "[{3}] {2} in {0}.{1}",
                                        _methodName, _className,
                                        ex.GetType().Name, DateTime.Now));
                                _logService.AddLogMessage("-------------------");
                                throw;
                        }
                }
                _logService.AddLogMessage("-------------------");
        }
}


Да, это выглдит несколько пугающе! Но не беспокойтесь, давайте разберем все по кусочкам. CompileTimeInitialize и RuntimeInitialize вовсе не выглядят сильно отличными от тех, что описаны раньше: они всего лишь запоминают имена класса и метода и инициализируя сервисы, которые нам необходимы.
Для метода OnInvoke, давайте рассмотрим четыре возможных сценария работы:
  • Если метод отрабатывает без исключения, то это значит что все отлично работает.
  • Работа метода заканчивается неудачей и он выбрасывает исключение DataException, и мы еще не достигли ограничения в количестве повторов. Этот код находится в первой части оператора if/else первого блока catch{}
  • Работа метода заканчивается неудачей и он выбрасывает исключение DataException, и мы достигли ограничения в количестве повторов. Этот код находится во второй части оператора if/else первого блока catch{}
  • Метод выбрасывает другое исключение, сигнализируя о критической ошибке и невозможности повторов. За эту ситуацию отвечает код во втором блоке Catch{}

В моем простом приложении, я сделал сервис CharityService, который вылетает с ошибкой DataException в 50% случаев, а в оставшихся 50% — заканчивается удачно. Также примерно в 1 из 50 вызовов он возвращает не DataException, а какой-то другой. Если запустить пример, то сможете увидеть окно лога, в котором будет зафиксировано огромное количество попыток, и вызовов Begin, Rollback и Commit. Также пользователь увидит сообщение об ошибке в случае если программа сделала попыток больше чем разрешено либо если произошло исключение не явлюяющееся DataException.

Используя этот аспект, вы наделяете свое приложение некоторой роботизированностью в решении такого рода проблем. Код аспекта стал больше но он управляем. Если вы захотите получить что-то сложнее, тут будет иметь место рефакторинг: разделение на классы и методы, и прочие операции. Также можно использовать этот способ для того чтобы выстроить многопоточные обращения к методу в один поток.

Ссылки:
Теги:
Хабы:
Всего голосов 5: ↑2 и ↓3-1
Комментарии0

Публикации