Home > Back-end >  Thread Safe file writer lock release issue
Thread Safe file writer lock release issue

Time:11-05

I am running a .Net Web API. I have below code for creating a log writer function which is thread safe and can be used inside Parallel.ForEach() function without causing any issues due to race conditions.

The problem I am facing is, with limiting the size of the Log file, and creating a new log file by backing up the old one. When the file size reaches max size defined in the code it is supposed to copy the existing file to Backup_currentfilename.log and recreate new log file as a fresh one. However, when it reaches the max size, it is creating the backup file successfully but when it tries to delete the old file, it is in use by the static constructor method:

    private static readonly string ApplicationLogFilePath = ConfigurationManager.AppSettings.Get("ApplicationLogFilePath");
    private static readonly string ApplicationLogFileName = ConfigurationManager.AppSettings.Get("ApplicationLogFileName");
    private static readonly long LogFile_MaxSizeInBytes = 5242880;
    private static readonly string Updated_ApplicationLogFilePath = ApplicationLogFilePath.EndsWith("\\") ? ApplicationLogFilePath : $"{ApplicationLogFilePath}\\";

    public class ThreadSafeLogger
    {
        private static readonly string TSLogFileName = ConfigurationManager.AppSettings.Get("TSLogFileName");
        private static readonly string TSLogFilePath = $"{Updated_ApplicationLogFilePath}{TSLogFileName}";

        static readonly TextWriter tw;

        static ThreadSafeLogger()
        {
            try
            {
                try
                {
                    FileInfo logFileInfo = new FileInfo(TSLogFilePath);

                    if (logFileInfo.FullName.EndsWith("\\") || string.IsNullOrEmpty(logFileInfo.Name) || !Regex.IsMatch(TSLogFilePath, @"[A-z]:[\\](?:\w [\\]) (\w \.(?:([tT][xX][tT]|[lL][oO][gG])))"))
                    {
                        // Give a correct file name
                        TSLogFilePath = $"{Updated_ApplicationLogFilePath}TSLog.log";
                    }
                }
                catch (Exception)
                {
                    // Give a correct file name
                    TSLogFilePath = $"{Updated_ApplicationLogFilePath}TSLog.log";
                }

                // Create a backup of existing log file if size is more than xxxx bytes
                try
                {
                    if (tw != null)
                    {
                        tw.Close();
                    }

                    try
                    {
                        FileInfo fileInfo = new FileInfo(TSLogFilePath);

                        // Move the file to circular log
                        if (fileInfo.Length > LogFile_MaxSizeInBytes)
                        {
                            File.Copy(TSLogFilePath, $"{Updated_ApplicationLogFilePath}BACKUP_{TSLogFileName}", true);
                            File.Delete(TSLogFilePath);
                        }

                        // Create new empty log file
                        if (!File.Exists(TSLogFilePath))
                        {
                            Directory.CreateDirectory(Updated_ApplicationLogFilePath);
                            File.CreateText(TSLogFilePath);
                        }
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine(ex.Message   " > GlobalHelper > ThreadSafeLogger()");
                    }
                }
                catch (Exception ex)
                {
                    // Ignore errors
                    Trace.WriteLine(ex.Message   " > GlobalHelper > ThreadSafeLogger()");
                }

                tw = TextWriter.Synchronized(File.AppendText(TSLogFilePath));
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message   " > GlobalHelper > ThreadSafeLogger()");
            }
        }

        ~ThreadSafeLogger()
        {
            try
            {
                tw.Close();
            }
            catch (Exception ex)
            {
                Trace.WriteLine($"Error in ~ThreadSafeLogger(): {ex.Message}");
            }
        }

        public static void WriteTSLog(string VerboseText, [CallerLineNumber] int LineNumber = 0, [CallerMemberName] string SourceFunction = null, [CallerFilePath] string FilePath = null)
        {
            DateTime TimeStamp = DateTime.Now;

            try
            {
                // Create a backup of existing log file if size is more than xxxx bytes
                try
                {
                    try
                    {
                        FileInfo fileInfo = new FileInfo(TSLogFilePath);

                        // Move the file to circular log
                        if (fileInfo.Length > LogFile_MaxSizeInBytes)
                        {
                            File.Copy(TSLogFilePath, $"{Updated_ApplicationLogFilePath}BACKUP_{TSLogFileName}", true);
                            File.Delete(TSLogFilePath);
                        }

                        // Create new empty log file
                        if (!File.Exists(TSLogFilePath))
                        {
                            Directory.CreateDirectory(Updated_ApplicationLogFilePath);
                            File.CreateText(TSLogFilePath);
                        }
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine(ex.Message   " > GlobalHelper > WriteTSLog()");
                    }
                }
                catch (Exception ex)
                {
                    // Ignore errors
                    Trace.WriteLine(ex.Message   " > GlobalHelper > WriteTSLog()");
                }
                
                // Extract filename from file path
                string FileName = "";
                try
                {
                    FileInfo fileInfo = new FileInfo(FilePath);

                    FileName = fileInfo.Name;
                }
                catch (Exception)
                {
                    FileName = FilePath;
                }

                TSLog($"[{TimeStamp:dd-MMM-yyyy HH:mm:ss.fff}][{LineNumber}][{FileName} > {SourceFunction}()]: {VerboseText}", tw);
            }
            catch (Exception ex)
            {
                try
                {
                    using (EventLog eventLog = new EventLog("Application"))
                    {
                        eventLog.Source = "Application";
                        eventLog.WriteEntry($"ThreadSafeLogger: failed to write to log file '{TSLogFilePath}'.{Environment.NewLine}Message:'[{TimeStamp:dd-MMM-yyyy HH:mm:ss.fff}][{LineNumber}][{FilePath} > {SourceFunction}()]: {VerboseText}'.{Environment.NewLine}Reason: {ex.Message}", EventLogEntryType.Error, 1111);
                    }
                }
                catch
                {
                    Trace.WriteLine(ErrorMessagePrefix   " > GlobalHelper > WriteTSLog");
                }
            }
        }

        private static readonly object _syncObject = new object();

        private static void TSLog(string LogMessage, TextWriter w)
        {
            try
            {
                lock (_syncObject)
                {
                    w.WriteLine(LogMessage);
                    w.Flush();
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine($"Error in TSLog(): {ex.Message}");
            }
        }
    }

The current log file always remains open inside the static method, and I can't perform circular logging unless I restart the application.

In above code I tried adding a destructor method which is never called (for obvious reason). How can I solve this problem?

CodePudding user response:

I would buffer the log writes instead of writing to disk directly. I.e Create a concurrentQueue that can be written to from any thread, and create one single thread that reads from the queue and does the writing to disk. This single thread can safely move the file, since it is the only one that has access to the file. This should also be faster, since each thread that wants to log do not have to compete for the single lock.

I would also not use a static methods to do the logging, rather create a class that does all the logging, and if you want global access, create a singleton instance of this class.

For example

private BlockingCollection<string> logQueue = new();
private Task logTask;
public void StartLogThread()
{
    logTask = Task.Factory.StartNew(WriteThreadMain, TaskCreationOptions.LongRunning);
}
public void WriteToLog(string message) => logQueue.Add(message);

private void WriteThreadMain()
{
    // Create file
    foreach (var message in logQueue.GetConsumingEnumerable())
    {
        // Write message
    }
    // Close file
}

public Task CloseLogThread()
{
    logQueue.CompleteAdding();
    return logTask;
}

Or just use a logging library that does all of this for you, that would likely be faster, more reliable, more flexible and better all around.

  • Related