Monday, 28 July 2014

Несколько параллельных логов в log4net

Появилась необходимость вести несколько параллельных логов приложения.
Внутри приложения выполняются отдельные не связанные друг с другом задачи, генерирующие большой объём логов. Было решено разделить логи, и вести отдельныйлог для каждой из задач.

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

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

/// <summary>
    /// Class incapsulating a per-task logging functionality.
    /// </summary>
    internal static class TaskLogger
    {
        private static readonly IDictionary<intILog> loggers = 
            new Dictionary<intILog>();
        private static Hierarchy hier;
        private static readonly RollingFileAppender mainAppender;
 
        static TaskLogger()
        {
            hier = (Hierarchy)LogManager.GetRepository();
            mainAppender = hier.GetAppenders()[0] as RollingFileAppender;
        }
 
        /// <summary>
        /// Creates a logger if there in no logger for a task yet, 
        /// and puts it to cache.
        /// </summary>
        /// <param name="taskId"></param>
        /// <param name="message"></param>
        public static void Log(int taskId, string message)
        {
            if (!loggers.ContainsKey(taskId))
            {
                lock (loggers)
                {
                    if (!loggers.ContainsKey(taskId))
                    {
                        loggers.Add(taskId, CreatePerTaskLogger(taskId));
                    }
                }
            }
            loggers[taskId].Info(message);
        }
 
        /// <summary>
        /// Removes a logger from cache when it's not required.
        /// </summary>
        /// <param name="taskId"></param>
        public static void RemoveLogger(int taskId)
        {
            if (loggers.ContainsKey(taskId))
            {
                lock (loggers)
                {
                    if (loggers.ContainsKey(taskId))
                    {
                        loggers.Remove(taskId);
                    }
                }
            }
        }
 
        /// <summary>
        /// Creates an additional logger to log task events in a separate file.
        /// </summary>
        /// <param name="taskId">Task ID.</param>
        /// <returns>New Logger.</returns>
        private static ILog CreatePerTaskLogger(int taskId)
        {
            FileAppender appender = CreateAppender(mainAppender, taskId);
            Logger logger = new OurLogger(String.Format("Logger_{0}", taskId));
            logger.Level = Level.All;
            //logger.RemoveAllAppenders();
            logger.AddAppender(appender);
            logger.Hierarchy = hier;
            return new LogImpl(logger);
        }
 
        /// <summary>
        /// Creates a new appender for a new logger.
        /// </summary>
        /// <param name="source">Source appender to copy settings from.</param>
        /// <param name="taskId">Id of the task being logged.</param>
        /// <returns></returns>
        private static RollingFileAppender CreateAppender(RollingFileAppender source, int taskId)
        {
            RollingFileAppender result = new RollingFileAppender();
            result.AppendToFile = source.AppendToFile;
            result.Encoding = source.Encoding;
            result.ErrorHandler = source.ErrorHandler;
            string logsFolder = Path.GetDirectoryName(source.File);
            result.File = Path.Combine(logsFolder, String.Format("Task_{0}.log", taskId));
            result.ImmediateFlush = source.ImmediateFlush;
            result.Layout = source.Layout;
            result.LockingModel = new FileAppender.MinimalLock();
            result.Name = String.Format("Appender_{0}", taskId);
            result.SecurityContext = source.SecurityContext;
            result.CountDirection = source.CountDirection;
            result.DatePattern = source.DatePattern;
            result.DateTimeStrategy = source.DateTimeStrategy;
            result.MaxFileSize = source.MaxFileSize;
            result.MaxSizeRollBackups = source.MaxSizeRollBackups;
            result.MaximumFileSize = source.MaximumFileSize;
            result.PreserveLogFileNameExtension = source.PreserveLogFileNameExtension;
            result.RollingStyle = source.RollingStyle;
            result.StaticLogFileName = source.StaticLogFileName;
            return result;
        }
    }

Изменения кэша аппендеров защищены оператором lock. После завершения (удаления) задачи необходимо удалить аппендер из кэша самостоятельно. Следующий тестовый код использует класс, запуская новые потоки в качестве задач: 

public static class Program
    {
        public static void Main(string[] args)
        {
            GlobalLogger.Log("Starting tasks.");
            new Thread(Run).Start();
            new Thread(Run).Start();
            Run();
            GlobalLogger.Log("Finished logging messages from tasks.");
            GlobalLogger.Log("A last message for task!");
        }
 
        private static void Run()
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            log4net.GlobalContext.Properties["ThreadId"] = threadId;
            Thread.Sleep(threadId * threadId);
            TaskLogger.Log(threadId, String.Format("Hello from Task {0}!", threadId));
            GlobalLogger.Log(String.Format("Global message from task {0}", threadId));
            TaskLogger.RemoveLogger(threadId);
        }
    }

Пара вспомогательных классов, используемых в коде: глобальный логгер для событий, не привязанных к конкретной задаче

internal static class GlobalLogger
    {
        private static readonly ILog CommonLogger;
 
        static GlobalLogger()
        {
            XmlConfigurator.Configure();
            CommonLogger = LogManager.GetLogger("TestLogger");
        }
 
        public static void Log(string message)
        {
            CommonLogger.Info(message);
        }
    }

...и наследник логфонетовского логгера:

internal class OurLogger : Logger
    {
        public OurLogger(string name)
            : base(name)
        {
        }
    }




Thursday, 12 June 2014

Unwrap Domain Exception

Здесь можно узнать о том, как правильно развернуть FaultException, пришедший к нам через канал WCF, и достать из него оригинальный DomainException.

http://maonet.wordpress.com/2010/02/10/unwrap-wcf-faultexception/

Tuesday, 3 June 2014

Пример синхронизации двух потоков с помощью AutoResetEvent на C#

В этом простом примере задача стояла так: с помощью двух потоков выводить последовательно буквы А и В, при этом

- один поток выводит только А, второй только В,
- буквы должны строго чередоваться
- первой должна быть выведена буква А

Это реализовано с помощью класса Tester, в котором запускаются два потока, синхронизированные с помощью двух соответствующих каждому потоку объектов AutoResetEvent.

Текст класса:

using System;
using System.Threading;
 
namespace AutoResetEvent
{
    /// <summary>
    /// A and B should be in ABAB order and A should be always typed first
    /// </summary>
    internal class Tester
    {
        private EventWaitHandle ev1 = new System.Threading.AutoResetEvent(false);
        private EventWaitHandle ev2 = new System.Threading.AutoResetEvent(false);
        private Thread thread1;
        private Thread thread2;
        private bool initialized = false;
        private object locker = new object();
 
        public void Start()
        {
            thread1 = new Thread(Worker1);
            ev1.Set();
            thread1.Start();
            thread2 = new Thread(Worker2);
            thread2.Start();
        }
 
        public void Stop()
        {
            thread1.Abort();
            thread2.Abort();
        }
 
        private void Worker1()
        {
            while (true)
            {
                ev1.WaitOne();
                Console.WriteLine("A");
                ev2.Set();
            }
        }
 
        private void Worker2()
        {
            while (true)
            {
                ev2.WaitOne();
                Console.WriteLine("B");
                ev1.Set();
            }
        }
    }
}

Wednesday, 23 April 2014

Открываем на чтение файл, открытый в другом потоке или процессе

Просто так он не открывается, придётся применить флаги:

FileStream configFile = new FileStream(engineConfigFilepath,
                    FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

С этими флагами всё должно работать, даже если файл открыт на редактирование где-то ещё.

Tuesday, 15 April 2014

WIX: проверяем установлен ли .NET Framework перед установкой приложения

Типовая задача: наше приложение разработано на основе .NET Framework 4, и инсталлятор должен проверить установлен ли фреймворк на машине перед началом установки приложения.

Следующие шаги описаны в документации WIX, информация есть и на stackoverflow

Для реализации задуманного в проекте инсталлятора надо добавить ссылку на сборку WixNetFxExtension.dll (у меня он расположен в c:\Program Files (x86)\WiX Toolset v3.8\bin\)

После чего в файл Product.wxs (если, конечно, наш главный файл проекта называется так) добавляем строку

<PropertyRef Id="NETFRAMEWORK40FULL" />

Это свойство доступно только в версии WIX  не ниже 3.5.

Затем на основе этого свойства формируем условие продолжения установки, задавая сообщение об ошибке: 

<Condition Message='This setup requires Microsoft .NET Framework 4.0 Full package or greater needs to be installed for this installation to continue.'>
  <![CDATA[Installed OR NETFRAMEWORK40FULL]]>
</Condition>

Осталось только найти машину без установленного четвёртого фреймворка и проверить наш новый инсталлятор. 

То же самое работает и с другими версиями фреймворка, соответствующие свойства расширения описаны по ссылке на документацию WIX.

Проект с этими изменениями доступен в моём Git-овском хранилище учебных проектов на BitBucket.


Monday, 14 April 2014

Unhandled Exception в .NET – Разбираемся что к чему.

В книге Juval Lowy "Programming WCF Services" я встретил следующее замечание:
"In traditional .NET programming , any unhandled exception (Except ThreadAbortException) immediately terminated the AppDomain (and thus, often the process) in which it occurred."
Потом в отзывах к книге автора упрекнули в незнании .NET, поэтому я решил разобраться прав ли автор хотя бы в этом утверждении, для чего решил не экспериментировать, а обратиться за разъяснениям к авторитетам:

Джеффри Рихтер пишет во втором издании "CLR via C#", что
"Обнаружив в процессе поток с необработанным исключением, CLR немедленно уничтожает этот поток."

То есть, по этому утверждению Рихтера, при возникновении необработанного исключения в отдельном потоке завершается только поток, в котором возникло исключение. Рушится ли домен? Завершается ли процесс? О каком исключении из правил для ThreadAbortException говорит Лёви?

В главе 21 второго издания Рихтера говорится о том, что происходит с потоками при выгрузке appDomain  с помощью метода Unload():

- потоки приостанавливаются
- во всех потоках, выполняющих (или тех, что в скором времени могут его выполнять) генерируется исключение ThreadAbortException, инициируя выполнение всех блоков finally.

- если этот экземпляр ThreadAbortException остался необработанным, исключение проглатывается, поток завершается, но приложению в целом разрешено продолжить работу.
Далее, цитирую:
Такое поведение отличается от стандартного, потому что в любых других ситуациях при возникновении необработанного исключения CLR уничтожает процесс.

Это уже более похоже на то, о чём говорит Лёви – исключение ThreadAbortException, генерируемое в потоках при выгрузке домена, не уничтожает процесс, даже не будучи перехваченным.

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

 

Tuesday, 11 March 2014

Курсоры в SQL Server

Этот пост – просто хранилище для кода, который я часто беру для реализации курсоров.

DECLARE @vin NVARCHAR(17)

DECLARE myCursor CURSOR
    FOR SELECT vin FROM VEHICLE
OPEN myCursor
FETCH NEXT FROM myCursor into @vin;
WHILE @@FETCH_STATUS = 0
    BEGIN
        print @vin
        FETCH NEXT FROM myCursor into @vin;
    END
CLOSE myCursor
DEALLOCATE myCursor

 

Определяем установочный каталог приложения

Существует, безусловно, несколько способов узнать куда именно установлена та или иная программа. Рассмотрим тот, который использует нативные функции MSI.

Воспользуемся функцией MsiGetComponentPath, определённой в компоненте MSI.dll. Нам понадобится Product Id Приложения, и Component Id какого-либо из его компонентов. Оба - гуиды, которые известны для некоторых приложений, и поддаются гуглёжке. Для других придётся вскрывать инсталлятор чем-то вроде Orca.

Импортируем нативный метод в C#-код: 

        [DllImport("MSI.DLL", CharSet = CharSet.Auto)]
        private static extern UInt32 MsiGetComponentPath(
            string szProduct,
            string szComponent,
            StringBuilder lpPathBuf,
            ref int pcchBuf);

Далее создаём метод-обёртку для более удобного использования: 

        public static string GetInstallationPath(string productId, string componentId)
        {
            string componentPath = GetComponentPath(productId, componentId);
            if (String.IsNullOrEmpty(componentPath))
            {
                return String.Empty;
            }
            return Path.GetDirectoryName(componentPath);
        }


Можем пользоваться: 

        private string path = GetInstallationPath(
            "{B7BC7C6F-9A4E-4973-BE84-ECA8E3427C97}",
            "{4093449C-7949-4E96-B071-2AADB06EFC12}");

Обращаем внимание на формат GUID - они должны быть именно такие, с фигурными скобками.

Saturday, 8 March 2014

Задача разузлования деталей на SQL

Задача приведена в учебнике Дейта "Введение в системы баз данных" под номером 4.2.

Допустим есть следующая таблица PART_STRUCTURE:

CREATE TABLE Study.dbo.PART_STRUCTURE
(MAJOR_P NVARCHAR(3),
MINOR_P NVARCHAR(3),
QTY INT,
PRIMARY KEY (MAJOR_P, MINOR_P))

INSERT INTO Study.dbo.PART_STRUCTURE
(MAJOR_P, MINOR_P, QTY)
values('P1', 'P2', 2),
('P1', 'P3', 4),
('P2', 'P3', 1),
('P2', 'P4', 3),
('P3', 'P5', 9),
('P4', 'P5', 8),
('P5', 'P6', 3)

Требуется написать на языке SQL программу для получения списка всех компонентов данной детали на всех имеющихся уровнях.

Поскольку по сути мы выполняем обход иерархической древовидной структуры, применим рекурсию:

Применим "Recursive Queries Using Common Table Expressions", поскольку в следующей статье на технете [1] встречаем выражение "Returning hierarchical data is a common use of recursive queries" – именно то, что нам надо для решения задачи.

Итак, CTE или Common Table Expressions – инструмент Transact-SQL, их базовое использование описано в [2]:

В итоге получилось следующее:

WITH DETAIL_CTE (MINOR_PARTS_NAMES)
AS(    SELECT MINOR_P FROM PART_STRUCTURE WHERE MAJOR_P='P1'
    UNION ALL
    SELECT MINOR_P FROM PART_STRUCTURE PS INNER JOIN DETAIL_CTE DC
    ON PS.MAJOR_P=DC.MINOR_PARTS_NAMES
)SELECT * FROM DETAIL_CTE


Этот запрос выдаёт состав деталей на каждом из уровней, но не выдаёт их количество, и, что хуже, не показывает на каком конкретно уровне находится деталь.

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

WITH DETAIL_CTE (MINOR_PARTS_NAMES, LEVEL)
AS(    SELECT MINOR_P, 1 as LEVEL FROM PART_STRUCTURE WHERE MAJOR_P='P1'
    UNION ALL
    SELECT MINOR_P, DC.LEVEL+1 FROM PART_STRUCTURE PS INNER JOIN DETAIL_CTE DC
    ON PS.MAJOR_P=DC.MINOR_PARTS_NAMES
)SELECT * FROM DETAIL_CTE


Первый подзапрос CTE задаёт начальное значение счётчика уровня, равное 1 (не ноль, т.к. считаем нулевым уровнем уровень самой детали, подвергаемой разузлованию)

На рекурсивном подъёме (или спуске?) мы инкрементируем это значение при каждом рекурсивном вызове, таким образом получая номер уровня.

Теперь мы видим какой тип детали находится на каком уровне. Добавим отображение количества деталей по каждому из уровней разузлования:

WITH DETAIL_CTE (LEVEL, MINOR_PARTS_NAMES, QTY)
AS(SELECT 1 as LEVEL, MINOR_P, QTY FROM PART_STRUCTURE WHERE MAJOR_P='P1'
UNION ALL
SELECT DC.LEVEL+1, MINOR_P, PS.QTY FROM PART_STRUCTURE PS INNER JOIN DETAIL_CTE DC
ON PS.MAJOR_P=DC.MINOR_PARTS_NAMES
)SELECT * FROM DETAIL_CTE WHERE LEVEL=1

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

LEVEL    MINOR_PARTS_NAMES    QTY
1                            P2                                  2
1                            P3                                  4
2                            P5                                  9
3                            P6                                  3
2                            P3                                  1
2                            P4                                  3
3                            P5                                  8
4                            P6                                  3
3                            P5                                  9
4                            P6                                  3

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

Ссылки:

[1] – Recursive Queries Using Common Table Expressions, Microsoft Technet article
[2] – CTEs, Microsoft Technet article