Создание компенсирующего менеджера ресурсов
Компенсирующий менеджер ресурсов позволяет использовать в транзакциях среды COM+ какие либо ресурсы, которые не имеют прямой поддержки транзакций COM+. Этот механизм включает следующие классы из пространства имен System.EnterpriseServices.CompensatingResourceManager:
-
журнал операций (log), заполняемый операциями над ресурсом;
-
секретарь (класс Clerk), ведущий журнал операций;
-
компенсатор (наследника класса Compensator), который восстанавливает первоначальное состояние ресурса в случае отката транзакции и вносит изменения в ресурс при успехе транзакции.
Сам менеджер ресурсов должен решить задачу изоляции ресурса в рамках транзакции и ведения журнала операций. При успехе транзакции компенсатор вносит постоянные изменения или отменяет их в соответствии с журналом операций. Следует отметить, что какие-либо временные изменения, производимые менеджером с ресурсом, могут вероятно нарушить требования изоляции транзакции, поскольку могут быть видимыми для внешних по отношению к транзакции объектов. С другой стороны, объекты внутри транзакции должны наблюдать происходящие с ресурсом изменения до успешного завершения транзакции.
В качестве примера менеджера ресурсов рассмотрим работу с файлами небольшого размера. Менеджер ресурсов предоставляет следующие сервисы:
- открытие документа на чтение;
- открытие документа на перезапись;
- закрыть файл с сохранением изменений.
С целью изоляции транзакций запись в файл происходит в момент успешного завершения транзакции. До этого момента данные хранятся в памяти. Такое решение безусловно не является эффективным с точки зрения траты ресурсов решение, оно используется в качестве примера. При чтении в рамках транзакции документа, уже ранее измененного в этой же транзакции, вместо чтения из файла происходит чтение последней относящийся к этому файлу записи из журнала операций. Журнал операций хранит пары из имени файла (в нижнем регистре) и содержимого файла.
Пример компилируется и запускается make файлом, который можно передать как параметр входящей в состав .NET SDK утилите nmake.exe.
Этот файл имеет следующее содержание.
# Файл: makefile all: CrmSample.exe # сборка должна быть подписана CrmSample.key: sn -k CrmSample.key CrmSample.exe: CrmSample.cs CrmSample.Key csc /r:System.EnterpriseServices.dll CrmSample.cs /keyfile:CrmSample.key install: # установить приложение COM+ regsvcs CrmSample.exe uninstall: regsvcs –u CrmSample.exe
Поскольку для регистрации сборки как приложения COM+необходимо, чтобы она была подписана, то вызовом утилиты sn.exe из состава .NET SKD создается пара ключей. Затем компилятор языка C# csc.exe создает сборку, используя этот файл ключей. При команде nmake install сборка устанавливается в качестве приложения COM+ вызовом утилиты regsvcs.exe Microsoft Windows.
Далее будет рассмотрено содержание файла CrmSample.cs.
// Файл CrmSample.cs using System; using System.IO; using System.Collections.Generic; using System.EnterpriseServices; using System.EnterpriseServices.CompensatingResourceManager;
[assembly: ApplicationActivation(ActivationOption.Server)] [assembly: ApplicationAccessControl(false)] [assembly: ApplicationCrmEnabled] [assembly: ApplicationName("Seva CRM")] [assembly: Description("Пример на использование CRM")]
Данные атрибуты сборки из пространства имен System.EnterpriseServices управляют параметрами создаваемого приложения COM+:
-
ApplicationActivation – задает тип приложения COM+ (серверный или библиотечный);
-
ApplicationCrmEnabled – необходим для использования CRM в приложении COM+;
-
ApplicationAccessControl – управляет контролем доступа к приложению, в данном примере – отключен;
-
Атрибут System.ComponentModel.DescriptionAttribute задает описание сборки.
Класс StreamLog содержит статический метод для записи в файл буфера, вызываемый при завершении транзакции.
public static class StreamLog { public static void Save(LogRecord log) { if (log.Record is object[]) { object[] record = (object[])log.Record; string fileName = (string) record[0]; byte[] buffer = (byte[]) record[1];
using (FileStream file = new FileStream(fileName, FileMode.Create, FileAccess.Write)) { file.Write(buffer, 0, buffer.Length); } } } // Save() } // StreamLog
Класс StreamCache помогает организовать кеширование содержимого файлов, использующихся в транзакции.
public class StreamCache { private string fileName;
private MemoryStream stream;
public MemoryStream Stream { get { return stream;} }
public string FileName { get { return fileName;} }
public StreamCache(string streamFileName) { fileName = streamFileName; stream = new MemoryStream(); }
Метод Reopen при необходимости открывает повторно закрытый поток. Поскольку такая операция не поддерживается классом MemoryStream, то сначала в массив записывается все содержимое потока, затем создается новый поток, куда записываются сохраненные данные.
public void Reopen() { if (!stream.CanRead) { stream.Close(); byte[] buffer = stream.ToArray(); stream.Dispose(); stream = new MemoryStream(); stream.Write(buffer, 0, buffer.Length); stream.Seek(0, SeekOrigin.Begin); } } // Reopen()
При открытии файла для чтения вызывается метод Read, считывающий все содержимое файла в поток типа MemoryStream.
public void Read() { byte[] buffer = new byte[32*1024]; using (Stream inputStream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) { while (true) { int read = inputStream.Read(buffer, 0, buffer.Length); if (read <= 0) { break; } Stream.Write(buffer, 0, read); } } } // Read()
} // StreamCache
Класс StreamCrm является менеджером ресурсов, используемым объектами COM+. Он является COM+ объектом, поэтому несколько участвующих в транзакции компонент COM+ могут работать с одним менеджером ресурсов данного типа. Менеджер содержит кеш для реализации отложенной до завершения транзакции записи в файл.
public class StreamCrm: ServicedComponent { private Dictionary<string, StreamCache> cache;
public StreamCrm() { cache = new Dictionary<string, StreamCache>(); }
Метод StreamCrm.CheckCache проверяет, есть ли в кеше информация о данном файле. При отсутствии ее в кеше в случае открытия файла на чтение происходит считывания всего содержимого файла в кеш, в противном случае связанный с файлом поток открывается повторно вызовом метода StreamCache.Reopen.
private StreamCache CheckCache(string fileName, bool read) { StreamCache streamCache; string key = Path.GetFullPath(fileName).ToLower(); if (!cache.ContainsKey(key)) { streamCache = new StreamCache(fileName); cache.Add(key, streamCache); if (read) { streamCache.Read(); }; } else { streamCache = cache[key]; streamCache.Reopen(); };
return streamCache; }
Метод StreamCrm.ReadFromFile вызывается клиентом, желающим читать из файла, метод StreamCrm.WriteToFile – желающим перезаписать файл.
public MemoryStream ReadFromFile(string fileName) { return CheckCache(fileName, true).Stream; }
public MemoryStream WriteToFile(String fileName) { return CheckCache(fileName, false).Stream; }
Метод StreamCrm.Flush прекращает работу с потоком, открытым для записи и сохраняет сделанные изменения в записи для журнала секретаря, которую возвращает в качестве своего результата.
public object[] Flush(String fileName) { StreamCache streamCache = CheckCache(fileName, false); streamCache.Stream.Close(); object[] record = new object[2]; record[0] = fileName; record[1] = streamCache.Stream.ToArray(); return record; }
Статический метод StreamCrm.CreateClerk создает секретаря для ведения журнала операций над ресурсом. Вызов статического метода не приводит к удаленному вызову и смене контекста.
public static Clerk CreateClerk() { return new Clerk(typeof(StreamCompensator), "Compensator", CompensatorOptions.AllPhases); } }
Класс StreamCompensator наследуется от класса Compensator и служит для сохранения результатов транзакции в случае успеха или возврате данных в первоначальное состояние в случае отката транзакции.
public class StreamCompensator : System.EnterpriseServices.CompensatingResourceManager.Compensator { private bool prepared = false;
Методы c суффиксом Prepare вызываются для проверки записей в журнале перед завершением или откатом транзакции. Метод PrepareRecord должен вернуть false, если запись журнала должна быть использована. Поскольку журнал записей не контролирует типы записей, это должен сделать метод PrepareRecord.
public override void BeginPrepare () { }
public override bool PrepareRecord(LogRecord log) { prepared = false; if (!(log.Record is object[])) return true; object[] record = log.Record as object[]; if (record.Length != 2) return true; if (!(record[0] is string) || !(record[1] is byte[])) return true; prepared = true; return false; }
Метод EndPrepare возвращает сохраненный в поле объекта результат проверки записи. Если он возвращает true, то возможно успешное завершение транзакции.
public override bool EndPrepare () { return prepared; }
Методы c суффиксом Commit вызываются для сохранения результата транзакции. Если метод CommitRecord возвращает истинное значение, то запись можно исключить из журнала операций.
public override void BeginCommit (bool commit) { }
public override bool CommitRecord (LogRecord log) { StreamLog.Save(log); return true; }
public override void EndCommit () { }
Группа методов с суффиксом Abort служит для возвращения ресурсу первоначального состояния при откате транзакции. Поскольку созданный менеджер ресурсов изменяет состояние файлов меняется только при успешном завершении транзакции, то эти методы не содержат никаких действий.
public override void BeginAbort (bool abort) { } public override bool AbortRecord (LogRecord log) { return true; } public override void EndAbort () { } } // StreamCrm
Ниже приведен пример двух классов COM+, которые используют созданный менеджер ресурсов. Напомним, что для регистрации в качестве COM+ компоненты классы должен иметь публичный конструктор без параметров. Атрибут TransactionAttribute управляет использованием транзакций COM+. Класс SampleCrmClient2 содержит метод ReadLine, читающий из файла строчку с использованием созданного менеджера ресурсов.
[Transaction(TransactionOption.Required)] public class SampleCrmClient2: ServicedComponent { public SampleCrmClient2() { }
public string ReadLine(StreamCrm crm, string fileName) { using (StreamReader reader = new StreamReader(crm.ReadFromFile(fileName))) { return line = reader.ReadLine(); } } }
Метод SampleCrmClient.DoSomeWork демонстрирует использование CRM. Смена текущей директории вызвана тем, что по умолчанию для серверных компонент COM+ текущей является директория %systemroot%\system32.
[Transaction(TransactionOption.RequiresNew)] public class SampleCrmClient1: ServicedComponent { public SampleCrmClient1() { }
public void DoSomeWork(string dir) { const string fileName1 = "sample1.txt"; const string fileName2 = "sample2.txt";
Environment.CurrentDirectory = dir; StreamCrm crm = new StreamCrm(); Clerk clerk = StreamCrm.CreateClerk();
Метод производит действия с двумя файлами.
Сначала в поток, связанный с первым из двух файлов, записывается некоторая строка. Собственно запись в файл в этот момент не происходит, записанные данные запоминаются в кеше менеджера ресурсов после вызова метода StreamCrm.Flush. Этот метод возвращает запись, которая помещается в журнал секретаря и будет использована при завершении транзакции.
using (StreamWriter writer = new StreamWriter(crm.WriteToFile(fileName1))) { writer.Write(Environment.CurrentDirectory); } // добавление в журнал записи clerk.WriteLogRecord(crm.Flush(fileName1));
Затем из этого файла строка считывается другим объектом COM+ класса SampleCrmClient2. Таким образом, если объекты в пределах одной транзакции будут обращаться к файлам при помощи одного экземпляра менеджера ресурсов, то изменения, внесенные одним объектом, будут видны другим объектам транзакции, но не видны снаружи до ее завершения.
String tempString = ""; using (SampleCrmClient2 client2 = new SampleCrmClient2()) { tempString = client2.ReadLine(crm, Path.GetFullPath(fileName1)); } Считанные объектом данные записываются во второй файл.
using (StreamWriter writer = new StreamWriter(crm.WriteToFile(fileName2))) { writer.WriteLine(String.Format("Считано из файла [{0}]:", fileName1)); writer.WriteLine(tempString); }
// добавление в журнал записи clerk.WriteLogRecord(crm.Flush(fileName2));
// успешное завершение транзакциии, сохранение изменений в файлах ContextUtil.SetComplete(); } }
Класс CrmTest содержит метод Main. Вместо конструкции try .. finally … Dispose в C# следует использовать оператор using с тождественным результатом. Однако в данном примере хотелось бы показать, что при использовании объектов среды EnterpriseServices/COM+ для них следует вызывать Dispose в явном или неявном (через оператор using) виде.
class CrmTest { public static void Main() { SampleCrmClient1 client1 = new SampleCrmClient1(); try { client1.DoSomeWork(Environment.CurrentDirectory); } finally { client1.Dispose(); } } }
Кроме работы с компенсирующим менеджером ресурсов, на данном примере хотелось бы показать, что хотя сборка мусора упрощает работу программиста, но при использовании объектов с интерфейсом IDisposable всегда следует вызывать их метод Dispose (явно или неявно) для своевременного освобождения ресурсов.
Содержание раздела