В службе Windows Наблюдателя за файловой системой С# происходит утечка памяти, и невозможно отследить причину утечки памяти.

В течение последних нескольких дней я наблюдал за созданной мной службой Windows, так как был уверен, что в ней есть утечка памяти. Как оказалось, я прав — использование памяти увеличилось с 41 МБ до 75 МБ за последние несколько дней.

Все, что делает этот сервис, это просмотр каталога файлов, и каждый раз, когда файл создается там, он загружает его в нашу систему управления контентом (Microfocus Content Manager); также выполняются некоторые другие разные задачи, такие как запись в журнал событий при возникновении определенных исключений и запись сообщений о статусе загрузки в файл журнала.

Одна идея, которую я хотел бы попытаться использовать для поиска этой утечки, заключается в использовании чего-то вроде профилировщика .NET CLR, как предложено в ответ на этот вопрос. Однако профилировщик, похоже, не работает для служб Windows (поэтому мне как-то нужно изменить службу, которую я превратил в консольное приложение?), и я не уверен, что он может профилировать приложение во время его работы?

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

using HP.HPTRIM.SDK;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Configuration;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace BailiffReturnsUploader
{
    public partial class BailiffReturnsService : ServiceBase
    {
        private string FileWatchPath = ConfigurationManager.AppSettings["FileWatchPath"];
        private string UploadLogPath = ConfigurationManager.AppSettings["UploadLogPath"];
        private string UploadErrorLocation = ConfigurationManager.AppSettings["UploadErrorLocation"];
        private string ContentManagerEnviro = ConfigurationManager.AppSettings["ContentManagerEnviro"];

        public BailiffReturnsService()
        {
            InitializeComponent();
            bailiffEventLogger = new EventLog();
            if(!EventLog.SourceExists("BailiffReturnsSource"))
            {
                EventLog.CreateEventSource("BailiffReturnsSource", "BailiffReturnsLog");
            }
            bailiffEventLogger.Source = "BailiffReturnsSource";
            bailiffEventLogger.Log = "BailiffReturnsLog";
        }

        protected override void OnStart(string[] args)
        {
            try
            {
                TrimApplication.Initialize();

                BailiffReturnsFileWatcher = new FileSystemWatcher(FileWatchPath)
                {
                    NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Attributes,
                    Filter = "*.*"
                };
                // I think this is the problematic part, the event registration?
                BailiffReturnsFileWatcher.Created += new FileSystemEventHandler(OnCreate);
                BailiffReturnsFileWatcher.EnableRaisingEvents = true;

                bailiffEventLogger.WriteEntry("Service has started", EventLogEntryType.Information);
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(string.Format("Could not create file listener : {0}", ex.Message), EventLogEntryType.Error);
            }
        }

        protected override void OnStop()
        {
            bailiffEventLogger.WriteEntry("Service has stopped", EventLogEntryType.Information);
            Dispose();
        }

        /// <summary>
        /// Handler for file
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnCreate(object sender, FileSystemEventArgs e)
        {
            try
            {
                int attempts = 0;
                FileInfo fileInfo = new FileInfo(e.FullPath);

                while (IsFileLocked(fileInfo))
                {
                    attempts++;
                    CreateUploadLog(UploadLogPath, string.Format("Info : {0} is locked, trying again. Attempt #{1}.", fileInfo.Name, attempts));
                    if (attempts == 5)
                    {
                        CreateUploadLog(UploadLogPath, string.Format("Error : {0} is locked, could not access file within 5 attempts.", fileInfo.Name));
                        bailiffEventLogger.WriteEntry(string.Format("Error : {0} is locked, could not access file within 5 attempts.", fileInfo.Name), EventLogEntryType.Error);
                        break;
                    }
                    Thread.Sleep(1500);
                }

                bool isSaveSuccess = SaveToTrim(e.FullPath);
                if(isSaveSuccess)
                {
                    DeleteFile(e.FullPath);
                }
                else
                {
                    MoveFileToError(e.FullPath);
                }

                fileInfo = null;
                Dispose();
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(string.Format("Error while saving or deleting file : {0}", ex.Message), EventLogEntryType.Error);
            }
        }

        /// <summary>
        /// Attemps to upload file to content manager.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        bool SaveToTrim(string path)
        {
            string pathFileNoExt = Path.GetFileNameWithoutExtension(path);

            try
            {
                string[] pathArgs = pathFileNoExt.Split(new string[] { "_" }, StringSplitOptions.RemoveEmptyEntries);
                // Note for stack overflow: These classes and methods are part of an SDK provided by a 3rd party that I'm using to upload documents 
                // into their content management system. I'm not sure, but I don't think the memory leak is occuring at this part.
                
                using (Database dbCntMgr = new Database { Id = ContentManagerEnviro })
                {
                    
                    // Connect to the content manager database.
                    try
                    {
                        dbCntMgr.Connect();
                    }
                    catch (Exception ex)
                    {
                        bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Error);
                        CreateUploadLog(UploadLogPath, "Failed to connect to content manager.");
                        return false;
                    }

                    // Create the record based on record type, set default assignee and default bailiff type.
                    RecordType oRecordType = new RecordType(dbCntMgr, "Revenues - Bailiff");
                    Record oRecord = new Record(dbCntMgr, oRecordType);
                    oRecord.Assignee = new Location(dbCntMgr, "Bailiff Returns Pending");
                    oRecord.SetFieldValue(new FieldDefinition(dbCntMgr, "Bailiff Type"), new UserFieldValue("Liability Order Return"));

                    // Set the default container, not changed if no result is found.
                    Record oContainer;
                    oContainer = new Record(dbCntMgr, "014/1065/0973/55198");

                    // Search via property ID and "80" for Revenues Property File
                    TrimMainObjectSearch search = new TrimMainObjectSearch(dbCntMgr, BaseObjectTypes.Record);
                    TrimSearchClause trimSearchClause = new TrimSearchClause(dbCntMgr, BaseObjectTypes.Record, new FieldDefinition(dbCntMgr, "Property ID"));
                    trimSearchClause.SetCriteriaFromString(pathArgs[2].Substring(2));
                    search.AddSearchClause(trimSearchClause);
                    trimSearchClause = new TrimSearchClause(dbCntMgr, BaseObjectTypes.Record, SearchClauseIds.RecordType);
                    trimSearchClause.SetCriteriaFromString("80");
                    search.AddSearchClause(trimSearchClause);

                    // Sets the container to found record if any are found.
                    foreach (Record record in search)
                    {
                        //oContainer = new Record(dbCntMgr, record.Uri);
                        oContainer = record;
                    }

                    // Once container is finalised, set record container to located container.
                    oRecord.Container = oContainer;

                    // Set the title to name
                    oRecord.Title = pathArgs[3];

                    // Set the input document.
                    InputDocument oInputDocument = new InputDocument();
                    oInputDocument.SetAsFile(path);
                    oRecord.SetDocument(oInputDocument, false, false, "Created Via Bailiff Content Manager Uploader service.");

                    // Save if valid, print error if not.
                    if (oRecord.Verify(false))
                    {
                        oRecord.Save();
                        CreateUploadLog(UploadLogPath, string.Format("File uploaded : {0}", Path.GetFileNameWithoutExtension(path)));
                        return true;
                    }
                    else
                    {
                        CreateUploadLog(UploadLogPath, string.Format("Upload of {0} file attempt did not meet validation criteria. Not uploaded.", Path.GetFileNameWithoutExtension(path)));
                        return false;
                    }
                }
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Error);
                return false;
            }
        }

        /// <summary>
        /// Deletes file when successfully uploaded.
        /// </summary>
        /// <param name="path"></param>
        void DeleteFile(string path)
        {
            try
            {
                string pathFileNoExt = Path.GetFileNameWithoutExtension(path);

                // If file exists, delete.
                if (File.Exists(path))
                {
                    File.Delete(path);
                    CreateUploadLog(UploadLogPath, string.Format("File deleted from Upload folder : {0}", pathFileNoExt));
                }
                else
                {
                    CreateUploadLog(UploadLogPath, string.Format("Error deleting file from upload folder : {0}", pathFileNoExt));
                }
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Warning);
                CreateUploadLog(UploadLogPath, ex.Message);
            }
        }

        /// <summary>
        /// Moves non uploaded files (failed to upload) to an error location as specified in the app config.
        /// </summary>
        /// <param name="path"></param>
        void MoveFileToError(string path)
        {
            try
            {
                string pathFileNoExt = Path.GetFileName(path);

                // If directory and file exist, attempt move.
                if(Directory.Exists(UploadErrorLocation))
                {
                    if(File.Exists(path))
                    {
                        if(File.Exists(Path.Combine(UploadErrorLocation, pathFileNoExt)))
                        {
                            File.Delete(Path.Combine(UploadErrorLocation, pathFileNoExt));
                        }

                        File.Move(path, Path.Combine(UploadErrorLocation, pathFileNoExt));
                    } else
                    {
                        CreateUploadLog(UploadLogPath, "Could not move non-uploaded file to error location");
                    }
                } else
                {
                    CreateUploadLog(UploadLogPath, "Could not move non-uploaded file to error location, does the error folder exist?");
                }

            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry("Error while moving file to error location : " + ex.Message, EventLogEntryType.Warning);
                CreateUploadLog(UploadLogPath, ex.Message);
            }
        }

        /// <summary>
        /// Takes full path of upload log path and a message to add to the upload log. Upload log location is specified in the app config.
        /// </summary>
        /// <param name="fullPath"></param>
        /// <param name="message"></param>
        private void CreateUploadLog(string fullPath, string message)
        {
            try
            {
                //StreamWriter streamWriter;

                // If file does not exist, create.
                if (!File.Exists(Path.Combine(fullPath, "UploadLog_" + DateTime.Now.ToString("ddMMyyyy") + ".txt")))
                {
                    using (StreamWriter streamWriter = File.CreateText(Path.Combine(fullPath, "UploadLog_" + DateTime.Now.ToString("ddMMyyyy") + ".txt")))
                    {
                        streamWriter.Close();
                    }
                }
                // Append text to file.
                using (StreamWriter streamWriter = File.AppendText(Path.Combine(fullPath, "UploadLog_" + DateTime.Now.ToString("ddMMyyyy") + ".txt")))
                {
                    streamWriter.WriteLine(string.Format("{0} -- {1}", DateTime.Now.ToString(), message));
                    streamWriter.Close();
                }
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Warning);
            }
        }

        /// <summary>
        /// Attempts to access the file, returns true if file is locked, false if file is not locked.
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        private bool IsFileLocked(FileInfo file)
        {
            try
            {
                using(FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None))
                {
                    stream.Close();
                }
            }
            catch(IOException)
            {
                return true;
            }

            return false;
        }
    }
}

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

  1. Регистрация/поднятие события. После нескольких поисков и хорошего часа или двух экспериментов у меня появилось подозрение, что именно создание и делегирование события вызывает утечку памяти. Что-то связанное с тем, чтобы не отменять регистрацию или удалять событие после его завершения?
  2. Сторонний SDK, который выполняет взаимодействие/загрузку, содержит утечку. Это может быть возможно, если в ответах будет указано, что это является причиной, тогда я буду решать эту проблему с сопровождающими SDK. Я несколько уверен, что это не причина утечки, так как SDK содержит режим отладки, который можно включить с помощью метода, который выводит любые неутилизированные объекты в журнал событий, попробовав это, он не показал никаких объектов. не утилизируется.
  3. Запись файла журнала загрузки. Может ли быть причиной процесс записи в файл с событиями загрузки и т. д.? У меня нет большой уверенности в том, что это утечка памяти, так как операторы using используются для стримера, но может быть?

Что ты думаешь?


person Jake H    schedule 13.08.2020    source источник
comment
Как правило, при работе со службами я всегда начинаю с проекта консольного приложения, а после отладки и тестирования использую проверенный код в приложении проекта службы. Службы трудно отлаживать.   -  person aamartin2k    schedule 22.09.2020
comment
После быстрого прочтения вашего кода сосредоточьтесь на методе OnCreate. Вы используете цикл while, чтобы проверить, заблокирован ли файл, увеличить счетчик попыток и подождать. Если файл не заблокирован, выполняется строка: bool isSaveSuccess = SaveToTrim(e.FullPath);. Если файл заблокирован после 5 попыток, цикл прерывается и выполняется строка: bool isSaveSuccess = SaveToTrim(e.FullPath);. Если SaveToTrim нужен разблокированный доступ к файлу, это неправильно.   -  person aamartin2k    schedule 22.09.2020
comment
@ aamartin2k Спасибо за ответ, я действительно выделил это в консольный проект, чтобы провести дополнительное тестирование, и благодаря этому я почти уверен, что проблема связана со сторонним SDK, который мне нужно использовать для этого. И да, я намерен запустить метод SaveToTrim() после 5-й неудачной попытки - в этом случае он пытается прочитать файл и просто регистрирует ошибку блокировки файла, задержка предназначена для того, чтобы попытаться уменьшить вероятность этого.   -  person Jake H    schedule 23.09.2020