Skip to content

Commit

Permalink
feat: Implement reconnect strategies (again)
Browse files Browse the repository at this point in the history
  • Loading branch information
angelobreuer committed Sep 29, 2023
1 parent 41adf69 commit 11c61d7
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 121 deletions.
12 changes: 12 additions & 0 deletions samples/Lavalink4NET.Samples.InactivityTracking/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Lavalink4NET.DiscordNet.ExampleBot;
using Lavalink4NET.Extensions;
using Lavalink4NET.InactivityTracking.Extensions;
using Lavalink4NET.InactivityTracking.Trackers.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Expand All @@ -19,4 +20,15 @@
builder.Services.AddInactivityTracking();
builder.Services.AddLogging(x => x.AddConsole().SetMinimumLevel(LogLevel.Trace));

builder.Services.ConfigureInactivityTracking(x =>
{
});

builder.Services.Configure<UsersInactivityTrackerOptions>(options =>
{
options.Threshold = 1;
options.Timeout = TimeSpan.FromSeconds(30);
options.ExcludeBots = true;
});

builder.Build().Run();
24 changes: 24 additions & 0 deletions src/Lavalink4NET.Tests/ExponentialBackoffReconnectStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Lavalink4NET.Tests;

using System;
using System.Threading.Tasks;
using Lavalink4NET.Socket;
using Microsoft.Extensions.Options;
using Xunit;

public sealed class ExponentialBackoffReconnectStrategyTests
{
[Fact]
public async Task DefaultReconnectStrategyTestAsync()
{
var strategy = new ExponentialBackoffReconnectStrategy(
Options.Create(new ExponentialBackoffReconnectStrategyOptions()));

Assert.Equal(TimeSpan.FromSeconds(2), await strategy.GetNextDelayAsync(DateTimeOffset.UtcNow, 1));
Assert.Equal(TimeSpan.FromSeconds(4), await strategy.GetNextDelayAsync(DateTimeOffset.UtcNow, 2));
Assert.Equal(TimeSpan.FromSeconds(8), await strategy.GetNextDelayAsync(DateTimeOffset.UtcNow, 3));
Assert.Equal(TimeSpan.FromSeconds(16), await strategy.GetNextDelayAsync(DateTimeOffset.UtcNow, 4));
Assert.Equal(TimeSpan.FromSeconds(60), await strategy.GetNextDelayAsync(DateTimeOffset.UtcNow, 50));
Assert.Equal(TimeSpan.FromSeconds(60), await strategy.GetNextDelayAsync(DateTimeOffset.UtcNow, 50000));
}
}
40 changes: 0 additions & 40 deletions src/Lavalink4NET.Tests/ReconnectStrategiesTests.cs

This file was deleted.

41 changes: 0 additions & 41 deletions src/Lavalink4NET/Events/ReconnectAttemptEventArgs.cs

This file was deleted.

32 changes: 32 additions & 0 deletions src/Lavalink4NET/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static IServiceCollection AddLavalinkCore(this IServiceCollection service
services.TryAddSingleton<IIntegrationManager, IntegrationManager>();
services.TryAddSingleton<ILavalinkApiClientProvider, LavalinkApiClientProvider>();
services.TryAddSingleton<ILavalinkSessionProvider, LavalinkSessionProvider>();
services.TryAddSingleton<IReconnectStrategy, ExponentialBackoffReconnectStrategy>();

services.AddHostedService<AudioServiceHost>();

Expand All @@ -52,4 +53,35 @@ public static IServiceCollection ConfigureLavalink(this IServiceCollection servi
{
return services.Configure(options);
}

public static IServiceCollection AddReconnectStrategy<TReconnectStrategy>(this IServiceCollection services)
where TReconnectStrategy : class, IReconnectStrategy
{
ArgumentNullException.ThrowIfNull(services);

services.Replace(ServiceDescriptor.Singleton<IReconnectStrategy, TReconnectStrategy>());

return services;
}

public static IServiceCollection AddReconnectStrategy<TReconnectStrategy>(this IServiceCollection services, Func<IServiceProvider, TReconnectStrategy> implementationFactory)
where TReconnectStrategy : class, IReconnectStrategy
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(implementationFactory);

services.Replace(ServiceDescriptor.Singleton<IReconnectStrategy, TReconnectStrategy>(implementationFactory));

return services;
}

public static IServiceCollection AddReconnectStrategy<TReconnectStrategy>(this IServiceCollection services, TReconnectStrategy reconnectStrategy)
where TReconnectStrategy : class, IReconnectStrategy
{
ArgumentNullException.ThrowIfNull(services);

services.Replace(ServiceDescriptor.Singleton<IReconnectStrategy>(reconnectStrategy));

return services;
}
}
20 changes: 0 additions & 20 deletions src/Lavalink4NET/ReconnectStrategies.cs

This file was deleted.

14 changes: 0 additions & 14 deletions src/Lavalink4NET/ReconnectStrategy.cs

This file was deleted.

34 changes: 34 additions & 0 deletions src/Lavalink4NET/Socket/ExponentialBackoffReconnectStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Lavalink4NET.Socket;

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

public sealed class ExponentialBackoffReconnectStrategy : IReconnectStrategy
{
private readonly ExponentialBackoffReconnectStrategyOptions _options;

public ExponentialBackoffReconnectStrategy(IOptions<ExponentialBackoffReconnectStrategyOptions> options)
{
ArgumentNullException.ThrowIfNull(options);

_options = options.Value;
}

public ValueTask<TimeSpan?> GetNextDelayAsync(DateTimeOffset interruptedAt, int attempt, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (attempt is < 1)
{
throw new ArgumentOutOfRangeException(nameof(attempt));
}

var factor = Math.Pow(_options.BackoffMultiplier, attempt - 1);
var delayTicks = _options.InitialDelay.TotalSeconds * factor;
var backoff = TimeSpan.FromSeconds(Math.Clamp(delayTicks, _options.MinimumDelay.TotalSeconds, _options.MaximumDelay.TotalSeconds));

return new ValueTask<TimeSpan?>(backoff);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Lavalink4NET.Socket;

using System;

public sealed class ExponentialBackoffReconnectStrategyOptions
{
public TimeSpan MaximumDelay { get; set; } = TimeSpan.FromMinutes(1);

public TimeSpan MinimumDelay { get; set; } = TimeSpan.FromSeconds(2);

public TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(2);

public double BackoffMultiplier { get; set; } = 2.0;
}
10 changes: 10 additions & 0 deletions src/Lavalink4NET/Socket/IReconnectStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Lavalink4NET.Socket;

using System;
using System.Threading;
using System.Threading.Tasks;

public interface IReconnectStrategy
{
ValueTask<TimeSpan?> GetNextDelayAsync(DateTimeOffset interruptedAt, int attempt, CancellationToken cancellationToken = default);
}
40 changes: 36 additions & 4 deletions src/Lavalink4NET/Socket/LavalinkSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,37 @@
using Lavalink4NET.Protocol;
using Lavalink4NET.Protocol.Payloads;
using Lavalink4NET.Rest;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

internal sealed class LavalinkSocket : ILavalinkSocket
{
private readonly Channel<IPayload> _channel;
private readonly IHttpMessageHandlerFactory _httpMessageHandlerFactory;
private readonly IReconnectStrategy _reconnectStrategy;
private readonly ISystemClock _systemClock;
private readonly ILogger<LavalinkSocket> _logger;
private readonly IOptions<LavalinkSocketOptions> _options;
private bool _disposed;

public LavalinkSocket(
IHttpMessageHandlerFactory httpMessageHandlerFactory,
IReconnectStrategy reconnectStrategy,
ISystemClock systemClock,
ILogger<LavalinkSocket> logger,
IOptions<LavalinkSocketOptions> options)
{
ArgumentNullException.ThrowIfNull(httpMessageHandlerFactory);
ArgumentNullException.ThrowIfNull(reconnectStrategy);
ArgumentNullException.ThrowIfNull(systemClock);
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(options);

Label = options.Value.Label ?? $"Lavalink-{CorrelationIdGenerator.GetNextId()}";
_httpMessageHandlerFactory = httpMessageHandlerFactory;
_reconnectStrategy = reconnectStrategy;
_systemClock = systemClock;
_logger = logger;
_options = options;
_channel = Channel.CreateUnbounded<IPayload>();
Expand Down Expand Up @@ -77,6 +86,7 @@ async ValueTask<WebSocket> ConnectWithRetryAsync(CancellationToken cancellationT
cancellationToken.ThrowIfCancellationRequested();

var attempt = 0;
var interruptedSince = default(DateTimeOffset?);

while (true)
{
Expand Down Expand Up @@ -111,17 +121,36 @@ await webSocket
.ConfigureAwait(false);
#endif

interruptedSince = null;
attempt = 0;

_logger.ConnectionEstablished(Label);

return webSocket;
}
catch (Exception exception) when (attempt++ < 10)
catch (Exception exception)
{
await Task
.Delay(2500, cancellationToken)
_logger.FailedToConnect(Label, exception);

interruptedSince ??= _systemClock.UtcNow;

var reconnectDelay = await _reconnectStrategy
.GetNextDelayAsync(interruptedSince.Value, ++attempt, cancellationToken)
.ConfigureAwait(false);

_logger.FailedToConnect(Label, exception);
if (reconnectDelay is null)
{
throw;
}

if (reconnectDelay.Value > TimeSpan.Zero)
{
_logger.WaitingBeforeReconnect(Label, reconnectDelay.Value.TotalSeconds);

await Task
.Delay(reconnectDelay.Value, cancellationToken)
.ConfigureAwait(false);
}
}
}
}
Expand Down Expand Up @@ -233,4 +262,7 @@ internal static partial class Logging

[LoggerMessage(3, LogLevel.Debug, "[{Label}] Failed to connect to the Lavalink node.", EventName = nameof(FailedToConnect))]
public static partial void FailedToConnect(this ILogger<LavalinkSocket> logger, string label, Exception exception);

[LoggerMessage(4, LogLevel.Debug, "[{Label}] Waiting {Duration} second(s) before reconnecting to the node.", EventName = nameof(WaitingBeforeReconnect))]
public static partial void WaitingBeforeReconnect(this ILogger<LavalinkSocket> logger, string label, double duration);
}
Loading

0 comments on commit 11c61d7

Please sign in to comment.