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