{"id":941,"date":"2024-06-25T11:19:58","date_gmt":"2024-06-25T09:19:58","guid":{"rendered":"https:\/\/blog.adameczek.pl\/?p=941"},"modified":"2024-06-26T13:32:12","modified_gmt":"2024-06-26T11:32:12","slug":"chyba-cos-wpadlo-nowego-czyli-nasluchiwanie-na-nowe-pliki-przez-kontener","status":"publish","type":"post","link":"https:\/\/blog.adameczek.pl\/index.php\/2024\/06\/25\/chyba-cos-wpadlo-nowego-czyli-nasluchiwanie-na-nowe-pliki-przez-kontener\/","title":{"rendered":"Widz\u0119 ciemno\u015b\u0107! Czyli aplikacja w kontenerze nas\u0142uchuje na nowe pliki"},"content":{"rendered":"\n<p><\/p>\n\n\n\n<p>Czasem aplikacja ma reagowa\u0107 na zmiany zachodz\u0105ce w systemie plik\u00f3w aby, np. przetwarza\u0107 nowe pliki, kt\u00f3re w\u0142a\u015bnie kto\u015b do niego doda\u0142. Nadaje si\u0119 do tego \u015bwietnie klasa <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.io.filesystemwatcher?view=net-8.0\">FileSystemWatcher<\/a> (FSW). Dzia\u0142a w Windows i pod przy odpowiedniej konfiguracji tak\u017ce pod Linuxem. Ale s\u0142odycz si\u0119 ko\u0144czy po skonteneryzowaniu aplikacji. Trafi\u0142 mi si\u0119 w\u0142a\u015bnie taki przypadek. Kontener nie dostaje notyfikacji o zmianach w systemie plik\u00f3w woluminu zamontowanego do kontenera. Zupe\u0142na cisza, albo cytuj\u0105c klasyka \u201eCiemno\u015b\u0107, widz\u0119 ciemno\u015b\u0107!\u201d<\/p>\n\n\n\n<p>Przyczyn\u0105 tego stanu rzeczy s\u0105 r\u00f3\u017cne mechanizmy generowania i transmisji zdarze\u0144 o zmianie w systemie plik\u00f3w przez r\u00f3\u017cne OS, systemy plik\u00f3w, protoko\u0142y sieciowe. FileSystemWatcher instancjonuje obiekt inotify dla ka\u017cdego \u015bledzonego folderu, kt\u00f3ry nas\u0142uchuje na zdarzenia generowane przez system plik\u00f3w. Je\u015bli jednak nast\u0105pi b\u0142\u0105d sieci, to zdarzenie nie dotrze. Je\u015bli przepe\u0142ni si\u0119 bufor zdarze\u0144, to zdarzenie nie dotrze, Je\u015bli system plik\u00f3w nie potrafi w spos\u00f3b powtarzalny wys\u0142a\u0107 powiadomie\u0144, to one nie b\u0119d\u0105 dociera\u0142y. Tych je\u017celi\u2026 jest sporo i zawsze ko\u0144cz\u0105 si\u0119  tym, \u017ce zdarzenie nie dotrze. FSW sprawdza si\u0119 w zasadzie jedynie dla dysk\u00f3w lokalnych i to najlepiej pod Windows.<\/p>\n\n\n\n<p>A tymczasem w folderze jest coraz wi\u0119cej plik\u00f3w, a aplikacja nie reaguje. Co robi\u0107? Pooling udzia\u0142u sieciowego? Nic innego nie pozostaje.<\/p>\n\n\n\n<p>Mo\u017cna do tego zastosowa\u0107 <em>PhysicalFileProvider<\/em>. W odr\u00f3\u017cnieniu od FSW, nie pozwala on na wyb\u00f3r rodzaju zdarze\u0144, kt\u00f3re notyfikuje, ale powinien wystarczy\u0107 przynajmniej jako \u017ar\u00f3d\u0142o informacji, \u017ce \u201eco\u015b si\u0119 zmieni\u0142o\u201d. Po prostu zostanie ustawiony cykliczny pooling folderu. Nie ma innego wyj\u015bcia w Dockerze. Sami b\u0119dziemy musieli dowiedzie\u0107 si\u0119, jak zmiana zasz\u0142a. Jedynie co wiadomo, to \u017ce zasz\u0142a. Ale dobre i to.<\/p>\n\n\n\n<p>Za to wykonamy porz\u0105dn\u0105 implementacj\u0119 obs\u0142ugi kolejki zdarze\u0144. Przecie\u017c mo\u017ce si\u0119 zdarzy\u0107, \u017ce zdarzenia o nowych plikach b\u0119d\u0105 sp\u0142ywa\u0142y szybciej ni\u017c trwa ich obs\u0142uga. Wykorzystamy generyczn\u0105 <em>BlockingCollection <\/em>do jednoczesnego dodawania i konsumowania zdarze\u0144. To kolejka FIFO, kt\u00f3ra jest bezpieczna dla wielu w\u0105tk\u00f3w (thread-safe). Jeden w\u0105tek mo\u017ce dodawa\u0107 zdarzenia informuj\u0105ce o nowych plikach, a inny obs\u0142ugiwa\u0107 te zdarzenia. Nasz DirectoryWatcher ma nast\u0119puj\u0105cy interfejs:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-file=\"IDirectoryWatcher.cs\" data-lang=\"C#\"><code>public interface IDirectoryWatcher\n{\n    void RegisterCallback(Action<string> callback);\n    Task StartWatching(CancellationToken cancellationToken);\n    void Dispose();\n}<\/code><\/pre><\/div>\n\n\n\n<p>Po zainstancjonowaniu <em>DirectoryWatchera <\/em>rejestruje si\u0119 callback. Jest on wo\u0142any w momencie obs\u0142ugi zdarzenia pobieranego z kolejki. W przyk\u0142adzie callbackiem jest medoda synchroniczna. Jest ona wo\u0142ana w osobnym w\u0105tku, tym samym kt\u00f3ry obs\u0142uguje kolejk\u0119. Obs\u0142uga callbacka nie nie blokuje dzia\u0142ania pozosta\u0142ego kodu. W razie potrzeby mo\u017cna \u0142atwo zmieni\u0107 kod, aby wywo\u0142anie by\u0142o asynchroniczne.<\/p>\n\n\n\n<p>Potem wystarczy uruchomi\u0107 <em>DirectoryWatcher<\/em> wywo\u0142uj\u0105c <em>StartWatching<\/em>. Metoda wygl\u0105da tak:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>public async Task StartWatching(CancellationToken ct = default)\n{\n    _fileSystemEventBuffer = new BlockingCollection<IFileInfo>(EventBufferSize);\n\n    CreateFileWatcher();\n    _fileWatcherCallback = WatchForFileChanges();\n\n    var processBuffer = Task.Run(() =>\n    {\n        foreach (var fileInfo in _fileSystemEventBuffer.GetConsumingEnumerable(ct))\n        {\n            _callback?.Invoke(fileInfo.PhysicalPath!);\n            LogWhenBufferIsEmpty();\n        }\n    }, ct);\n\n    \/\/ Implement resilience to transient IO errors\n    _files = GetFiles();\n    BufferFiles();\n\n    await processBuffer.ConfigureAwait(false);\n}<\/code><\/pre><\/div>\n\n\n\n<p>Deklaracje zmiennych i sta\u0142ych na razie pomin\u0119. Zostan\u0105 pokazane w dalszej cz\u0119\u015bci. Podobnie metody <em>CreateFileWatcher()<\/em> i <em>WatchForFileChanges()<\/em>. Teraz opisz\u0119 dzia\u0142anie typu <em>BlockingCollection<\/em>. Tworzymy bufor <code>new BlockingCollection(EventBufferSize)<\/code>. W linii 8 uruchamiany jest w\u0105tek, kt\u00f3ry pobiera z kolejki informacje o nowych plikach i wywo\u0142uje callback. Kluczowe tu jest, \u017ce p\u0119tla si\u0119 \u201enie ko\u0144czy\u201d. Po skonsumowaniu wszyskich zdarze\u0144 zostaje \u201eu\u015bpiona\u201d i wznawia dzia\u0142anie kiedy do kolekcji wpadn\u0105 nowe elementy. A mog\u0105 one by\u0107 dodawane w ka\u017cdym momencie w innym w\u0105tku. Mo\u017cemy zatem spokojnie w swoim tempie obs\u0142ugiwa\u0107 zdarzenia nie martwi\u0105c si\u0119 o zablokowanie mo\u017cliwo\u015bci ich dodawania. W linii 18 pobieramy informacje o plikach, bo na starcie programu zak\u0142adamy, \u017ce wszystkie pliki s\u0105 nowe. Definicja metody GetFiles():<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>private List<IFileInfo> GetFiles() => _fileWatcher!.GetDirectoryContents(string.Empty).ToList();<\/code><\/pre><\/div>\n\n\n\n<p>Nast\u0119pnie wszyskie dane wrzucamy do bufora (linia 19). Kiedy tylko zostanie dodany pierwszy rekord, wznawia prac\u0119 p\u0119tla foreach z linii 10 i wywo\u0142ywany jest callback.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>private void BufferFiles()\n{\n    foreach (var fileInfo in _files!)\n    {\n        if (!_fileSystemEventBuffer!.TryAdd(fileInfo))\n            Console.WriteLine($&quot;Buffer size exceeded ({EventBufferSize}) or buffer is disposed&quot;);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p>I do by by\u0142o na tyle, je\u015bli idzie o mechanizm bufora. Wielow\u0105tkowo dodajemy do niego i konsumujemy obiekty. Dla reszty kodu dzia\u0142anie \u201eg\u0142\u00f3wnej p\u0119tli\u201d odbywa si\u0119 \u201ew tle\u201d nie wp\u0142ywaj\u0105c na prac\u0119 innych w\u0105tk\u00f3w. Nie jest to w\u0105tek t\u0142a, ale dzi\u0119ki asynchroniczno\u015bci i bezpiecznemu dla w\u0105tk\u00f3w api <em>BlockingCollection <\/em>mo\u017cemy tak roboczo przyj\u0105\u0107.<\/p>\n\n\n\n<p>Do om\u00f3wienia zosta\u0142 mechanizm notyfikacji o nowych plikach w folderze. Na pocz\u0105tek metoda <em>CreateFileWatcher()<\/em> wo\u0142ana w linii 5 metody <em>StartWatching()<\/em>. Instancjonujemy <em>PhysicalFileProvider<\/em>, kt\u00f3ry nie b\u0119dzie bra\u0142 pod uwag\u0119 plik\u00f3w ukrytych (tak\u017ce z kropk\u0105) ani systemowych. B\u0119dzie za to cyklicznie si\u0119ga\u0142 do folderu na okoliczno\u015b\u0107 wykrycia zmian w systemie plik\u00f3w. Nic specjalnego.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>private void CreateFileWatcher()\n{\n    _fileWatcher = new PhysicalFileProvider(_directoryToWatch, ExclusionFilters.Sensitive)\n    {\n        UsePollingFileWatcher = true,\n        UseActivePolling = true\n    };\n}<\/code><\/pre><\/div>\n\n\n\n<p>Za to metoda <em>WatchForFileChanges()<\/em> a w\u0142a\u015bciwie to co ona zapocz\u0105tkowuje jest bardziej interesuj\u0105ce.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>private static ulong _changeLevel;\nprivate IChangeToken? _changeToken;\nprivate IDisposable? _fileWatcherCallback;\n\nprivate IDisposable WatchForFileChanges()\n{\n    _changeToken = _fileWatcher!.Watch(&quot;**\/*.*&quot;);\n    return _changeToken.RegisterChangeCallback(_ => NotifyFileChange(), default);\n}\n\nprivate void NotifyFileChange()\n{\n    Console.WriteLine(&quot;Directory has changed. Callback invoked&quot;);\n\n    _fileWatcherCallback = WatchForFileChanges();\n\n    if (0 == Interlocked.CompareExchange(ref _changeLevel, 1, 0))\n        BufferNewFiles();\n    else\n        Interlocked.Exchange(ref _changeLevel, 2);\n}<\/code><\/pre><\/div>\n\n\n\n<p>W linii 7 wskazujemy jakie pliki nas interesuj\u0105 u\u017cywaj\u0105c filtra. Metoda <em>Watch()<\/em> zwraca <em>IChangeToken<\/em>, kt\u00f3ry b\u0119dzie notyfikowany o zmianach \u015bledzonych plik\u00f3w. Nast\u0119pnie temu tokenowi wskazujemy callback jaki ma wywo\u0142a\u0107 w wyniku notyfikacji (<em>NotifyFileChange<\/em>). W moich pr\u00f3bach po jednorazowej notyfikacji <em>IChangeToken<\/em> przestawa\u0142 by\u0107 u\u017cyteczny. Albo przestawa\u0142 by\u0107 notyfikowany o zmianach, albo rejestacja callbacka wygasa\u0142a. W ka\u017cdym razie w linii 15 ponownie wo\u0142am metod\u0119 <em>WatchForFileChanges()<\/em>, aby notyfikacje zn\u00f3w dzia\u0142a\u0142y. A chcemy, \u017ceby dzia\u0142a\u0142y nieustannie nas\u0142uchuj\u0105c na nowe pliki.<\/p>\n\n\n\n<p>Teraz robi si\u0119 ciekawiej. Trzeba jako\u015b ogarn\u0105\u0107 r\u00f3\u017cne stany w jakich mo\u017ce si\u0119 znale\u017a\u0107 <em>Watcher<\/em>. Wyr\u00f3\u017cni\u0142em trzy stany:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>0 \u2013 nie trwa proces buforowania (dodawania zmian do kolekcji), brak notyfikacji o zmianach<\/li>\n\n\n\n<li>1 \u2013 trwa proces buforowania, brak notyfikacji o zmianach, <\/li>\n\n\n\n<li>2 \u2013 trwa proces buforowania i nadesz\u0142a notyfikacja o zmianach<\/li>\n<\/ul>\n\n\n\n<p>Do przechowywania informacji, w jakim stanie jest <em>Watcher<\/em>, wykorzysta\u0142em zmienn\u0105 statyczn\u0105, do kt\u00f3rej dost\u0119p uzyskuj\u0119 przez klas\u0119 <em>Interlocked<\/em>. Ta klasa zapewnia dost\u0119p do zmiennej w spos\u00f3b bezpieczny z r\u00f3\u017cnych w\u0105tk\u00f3w (<em>thread-safe<\/em>). <\/p>\n\n\n\n<p>W metodzie <em>NotifyFileChange() <\/em>je\u015bli mamy stan 0, to buforujemy nowe pliki i nast\u0119puje zmiana stanu na 1. W przeciwnych przypadku ustawiamy stan na 2. Co si\u0119 dzieje dalej, pokazuj\u0105 kolejne listingi.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>private void BufferNewFiles()\n{\n    do\n    {\n        Interlocked.CompareExchange(ref _changeLevel, 1, 2);\n        \n        var filesActual = GetFiles();\n        var newFiles = filesActual.ExceptBy(_files!.Select(f => f.Name), fi => fi.Name);\n        _files = filesActual;\n\n        foreach (var fileInfo in newFiles)\n        {\n            if (!_fileSystemEventBuffer?.TryAdd(fileInfo) ?? false)\n                Console.WriteLine($&quot;Buffer size exceeded ({EventBufferSize}) or buffer is disposed&quot;);\n        }\n\n    } while (1 < Interlocked.Read(ref _changeLevel));\n\n    Interlocked.Exchange(ref _changeLevel, 0);\n}<\/code><\/pre><\/div>\n\n\n\n<p>Metoda <em>BufferNewFiles()<\/em> na pocz\u0105tek zmienia stan na 1 \u201etrwa proces buforowania\u201d. Pobiera aktualn\u0105 list\u0119 plik\u00f3w w folderze, por\u00f3wnuje z zachowan\u0105 w pami\u0119ci i nowe pliki dodaje do bufora. Zastosowa\u0142em proste por\u00f3wnanie nazw plik\u00f3w, co nie zawsze musi by\u0107 jednoznaczne. Gdyby\u015bmy chcieli wykrywa\u0107 zmian\u0119 plik\u00f3w (a nie dodanie nowych), to mo\u017cna by liczy\u0107 hash zawarto\u015bci i przechowywa\u0107 go jako warto\u015b\u0107 w s\u0142owniku, a znormalizowan\u0105 nazw\u0119 (albo jej hash) jako klucz. Ale w tym wpisie skupiam si\u0119 na funkcji bezpiecznego i skutecznego notyfikowania o zmianach w folderze na poziomie systemu plik\u00f3w.<\/p>\n\n\n\n<p>Po dodaniu plik\u00f3w do bufora, sprawdzana jest aktualna warto\u015b\u0107 <em>_changeLevel<\/em>, Je\u015bli jest 2, czyli w trakcie aktualizacji bufora pojawi\u0142a si\u0119 notyfikacja o zmianach, powtarzamy proces buforowania. Je\u015bli nie, to ko\u0144czy si\u0119 proces buforowania a stan przyjmuje warto\u015b\u0107 0. Watcher oczekuje na notyfikacj\u0119.<\/p>\n\n\n\n<p>W praktyce zdarzaj\u0105 si\u0119 sytuacje, \u017ce notyfikacja nie nadchodzi. Pisa\u0142em o tym na pocz\u0105tku. Mo\u017cna przyj\u0105\u0107 dwie strategie wobec takich przypak\u00f3w. Albo czeka si\u0119 na nast\u0119pn\u0105 notyfikacj\u0119, po kt\u00f3rej i tak badana jest zawarto\u015b\u0107 folderu, wi\u0119c nowy plik nie umknie. Ale je\u017celi za\u0142o\u017cenie biznesowe jest takie, \u017ce nowe pliki mog\u0105 si\u0119 pojawia\u0107 w takich du\u017cych odst\u0119pach, \u017ce nie mo\u017cna sobie pozwoli\u0107 na oczekiwanie z przetworzeniem pomini\u0119tego pliku, to trzeba znale\u017a\u0107 inne rozwi\u0105zanie. Ja przyj\u0105\u0142em drugi scenariusz. Timer co jaki\u015b czas sprawdza folder nawet jak notyfikacja nie nadejdzie. Natomiast robi to w du\u017cych interwa\u0142ach i jego praca jest wstrzymywana, je\u015bli pliki s\u0105 buforowane (<code>_changeLevel != 0<\/code>). Timer jest widoczny poni\u017cej na pe\u0142nym listu Watchera. <\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-file=\"DirectoryWatcherWithPolling.cs\" data-lang=\"C#\"><code>using System.Collections.Concurrent;\nusing Microsoft.Extensions.FileProviders;\nusing Microsoft.Extensions.FileProviders.Physical;\nusing Microsoft.Extensions.Primitives;\n\nnamespace DirectoryWatcher.DirectoryWatcher;\n\npublic class DirectoryWatcherWithPolling : IDirectoryWatcher, IDisposable\n{\n    private const int EventBufferSize = 100000;\n    private const int DirectoryPollingInterval = 300;\n    \n    private readonly string _directoryToWatch;\n    private static ulong _changeLevel;\n    private Action<string>? _callback;\n    private IEnumerable<IFileInfo>? _files;\n    private BlockingCollection<IFileInfo>? _fileSystemEventBuffer;\n    private PhysicalFileProvider? _fileWatcher;\n    private IChangeToken? _changeToken;\n    private IDisposable? _fileWatcherCallback;\n    private PeriodicTimer? _timer;\n\n    public DirectoryWatcherWithPolling(string directoryToWatch)\n    {\n        if (string.IsNullOrEmpty(directoryToWatch) || !Directory.Exists(directoryToWatch))\n            throw new ArgumentException(&quot;Directory can not be empty string&quot;);\n        \n        _directoryToWatch = directoryToWatch;\n    }\n\n    public void RegisterCallback(Action<string>? callback) => _callback = callback;\n    \n    public async Task StartWatching(Action<string>? callback, CancellationToken ct = default)\n    {\n        RegisterCallback(callback);\n        await StartWatching(ct);\n    }\n\n    public async Task StartWatching(CancellationToken ct = default)\n    {\n        try\n        {\n            _timer = new PeriodicTimer(TimeSpan.FromSeconds(DirectoryPollingInterval));\n            _fileSystemEventBuffer = new BlockingCollection<IFileInfo>(EventBufferSize);\n\n            CreateFileWatcher();\n            _fileWatcherCallback = WatchForFileChanges();\n\n            var processBuffer = Task.Run(() =>\n            {\n                foreach (var fileInfo in _fileSystemEventBuffer.GetConsumingEnumerable(ct))\n                {\n                    _callback?.Invoke(fileInfo.PhysicalPath!);\n                    LogWhenBufferIsEmpty();\n                }\n            }, ct);\n\n            \/\/ TODO Implement resilience policy\n            _files = GetFiles();\n            BufferFiles();\n\n            while (await _timer.WaitForNextTickAsync(ct))\n            {\n                if (Interlocked.Read(ref _changeLevel) != 0) continue;\n\n                _fileWatcher?.Dispose();\n                Console.WriteLine(&quot;Directory polling upon timer&quot;);\n\n                BufferNewFiles();\n                CreateFileWatcher();\n                _fileWatcherCallback = WatchForFileChanges();\n            }\n\n            await processBuffer.ConfigureAwait(false);\n        }\n        catch (OperationCanceledException)\n        {\n            \/\/ ignore\n        }\n        finally\n        {\n            Dispose();\n        }\n    }\n\n    private void LogWhenBufferIsEmpty()\n    {\n        if (_fileSystemEventBuffer?.Count == 0)\n            Console.WriteLine(&quot;Synchronisation buffer is empty&quot;);\n    }\n\n    private List<IFileInfo> GetFiles() =>\n        _fileWatcher!.GetDirectoryContents(string.Empty).ToList();\n\n    private void CreateFileWatcher()\n    {\n        \/\/ TODO Implement resilience policy\n        _fileWatcher = new PhysicalFileProvider(_directoryToWatch, ExclusionFilters.Sensitive)\n        {\n            UsePollingFileWatcher = true,\n            UseActivePolling = true\n        };\n    }\n\n    private IDisposable WatchForFileChanges()\n    {\n        _changeToken = _fileWatcher!.Watch(&quot;**\/*.*&quot;);\n        return _changeToken.RegisterChangeCallback(_ => NotifyFileChange(), default);\n    }\n\n    private void NotifyFileChange()\n    {\n        Console.WriteLine(&quot;Directory has changed. Callback invoked&quot;);\n\n        _fileWatcherCallback = WatchForFileChanges();\n\n        if (0 == Interlocked.CompareExchange(ref _changeLevel, 1, 0))\n            BufferNewFiles();\n        else\n            Interlocked.Exchange(ref _changeLevel, 2);\n    }\n    \n    private void BufferFiles()\n    {\n        foreach (var fileInfo in _files!)\n        {\n            if (!_fileSystemEventBuffer!.TryAdd(fileInfo))\n                Console.WriteLine($&quot;Buffer size exceeded ({EventBufferSize}) or buffer is disposed&quot;);\n        }\n    }\n    \n    private void BufferNewFiles()\n    {\n        do\n        {\n            Interlocked.CompareExchange(ref _changeLevel, 1, 2);\n            \n            \/\/ TODO Implement resilience policy\n            var filesActual = GetFiles();\n            var newFiles = filesActual.ExceptBy(_files!.Select(f => f.Name), fi => fi.Name);\n            _files = filesActual;\n\n            foreach (var fileInfo in newFiles)\n            {\n                if (!_fileSystemEventBuffer?.TryAdd(fileInfo) ?? false)\n                    Console.WriteLine($&quot;Buffer size exceeded ({EventBufferSize}) or buffer is disposed&quot;);\n            }\n\n        } while (1 < Interlocked.Read(ref _changeLevel));\n\n        Interlocked.Exchange(ref _changeLevel, 0);\n    }\n\n    private bool _isDisposed;\n    public void Dispose() => Dispose(true);\n    private void Dispose(bool disposing)\n    {\n        if (_isDisposed) return;\n\n        if (disposing)\n        {\n            _fileWatcherCallback?.Dispose();\n            _fileSystemEventBuffer?.Dispose();\n            _timer?.Dispose();\n        }\n        _isDisposed = true;\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Uwagi na koniec. Skupi\u0142em si\u0119 na pokazaniu, jak sprawnie nas\u0142uchiwa\u0107 na zmiany w systemie plik\u00f3w przez aplikacj\u0119 dzia\u0142aj\u0105c\u0105 w kontenerze Dockera, gdzie nie zadzia\u0142a <em>FileSystemWatcher<\/em>. Zaprezentowany mechanizm dzia\u0142a bardzo dobrze. W produkcji klasa <em>DirectoryWatcherWithPolling <\/em>jest instancjonowana z kontenera DI. Wstrzykiwany jest logger (zamiast wyj\u015bcia na konsol\u0119), polityki odporno\u015bci na b\u0142\u0119dy IO (ka\u017cde odwo\u0142anie do dysku jest \u201euodpornione\u201d na chwilowe b\u0142\u0119dy IO) i ustawienia.<\/p>\n<\/blockquote>\n\n\n\n<p>Na <a href=\"https:\/\/github.com\/madameczek\/directorywatcher\">Githubie <\/a>mo\u017cna zapozna\u0107 si\u0119 z przyk\u0142adow\u0105 aplikacj\u0105, wykorzystuj\u0105c\u0105 <em>DirectoryWatcher<\/em>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Kiedy chcesz, aby skonteneryzowana aplikacja nas\u0142uchiwa\u0142a na zmiany w systemie plik\u00f3w, sprawy nieco si\u0119 komplikuj\u0105.<\/p>\n","protected":false},"author":1,"featured_media":822,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"0","ocean_second_sidebar":"0","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"0","ocean_custom_header_template":"0","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"0","ocean_menu_typo_font_family":"0","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":"0","_jetpack_memberships_contains_paid_content":false,"ocean_post_oembed":"","ocean_post_self_hosted_media":"","ocean_post_video_embed":"","ocean_link_format":"","ocean_link_format_target":"self","ocean_quote_format":"","ocean_quote_format_link":"post","ocean_gallery_link_images":"off","ocean_gallery_id":[],"footnotes":""},"categories":[10,21,12],"tags":[19,15,23,18],"class_list":["post-941","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net","category-c","category-kubernetes","tag-async-await","tag-csharp","tag-docker","tag-net","entry","has-media"],"jetpack_featured_media_url":"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/Csharp_logo.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/941","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/comments?post=941"}],"version-history":[{"count":36,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/941\/revisions"}],"predecessor-version":[{"id":1007,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/941\/revisions\/1007"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media\/822"}],"wp:attachment":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media?parent=941"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/categories?post=941"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/tags?post=941"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}