Implement debounce to avoid thrashing disk on settings update
This commit is contained in:
parent
3783dac472
commit
2b30158764
13 changed files with 94 additions and 87 deletions
|
|
@ -70,7 +70,7 @@ public class AuthenticationService(
|
|||
//SetOfflineMode(false);
|
||||
TemporarilyOffline = false;
|
||||
|
||||
await settingsProvider.UpdateAsync(s =>
|
||||
settingsProvider.Update(s =>
|
||||
{
|
||||
s.Authentication.ServerAddress = serverAddress;
|
||||
s.Authentication.AccessToken = token.AccessToken;
|
||||
|
|
@ -100,7 +100,7 @@ public class AuthenticationService(
|
|||
|
||||
await client.Authentication.RegisterAsync(username, password, passwordConfirmation);
|
||||
|
||||
await settingsProvider.UpdateAsync(s =>
|
||||
settingsProvider.Update(s =>
|
||||
{
|
||||
s.Authentication.ServerAddress = client.Connection.GetServerAddress();
|
||||
s.Authentication.AccessToken = tokenProvider.GetToken();
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
var token = await client.Authentication.AuthenticateAsync(options.Username, options.Password, client.Connection.GetServerAddress());
|
||||
|
||||
await client.Settings.UpdateAsync(s =>
|
||||
client.Settings.Update(s =>
|
||||
{
|
||||
s.Authentication.AccessToken = token.AccessToken;
|
||||
s.Authentication.RefreshToken = token.RefreshToken;
|
||||
|
|
@ -276,7 +276,7 @@ namespace LANCommander.Launcher.Services
|
|||
{
|
||||
await client.Authentication.LogoutAsync();
|
||||
|
||||
await client.Settings.UpdateAsync(s =>
|
||||
client.Settings.Update(s =>
|
||||
{
|
||||
s.Authentication.AccessToken = String.Empty;
|
||||
s.Authentication.RefreshToken = String.Empty;
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
async Task SaveSettingsAsync()
|
||||
{
|
||||
await settingsProvider.UpdateAsync(s =>
|
||||
settingsProvider.Update(s =>
|
||||
{
|
||||
s.Filter.Title = Filter.Title;
|
||||
s.Filter.GroupBy = Filter.GroupBy;
|
||||
|
|
|
|||
|
|
@ -62,10 +62,7 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
Process.Start(process);
|
||||
|
||||
await client.Settings.UpdateAsync(s =>
|
||||
{
|
||||
s.Launcher.LaunchCount = 0;
|
||||
});
|
||||
client.Settings.Update(s => s.Launcher.LaunchCount = 0);
|
||||
|
||||
Logger?.LogInformation("Shutting down to get out of the way");
|
||||
|
||||
|
|
|
|||
|
|
@ -90,11 +90,8 @@ namespace LANCommander.Launcher
|
|||
var tokenProvider = app.Services.GetService<ITokenProvider>()!;
|
||||
|
||||
tokenProvider.SetToken(settingsProvider.CurrentValue.Authentication.AccessToken);
|
||||
|
||||
settingsProvider.UpdateAsync(s =>
|
||||
{
|
||||
s.LaunchCount++;
|
||||
});
|
||||
|
||||
settingsProvider.Update(s => s.LaunchCount++);
|
||||
|
||||
Logger?.Debug("Starting application!");
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace LANCommander.Launcher.Services
|
|||
private readonly ResourceManager _resourceManager;
|
||||
|
||||
public LocalizationService(IOptions<Settings> settings)
|
||||
{ ;
|
||||
{
|
||||
_resourceManager = new ResourceManager("LANCommander.Launcher.Resources.SharedResources", typeof(LocalizationService).Assembly);
|
||||
|
||||
var cultureInfo = new CultureInfo(settings.Value.Culture);
|
||||
|
|
|
|||
|
|
@ -173,14 +173,4 @@
|
|||
Loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async Task OfflineMode()
|
||||
{
|
||||
await Client.Settings.UpdateAsync(s =>
|
||||
{
|
||||
s.Authentication.OfflineModeEnabled = true;
|
||||
});
|
||||
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ else if (!ConnectionClient.IsConfigured())
|
|||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ConnectionClient.OnConnect += (sender, args) => StateHasChanged();
|
||||
ConnectionClient.OnOfflineModeEnabled += (sender, args) => StateHasChanged();
|
||||
ConnectionClient.OnConnect += (sender, args) => InvokeAsync(StateHasChanged);
|
||||
ConnectionClient.OnOfflineModeEnabled += (sender, args) => InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
Importing = true;
|
||||
ImportStatusIndex = 0;
|
||||
ImportStatusTotal = 0;
|
||||
await NotifyProgress(ImportProgressState.Importing);
|
||||
NotifyProgress(ImportProgressState.Importing);
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
MessageService.Info(LocalizationService.GetString("ImportStarted"), 2.5);
|
||||
|
|
@ -66,12 +66,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
private async Task NotifyProgress(ImportProgressState state)
|
||||
private void NotifyProgress(ImportProgressState state)
|
||||
{
|
||||
if (!Progress.HasDelegate)
|
||||
return;
|
||||
|
||||
await Progress.InvokeAsync(new ImportProgressEventArgs
|
||||
Progress.InvokeAsync(new ImportProgressEventArgs
|
||||
{
|
||||
State = state,
|
||||
Index = ImportStatusIndex,
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
ImportStatusIndex = 0;
|
||||
ImportStatusTotal = 0;
|
||||
|
||||
await NotifyProgress(ImportProgressState.Imported);
|
||||
NotifyProgress(ImportProgressState.Imported);
|
||||
MessageService.Success(LocalizationService.GetString("ImportComplete"), 3);
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
ImportStatusIndex = 0;
|
||||
ImportStatusTotal = 0;
|
||||
|
||||
await NotifyProgress(ImportProgressState.Failed);
|
||||
NotifyProgress(ImportProgressState.Failed);
|
||||
MessageService.Error(LocalizationService.GetString("ImportFailed"), 3);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
ImportStatusIndex = status.Index;
|
||||
ImportStatusTotal = status.Total;
|
||||
|
||||
await NotifyProgress(ImportProgressState.Updated);
|
||||
NotifyProgress(ImportProgressState.Updated);
|
||||
}
|
||||
|
||||
public enum ImportProgressState
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
// Client.DefaultInstallDirectory = _settings.Games.InstallDirectories.First();
|
||||
Client.Scripts.Debug = SettingsProvider.CurrentValue.Debug.EnableScriptDebugging;
|
||||
|
||||
await SettingsProvider.UpdateAsync(s =>
|
||||
SettingsProvider.Update(s =>
|
||||
{
|
||||
s.Games.InstallDirectories = _installDirectories.Select(d => d.Path).ToArray();
|
||||
s.Media.StoragePath = _settings.Media.StoragePath;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@ namespace LANCommander.SDK.Abstractions;
|
|||
public interface ISettingsProvider
|
||||
{
|
||||
Settings CurrentValue { get; }
|
||||
Task UpdateAsync(Settings settings);
|
||||
Task UpdateAsync(Action<Settings> patch);
|
||||
void Update(Action<Settings> patch);
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LANCommander.SDK;
|
||||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Factories;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using Settings = LANCommander.SDK.Models.Settings;
|
||||
|
|
@ -13,6 +17,13 @@ public class SettingsProvider<TSettings> : ISettingsProvider
|
|||
{
|
||||
private readonly string _filePath;
|
||||
private readonly IOptionsMonitor<TSettings> _optionsMonitor;
|
||||
|
||||
private readonly TimeSpan _debounceDelay = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
private readonly SemaphoreSlim _ioGate = new(1, 1);
|
||||
private readonly object _debounceLock = new();
|
||||
|
||||
private CancellationTokenSource? _saveCts;
|
||||
|
||||
public TSettings CurrentValue => _optionsMonitor.CurrentValue;
|
||||
|
||||
|
|
@ -22,62 +33,81 @@ public class SettingsProvider<TSettings> : ISettingsProvider
|
|||
{
|
||||
_filePath = Path.Join(AppPaths.GetConfigDirectory(), Settings.SETTINGS_FILE_NAME);
|
||||
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
var template = new TSettings();
|
||||
Save(template);
|
||||
}
|
||||
|
||||
_optionsMonitor = optionsMonitor;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TSettings settings) => await SaveAsync(settings);
|
||||
|
||||
public async Task UpdateAsync(Action<TSettings> patch)
|
||||
public void Update(Action<TSettings> mutator)
|
||||
{
|
||||
patch.Invoke(_optionsMonitor.CurrentValue);
|
||||
await SaveAsync(_optionsMonitor.CurrentValue);
|
||||
mutator.Invoke(_optionsMonitor.CurrentValue);
|
||||
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
// ISettingsProvider (non-generic) explicit impls
|
||||
async Task ISettingsProvider.UpdateAsync(Settings settings)
|
||||
void ISettingsProvider.Update(Action<Settings> mutator)
|
||||
{
|
||||
// Allow callers who only know about base Settings to update
|
||||
if (settings is TSettings typed)
|
||||
mutator.Invoke(_optionsMonitor.CurrentValue);
|
||||
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
private void ScheduleSave()
|
||||
{
|
||||
CancellationTokenSource? ctsToStart;
|
||||
|
||||
lock (_debounceLock)
|
||||
{
|
||||
await UpdateAsync(typed);
|
||||
_saveCts?.Cancel();
|
||||
_saveCts?.Dispose();
|
||||
_saveCts = new CancellationTokenSource();
|
||||
ctsToStart = _saveCts;
|
||||
}
|
||||
else
|
||||
|
||||
_ = DebouncedSaveAsync(ctsToStart!.Token);
|
||||
}
|
||||
|
||||
private async Task DebouncedSaveAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_debounceDelay, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
var current = _optionsMonitor.CurrentValue;
|
||||
|
||||
await UpdateAsync(current);
|
||||
}
|
||||
|
||||
await _ioGate.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await SaveAsync(CurrentValue, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ioGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
async Task ISettingsProvider.UpdateAsync(Action<Settings> patch)
|
||||
private async Task SaveAsync(TSettings settings, CancellationToken ct)
|
||||
{
|
||||
// Patch the current derived instance through the base type view
|
||||
var current = _optionsMonitor.CurrentValue;
|
||||
patch(current);
|
||||
await UpdateAsync(current);
|
||||
}
|
||||
|
||||
private void Save(TSettings settings)
|
||||
{
|
||||
var serializer = new SerializerBuilder()
|
||||
.WithNamingConvention(PascalCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
File.WriteAllText(_filePath, serializer.Serialize(settings));
|
||||
}
|
||||
|
||||
private async Task SaveAsync(TSettings settings)
|
||||
{
|
||||
var serializer = new SerializerBuilder()
|
||||
.WithNamingConvention(PascalCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
await File.WriteAllTextAsync(_filePath, serializer.Serialize(settings));
|
||||
var fso = new FileStreamOptions
|
||||
{
|
||||
Mode = FileMode.OpenOrCreate,
|
||||
Access = FileAccess.Write,
|
||||
Share = FileShare.None,
|
||||
Options = FileOptions.Asynchronous | FileOptions.WriteThrough,
|
||||
};
|
||||
|
||||
await using (var fs = new FileStream(_filePath, fso))
|
||||
await using (var writer = new StreamWriter(fs, Encoding.UTF8))
|
||||
{
|
||||
var serializer = YamlSerializerFactory.Create();
|
||||
var serialization = serializer.Serialize(settings);
|
||||
|
||||
await writer.WriteAsync(serialization).WaitAsync(ct);
|
||||
|
||||
await writer.FlushAsync(ct).ConfigureAwait(false);
|
||||
await fs.FlushAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,10 +69,7 @@ public class ConnectionClient(
|
|||
{
|
||||
if (await PingAsync(uri))
|
||||
{
|
||||
await settingsProvider.UpdateAsync(s =>
|
||||
{
|
||||
s.Authentication.ServerAddress = uri;
|
||||
});
|
||||
settingsProvider.Update(s => s.Authentication.ServerAddress = uri);
|
||||
|
||||
logger?.LogInformation("Successfully discovered server at {ServerAddress}", uri.ToString());
|
||||
|
||||
|
|
@ -99,7 +96,7 @@ public class ConnectionClient(
|
|||
if (!rpc.IsConnected())
|
||||
await rpc.ConnectAsync(GetServerAddress());
|
||||
|
||||
await settingsProvider.UpdateAsync(s => s.Authentication.OfflineModeEnabled = false);
|
||||
settingsProvider.Update(s => s.Authentication.OfflineModeEnabled = false);
|
||||
|
||||
OnConnect?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
|
|
@ -120,10 +117,7 @@ public class ConnectionClient(
|
|||
{
|
||||
await DisconnectAsync();
|
||||
|
||||
await settingsProvider.UpdateAsync(s =>
|
||||
{
|
||||
s.Authentication.OfflineModeEnabled = true;
|
||||
});
|
||||
settingsProvider.Update(s => s.Authentication.OfflineModeEnabled = true);
|
||||
|
||||
OnOfflineModeEnabled?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue