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">
<ItemGroup>
<PackageReference Include="FluentValidation" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="Autofac" />
<PackageReference Include="FluentValidation" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="YamlDotNet" />
</ItemGroup>
</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 Func<ICustomFormatUpdater> _customFormatUpdaterFactory;
private readonly ILogger _log;
private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory;
public RadarrCommand(
ILogger logger,
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
IConfigurationLoader<RadarrConfiguration> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(logger, loggingLevelSwitch, logJanitor)
: base(log, loggingLevelSwitch, logJanitor)
{
_log = log;
_configLoader = configLoader;
_qualityUpdaterFactory = qualityUpdaterFactory;
_customFormatUpdaterFactory = customFormatUpdaterFactory;
@ -58,7 +60,7 @@ namespace Trash.Command
}
catch (FlurlHttpException e)
{
Log.Error(e, "HTTP error while communicating with Radarr");
_log.Error(e, "HTTP error while communicating with Radarr");
ExitDueToFailure();
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using AutoMapper;
using FluentAssertions;
using Newtonsoft.Json;
@ -55,7 +57,6 @@ namespace TrashLib.Tests.Sonarr.Api
var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1)));
_ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored;
result.Should().BeEquivalentTo(new SonarrReleaseProfile
{
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)));
_ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored;
result.Should().BeEquivalentTo(dataV2);
}
[Test]
public void Send_v2_to_v1()
public async Task Send_v2_to_v1()
{
using var ctx = new TestContext();
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 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"});
}
[Test]
public void Send_v2_to_v2()
public async Task Send_v2_to_v2()
{
using var ctx = new TestContext();
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 sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
var result = sut.CompatibleReleaseProfileForSending(data);
var result = await sut.CompatibleReleaseProfileForSendingAsync(data);
result.Should().BeEquivalentTo(data);
}

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

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

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Reactive.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
@ -22,9 +24,10 @@ namespace TrashLib.Sonarr.Api
_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
: _mapper.Map<SonarrReleaseProfileV1>(profile);
}

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

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Common.Extensions;
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(
$"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");
}
}
@ -128,7 +136,7 @@ namespace TrashLib.Sonarr.ReleaseProfile
private async Task ProcessReleaseProfiles(IDictionary<string, ProfileData> profiles,
ReleaseProfileConfig config)
{
DoVersionEnforcement();
await DoVersionEnforcement();
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.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Flurl.Http;
using TrashLib.Config;
@ -8,30 +8,32 @@ namespace TrashLib.Sonarr
{
public class SonarrCompatibility : ISonarrCompatibility
{
private Version _version = new();
public SonarrCompatibility(IServerInfo serverInfo)
{
var task = serverInfo.BuildRequest()
.AppendPathSegment("system/status")
.GetJsonAsync();
task.ToObservable()
Capabilities = Observable.FromAsync(
async () => await serverInfo.BuildRequest()
.AppendPathSegment("system/status")
.GetJsonAsync(), NewThreadScheduler.Default)
.Timeout(TimeSpan.FromSeconds(15))
.Select(x => new Version(x.version))
.Subscribe(x => _version = x);
.Select(BuildCapabilitiesObject)
.Replay(1)
.AutoConnect();
}
public bool SupportsNamedReleaseProfiles =>
_version >= new Version(MinimumVersion);
public IObservable<SonarrCapabilities> Capabilities { get; }
public Version MinimumVersion => new("3.0.4.1098");
// 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 =>
_version >= new Version("3.0.6.1355");
private SonarrCapabilities BuildCapabilitiesObject(Version version)
{
return new SonarrCapabilities(version)
{
SupportsNamedReleaseProfiles =
version >= MinimumVersion,
public string InformationalVersion => _version.ToString();
public string MinimumVersion => "3.0.4.1098";
ArraysNeededForReleaseProfileRequiredAndIgnored =
version >= new Version("3.0.6.1355")
};
}
}
}

Loading…
Cancel
Save