fix(ha): read transcode store config from the variable form deployments set
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

The startup configuration only reads JELLYFIN_ prefixed environment
variables, so the bare Jellyfin__TranscodeStore__* form used by the chart,
the manifests and the README is dropped and the Redis store is never
registered. Nothing logs the selected store, so the fallback is invisible.

- Read bare Jellyfin__* variables into the Jellyfin:* configuration root
- Keep an explicit JELLYFIN_ variable winning over the bare form
- Log the selected transcode session store at startup
- Ping Redis once at startup and log an unreachable store at Error
- Log endpoints only, never the connection string
This commit is contained in:
2026-09-13 13:10:52 +10:00
parent 25269263ce
commit 95220e2ed6
13 changed files with 640 additions and 27 deletions
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;
namespace Jellyfin.Server.Extensions;
/// <summary>
/// Extensions for building the application configuration.
/// </summary>
public static class ConfigurationBuilderExtensions
{
/// <summary>
/// The environment variable prefix that maps onto this fork's own <c>Jellyfin:*</c> configuration
/// keys, for example <c>Jellyfin__TranscodeStore__RedisConnectionString</c>.
/// </summary>
public const string JellyfinSectionEnvironmentPrefix = "Jellyfin__";
/// <summary>
/// The configuration section root this fork keeps its own settings under.
/// </summary>
public const string JellyfinSectionRoot = "Jellyfin";
/// <summary>
/// Adds environment variables named <c>Jellyfin__Section__Key</c> as the configuration keys
/// <c>Jellyfin:Section:Key</c>.
/// </summary>
/// <remarks>
/// The base configuration only reads <c>JELLYFIN_</c> prefixed environment variables, so the
/// unprefixed form every manifest, chart and document uses would otherwise be dropped and the
/// feature it configures would stay off with no error.
/// </remarks>
/// <param name="builder">The configuration builder.</param>
/// <returns>The updated configuration builder.</returns>
public static IConfigurationBuilder AddJellyfinSectionEnvironmentVariables(this IConfigurationBuilder builder)
{
// Read through the framework provider so "__" to ":" normalisation and case handling match the
// prefixed form exactly; the prefix it strips is then put back as the section root.
var scoped = new ConfigurationBuilder()
.AddEnvironmentVariables(JellyfinSectionEnvironmentPrefix)
.Build();
var entries = scoped.AsEnumerable()
.Where(entry => entry.Value is not null)
.Select(entry => new KeyValuePair<string, string?>(
ConfigurationPath.Combine(JellyfinSectionRoot, entry.Key),
entry.Value))
.ToList();
return builder.AddInMemoryCollection(entries);
}
}
@@ -0,0 +1,87 @@
using System;
using System.Linq;
using Emby.Server.Implementations.MediaEncoding;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace Jellyfin.Server.Extensions;
/// <summary>
/// Extensions for registering the transcode session store.
/// </summary>
public static class TranscodeStoreServiceCollectionExtensions
{
/// <summary>
/// Registers the transcode session store, Redis-backed when a connection string is configured and
/// no-op otherwise, and reports the selected store at <see cref="LogLevel.Information"/>.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="configuration">The configuration to read <c>Jellyfin:TranscodeStore</c> from.</param>
/// <param name="logger">The logger to report the selected store on.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddTranscodeSessionStore(
this IServiceCollection serviceCollection,
IConfiguration configuration,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(logger);
serviceCollection.Configure<TranscodeStoreOptions>(configuration.GetSection(TranscodeStoreOptions.ConfigurationSection));
var redisConnectionString = configuration[TranscodeStoreOptions.RedisConnectionStringKey];
if (string.IsNullOrEmpty(redisConnectionString))
{
logger.LogInformation(
"Transcode session store: {Store}. Cross-pod transcode takeover is off; set {Key} to enable it.",
nameof(NullTranscodeSessionStore),
TranscodeStoreOptions.RedisConnectionStringKey);
return serviceCollection.AddSingleton<ITranscodeSessionStore, NullTranscodeSessionStore>();
}
logger.LogInformation(
"Transcode session store: {Store} on {Endpoints}.",
nameof(RedisTranscodeSessionStore),
DescribeEndpoints(redisConnectionString));
serviceCollection.AddSingleton<IConnectionMultiplexer>(sp =>
{
try
{
return ConnectionMultiplexer.Connect(redisConnectionString);
}
catch (Exception ex)
{
sp.GetRequiredService<ILogger<CoreAppHost>>()
.LogError(ex, "Failed to connect to Redis. Check the {Key} configuration.", TranscodeStoreOptions.RedisConnectionStringKey);
throw;
}
});
serviceCollection.AddSingleton<ITranscodeSessionStore, RedisTranscodeSessionStore>();
serviceCollection.AddHostedService<TranscodeStoreConnectivityProbe>();
return serviceCollection;
}
/// <summary>
/// Renders the endpoints of a connection string for logging. The connection string itself is never
/// logged because it can carry a password.
/// </summary>
private static string DescribeEndpoints(string redisConnectionString)
{
try
{
return string.Join(
',',
ConfigurationOptions.Parse(redisConnectionString).EndPoints.Select(endpoint => endpoint.ToString()));
}
catch (ArgumentException)
{
return "(unparsable connection string)";
}
}
}