ba4d487c65
Move the pending requests and authorized secrets out of process so the initiate, authorize and exchange legs can land on different replicas.
92 lines
3.1 KiB
C#
92 lines
3.1 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using StackExchange.Redis;
|
|
using Testcontainers.Redis;
|
|
|
|
namespace Jellyfin.Server.Tests.HighAvailability;
|
|
|
|
/// <summary>
|
|
/// Hands out a Redis server for the tests that need one. A server named by <c>JELLYFIN_TEST_REDIS</c> is
|
|
/// used as is, so CI can run one beside the step instead of a docker daemon of its own; without it a
|
|
/// container is started through testcontainers.
|
|
/// </summary>
|
|
public sealed class RedisTestServer : IAsyncDisposable
|
|
{
|
|
/// <summary>
|
|
/// The connection string of an already running server.
|
|
/// </summary>
|
|
public const string ConnectionStringVariable = "JELLYFIN_TEST_REDIS";
|
|
|
|
private readonly RedisContainer? _container;
|
|
|
|
private RedisTestServer(RedisContainer? container, string connectionString)
|
|
{
|
|
_container = container;
|
|
ConnectionString = connectionString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the connection string of the running server.
|
|
/// </summary>
|
|
public string ConnectionString { get; }
|
|
|
|
/// <summary>
|
|
/// Starts or attaches to a Redis server and waits until it accepts connections.
|
|
/// </summary>
|
|
/// <returns>The running server.</returns>
|
|
public static async Task<RedisTestServer> StartAsync()
|
|
{
|
|
var provided = Environment.GetEnvironmentVariable(ConnectionStringVariable);
|
|
if (!string.IsNullOrWhiteSpace(provided))
|
|
{
|
|
var attached = new RedisTestServer(null, provided);
|
|
await attached.WaitUntilReadyAsync().ConfigureAwait(false);
|
|
return attached;
|
|
}
|
|
|
|
var container = new RedisBuilder("redis:7-alpine").Build();
|
|
await container.StartAsync().ConfigureAwait(false);
|
|
|
|
var started = new RedisTestServer(container, container.GetConnectionString());
|
|
await started.WaitUntilReadyAsync().ConfigureAwait(false);
|
|
return started;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens a connection of its own, so each in-process stand-in for a replica talks to the server the
|
|
/// way a separate pod would.
|
|
/// </summary>
|
|
/// <returns>A new multiplexer.</returns>
|
|
public async Task<IConnectionMultiplexer> ConnectAsync()
|
|
=> await ConnectionMultiplexer.ConnectAsync(ConnectionString).ConfigureAwait(false);
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_container is not null)
|
|
{
|
|
await _container.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task WaitUntilReadyAsync()
|
|
{
|
|
for (var attempt = 1; ; attempt++)
|
|
{
|
|
try
|
|
{
|
|
var connection = await ConnectionMultiplexer.ConnectAsync(ConnectionString).ConfigureAwait(false);
|
|
await using (connection.ConfigureAwait(false))
|
|
{
|
|
await connection.GetDatabase().PingAsync().ConfigureAwait(false);
|
|
return;
|
|
}
|
|
}
|
|
catch (RedisException) when (attempt < 60)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|