Skip to content

Commit

Permalink
Merge pull request #34 from configcat/replace-filesystemwatcher
Browse files Browse the repository at this point in the history
Replace file system watcher with polling
  • Loading branch information
z4kn4fein authored Apr 6, 2022
2 parents ddb18a0 + 01b64b9 commit bbd3e51
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 77 deletions.
1 change: 1 addition & 0 deletions .github/workflows/linux-macOS-CI.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Build on Linux and macOS
on:
push:
branches: [ master ]
paths-ignore:
- '**.md'
- 'appveyor*'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/sonar-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: SonarCloud Analysis
on:
push:
branches: [ master ]
paths-ignore:
- '**.md'
- 'appveyor*'
Expand Down
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
30 changes: 26 additions & 4 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 @@ -360,15 +360,37 @@ public async Task LocalFile_Watcher_Reload()
});

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

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

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

File.Delete(SampleFileToCreate);
}

[TestMethod]
public async Task LocalFile_Watcher_Reload_Sync()
{
await CreateFileAndWriteContent(SampleFileToCreate, "initial");

using var client = new ConfigCatClient(options =>
{
options.SdkKey = "localhost";
options.FlagOverrides = FlagOverrides.LocalFile(SampleFileToCreate, true, OverrideBehaviour.LocalOnly);
options.Logger.LogLevel = LogLevel.Info;
});

Assert.AreEqual("initial", client.GetValue("fakeKey", string.Empty));
await Task.Delay(100);
await WriteContent(SampleFileToCreate, "modified");
await Task.Delay(1500);

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

File.Delete(SampleFileToCreate);
}

private static string GetJsonContent(string value)
{
return $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"{value}\", \"p\": [] ,\"r\": [] }} }} }}";
Expand Down
119 changes: 53 additions & 66 deletions src/ConfigCatClient/Override/LocalFileDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,80 +11,81 @@ 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 cancellationTokenSource = 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.");
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.ReloadFile();

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

public IDictionary<string, Setting> GetOverrides()
{
if (this.overrideValues != null) return this.overrideValues;
this.syncInit.WaitOne();
return this.overrideValues ?? new Dictionary<string, Setting>();
}
public IDictionary<string, Setting> GetOverrides() => this.overrideValues ?? new Dictionary<string, Setting>();

public Task<IDictionary<string, Setting>> GetOverridesAsync() => Task.FromResult(this.overrideValues ?? new Dictionary<string, Setting>());


public async Task<IDictionary<string, Setting>> GetOverridesAsync()
private void StartWatch()
{
if (this.overrideValues != null) return this.overrideValues;
await this.asyncInit.Task.ConfigureAwait(false);
return this.overrideValues ?? new Dictionary<string, Setting>();
Task.Run(async () =>
{
this.logger.Information($"Watching {this.fullPath} for changes.");
while (!this.cancellationTokenSource.IsCancellationRequested)
{
try
{
try
{
var lastWriteTime = File.GetLastWriteTimeUtc(this.fullPath);
if (lastWriteTime > this.fileLastWriteTime)
{
this.logger.Information($"Reload file {this.fullPath}.");
this.ReloadFile();
}
}

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

private async Task ReadFileAsync(string filePath)
private void ReloadFile()
{
if (Interlocked.CompareExchange(ref this.isReading, 1, 0) != 0)
return;

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 reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync();
var content = File.ReadAllText(this.fullPath);
var simplified = content.DeserializeOrDefault<SimplifiedConfig>();
if (simplified?.Entries != null)
{
Expand All @@ -103,37 +104,23 @@ private async Task ReadFileAsync(string filePath)
if (i >= MAX_WAIT_ITERATIONS)
throw;

await Task.Delay(WAIT_TIME_FOR_UNLOCK);
Thread.Sleep(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.SetInitialized();
this.fileLastWriteTime = File.GetLastWriteTimeUtc(this.fullPath);
}
}

private void SetInitialized()
{
this.syncInit.Set();
this.asyncInit.TrySetResult(true);
}

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

private sealed class SimplifiedConfig
Expand Down

0 comments on commit bbd3e51

Please sign in to comment.