Skip to content

Commit

Permalink
Replace file system watcher with polling
Browse files Browse the repository at this point in the history
  • Loading branch information
z4kn4fein committed Apr 5, 2022
1 parent ddb18a0 commit 90a715a
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 53 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### 6.5.0
- Replace `FileSystemWatcher` with file polling in local file override data source.

### 6.4.12
- Fix various local file override data source issues.

### 6.4.9
- Move the PollingMode option to public scope.

Expand Down
2 changes: 1 addition & 1 deletion DEPLOY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Steps to Deploy
1. Run tests
2. Set version in `appveyor.yml` (e.g: from `version: 2.5.{build}` to `version: 2.6.{build}`)
2. Set version in `appveyor.yml` (e.g: from `build_version: 6.5.0` to `build_version: 6.5.1`)
3. Update release notes in ConfigCatClient.csproj (PackageReleaseNotes)
4. Push to `master`
5. Deploy to NuGet.org
Expand Down
14 changes: 8 additions & 6 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
version: 6.4.{build}
environment:
build_version: 6.5.0
version: $(build_version)-{build}
image: Visual Studio 2022
configuration: Release
skip_commits:
Expand All @@ -8,11 +10,11 @@ skip_commits:
dotnet_csproj:
patch: true
file: '**\*.csproj'
version: '{version}'
package_version: '{version}'
assembly_version: '{version}'
file_version: '{version}'
informational_version: '{version}'
version: '{build_version}'
package_version: '{build_version}'
assembly_version: '{build_version}'
file_version: '{build_version}'
informational_version: '{build_version}'
install:
- cmd: dotnet tool install -g InheritDocTool
build_script:
Expand Down
6 changes: 3 additions & 3 deletions src/ConfigCat.Client.Tests/OverrideTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public void LocalFile_Default_WhenErrorOccures()
}

[TestMethod]
public void LocalFile_Reload()
public void LocalFile_Read()
{
using var client = new ConfigCatClient(options =>
{
Expand All @@ -137,7 +137,7 @@ public void LocalFile_Reload()
}

[TestMethod]
public async Task LocalFileAsync_Reload()
public async Task LocalFileAsync_Read()
{
using var client = new ConfigCatClient(options =>
{
Expand Down Expand Up @@ -362,7 +362,7 @@ public async Task LocalFile_Watcher_Reload()
Assert.AreEqual("initial", await client.GetValueAsync("fakeKey", string.Empty));

await WriteContent(SampleFileToCreate, "modified");
await Task.Delay(400);
await Task.Delay(1100);

Assert.AreEqual("modified", await client.GetValueAsync("fakeKey", string.Empty));

Expand Down
107 changes: 64 additions & 43 deletions src/ConfigCatClient/Override/LocalFileDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,31 @@ namespace ConfigCat.Client.Override
internal sealed class LocalFileDataSource : IOverrideDataSource
{
const int WAIT_TIME_FOR_UNLOCK = 200; // ms
const int MAX_WAIT_ITERATIONS = 50;
const int MAX_WAIT_ITERATIONS = 50; // ms
const int FILE_POLL_INTERVAL = 1000; // ms

private int isReading;
private DateTime fileLastWriteTime;
private readonly string fullPath;
private readonly ILogger logger;
private readonly FileSystemWatcher fileSystemWatcher;
private readonly TaskCompletionSource<bool> asyncInit = new();
private readonly ManualResetEvent syncInit = new(false);
private readonly CancellationTokenSource pollerCancellationTokenSource = new();

private volatile IDictionary<string, Setting> overrideValues;

public LocalFileDataSource(string filePath, bool autoReload, ILogger logger)
{
this.fullPath = Path.GetFullPath(filePath);
if (autoReload)
if (!File.Exists(filePath))
{
var directory = Path.GetDirectoryName(this.fullPath);
if (string.IsNullOrWhiteSpace(directory))
{
logger.Error($"Directory of {this.fullPath} not found to watch.");
}
else
{
this.fileSystemWatcher = new FileSystemWatcher(directory);
this.fileSystemWatcher.Changed += OnChanged;
this.fileSystemWatcher.Created += OnChanged;
this.fileSystemWatcher.Renamed += OnChanged;
this.fileSystemWatcher.EnableRaisingEvents = true;
logger.Information($"Watching {this.fullPath} for changes.");
}
logger.Error($"File {filePath} does not exist.");
this.SetInitialized();
return;
}

this.fullPath = Path.GetFullPath(filePath);
this.logger = logger;
_ = this.ReadFileAsync(this.fullPath);
}

private async void OnChanged(object sender, FileSystemEventArgs e)
{
// filter out events on temporary files
if (e.FullPath != this.fullPath)
return;

this.logger.Information($"Reload file {e.FullPath}.");
await this.ReadFileAsync(e.FullPath);
this.StartFileReading(autoReload);
}

public IDictionary<string, Setting> GetOverrides()
Expand All @@ -71,18 +52,64 @@ public async Task<IDictionary<string, Setting>> GetOverridesAsync()
return this.overrideValues ?? new Dictionary<string, Setting>();
}

private async Task ReadFileAsync(string filePath)
private void StartFileReading(bool autoReload)
{
if (Interlocked.CompareExchange(ref this.isReading, 1, 0) != 0)
return;
Task.Run(async () =>
{
try
{
await ReadFileAsync();

if (autoReload)
{
await this.StartWatchAsync();
}
}
finally
{
this.SetInitialized();
}
});
}

private async Task StartWatchAsync()
{
this.logger.Information($"Watching {this.fullPath} for changes.");
while (!this.pollerCancellationTokenSource.IsCancellationRequested)
{
try
{
try
{
var lastWriteTime = File.GetLastWriteTimeUtc(this.fullPath);
if (lastWriteTime > this.fileLastWriteTime)
{
this.logger.Information($"Reload file {this.fullPath}.");
await ReadFileAsync();
}
}

finally
{
await Task.Delay(FILE_POLL_INTERVAL, this.pollerCancellationTokenSource.Token);
}
}
catch (OperationCanceledException)
{
// ignore exceptions from cancellation.
}
}
}

private async Task ReadFileAsync()
{
try
{
for (int i = 1; i <= MAX_WAIT_ITERATIONS; i++)
{
try
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var stream = new FileStream(this.fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync();
var simplified = content.DeserializeOrDefault<SimplifiedConfig>();
Expand All @@ -105,15 +132,15 @@ private async Task ReadFileAsync(string filePath)

await Task.Delay(WAIT_TIME_FOR_UNLOCK);
}
}
}
}
catch (Exception ex)
{
this.logger.Error($"Failed to read file {filePath}. {ex}");
this.logger.Error($"Failed to read file {this.fullPath}. {ex}");
}
finally
{
Interlocked.Exchange(ref this.isReading, 0);
this.fileLastWriteTime = File.GetLastWriteTimeUtc(this.fullPath);
this.SetInitialized();
}
}
Expand All @@ -126,13 +153,7 @@ private void SetInitialized()

public void Dispose()
{
if (this.fileSystemWatcher != null)
{
this.fileSystemWatcher.Changed -= OnChanged;
this.fileSystemWatcher.Created -= OnChanged;
this.fileSystemWatcher.Renamed -= OnChanged;
this.fileSystemWatcher.Dispose();
}
this.pollerCancellationTokenSource.Cancel();
this.syncInit.Dispose();
}

Expand Down

0 comments on commit 90a715a

Please sign in to comment.