fix(sonarr): race condition during version retrieval

pull/47/head
Robert Dailey 3 years ago
parent 1772e7c9fd
commit 6036be0d29

@ -1,8 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="Autofac" /> <PackageReference Include="Autofac" />
<PackageReference Include="FluentValidation" />
<PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="YamlDotNet" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -0,0 +1,49 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using Serilog;
namespace Common.Extensions
{
public static class RxExtensions
{
public static IObservable<T> Spy<T>(this IObservable<T> source, ILogger log, string? opName = null)
{
opName ??= "IObservable";
log.Debug("{OpName}: Observable obtained on Thread: {ThreadId}",
opName,
Thread.CurrentThread.ManagedThreadId);
return Observable.Create<T>(obs =>
{
log.Debug("{OpName}: Subscribed to on Thread: {ThreadId}",
opName,
Thread.CurrentThread.ManagedThreadId);
try
{
var subscription = source
.Do(
x => log.Debug("{OpName}: OnNext({Result}) on Thread: {ThreadId}", opName, x,
Thread.CurrentThread.ManagedThreadId),
ex => log.Debug("{OpName}: OnError({Result}) on Thread: {ThreadId}", opName, ex.Message,
Thread.CurrentThread.ManagedThreadId),
() => log.Debug("{OpName}: OnCompleted() on Thread: {ThreadId}", opName,
Thread.CurrentThread.ManagedThreadId))
.Subscribe(obs);
return new CompositeDisposable(
subscription,
Disposable.Create(() => log.Debug(
"{OpName}: Cleaned up on Thread: {ThreadId}",
opName,
Thread.CurrentThread.ManagedThreadId)));
}
finally
{
log.Debug("{OpName}: Subscription completed", opName);
}
});
}
}
}

@ -20,17 +20,19 @@ namespace Trash.Command
{ {
private readonly IConfigurationLoader<RadarrConfiguration> _configLoader; private readonly IConfigurationLoader<RadarrConfiguration> _configLoader;
private readonly Func<ICustomFormatUpdater> _customFormatUpdaterFactory; private readonly Func<ICustomFormatUpdater> _customFormatUpdaterFactory;
private readonly ILogger _log;
private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory; private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory;
public RadarrCommand( public RadarrCommand(
ILogger logger, ILogger log,
LoggingLevelSwitch loggingLevelSwitch, LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor, ILogJanitor logJanitor,
IConfigurationLoader<RadarrConfiguration> configLoader, IConfigurationLoader<RadarrConfiguration> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory, Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory) Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(logger, loggingLevelSwitch, logJanitor) : base(log, loggingLevelSwitch, logJanitor)
{ {
_log = log;
_configLoader = configLoader; _configLoader = configLoader;
_qualityUpdaterFactory = qualityUpdaterFactory; _qualityUpdaterFactory = qualityUpdaterFactory;
_customFormatUpdaterFactory = customFormatUpdaterFactory; _customFormatUpdaterFactory = customFormatUpdaterFactory;
@ -58,7 +60,7 @@ namespace Trash.Command
} }
catch (FlurlHttpException e) catch (FlurlHttpException e)
{ {
Log.Error(e, "HTTP error while communicating with Radarr"); _log.Error(e, "HTTP error while communicating with Radarr");
ExitDueToFailure(); ExitDueToFailure();
} }
} }

@ -19,18 +19,20 @@ namespace Trash.Command
public class SonarrCommand : ServiceCommand public class SonarrCommand : ServiceCommand
{ {
private readonly IConfigurationLoader<SonarrConfiguration> _configLoader; private readonly IConfigurationLoader<SonarrConfiguration> _configLoader;
private readonly ILogger _log;
private readonly Func<IReleaseProfileUpdater> _profileUpdaterFactory; private readonly Func<IReleaseProfileUpdater> _profileUpdaterFactory;
private readonly Func<ISonarrQualityDefinitionUpdater> _qualityUpdaterFactory; private readonly Func<ISonarrQualityDefinitionUpdater> _qualityUpdaterFactory;
public SonarrCommand( public SonarrCommand(
ILogger logger, ILogger log,
LoggingLevelSwitch loggingLevelSwitch, LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor, ILogJanitor logJanitor,
IConfigurationLoader<SonarrConfiguration> configLoader, IConfigurationLoader<SonarrConfiguration> configLoader,
Func<IReleaseProfileUpdater> profileUpdaterFactory, Func<IReleaseProfileUpdater> profileUpdaterFactory,
Func<ISonarrQualityDefinitionUpdater> qualityUpdaterFactory) Func<ISonarrQualityDefinitionUpdater> qualityUpdaterFactory)
: base(logger, loggingLevelSwitch, logJanitor) : base(log, loggingLevelSwitch, logJanitor)
{ {
_log = log;
_configLoader = configLoader; _configLoader = configLoader;
_profileUpdaterFactory = profileUpdaterFactory; _profileUpdaterFactory = profileUpdaterFactory;
_qualityUpdaterFactory = qualityUpdaterFactory; _qualityUpdaterFactory = qualityUpdaterFactory;
@ -60,7 +62,7 @@ namespace Trash.Command
} }
catch (FlurlHttpException e) catch (FlurlHttpException e)
{ {
Log.Error(e, "HTTP error while communicating with Sonarr"); _log.Error(e, "HTTP error while communicating with Sonarr");
ExitDueToFailure(); ExitDueToFailure();
} }
} }

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using AutoMapper; using AutoMapper;
using FluentAssertions; using FluentAssertions;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -55,7 +57,6 @@ namespace TrashLib.Tests.Sonarr.Api
var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1))); var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1)));
_ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored;
result.Should().BeEquivalentTo(new SonarrReleaseProfile result.Should().BeEquivalentTo(new SonarrReleaseProfile
{ {
Ignored = new List<string> {"one", "two", "three"} Ignored = new List<string> {"one", "two", "three"}
@ -73,38 +74,43 @@ namespace TrashLib.Tests.Sonarr.Api
var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV2))); var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV2)));
_ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored;
result.Should().BeEquivalentTo(dataV2); result.Should().BeEquivalentTo(dataV2);
} }
[Test] [Test]
public void Send_v2_to_v1() public async Task Send_v2_to_v1()
{ {
using var ctx = new TestContext(); using var ctx = new TestContext();
var compat = Substitute.For<ISonarrCompatibility>(); var compat = Substitute.For<ISonarrCompatibility>();
compat.ArraysNeededForReleaseProfileRequiredAndIgnored.Returns(false); compat.Capabilities.Returns(new[]
{
new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = false}
}.ToObservable());
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}}; var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
var result = sut.CompatibleReleaseProfileForSending(data); var result = await sut.CompatibleReleaseProfileForSendingAsync(data);
result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"}); result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"});
} }
[Test] [Test]
public void Send_v2_to_v2() public async Task Send_v2_to_v2()
{ {
using var ctx = new TestContext(); using var ctx = new TestContext();
var compat = Substitute.For<ISonarrCompatibility>(); var compat = Substitute.For<ISonarrCompatibility>();
compat.ArraysNeededForReleaseProfileRequiredAndIgnored.Returns(true); compat.Capabilities.Returns(new[]
{
new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = true}
}.ToObservable());
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}}; var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
var result = sut.CompatibleReleaseProfileForSending(data); var result = await sut.CompatibleReleaseProfileForSendingAsync(data);
result.Should().BeEquivalentTo(data); result.Should().BeEquivalentTo(data);
} }

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Api.Objects;
@ -5,7 +6,7 @@ namespace TrashLib.Sonarr.Api
{ {
public interface ISonarrReleaseProfileCompatibilityHandler public interface ISonarrReleaseProfileCompatibilityHandler
{ {
object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile); Task<object> CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile);
SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile); SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile);
} }
} }

@ -54,16 +54,18 @@ namespace TrashLib.Sonarr.Api
public async Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate) public async Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate)
{ {
var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(profileToUpdate);
await BaseUrl() await BaseUrl()
.AppendPathSegment($"releaseprofile/{profileToUpdate.Id}") .AppendPathSegment($"releaseprofile/{profileToUpdate.Id}")
.PutJsonAsync(_profileHandler.CompatibleReleaseProfileForSending(profileToUpdate)); .PutJsonAsync(profileToSend);
} }
public async Task<SonarrReleaseProfile> CreateReleaseProfile(SonarrReleaseProfile newProfile) public async Task<SonarrReleaseProfile> CreateReleaseProfile(SonarrReleaseProfile newProfile)
{ {
var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(newProfile);
var response = await BaseUrl() var response = await BaseUrl()
.AppendPathSegment("releaseprofile") .AppendPathSegment("releaseprofile")
.PostJsonAsync(_profileHandler.CompatibleReleaseProfileForSending(newProfile)) .PostJsonAsync(profileToSend)
.ReceiveJson<JObject>(); .ReceiveJson<JObject>();
return _profileHandler.CompatibleReleaseProfileForReceiving(response); return _profileHandler.CompatibleReleaseProfileForReceiving(response);

@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reactive.Linq;
using System.Threading.Tasks;
using AutoMapper; using AutoMapper;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema;
@ -22,9 +24,10 @@ namespace TrashLib.Sonarr.Api
_mapper = mapper; _mapper = mapper;
} }
public object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile) public async Task<object> CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile)
{ {
return _compatibility.ArraysNeededForReleaseProfileRequiredAndIgnored var capabilities = await _compatibility.Capabilities.LastAsync();
return capabilities.ArraysNeededForReleaseProfileRequiredAndIgnored
? profile ? profile
: _mapper.Map<SonarrReleaseProfileV1>(profile); : _mapper.Map<SonarrReleaseProfileV1>(profile);
} }

@ -1,10 +1,10 @@
using System;
namespace TrashLib.Sonarr namespace TrashLib.Sonarr
{ {
public interface ISonarrCompatibility public interface ISonarrCompatibility
{ {
bool SupportsNamedReleaseProfiles { get; } IObservable<SonarrCapabilities> Capabilities { get; }
bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; } Version MinimumVersion { get; }
string InformationalVersion { get; }
string MinimumVersion { get; }
} }
} }

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Common.Extensions; using Common.Extensions;
using Serilog; using Serilog;
@ -53,12 +54,19 @@ namespace TrashLib.Sonarr.ReleaseProfile
} }
} }
private void DoVersionEnforcement() private async Task DoVersionEnforcement()
{ {
if (!_compatibility.SupportsNamedReleaseProfiles) // _compatibility.Capabilities
// .Where(x => !x.SupportsNamedReleaseProfiles)
// .Subscribe(x => throw new VersionException(
// $"Your Sonarr version {x.Version} does not meet the minimum " +
// $"required version of {_compatibility.MinimumVersion} to use this program"));
var capabilities = await _compatibility.Capabilities.LastAsync();
if (!capabilities.SupportsNamedReleaseProfiles)
{ {
throw new VersionException( throw new VersionException(
$"Your Sonarr version {_compatibility.InformationalVersion} does not meet the minimum " + $"Your Sonarr version {capabilities.Version} does not meet the minimum " +
$"required version of {_compatibility.MinimumVersion} to use this program"); $"required version of {_compatibility.MinimumVersion} to use this program");
} }
} }
@ -128,7 +136,7 @@ namespace TrashLib.Sonarr.ReleaseProfile
private async Task ProcessReleaseProfiles(IDictionary<string, ProfileData> profiles, private async Task ProcessReleaseProfiles(IDictionary<string, ProfileData> profiles,
ReleaseProfileConfig config) ReleaseProfileConfig config)
{ {
DoVersionEnforcement(); await DoVersionEnforcement();
List<int> tagIds = new(); List<int> tagIds = new();

@ -0,0 +1,27 @@
using System;
namespace TrashLib.Sonarr
{
public record SonarrCapabilities
{
public SonarrCapabilities()
{
Version = new Version();
}
public SonarrCapabilities(Version version)
{
Version = version;
}
public Version Version { get; }
public bool SupportsNamedReleaseProfiles { get; init; }
// Background: Issue #16 filed which points to a backward-breaking API
// change made in Sonarr at commit [deed85d2f].
//
// [deed85d2f]: https://github.com/Sonarr/Sonarr/commit/deed85d2f9147e6180014507ef4f5af3695b0c61
public bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; init; }
}
}

@ -1,6 +1,6 @@
using System; using System;
using System.Reactive.Concurrency;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Flurl.Http; using Flurl.Http;
using TrashLib.Config; using TrashLib.Config;
@ -8,30 +8,32 @@ namespace TrashLib.Sonarr
{ {
public class SonarrCompatibility : ISonarrCompatibility public class SonarrCompatibility : ISonarrCompatibility
{ {
private Version _version = new();
public SonarrCompatibility(IServerInfo serverInfo) public SonarrCompatibility(IServerInfo serverInfo)
{ {
var task = serverInfo.BuildRequest() Capabilities = Observable.FromAsync(
.AppendPathSegment("system/status") async () => await serverInfo.BuildRequest()
.GetJsonAsync(); .AppendPathSegment("system/status")
.GetJsonAsync(), NewThreadScheduler.Default)
task.ToObservable() .Timeout(TimeSpan.FromSeconds(15))
.Select(x => new Version(x.version)) .Select(x => new Version(x.version))
.Subscribe(x => _version = x); .Select(BuildCapabilitiesObject)
.Replay(1)
.AutoConnect();
} }
public bool SupportsNamedReleaseProfiles => public IObservable<SonarrCapabilities> Capabilities { get; }
_version >= new Version(MinimumVersion); public Version MinimumVersion => new("3.0.4.1098");
// Background: Issue #16 filed which points to a backward-breaking API private SonarrCapabilities BuildCapabilitiesObject(Version version)
// change made in Sonarr at commit [deed85d2f]. {
// return new SonarrCapabilities(version)
// [deed85d2f]: https://github.com/Sonarr/Sonarr/commit/deed85d2f9147e6180014507ef4f5af3695b0c61 {
public bool ArraysNeededForReleaseProfileRequiredAndIgnored => SupportsNamedReleaseProfiles =
_version >= new Version("3.0.6.1355"); version >= MinimumVersion,
public string InformationalVersion => _version.ToString(); ArraysNeededForReleaseProfileRequiredAndIgnored =
public string MinimumVersion => "3.0.4.1098"; version >= new Version("3.0.6.1355")
};
}
} }
} }

Loading…
Cancel
Save