You've already forked Extensions.Configuration.EntityFrameworkCore
Improve exception/error handling
This commit is contained in:
@@ -52,22 +52,29 @@ internal class PostgreSQLNotificationConfigurationReloader : Microsoft.Extension
|
||||
{
|
||||
var dbConnection = Initialise();
|
||||
|
||||
do
|
||||
try
|
||||
{
|
||||
await ListenForNotifications(stoppingToken);
|
||||
|
||||
try
|
||||
do
|
||||
{
|
||||
await dbConnection.WaitAsync(stoppingToken);
|
||||
try
|
||||
{
|
||||
await dbConnection.WaitAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
_logger?.LogWarning(e, "Exception while waiting for notifications on channel '{channel}'", _options.ChannelName);
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e is not TaskCanceledException)
|
||||
{
|
||||
_logger?.LogWarning(e, "Exception while waiting for notifications on channel '{channel}'", _options.ChannelName);
|
||||
}
|
||||
}
|
||||
while (ConnectionState.Open == dbConnection.State || await IsReconnectionPossible(stoppingToken));
|
||||
while (ConnectionState.Open == dbConnection.State || await TryReconnect(stoppingToken));
|
||||
|
||||
_logger?.LogWarning("Giving up listening for notifications on channel '{channel}' because reconnection attempts exhausted. Configuration updates from database will no longer occur", _options.ChannelName);
|
||||
_logger?.LogWarning("Giving up listening for notifications on channel '{channel}' because reconnection attempts exhausted. Configuration updates from database will no longer occur", _options.ChannelName);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
_logger?.LogInformation(e, "Exiting due to signal from stopping token");
|
||||
}
|
||||
}
|
||||
|
||||
private Npgsql.NpgsqlConnection Initialise()
|
||||
@@ -76,8 +83,37 @@ internal class PostgreSQLNotificationConfigurationReloader : Microsoft.Extension
|
||||
dbConnection.Notification += OnNotification;
|
||||
|
||||
return dbConnection;
|
||||
|
||||
void OnNotification(object sender, Npgsql.NpgsqlNotificationEventArgs args)
|
||||
{
|
||||
_logger?.LogTrace("Received notification '{payload}' on channel '{channel}'", args.Payload, args.Channel);
|
||||
if (args.Channel != _options.ChannelName)
|
||||
return;
|
||||
|
||||
using (var previousCts = Interlocked.Exchange(ref _cts, new()))
|
||||
{
|
||||
previousCts.Cancel();
|
||||
}
|
||||
|
||||
ReadOnlySpan<string> keyValue = args.Payload.Split('=', 2); // DEBT: Do split without heap allocation
|
||||
switch (keyValue.Length)
|
||||
{
|
||||
case 2:
|
||||
_configProvider.Set(keyValue[0], keyValue[1]);
|
||||
break;
|
||||
case 1:
|
||||
_configProvider.Remove(keyValue[0]);
|
||||
break;
|
||||
default:
|
||||
_logger?.LogWarning("Invalid '{channel}' payload '{payload}'", args.Channel, args.Payload);
|
||||
return; // Don't proceed to debounced reload
|
||||
}
|
||||
|
||||
_ = DebouncedProviderReload(); // Intentionally running without awaiting, may be cancelled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task ListenForNotifications(CancellationToken stoppingToken)
|
||||
{
|
||||
var listenStatement = $"LISTEN {_options.ChannelName}";
|
||||
@@ -86,40 +122,12 @@ internal class PostgreSQLNotificationConfigurationReloader : Microsoft.Extension
|
||||
_logger?.LogInformation("Listening for notifications on channel '{channel}'", _options.ChannelName);
|
||||
}
|
||||
|
||||
private void OnNotification(object sender, Npgsql.NpgsqlNotificationEventArgs args)
|
||||
{
|
||||
_logger?.LogTrace("Received notification '{payload}' on channel '{channel}'", args.Payload, args.Channel);
|
||||
if (args.Channel != _options.ChannelName)
|
||||
return;
|
||||
|
||||
using (var previousCts = Interlocked.Exchange(ref _cts, new()))
|
||||
{
|
||||
previousCts.Cancel();
|
||||
}
|
||||
|
||||
ReadOnlySpan<string> keyValue = args.Payload.Split('=', 2); // DEBT: Do split without heap allocation
|
||||
switch (keyValue.Length)
|
||||
{
|
||||
case 2:
|
||||
_configProvider.Set(keyValue[0], keyValue[1]);
|
||||
break;
|
||||
case 1:
|
||||
_configProvider.Remove(keyValue[0]);
|
||||
break;
|
||||
default:
|
||||
_logger?.LogWarning("Invalid '{channel}' payload '{payload}'", args.Channel, args.Payload);
|
||||
return; // Don't proceed to debounced reload
|
||||
}
|
||||
|
||||
var _ = DebouncedProviderReload(); // Intentionally running without awaiting, may be cancelled
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Trigger reload of the <see cref="IEntityFrameworkCoreDbSetConfigurationProvider"/> after waiting for a [cancelable] debounce period
|
||||
/// </summary>
|
||||
/// <remarks>Does not log <see cref="TaskCanceledException "/> each time debounce task is cancelled, unlike <see cref="SimpleDebouncedProviderReload"/></remarks>
|
||||
private async ValueTask DebouncedProviderReload()
|
||||
private async Task DebouncedProviderReload()
|
||||
{
|
||||
var delayTask = Task.Delay(_options.DebounceInterval);
|
||||
var cancelableTask = Task.Delay(Timeout.Infinite, _cts.Token);
|
||||
@@ -154,7 +162,7 @@ internal class PostgreSQLNotificationConfigurationReloader : Microsoft.Extension
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> IsReconnectionPossible(CancellationToken stoppingToken)
|
||||
private async Task<bool> TryReconnect(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Delay(_options.InitialReconnectionDelay, stoppingToken);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ This library enhances `RAIC.Extensions.Configuration.EntityFrameworkCore` with s
|
||||
|
||||
## Goals
|
||||
1. No polling!
|
||||
1. Updates happen in background via worker service (`IHostedService`)
|
||||
1. Only update settings which change rather than reloading all of them
|
||||
1. Updates happen in background via a [hosted service](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) implementation.
|
||||
1. Only update settings which change rather than updating them all
|
||||
|
||||
## Requirements
|
||||
* .NET 8
|
||||
@@ -14,8 +14,8 @@ This library enhances `RAIC.Extensions.Configuration.EntityFrameworkCore` with s
|
||||
## Gotchas
|
||||
* Setting values cannot be `null` (as signified by the `RequiredAttribute` on `ISetting.Value`)
|
||||
* Setting keys must not contain the `=` character (similar to `CommandLineConfigurationProvider` & `EnvironmentVariablesConfigurationProvider`)
|
||||
* Small window of opportunity for updates to be missed during reconnection process
|
||||
* Consider adding `Keepalive` to your conenction string (https://www.npgsql.org/doc/keepalive.html) if its not already present
|
||||
* Small window of opportunity for updates to be missed during reconnection process after any network dropouts or other connectivity flakiness
|
||||
* Consider adding `Keepalive` [parameter](https://www.npgsql.org/doc/keepalive.html) to your connection string if its not already present
|
||||
|
||||
## Known Issues
|
||||
* Not tested under load
|
||||
@@ -65,7 +65,7 @@ AFTER DELETE ON settings
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_setting_remove();
|
||||
```
|
||||
|
||||
Reccommend adding your SQL to the migration which adds the `Settings` table/view (or a new migration if that table/view already exists).
|
||||
Recommend adding your SQL to the migration which adds the `Settings` table/view (or a new migration if that table/view already exists).
|
||||
|
||||
## Usage Example
|
||||
|
||||
|
||||
Reference in New Issue
Block a user