Глобальные транзакции в сервис-ориентированной архитектуре и... 1С


В данной статье будет рассмотрен вопрос включения 1С в глобальные транзакции в рамках сервис-ориентированной архитектуры. Эта статья является продолжением статьи "Интеграция 1С с сервисной шиной OpenESB".

    В данной статье будет рассмотрен вопрос включения 1С в глобальные транзакции в рамках сервис-ориентированной архитектуры. Эта статья является продолжением статьи "Интеграция 1С с сервисной шиной OpenESB".

 

Введение

   Механизм транзакций является ключевым элементом, для построения надежных и отказоустойчивых информационных систем. Всем хорошо известна его полезность в реляционных СУБД - именно транзакции позволяют строить надежные системы, согласовано меняющие состояние и откатывающиеся, в случае сбоя, к предыдущему непротиворечивому состоянию. Ключевым свойствами транзакций, позволяющими все это осуществить, являются: атомарность, согласованность, изолированность и устойчивость(для их обозначения чаще используется аббревиатура ACID).

   Гораздо менее известен тот факт, что транзакции могут быть связаны с ресурсами отличными от традиционных баз данных. В общем случае транзакционным может быть назван любой ресурс, который поддерживает:

1. Явное или неявное начало транзакции, как группы согласованных действий.
2. Отмену всех действий в рамках транзакции и возвращение ресурса в исходное состояние.
3. Фиксацию всех действий, которые приводят ресурс к новому допустимому состоянию.

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

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

    Все основные программные платформы (а к ним я отношу прежде всего Java и .NET) пришли к осознанию важности обобщенного механизма управления разнообразными транзакционными ресурсами, и, что самое интересное, к согласованному управлению несколькими транзакционными ресурсами в рамках одной транзакции. В Жабе 2 Йо-Йо есть JTS (Java Transaction Service), а в дотнете свой механизм, гнездящийся в пространстве System.Transactions.

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

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

    В общем случае, механизм который рулит транзакцией называется "менеджером транзакции" (координатором), а рулежка транзакции сводится к рулежке транзакционными ресурсами, но не напрямую, а через некий адаптер, называемый "менеджером ресурса". Тут надо понимать, что терминология эта достаточна условна, и в другом месте вполне может использоваться термин "распределенные" вместо "глобальные" и пр.

   Наличие промежуточного слоя в виде менеджера ресурса позволяет включать в транзакцию самые разнообразные ресурсы, реализуя соответствующий менеджер. В .NET менеджер ресурса представляет собой по сути обработчик, который слушает состояние транзакции и при наступлении тех или иных событий предпринимает соответствующие действия в отношении подопечного ресурса.  Для примера, рассматриваемого в этой статье, создание такого менеджера ресурса для 1С будет играть ключевую роль - именно он позволит включать 1С в глобальные транзакции.

   В целом, схема такой глобальной транзакции будет иметь вид:

schem

   Природа же координатора нас волновать не будет.

 

Общие соображения

    Как ясно из введения, 1С поднятая через V81.Application прямо таки просится в качестве транзакционного ресурса. Тем более, что и делать-то ничего не надо - поддержка транзакций в 1С уже есть и все, что нужно сделать - это менеджер, который сможет управлять ими и при этом будет совместим с механизмом транзакций .NET'а.

   В качестве основы будет использован пример универсального адаптера OneCService из прошлой статьи, потребуется его только слегка переработать и дополнить. Поэтому детально OneCService здесь рассматриваться не будет.

    Доработки в основном коснутся:
1. Сервиса: нужно включить поддержку транзакций и включение транзакционного ресурса (1C через OneCAdapter) в транзакцию.
2. OnceAdapter'а: нужно добавить методы для управления транзакциями.
3. Создания менеджера, который будет управлять OneCAdpater'ом, и, как следствие, 1С'ом.

   При этом полная поддержка двухфазного протокола в менеджере ресурса реализована не будет - стадия подготовки будет оставлена пустой, так как сам ресурс (1С) не поддерживают двухфазное подтверждение. Также будет предпринят ряд шагов предотвращающий:

1. Потерю транзакции - дотнетовский менеджер транзакций там чего-то мудрит, так что периодически вылезала ошибка с неактивной транзакцией. Решение простое: не хранить OneCAdapter в менеджере ресурсов, а хранить его в параметрах домена .NET, использую в качестве ключа GUID, уникальный для экземпляра менеджера ресурсов.

2. Потерю ссылкой на V81.Application своей рантайм-обертки (RCW) - проявляется когда менеджер ресурсов получает команду завершить или откатить транзакцию, происходит это уже за рамками пользовательского кода, так что точная причина не понятна. Решение состоит в том, чтобы хранить также неуправляемый указатель на экземпляр V81.Application и в методах завершения/отмены транзакции восстанавливать по нему RCW.

  Также не будет реализовано повторное использование V81.Application - пул соединений. На каждый запрос буде подниматься свое соединение с 1С и включаться в транзакцию («любите ли Вы дедлоки так, как люблю их я?»).

 

Инструментарий

1. 1С 8.1 с файловой БД.
2. Sharpevelop 3.1  + .NET 3.5 SP1 + Windows SDK (там есть svcutil.exe, нужный для построения клиента к сервису).

 

Реализация

  Начнем с некоторых изменений, которые необходимо внести в сервис для включения поддержки транзакций:

[ServiceContract(Name="onecservice", Namespace="http://onecservice")]
public interface IOneCWebService
{
    [OperationContract(Name="ExecuteRequest")]
    [
TransactionFlow(TransactionFlowOption.Mandatory)]
   
ResultSet ExecuteRequest(string _file, string _usr, string _pwd, string _request);

    [
OperationContract(Name="ExecuteScript")]
    [
TransactionFlow(TransactionFlowOption.Mandatory)]
   
ResultSet ExecuteScript(string _file, string _usr, string _pwd, string _script);

    [
OperationContract(Name="ExecuteMethodWithXDTO")]
    [
TransactionFlow(TransactionFlowOption.Mandatory)]
   
ResultSet ExecuteMethodWithXDTO(string _file, string _usr, string _pwd, string _methodName, XmlNode[] _parameters);
}

 TransactionFlow.Mandatory гарантирует обязательное присутствие транзакции с клиентской стороны, иначе клиент получит отлуп, поэтому, кстати, данный пример не совместим с клиентами для изначального OneCService'а.

  Перейдем к менеджеру ресурса, который позволит включать 1С в транзакцию, как видно из кода он представляет собой обычной обработчик, реагирующий на изменение состояния транзакции(код приведен с сокращениями):

 

public class V8ResourceManager : IEnlistmentNotification, IDisposable
{
    private Guid        resourceGuid = Guid.NewGuid();
   
private AppDomain         domain = null;

   
public AppDomain Domain
    {
        set {.....}
        get {.....}
    }

    public OneCAdapter Adapter
    {
        set {......}
        get {......}
    }

    public Guid ResourceGuid
    {
        ......
   
}

    public void Prepare(PreparingEnlistment preparingEnlistment)
   
{
        ......
   
}

    public void Commit(Enlistment enlistment)
   
{
        try
       
{
            //Console.WriteLine("Commit GUID:" + resourceGuid);
           
Adapter.Commit();
           
enlistment.Done();
       
}
        catch (Exception _e)
       
{
            SimpleLogger.DefaultLogger.Severe("Error on commit: "+_e.ToString());
       
}
        finally
        {
            TryClose();
       
}
    }

    public void Rollback(Enlistment enlistment)
   
{
        .....
   
}

    public void InDoubt(Enlistment enlistment)
   
{
        .....
   
}

    .....

}

 

 Рассмотрим код, обеспечивающий включений 1С в транзакцию:

 

private void EnlistToTransaction(OneCAdapter _adapter)
{
    if (Transaction.Current != null)
   
{
        V8ResourceManager manager = new V8ResourceManager();
       
manager.Domain = AppDomain.CurrentDomain;
       
manager.Adapter = _adapter;

       
Transaction.Current.EnlistDurable(manager.ResourceGuid, manager, EnlistmentOptions.None);

       
_adapter.Begin();
   
}
    else
   
{
        Exception e = new Exception("Ambient transaction not found!");
       
SimpleLogger.DefaultLogger.Severe(e.ToString());
       
throw e;
   
}
}

   Все что теперь остается сделать - это добавить включение в транзакцию в каждый метод веб-сервиса и можно приступать к созданию клиента.

   Значительная часть в создании клиента приходится на автоматическую генерацию клиентского прокси для доступа к сервису, для чего потребуется svcutil.exe (есть в Windows SDK). bat-файл строящий прокси приведен в клиентском проекте, в архиве. Также будет сформирован конфигурационный файл, содержащий, в том числе, и настройки соединения с сервисом. Имея готовый прокси и конфигурацию к нему можно переходить к написанию клиента.

  Рассмотрим код клиента:

 

public static void Main(string[] args)
{
    using (TransactionScope t = new TransactionScope(TransactionScopeOption.Required))
   
{
        onecserviceClient client = new onecserviceClient();
        try
       
{
            //Первая база 1С через веб-сервис
           
ResultSet resultSet = client.ExecuteScript(
               
"C:\Work\OneCService\Base\First",
               
"", "",
               
"сотр = Справочники.Сотрудники.НайтиПоКоду(10);\n" +
               
"Если Не сотр.Пустая() Тогда сотр.ПолучитьОбъект().Удалить(); КонецЕсли;" +
               
"сотр = Справочники.Сотрудники.СоздатьЭлемент();\n" +
               
"сотр.Код = 10; " +
               
"сотр.Наименование = \""; " +
               
"сотр.Записать();"
                                                   
);
            if (
!resultSet.Error.Equals(""))
           
{
                Console.WriteLine("Error: "+resultSet.Error);
               
throw new Exception(resultSet.Error);
           
}

            //Вторая база 1С через веб-сервис
           
resultSet = client.ExecuteScript(
               
"C:\Work\OneCService\Base\Second",
               
"", "",
               
"сотр = Справочники.Сотрудники.НайтиПоКоду(10);\n" +
               
"Если Не сотр.Пустая() Тогда сотр.ПолучитьОбъект().Удалить(); КонецЕсли;" +
               
"сотр = Справочники.Сотрудники.СоздатьЭлемент();\n" +
               
"сотр.Код = 10; " +
               
"сотр.Наименование = \""; " +
               
"сотр.Записать();"
                                           
);
            if (
!resultSet.Error.Equals(""))
           
{
                Console.WriteLine("Error: "+resultSet.Error);
               
throw new Exception(resultSet.Error);
           
}

            //Завершение транзакции
           
t.Complete();
       
//try
       
finally
        {
            client.Close();
           
Console.ReadKey();
       
}
    //using TransactionScope
}

 

    Как видно из кода, клиент, рассмотренный в этом примере будет работать с двумя базами 1С, в каждой из которых есть справочник "Сотрудники". В ходе работы клиента в этот справочник будет добавляться Сиськин с кодом 10. Весь процесс будет происходить в глобальной транзакции, то есть, если при добавлении во вторую базу произойдет ошибка, то Сиськин не должен будет появится и в первой базе, несмотря на то, что скрипт уже выполнен.  Хочется также отметить, что выполнения каждого скрипта будет осуществляться через отдельный вызов сервиса (через HTTP) и, в общем случае, эти сервисы могут находится на разных узлах сети и обеспечивать взаимодействие с разными базами 1С.

   Таким образом пример соотвествует схеме:

transaction

Как обычно, видео с демонстрацией лежит здесь (не забываем переключаться в HD)

Архив с исходниками, бинарниками и базами прилагается.

Недостатки реализации

1. Отсутствие пула поднятых V81.Application (в разрезе баз и пользователей, с обязательной проверкой пароля).
2. Очень просто организовать блокировку, а если постараться, то и дедлок. Причем в отличии это предыдущей версии это все может быть растянуто по времени и перемешено с другим вызовами (как других баз 1С, так и обычных СУБД или удаленных сервисов).

Лучшее средство от головы... или неполиткорректные мысли о глобальных транзакциях

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

   С блокировкой все более или менее понятно: при параллельном доступе к ресурсу первый блокирует его, а все последующие ждут. Минусы состоят именно в том, что остальные ждут (любителям клюшек должны быть знакомы тормоза из-за блокировки таблицы журнала документов). И переключение на READ_UNCOMMITED ничего хорошего тоже не даст.

   Версионный механизм решает эту проблему: любой, кто изменяет ресурс работает со своей версией ("пишущие не блокируют читающих"). Но и тут есть свои минусы - вилка, которая возникает когда кто-то начал модификацию раньше чем сосед, а завершил позже. Тут в качестве примера можно привести СУБД Firebird.

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

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

 

Литература

1. Джувел Лёве «Создание служб WCF», O'REILLY/Питер.

 

P.S. Этот пример, как раз и иллюстрирует почему я так скептически отношусь ко встроенному движку веб-сервисов, который есть в 1С - такая задача ему не по зубам, равно как множество других вещей, например, поддержка стандарта WS-Security и пр. Причем я и не могу сказать, что эти вещи не нужны - движки, реализующие широкий набор стандартов, уже есть и они массово распространяются (WCF у каждого обладателя Висты и 7, Metro в каждом сервере приложений GlassFish). В конечном счете веб-сервисы становятся преобладающим средством взаимодействия и тут уже маркетинговой галочкой "поддержка веб-сервисов в 1С:Предприятие 8.1" не обойтись. Как обычно жду полезной критики и обсуждения.

 

 

Файлы обработки:

-