1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-14 11:23:42 +02:00

Command queue

New: Adding multiple series will queue them instead of running all at once
New: Slower scheduled tasks won't be block others from running
This commit is contained in:
Mark McDowall 2015-01-15 16:30:09 -08:00
parent 446d470f53
commit 638e3ca898
66 changed files with 1151 additions and 636 deletions

View File

@ -116,14 +116,12 @@ public void should_map_profile()
profileResource.InjectTo<Profile>();
}
[Test]
public void should_map_tracked_command()
{
var profileResource = new ApplicationUpdateCommand();
profileResource.InjectTo<CommandResource>();
var commandResource = new CommandModel { Body = new ApplicationUpdateCommand() };
commandResource.InjectTo<CommandResource>();
}
}

View File

@ -4,10 +4,9 @@
using NzbDrone.Api.Extensions;
using NzbDrone.Api.Mapping;
using NzbDrone.Api.Validation;
using NzbDrone.Common.Composition;
using NzbDrone.Common;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging;
using NzbDrone.SignalR;
@ -15,56 +14,53 @@
namespace NzbDrone.Api.Commands
{
public class CommandModule : NzbDroneRestModuleWithSignalR<CommandResource, Command>, IHandle<CommandUpdatedEvent>
public class CommandModule : NzbDroneRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
{
private readonly ICommandExecutor _commandExecutor;
private readonly IContainer _container;
private readonly ITrackCommands _trackCommands;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IServiceFactory _serviceFactory;
public CommandModule(ICommandExecutor commandExecutor,
public CommandModule(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster,
IContainer container,
ITrackCommands trackCommands)
IServiceFactory serviceFactory)
: base(signalRBroadcaster)
{
_commandExecutor = commandExecutor;
_container = container;
_trackCommands = trackCommands;
_commandQueueManager = commandQueueManager;
_serviceFactory = serviceFactory;
GetResourceById = GetCommand;
CreateResource = StartCommand;
GetResourceAll = GetAllCommands;
GetResourceAll = GetStartedCommands;
PostValidator.RuleFor(c => c.Name).NotBlank();
}
private CommandResource GetCommand(int id)
{
return _trackCommands.GetById(id).InjectTo<CommandResource>();
return _commandQueueManager.Get(id).InjectTo<CommandResource>();
}
private int StartCommand(CommandResource commandResource)
{
var commandType =
_container.GetImplementations(typeof(Command))
.Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
_serviceFactory.GetImplementations(typeof (Command))
.Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
dynamic command = Request.Body.FromJson(commandType);
command.Manual = true;
command.Trigger = CommandTrigger.Manual;
var trackedCommand = (Command)_commandExecutor.PublishCommandAsync(command);
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
return trackedCommand.Id;
}
private List<CommandResource> GetAllCommands()
private List<CommandResource> GetStartedCommands()
{
return ToListResource(_trackCommands.RunningCommands);
return ToListResource(_commandQueueManager.GetStarted());
}
public void Handle(CommandUpdatedEvent message)
{
if (message.Command.SendUpdatesToClient)
if (message.Command.Body.SendUpdatesToClient)
{
BroadcastResourceChange(ModelAction.Updated, message.Command.Id);
}

View File

@ -1,6 +1,7 @@
using System;
using Newtonsoft.Json;
using NzbDrone.Api.REST;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Api.Commands
{
@ -8,11 +9,75 @@ public class CommandResource : RestResource
{
public String Name { get; set; }
public String Message { get; set; }
public DateTime StartedOn { get; set; }
public DateTime StateChangeTime { get; set; }
public Boolean SendUpdatesToClient { get; set; }
public CommandStatus State { get; set; }
public Command Body { get; set; }
public CommandPriority Priority { get; set; }
public CommandStatus Status { get; set; }
public DateTime Queued { get; set; }
public DateTime? Started { get; set; }
public DateTime? Ended { get; set; }
public TimeSpan? Duration { get; set; }
public string Exception { get; set; }
public CommandTrigger Trigger { get; set; }
[JsonIgnore]
public string CompletionMessage { get; set; }
//Legacy
public CommandStatus State
{
get
{
return Status;
}
set { }
}
public Boolean Manual
{
get
{
return Trigger == CommandTrigger.Manual;
}
set { }
}
public DateTime StartedOn
{
get
{
return Queued;
}
set { }
}
public DateTime? StateChangeTime
{
get
{
if (Started.HasValue) return Started.Value;
return Ended;
}
set { }
}
public Boolean SendUpdatesToClient
{
get
{
if (Body != null) return Body.SendUpdatesToClient;
return false;
}
set { }
}
public DateTime? LastExecutionTime { get; set; }
public Boolean Manual { get; set; }
}
}
}

View File

@ -100,6 +100,14 @@ public void Clear()
_store.Clear();
}
public void ClearExpired()
{
foreach (var cached in _store.Where(c => c.Value.IsExpired()))
{
Remove(cached.Key);
}
}
public ICollection<T> Values
{
get

View File

@ -6,6 +6,7 @@ namespace NzbDrone.Common.Cache
public interface ICached
{
void Clear();
void ClearExpired();
void Remove(string key);
int Count { get; }
}

View File

@ -61,8 +61,8 @@ public void should_not_scan_if_series_root_folder_does_not_exist()
ExceptionVerification.ExpectedWarns(1);
Mocker.GetMock<ICommandExecutor>()
.Verify(v => v.PublishCommand(It.IsAny<CleanMediaFileDb>()), Times.Never());
Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.Clean(It.IsAny<Series>()), Times.Never());
}
[Test]
@ -80,8 +80,8 @@ public void should_not_scan_if_series_root_folder_is_empty()
ExceptionVerification.ExpectedWarns(1);
Mocker.GetMock<ICommandExecutor>()
.Verify(v => v.PublishCommand(It.IsAny<CleanMediaFileDb>()), Times.Never());
Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.Clean(It.IsAny<Series>()), Times.Never());
}
[Test]

View File

@ -6,7 +6,6 @@
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
@ -16,6 +15,7 @@ public class MediaFileTableCleanupServiceFixture : CoreTest<MediaFileTableCleanu
{
private const string DELETED_PATH = "ANY FILE WITH THIS PATH IS CONSIDERED DELETED!";
private List<Episode> _episodes;
private Series _series;
[SetUp]
public void SetUp()
@ -24,9 +24,8 @@ public void SetUp()
.Build()
.ToList();
Mocker.GetMock<ISeriesService>()
.Setup(s => s.GetSeries(It.IsAny<Int32>()))
.Returns(Builder<Series>.CreateNew().Build());
_series = Builder<Series>.CreateNew()
.Build();
Mocker.GetMock<IDiskProvider>()
.Setup(e => e.FileExists(It.Is<String>(c => !c.Contains(DELETED_PATH))))
@ -61,7 +60,7 @@ public void should_skip_files_that_exist_in_disk()
GivenEpisodeFiles(episodeFiles);
Subject.Execute(new CleanMediaFileDb(0));
Subject.Clean(_series);
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
}
@ -76,7 +75,7 @@ public void should_delete_non_existent_files()
GivenEpisodeFiles(episodeFiles);
Subject.Execute(new CleanMediaFileDb(0));
Subject.Clean(_series);
Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.Is<EpisodeFile>(e => e.RelativePath == DELETED_PATH), DeleteMediaFileReason.MissingFromDisk), Times.Exactly(2));
}
@ -92,7 +91,7 @@ public void should_delete_files_that_dont_belong_to_any_episodes()
GivenEpisodeFiles(episodeFiles);
GivenFilesAreNotAttachedToEpisode();
Subject.Execute(new CleanMediaFileDb(0));
Subject.Clean(_series);
Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.IsAny<EpisodeFile>(), DeleteMediaFileReason.NoLinkedEpisodes), Times.Exactly(10));
}
@ -102,7 +101,7 @@ public void should_unlink_episode_when_episodeFile_does_not_exist()
{
GivenEpisodeFiles(new List<EpisodeFile>());
Subject.Execute(new CleanMediaFileDb(0));
Subject.Clean(_series);
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.Is<Episode>(e => e.EpisodeFileId == 0)), Times.Exactly(10));
}
@ -117,7 +116,7 @@ public void should_not_update_episode_when_episodeFile_exists()
GivenEpisodeFiles(episodeFiles);
Subject.Execute(new CleanMediaFileDb(0));
Subject.Clean(_series);
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
}

View File

@ -1,121 +1,121 @@
using System;
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Messaging.Commands
{
[TestFixture]
public class CommandExecutorFixture : TestBase<CommandExecutor>
{
private Mock<IExecute<CommandA>> _executorA;
private Mock<IExecute<CommandB>> _executorB;
[SetUp]
public void Setup()
{
_executorA = new Mock<IExecute<CommandA>>();
_executorB = new Mock<IExecute<CommandB>>();
Mocker.GetMock<IServiceFactory>()
.Setup(c => c.Build(typeof(IExecute<CommandA>)))
.Returns(_executorA.Object);
Mocker.GetMock<IServiceFactory>()
.Setup(c => c.Build(typeof(IExecute<CommandB>)))
.Returns(_executorB.Object);
Mocker.GetMock<ITrackCommands>()
.Setup(c => c.FindExisting(It.IsAny<Command>()))
.Returns<Command>(null);
}
[Test]
public void should_publish_command_to_executor()
{
var commandA = new CommandA();
Subject.PublishCommand(commandA);
_executorA.Verify(c => c.Execute(commandA), Times.Once());
}
[Test]
public void should_publish_command_by_with_optional_arg_using_name()
{
Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command)))
.Returns(new List<Type> { typeof(CommandA), typeof(CommandB) });
Subject.PublishCommand(typeof(CommandA).FullName);
_executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once());
}
[Test]
public void should_not_publish_to_incompatible_executor()
{
var commandA = new CommandA();
Subject.PublishCommand(commandA);
_executorA.Verify(c => c.Execute(commandA), Times.Once());
_executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never());
}
[Test]
public void broken_executor_should_throw_the_exception()
{
var commandA = new CommandA();
_executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
.Throws(new NotImplementedException());
Assert.Throws<NotImplementedException>(() => Subject.PublishCommand(commandA));
}
[Test]
public void broken_executor_should_publish_executed_event()
{
var commandA = new CommandA();
_executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
.Throws(new NotImplementedException());
Assert.Throws<NotImplementedException>(() => Subject.PublishCommand(commandA));
VerifyEventPublished<CommandExecutedEvent>();
}
[Test]
public void should_publish_executed_event_on_success()
{
var commandA = new CommandA();
Subject.PublishCommand(commandA);
VerifyEventPublished<CommandExecutedEvent>();
}
}
public class CommandA : Command
{
public CommandA(int id = 0)
{
}
}
public class CommandB : Command
{
public CommandB()
{
}
}
}
//using System;
//using System.Collections.Generic;
//using Moq;
//using NUnit.Framework;
//using NzbDrone.Common;
//using NzbDrone.Core.Messaging.Commands;
//using NzbDrone.Core.Messaging.Commands.Tracking;
//using NzbDrone.Core.Messaging.Events;
//using NzbDrone.Test.Common;
//
//namespace NzbDrone.Core.Test.Messaging.Commands
//{
// [TestFixture]
// public class CommandExecutorFixture : TestBase<CommandExecutor>
// {
// private Mock<IExecute<CommandA>> _executorA;
// private Mock<IExecute<CommandB>> _executorB;
//
// [SetUp]
// public void Setup()
// {
// _executorA = new Mock<IExecute<CommandA>>();
// _executorB = new Mock<IExecute<CommandB>>();
//
// Mocker.GetMock<IServiceFactory>()
// .Setup(c => c.Build(typeof(IExecute<CommandA>)))
// .Returns(_executorA.Object);
//
// Mocker.GetMock<IServiceFactory>()
// .Setup(c => c.Build(typeof(IExecute<CommandB>)))
// .Returns(_executorB.Object);
//
//
// Mocker.GetMock<ITrackCommands>()
// .Setup(c => c.FindExisting(It.IsAny<Command>()))
// .Returns<Command>(null);
// }
//
// [Test]
// public void should_publish_command_to_executor()
// {
// var commandA = new CommandA();
//
// Subject.Push(commandA);
//
// _executorA.Verify(c => c.Execute(commandA), Times.Once());
// }
//
// [Test]
// public void should_publish_command_by_with_optional_arg_using_name()
// {
// Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command)))
// .Returns(new List<Type> { typeof(CommandA), typeof(CommandB) });
//
// Subject.Push(typeof(CommandA).FullName);
// _executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once());
// }
//
//
// [Test]
// public void should_not_publish_to_incompatible_executor()
// {
// var commandA = new CommandA();
//
// Subject.Push(commandA);
//
// _executorA.Verify(c => c.Execute(commandA), Times.Once());
// _executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never());
// }
//
// [Test]
// public void broken_executor_should_throw_the_exception()
// {
// var commandA = new CommandA();
//
// _executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
// .Throws(new NotImplementedException());
//
// Assert.Throws<NotImplementedException>(() => Subject.Push(commandA));
// }
//
//
// [Test]
// public void broken_executor_should_publish_executed_event()
// {
// var commandA = new CommandA();
//
// _executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
// .Throws(new NotImplementedException());
//
// Assert.Throws<NotImplementedException>(() => Subject.Push(commandA));
//
// VerifyEventPublished<CommandExecutedEvent>();
// }
//
// [Test]
// public void should_publish_executed_event_on_success()
// {
// var commandA = new CommandA();
// Subject.Push(commandA);
//
// VerifyEventPublished<CommandExecutedEvent>();
// }
// }
//
// public class CommandA : Command
// {
// public CommandA(int id = 0)
// {
// }
// }
//
// public class CommandB : Command
// {
//
// public CommandB()
// {
// }
// }
//
//}

View File

@ -1,19 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Update.Commands;
namespace NzbDrone.Core.Test.Messaging.Commands
{
[TestFixture]
public class CommandFixture
{
[Test]
public void default_values()
{
var command = new ApplicationUpdateCommand();
command.Id.Should().NotBe(0);
command.Name.Should().Be("ApplicationUpdate");
}
}
}

View File

@ -257,7 +257,6 @@
<Compile Include="MediaFiles\UpgradeMediaFileServiceFixture.cs" />
<Compile Include="Messaging\Commands\CommandEqualityComparerFixture.cs" />
<Compile Include="Messaging\Commands\CommandExecutorFixture.cs" />
<Compile Include="Messaging\Commands\CommandFixture.cs" />
<Compile Include="Messaging\Events\EventAggregatorFixture.cs" />
<Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
<Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />

View File

@ -8,7 +8,6 @@
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Test.TvTests.SeriesAddedHandlerTests
{

View File

@ -12,7 +12,7 @@
using NzbDrone.Common.Model;
using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Update;
using NzbDrone.Core.Update.Commands;
@ -163,7 +163,7 @@ public void should_not_extract_if_verification_fails()
{
Mocker.GetMock<IVerifyUpdates>().Setup(c => c.Verify(It.IsAny<UpdatePackage>(), It.IsAny<String>())).Returns(false);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
Mocker.GetMock<IArchiveService>().Verify(v => v.Extract(It.IsAny<String>(), It.IsAny<String>()), Times.Never());
}
@ -189,7 +189,7 @@ public void should_throw_if_script_is_not_set()
GivenInstallScript("");
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
@ -203,7 +203,7 @@ public void should_throw_if_script_is_null()
GivenInstallScript(null);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
@ -221,7 +221,7 @@ public void should_throw_if_script_path_does_not_exist()
.Setup(s => s.FileExists(scriptPath, StringComparison.Ordinal))
.Returns(false);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
@ -255,7 +255,7 @@ public void should_log_error_when_app_data_is_child_of_startup_folder()
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone\AppData".AsOsAgnostic);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1);
}
@ -265,7 +265,7 @@ public void should_log_error_when_app_data_is_same_as_startup_folder()
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1);
}
@ -278,7 +278,7 @@ public void should_log_error_when_startup_folder_is_not_writable()
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
ExceptionVerification.ExpectedErrors(1);
@ -293,7 +293,7 @@ public void should_log_when_install_cannot_be_started()
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName);
Subject.Execute(new ApplicationUpdateCommand());
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
ExceptionVerification.ExpectedErrors(1);

View File

@ -0,0 +1,38 @@
using System;
using Marr.Data.Converters;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Reflection;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Datastore.Converters
{
public class CommandConverter : EmbeddedDocumentConverter
{
public override object FromDB(ConverterContext context)
{
if (context.DbValue == DBNull.Value)
{
return null;
}
var stringValue = (string)context.DbValue;
if (stringValue.IsNullOrWhiteSpace())
{
return null;
}
var ordinal = context.DataRecord.GetOrdinal("Name");
var contract = context.DataRecord.GetString(ordinal);
var impType = typeof (Command).Assembly.FindTypeByName(contract + "Command");
if (impType == null)
{
throw new CommandNotFoundException(contract);
}
return Json.Deserialize(stringValue, impType);
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Globalization;
using Marr.Data.Converters;
using Marr.Data.Mapping;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Datastore.Converters
{
public class TimeSpanConverter : IConverter
{
public object FromDB(ConverterContext context)
{
if (context.DbValue == DBNull.Value)
{
return TimeSpan.Zero;
}
return TimeSpan.Parse(context.DbValue.ToString());
}
public object FromDB(ColumnMap map, object dbValue)
{
if (dbValue == DBNull.Value)
{
return DBNull.Value;
}
if (dbValue is TimeSpan)
{
return dbValue;
}
return TimeSpan.Parse(dbValue.ToString(), CultureInfo.InvariantCulture);
}
public object ToDB(object clrValue)
{
if (clrValue.ToString().IsNullOrWhiteSpace())
{
return null;
}
return ((TimeSpan)clrValue).ToString("c", CultureInfo.InvariantCulture);
}
public Type DbType { get; private set; }
}
}

View File

@ -0,0 +1,24 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(78)]
public class add_commands_table : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("Commands")
.WithColumn("Name").AsString().NotNullable()
.WithColumn("Body").AsString().NotNullable()
.WithColumn("Priority").AsInt32().NotNullable()
.WithColumn("Status").AsInt32().NotNullable()
.WithColumn("QueuedAt").AsDateTime().NotNullable()
.WithColumn("StartedAt").AsDateTime().Nullable()
.WithColumn("EndedAt").AsDateTime().Nullable()
.WithColumn("Duration").AsString().Nullable()
.WithColumn("Exception").AsString().Nullable()
.WithColumn("Trigger").AsInt32().NotNullable();
}
}
}

View File

@ -31,6 +31,7 @@
using NzbDrone.Core.Tv;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Datastore
{
@ -103,6 +104,8 @@ public static void Map()
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
Mapper.Entity<User>().RegisterModel("Users");
Mapper.Entity<CommandModel>().RegisterModel("Commands")
.Ignore(c => c.Message);
}
private static void RegisterMappers()
@ -125,6 +128,9 @@ private static void RegisterMappers()
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(Command), new CommandConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan), new TimeSpanConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan?), new TimeSpanConverter());
}
private static void RegisterProviderSettingConverter()

View File

@ -12,14 +12,17 @@ public class RedownloadFailedDownloadService : IHandleAsync<DownloadFailedEvent>
{
private readonly IConfigService _configService;
private readonly IEpisodeService _episodeService;
private readonly ICommandExecutor _commandExecutor;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
public RedownloadFailedDownloadService(IConfigService configService, IEpisodeService episodeService, ICommandExecutor commandExecutor, Logger logger)
public RedownloadFailedDownloadService(IConfigService configService,
IEpisodeService episodeService,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_configService = configService;
_episodeService = episodeService;
_commandExecutor = commandExecutor;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
@ -35,7 +38,7 @@ public void HandleAsync(DownloadFailedEvent message)
{
_logger.Debug("Failed download only contains one episode, searching again");
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds));
_commandQueueManager.Push(new EpisodeSearchCommand(message.EpisodeIds));
return;
}
@ -47,7 +50,7 @@ public void HandleAsync(DownloadFailedEvent message)
{
_logger.Debug("Failed download was entire season, searching again");
_commandExecutor.PublishCommandAsync(new SeasonSearchCommand
_commandQueueManager.Push(new SeasonSearchCommand
{
SeriesId = message.SeriesId,
SeasonNumber = seasonNumber
@ -58,7 +61,7 @@ public void HandleAsync(DownloadFailedEvent message)
_logger.Debug("Failed download contains multiple episodes, probably a double episode, searching again");
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds));
_commandQueueManager.Push(new EpisodeSearchCommand(message.EpisodeIds));
}
}
}

View File

@ -94,7 +94,7 @@ public void HandleAsync(ApplicationStartedEvent message)
public void Execute(CheckHealthCommand message)
{
PerformHealthCheck(c => message.Manual || c.CheckOnSchedule);
PerformHealthCheck(c => message.Trigger == CommandTrigger.Manual || c.CheckOnSchedule);
}
}
}

View File

@ -0,0 +1,19 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupCommandQueue : IHousekeepingTask
{
private readonly IManageCommandQueue _commandQueueManager;
public CleanupCommandQueue(IManageCommandQueue commandQueueManager)
{
_commandQueueManager = commandQueueManager;
}
public void Clean()
{
_commandQueueManager.CleanCommands();
}
}
}

View File

@ -0,0 +1,19 @@
using NzbDrone.Core.Instrumentation;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class TrimLogDatabase : IHousekeepingTask
{
private readonly ILogRepository _logRepo;
public TrimLogDatabase(ILogRepository logRepo)
{
_logRepo = logRepo;
}
public void Clean()
{
_logRepo.Trim();
}
}
}

View File

@ -8,7 +8,8 @@
namespace NzbDrone.Core.Housekeeping
{
public class HousekeepingService : IExecute<HousekeepingCommand>, IHandleAsync<ApplicationStartedEvent>
public class HousekeepingService : IExecute<HousekeepingCommand>,
IHandleAsync<ApplicationStartedEvent>
{
private readonly IEnumerable<IHousekeepingTask> _housekeepers;
private readonly Logger _logger;
@ -40,7 +41,7 @@ private void Clean()
}
}
// Vacuuming the log db isn't needed since that's done hourly at the TrimLogCommand.
// Vacuuming the log db isn't needed since that's done in a separate housekeeping task
_logger.Debug("Compressing main database after housekeeping");
_mainDb.Vacuum();
}

View File

@ -112,9 +112,9 @@ public void Execute(MissingEpisodeSearchCommand message)
{
List<Episode> episodes;
if (message.SeriesId > 0)
if (message.SeriesId.HasValue)
{
episodes = _episodeService.GetEpisodeBySeries(message.SeriesId)
episodes = _episodeService.GetEpisodeBySeries(message.SeriesId.Value)
.Where(e => e.Monitored &&
!e.HasFile &&
e.AirDateUtc.HasValue &&

View File

@ -4,7 +4,7 @@ namespace NzbDrone.Core.IndexerSearch
{
public class MissingEpisodeSearchCommand : Command
{
public int SeriesId { get; private set; }
public int? SeriesId { get; private set; }
public override bool SendUpdatesToClient
{

View File

@ -1,8 +0,0 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Instrumentation.Commands
{
public class TrimLogCommand : Command
{
}
}

View File

@ -9,7 +9,7 @@ public interface ILogService
PagingSpec<Log> Paged(PagingSpec<Log> pagingSpec);
}
public class LogService : ILogService, IExecute<TrimLogCommand>, IExecute<ClearLogCommand>
public class LogService : ILogService, IExecute<ClearLogCommand>
{
private readonly ILogRepository _logRepository;
@ -23,11 +23,6 @@ public PagingSpec<Log> Paged(PagingSpec<Log> pagingSpec)
return _logRepository.GetPaged(pagingSpec);
}
public void Execute(TrimLogCommand message)
{
_logRepository.Trim();
}
public void Execute(ClearLogCommand message)
{
_logRepository.Purge(vacuum: true);

View File

@ -12,7 +12,6 @@ public interface IScheduledTaskRepository : IBasicRepository<ScheduledTask>
void SetLastExecutionTime(int id, DateTime executionTime);
}
public class ScheduledTaskRepository : BasicRepository<ScheduledTask>, IScheduledTaskRepository
{

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NLog;
@ -15,15 +16,15 @@ public class Scheduler :
IHandle<ApplicationShutdownRequested>
{
private readonly ITaskManager _taskManager;
private readonly ICommandExecutor _commandExecutor;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
private static readonly Timer Timer = new Timer();
private static CancellationTokenSource _cancellationTokenSource;
public Scheduler(ITaskManager taskManager, ICommandExecutor commandExecutor, Logger logger)
public Scheduler(ITaskManager taskManager, IManageCommandQueue commandQueueManager, Logger logger)
{
_taskManager = taskManager;
_commandExecutor = commandExecutor;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
@ -33,24 +34,16 @@ private void ExecuteCommands()
{
Timer.Enabled = false;
var tasks = _taskManager.GetPending();
var tasks = _taskManager.GetPending().ToList();
_logger.Trace("Pending Tasks: {0}", tasks.Count);
foreach (var task in tasks)
{
_cancellationTokenSource.Token.ThrowIfCancellationRequested();
try
{
_commandExecutor.PublishCommand(task.TypeName, task.LastExecution);
}
catch (Exception e)
{
_logger.ErrorException("Error occurred while executing task " + task.TypeName, e);
}
_commandQueueManager.Push(task.TypeName, task.LastExecution, CommandPriority.Low, CommandTrigger.Scheduled);
}
}
finally
{
if (!_cancellationTokenSource.IsCancellationRequested)

View File

@ -10,10 +10,9 @@
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Housekeeping;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation.Commands;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Commands;
using NzbDrone.Core.Update.Commands;
@ -62,10 +61,9 @@ public void Handle(ApplicationStartedEvent message)
{
var defaultTasks = new[]
{
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName},
new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName},
new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName},
new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName},
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
@ -127,7 +125,7 @@ private int GetRssSyncInterval()
public void Handle(CommandExecutedEvent message)
{
var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.GetType().FullName);
var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.Body.GetType().FullName);
if (scheduledTask != null)
{

View File

@ -1,14 +0,0 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
{
public class CleanMediaFileDb : Command
{
public int SeriesId { get; private set; }
public CleanMediaFileDb(int seriesId)
{
SeriesId = seriesId;
}
}
}

View File

@ -33,27 +33,27 @@ public class DiskScanService :
private readonly IDiskProvider _diskProvider;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly ICommandExecutor _commandExecutor;
private readonly IConfigService _configService;
private readonly ISeriesService _seriesService;
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public DiskScanService(IDiskProvider diskProvider,
IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes,
ICommandExecutor commandExecutor,
IConfigService configService,
ISeriesService seriesService,
IMediaFileTableCleanupService mediaFileTableCleanupService,
IEventAggregator eventAggregator,
Logger logger)
{
_diskProvider = diskProvider;
_importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes;
_commandExecutor = commandExecutor;
_configService = configService;
_seriesService = seriesService;
_mediaFileTableCleanupService = mediaFileTableCleanupService;
_eventAggregator = eventAggregator;
_logger = logger;
}
@ -79,7 +79,7 @@ public void Scan(Series series)
}
_logger.ProgressInfo("Scanning disk for {0}", series.Title);
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id));
_mediaFileTableCleanupService.Clean(series);
if (!_diskProvider.FolderExists(series.Path))
{

View File

@ -3,19 +3,20 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles
{
public interface IMediaFileTableCleanupService
{
void Clean(Series series);
}
public class MediaFileTableCleanupService : IExecute<CleanMediaFileDb>
public class MediaFileTableCleanupService : IMediaFileTableCleanupService
{
private readonly IMediaFileService _mediaFileService;
private readonly IDiskProvider _diskProvider;
private readonly IEpisodeService _episodeService;
private readonly ISeriesService _seriesService;
private readonly Logger _logger;
public MediaFileTableCleanupService(IMediaFileService mediaFileService,
@ -27,15 +28,13 @@ public MediaFileTableCleanupService(IMediaFileService mediaFileService,
_mediaFileService = mediaFileService;
_diskProvider = diskProvider;
_episodeService = episodeService;
_seriesService = seriesService;
_logger = logger;
}
public void Execute(CleanMediaFileDb message)
public void Clean(Series series)
{
var seriesFile = _mediaFileService.GetFilesBySeries(message.SeriesId);
var series = _seriesService.GetSeries(message.SeriesId);
var episodes = _episodeService.GetEpisodeBySeries(message.SeriesId);
var seriesFile = _mediaFileService.GetFilesBySeries(series.Id);
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
foreach (var episodeFile in seriesFile)
{

View File

@ -0,0 +1,17 @@
namespace NzbDrone.Core.Messaging.Commands
{
public class CleanupCommandMessagingService : IExecute<MessagingCleanupCommand>
{
private readonly IManageCommandQueue _commandQueueManager;
public CleanupCommandMessagingService(IManageCommandQueue commandQueueManager)
{
_commandQueueManager = commandQueueManager;
}
public void Execute(MessagingCleanupCommand message)
{
_commandQueueManager.CleanCommands();
}
}
}

View File

@ -1,20 +1,9 @@
using System;
using FluentMigrator.Runner;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Commands.Tracking;
namespace NzbDrone.Core.Messaging.Commands
{
public abstract class Command : ModelBase, IMessage
public abstract class Command
{
private static readonly object Mutex = new object();
private static int _idCounter;
private readonly StopWatch _stopWatch;
public CommandStatus State { get; private set; }
public DateTime StateChangeTime { get; private set; }
public virtual Boolean SendUpdatesToClient
{
get
@ -23,63 +12,21 @@ public virtual Boolean SendUpdatesToClient
}
}
public TimeSpan Runtime
public virtual string CompletionMessage
{
get
{
return _stopWatch.ElapsedTime();
return "Completed";
}
}
public Boolean Manual { get; set; }
public Exception Exception { get; private set; }
public String Message { get; private set; }
public String Name { get; private set; }
public DateTime? LastExecutionTime { get; set; }
public CommandTrigger Trigger { get; set; }
protected Command()
public Command()
{
Name = GetType().Name.Replace("Command", "");
StateChangeTime = DateTime.UtcNow;
State = CommandStatus.Pending;
_stopWatch = new StopWatch();
Manual = false;
lock (Mutex)
{
Id = ++_idCounter;
}
}
public void Start()
{
_stopWatch.Start();
StateChangeTime = DateTime.UtcNow;
State = CommandStatus.Running;
SetMessage("Starting");
}
public void Failed(Exception exception, string message = "Failed")
{
_stopWatch.Stop();
StateChangeTime = DateTime.UtcNow;
State = CommandStatus.Failed;
Exception = exception;
SetMessage(message);
}
public void Completed(string message = "Completed")
{
_stopWatch.Stop();
StateChangeTime = DateTime.UtcNow;
State = CommandStatus.Completed;
SetMessage(message);
}
public void SetMessage(string message)
{
Message = message;
}
}
}
}

View File

@ -69,7 +69,7 @@ public bool Equals(Command x, Command y)
public int GetHashCode(Command obj)
{
return obj.Id.GetHashCode();
return obj.GetHashCode();
}
}
}

View File

@ -1,117 +1,59 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging;
namespace NzbDrone.Core.Messaging.Commands
{
public class CommandExecutor : ICommandExecutor
public class CommandExecutor : IHandle<ApplicationStartedEvent>,
IHandle<ApplicationShutdownRequested>
{
private readonly Logger _logger;
private readonly IServiceFactory _serviceFactory;
private readonly ITrackCommands _trackCommands;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IEventAggregator _eventAggregator;
private readonly TaskFactory _taskFactory;
public CommandExecutor(Logger logger, IServiceFactory serviceFactory, ITrackCommands trackCommands, IEventAggregator eventAggregator)
private static CancellationTokenSource _cancellationTokenSource;
private const int THREAD_LIMIT = 3;
public CommandExecutor(IServiceFactory serviceFactory,
IManageCommandQueue commandQueueManager,
IEventAggregator eventAggregator,
Logger logger)
{
var scheduler = new LimitedConcurrencyLevelTaskScheduler(3);
_logger = logger;
_serviceFactory = serviceFactory;
_trackCommands = trackCommands;
_commandQueueManager = commandQueueManager;
_eventAggregator = eventAggregator;
_taskFactory = new TaskFactory(scheduler);
}
public void PublishCommand<TCommand>(TCommand command) where TCommand : Command
private void ExecuteCommands()
{
Ensure.That(command, () => command).IsNotNull();
_logger.Trace("Publishing {0}", command.GetType().Name);
if (_trackCommands.FindExisting(command) != null)
try
{
_logger.Trace("Command is already in progress: {0}", command.GetType().Name);
return;
foreach (var command in _commandQueueManager.Queue(_cancellationTokenSource.Token))
{
try
{
ExecuteCommand((dynamic)command.Body, command);
}
catch (Exception ex)
{
_logger.ErrorException("Error occurred while executing task " + command.Name, ex);
}
}
}
_trackCommands.Store(command);
ExecuteCommand<TCommand>(command);
}
public void PublishCommand(string commandTypeName)
{
PublishCommand(commandTypeName, null);
}
public void PublishCommand(string commandTypeName, DateTime? lastExecutionTime)
{
dynamic command = GetCommand(commandTypeName);
command.LastExecutionTime = lastExecutionTime;
PublishCommand(command);
}
public Command PublishCommandAsync<TCommand>(TCommand command) where TCommand : Command
{
Ensure.That(command, () => command).IsNotNull();
_logger.Trace("Publishing {0}", command.GetType().Name);
var existingCommand = _trackCommands.FindExisting(command);
if (existingCommand != null)
catch (ThreadAbortException ex)
{
_logger.Trace("Command is already in progress: {0}", command.GetType().Name);
return existingCommand;
_logger.ErrorException(ex.Message, ex);
Thread.ResetAbort();
}
_trackCommands.Store(command);
// TODO: We should use async await (once we get 4.5) or normal Task Continuations on Command processing to prevent blocking the TaskScheduler.
// For now we use TaskCreationOptions 0x10, which is actually .net 4.5 HideScheduler.
// This will detach the scheduler from the thread, causing new Task creating in the command to be executed on the ThreadPool, avoiding a deadlock.
// Please note that the issue only shows itself on mono because since Microsoft .net implementation supports Task inlining on WaitAll.
if (Enum.IsDefined(typeof(TaskCreationOptions), (TaskCreationOptions)0x10))
{
_taskFactory.StartNew(() => ExecuteCommand<TCommand>(command)
, TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10)
.LogExceptions();
}
else
{
_taskFactory.StartNew(() => ExecuteCommand<TCommand>(command)
, TaskCreationOptions.PreferFairness)
.LogExceptions();
}
return command;
}
public Command PublishCommandAsync(string commandTypeName)
{
dynamic command = GetCommand(commandTypeName);
return PublishCommandAsync(command);
}
private dynamic GetCommand(string commandTypeName)
{
var commandType = _serviceFactory.GetImplementations(typeof(Command))
.Single(c => c.FullName.Equals(commandTypeName, StringComparison.InvariantCultureIgnoreCase));
return Json.Deserialize("{}", commandType);
}
private void ExecuteCommand<TCommand>(Command command) where TCommand : Command
private void ExecuteCommand<TCommand>(TCommand command, CommandModel commandModel) where TCommand : Command
{
var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType());
var handler = (IExecute<TCommand>)_serviceFactory.Build(handlerContract);
@ -120,47 +62,67 @@ private void ExecuteCommand<TCommand>(Command command) where TCommand : Command
try
{
_trackCommands.Start(command);
BroadcastCommandUpdate(command);
_commandQueueManager.Start(commandModel);
BroadcastCommandUpdate(commandModel);
if (!MappedDiagnosticsContext.Contains("CommandId") && command.SendUpdatesToClient)
if (!MappedDiagnosticsContext.Contains("CommandId"))
{
MappedDiagnosticsContext.Set("CommandId", command.Id.ToString());
MappedDiagnosticsContext.Set("CommandId", commandModel.Id.ToString());
}
handler.Execute((TCommand)command);
handler.Execute(command);
if (command.State == CommandStatus.Running)
{
_trackCommands.Completed(command);
}
_commandQueueManager.Complete(commandModel, command.CompletionMessage);
}
catch (Exception e)
catch (CommandFailedException ex)
{
_trackCommands.Failed(command, e);
_commandQueueManager.Fail(commandModel, ex.Message, ex);
throw;
}
catch (Exception ex)
{
_commandQueueManager.SetMessage(commandModel, "Failed");
_commandQueueManager.Fail(commandModel, "Failed", ex);
throw;
}
finally
{
BroadcastCommandUpdate(command);
_eventAggregator.PublishEvent(new CommandExecutedEvent(command));
BroadcastCommandUpdate(commandModel);
if (MappedDiagnosticsContext.Get("CommandId") == command.Id.ToString())
_eventAggregator.PublishEvent(new CommandExecutedEvent(commandModel));
if (MappedDiagnosticsContext.Get("CommandId") == commandModel.Id.ToString())
{
MappedDiagnosticsContext.Remove("CommandId");
}
}
_logger.Trace("{0} <- {1} [{2}]", command.GetType().Name, handler.GetType().Name, command.Runtime.ToString(""));
_logger.Trace("{0} <- {1} [{2}]", command.GetType().Name, handler.GetType().Name, commandModel.Duration.ToString());
}
private void BroadcastCommandUpdate(Command command)
private void BroadcastCommandUpdate(CommandModel command)
{
if (command.SendUpdatesToClient)
if (command.Body.SendUpdatesToClient)
{
_eventAggregator.PublishEvent(new CommandUpdatedEvent(command));
}
}
public void Handle(ApplicationStartedEvent message)
{
_cancellationTokenSource = new CancellationTokenSource();
for (int i = 0; i < THREAD_LIMIT; i++)
{
var thread = new Thread(ExecuteCommands);
thread.Start();
}
}
public void Handle(ApplicationShutdownRequested message)
{
_logger.Info("Shutting down task execution");
_cancellationTokenSource.Cancel(true);
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Messaging.Commands
{
public class CommandFailedException : NzbDroneException
{
public CommandFailedException(string message, params object[] args) : base(message, args)
{
}
public CommandFailedException(string message) : base(message)
{
}
public CommandFailedException(string message, Exception innerException, params object[] args) : base(message, innerException, args)
{
}
public CommandFailedException(string message, Exception innerException) : base(message, innerException)
{
}
public CommandFailedException(Exception innerException)
: base("Failed", innerException)
{
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Messaging.Commands
{
public class CommandModel : ModelBase, IMessage
{
public string Name { get; set; }
public Command Body { get; set; }
public CommandPriority Priority { get; set; }
public CommandStatus Status { get; set; }
public DateTime QueuedAt { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? EndedAt { get; set; }
public TimeSpan? Duration { get; set; }
public string Exception { get; set; }
public CommandTrigger Trigger { get; set; }
public string Message { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Messaging.Commands
{
public class CommandNotFoundException : NzbDroneException
{
public CommandNotFoundException(string contract)
: base("Couldn't find command " + contract)
{
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Messaging.Commands
{
public enum CommandPriority
{
Low = -1,
Normal = 0,
High = 1
}
}

View File

@ -0,0 +1,128 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.Messaging.Commands
{
public class CommandQueue : IProducerConsumerCollection<CommandModel>
{
private object Mutex = new object();
private List<CommandModel> _items;
public CommandQueue()
{
_items = new List<CommandModel>();
}
public IEnumerator<CommandModel> GetEnumerator()
{
List<CommandModel> copy = null;
lock (Mutex)
{
copy = new List<CommandModel>(_items);
}
return copy.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void CopyTo(Array array, int index)
{
lock (Mutex)
{
((ICollection)_items).CopyTo(array, index);
}
}
public int Count
{
get
{
return _items.Count;
}
}
public object SyncRoot
{
get
{
return Mutex;
}
}
public bool IsSynchronized
{
get
{
return true;
}
}
public void CopyTo(CommandModel[] array, int index)
{
lock (Mutex)
{
_items.CopyTo(array, index);
}
}
public bool TryAdd(CommandModel item)
{
Add(item);
return true;
}
public bool TryTake(out CommandModel item)
{
bool rval = true;
lock (Mutex)
{
if (_items.Count == 0)
{
item = default(CommandModel);
rval = false;
}
else
{
item = _items.Where(c => c.Status == CommandStatus.Queued)
.OrderByDescending(c => c.Priority)
.ThenBy(c => c.QueuedAt)
.First();
_items.Remove(item);
}
}
return rval;
}
public CommandModel[] ToArray()
{
CommandModel[] rval = null;
lock (Mutex)
{
rval = _items.ToArray();
}
return rval;
}
public void Add(CommandModel item)
{
lock (Mutex)
{
_items.Add(item);
}
}
}
}

View File

@ -0,0 +1,204 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Messaging.Commands
{
public interface IManageCommandQueue
{
CommandModel Push<TCommand>(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command;
CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified);
IEnumerable<CommandModel> Queue(CancellationToken cancellationToken);
CommandModel Get(int id);
List<CommandModel> GetStarted();
void SetMessage(CommandModel command, string message);
void Start(CommandModel command);
void Complete(CommandModel command, string message);
void Fail(CommandModel command, string message, Exception e);
void Requeue();
void CleanCommands();
}
public class CommandQueueManager : IManageCommandQueue, IHandle<ApplicationStartedEvent>
{
private readonly ICommandRepository _repo;
private readonly IServiceFactory _serviceFactory;
private readonly Logger _logger;
private readonly ICached<CommandModel> _commandCache;
private readonly BlockingCollection<CommandModel> _commandQueue;
public CommandQueueManager(ICommandRepository repo,
IServiceFactory serviceFactory,
ICacheManager cacheManager,
Logger logger)
{
_repo = repo;
_serviceFactory = serviceFactory;
_logger = logger;
_commandCache = cacheManager.GetCache<CommandModel>(GetType());
_commandQueue = new BlockingCollection<CommandModel>(new CommandQueue());
}
public CommandModel Push<TCommand>(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command
{
Ensure.That(command, () => command).IsNotNull();
_logger.Trace("Publishing {0}", command.Name);
_logger.Trace("Checking if command is queued or started: {0}", command.Name);
var existingCommands = _repo.FindQueuedOrStarted(command.Name);
var existing = existingCommands.SingleOrDefault(c => CommandEqualityComparer.Instance.Equals(c.Body, command));
if (existing != null)
{
_logger.Trace("Command is already in progress: {0}", command.Name);
return existing;
}
var commandModel = new CommandModel
{
Name = command.Name,
Body = command,
QueuedAt = DateTime.UtcNow,
Trigger = trigger,
Priority = priority,
Status = CommandStatus.Queued
};
_logger.Trace("Inserting new command: {0}", commandModel.Name);
_repo.Insert(commandModel);
_commandQueue.Add(commandModel);
_commandCache.Set(commandModel.Id.ToString(), commandModel);
return commandModel;
}
public CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified)
{
dynamic command = GetCommand(commandName);
command.LastExecutionTime = lastExecutionTime;
command.Trigger = trigger;
return Push(command, priority, trigger);
}
public IEnumerable<CommandModel> Queue(CancellationToken cancellationToken)
{
return _commandQueue.GetConsumingEnumerable(cancellationToken);
}
public CommandModel Get(int id)
{
return _commandCache.Get(id.ToString(), () => FindMessage(_repo.Get(id)));
}
public List<CommandModel> GetStarted()
{
_logger.Trace("Getting started commands");
return _commandCache.Values.Where(c => c.Status == CommandStatus.Started).ToList();
}
public void SetMessage(CommandModel command, string message)
{
command.Message = message;
_commandCache.Set(command.Id.ToString(), command);
}
public void Start(CommandModel command)
{
command.StartedAt = DateTime.UtcNow;
command.Status = CommandStatus.Started;
_logger.Trace("Marking command as started: {0}", command.Name);
_repo.Update(command);
_commandCache.Set(command.Id.ToString(), command);
}
public void Complete(CommandModel command, string message)
{
Update(command, CommandStatus.Completed, message);
}
public void Fail(CommandModel command, string message, Exception e)
{
command.Exception = e.ToString();
Update(command, CommandStatus.Failed, message);
}
public void Requeue()
{
foreach (var command in _repo.Queued())
{
_commandQueue.Add(command);
}
}
public void CleanCommands()
{
_logger.Trace("Cleaning up old commands");
_repo.Trim();
var old = _commandCache.Values.Where(c => c.EndedAt < DateTime.UtcNow.AddMinutes(5));
foreach (var command in old)
{
_commandCache.Remove(command.Id.ToString());
}
}
private dynamic GetCommand(string commandName)
{
commandName = commandName.Split('.').Last();
var commandType = _serviceFactory.GetImplementations(typeof(Command))
.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase));
return Json.Deserialize("{}", commandType);
}
private CommandModel FindMessage(CommandModel command)
{
var cachedCommand = _commandCache.Find(command.Id.ToString());
if (cachedCommand != null)
{
command.Message = cachedCommand.Message;
}
return command;
}
private void Update(CommandModel command, CommandStatus status, string message)
{
SetMessage(command, message);
command.EndedAt = DateTime.UtcNow;
command.Duration = command.EndedAt.Value.Subtract(command.StartedAt.Value);
command.Status = status;
_logger.Trace("Updating command status");
_repo.Update(command);
_commandCache.Set(command.Id.ToString(), command);
}
public void Handle(ApplicationStartedEvent message)
{
_logger.Trace("Orphaning incomplete commands");
_repo.OrphanStarted();
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Messaging.Commands
{
public interface ICommandRepository : IBasicRepository<CommandModel>
{
void Trim();
void OrphanStarted();
List<CommandModel> FindCommands(string name);
List<CommandModel> FindQueuedOrStarted(string name);
List<CommandModel> Queued();
List<CommandModel> Started();
}
public class CommandRepository : BasicRepository<CommandModel>, ICommandRepository
{
private readonly IDatabase _database;
public CommandRepository(IDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
_database = database;
}
public void Trim()
{
var date = DateTime.UtcNow.AddDays(-1);
Delete(c => c.EndedAt < date);
}
public void OrphanStarted()
{
var mapper = _database.GetDataMapper();
mapper.Parameters.Add(new SQLiteParameter("@orphaned", (int)CommandStatus.Orphaned));
mapper.Parameters.Add(new SQLiteParameter("@started", (int)CommandStatus.Started));
mapper.Parameters.Add(new SQLiteParameter("@ended", DateTime.UtcNow));
mapper.ExecuteNonQuery(@"UPDATE Commands
SET Status = @orphaned, EndedAt = @ended
WHERE Status = @started");
}
public List<CommandModel> FindCommands(string name)
{
return Query.Where(c => c.Name == name).ToList();
}
public List<CommandModel> FindQueuedOrStarted(string name)
{
return Query.Where(c => c.Name == name)
.AndWhere("[Status] IN (0,1)")
.ToList();
}
public List<CommandModel> Queued()
{
return Query.Where(c => c.Status == CommandStatus.Queued);
}
public List<CommandModel> Started()
{
return Query.Where(c => c.Status == CommandStatus.Started);
}
}
}

View File

@ -0,0 +1,13 @@
namespace NzbDrone.Core.Messaging.Commands
{
public enum CommandStatus
{
Queued = 0,
Started = 1,
Completed = 2,
Failed = 3,
Aborted = 4,
Cancelled = 5,
Orphaned = 6
}
}

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Messaging.Commands
{
public enum CommandTrigger
{
Unspecified = 0,
Manual = 1,
Scheduled = 2
}
}

View File

@ -1,12 +0,0 @@
using System;
namespace NzbDrone.Core.Messaging.Commands
{
public interface ICommandExecutor
{
void PublishCommand<TCommand>(TCommand command) where TCommand : Command;
void PublishCommand(string commandTypeName, DateTime? lastEecutionTime);
Command PublishCommandAsync<TCommand>(TCommand command) where TCommand : Command;
Command PublishCommandAsync(string commandTypeName);
}
}

View File

@ -0,0 +1,6 @@
namespace NzbDrone.Core.Messaging.Commands
{
public class MessagingCleanupCommand : Command
{
}
}

View File

@ -0,0 +1,20 @@
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Messaging.Commands
{
public class RequeueQueuedCommands : IHandle<ApplicationStartedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
public RequeueQueuedCommands(IManageCommandQueue commandQueueManager)
{
_commandQueueManager = commandQueueManager;
}
public void Handle(ApplicationStartedEvent message)
{
_commandQueueManager.Requeue();
}
}
}

View File

@ -1,10 +0,0 @@
namespace NzbDrone.Core.Messaging.Commands.Tracking
{
public enum CommandStatus
{
Pending,
Running,
Completed,
Failed
}
}

View File

@ -1,84 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.Messaging.Commands.Tracking
{
public interface ITrackCommands
{
Command GetById(int id);
Command GetById(string id);
void Completed(Command trackedCommand);
void Failed(Command trackedCommand, Exception e);
IEnumerable<Command> RunningCommands();
Command FindExisting(Command command);
void Store(Command command);
void Start(Command command);
}
public class CommandTrackingService : ITrackCommands, IExecute<TrackedCommandCleanupCommand>
{
private readonly ICached<Command> _cache;
public CommandTrackingService(ICacheManager cacheManager)
{
_cache = cacheManager.GetCache<Command>(GetType());
}
public Command GetById(int id)
{
return _cache.Find(id.ToString());
}
public Command GetById(string id)
{
return _cache.Find(id);
}
public void Start(Command command)
{
command.Start();
}
public void Completed(Command trackedCommand)
{
trackedCommand.Completed();
}
public void Failed(Command trackedCommand, Exception e)
{
trackedCommand.Failed(e);
}
public IEnumerable<Command> RunningCommands()
{
return _cache.Values.Where(c => c.State == CommandStatus.Running);
}
public Command FindExisting(Command command)
{
return RunningCommands().SingleOrDefault(t => CommandEqualityComparer.Instance.Equals(t, command));
}
public void Store(Command command)
{
if (command.GetType() == typeof(TrackedCommandCleanupCommand))
{
return;
}
_cache.Set(command.Id.ToString(), command);
}
public void Execute(TrackedCommandCleanupCommand message)
{
var old = _cache.Values.Where(c => c.State != CommandStatus.Running && c.StateChangeTime < DateTime.UtcNow.AddMinutes(-5));
foreach (var trackedCommand in old)
{
_cache.Remove(trackedCommand.Id.ToString());
}
}
}
}

View File

@ -1,16 +0,0 @@
using System;
namespace NzbDrone.Core.Messaging.Commands.Tracking
{
public class ExistingCommand
{
public Boolean Existing { get; set; }
public Command Command { get; set; }
public ExistingCommand(Boolean exisitng, Command trackedCommand)
{
Existing = exisitng;
Command = trackedCommand;
}
}
}

View File

@ -1,7 +0,0 @@
namespace NzbDrone.Core.Messaging.Commands.Tracking
{
public class TrackedCommandCleanupCommand : Command
{
}
}

View File

@ -1,15 +0,0 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Messaging.Events
{
public class CommandCreatedEvent : IEvent
{
public Command Command { get; private set; }
public CommandCreatedEvent(Command trackedCommand)
{
Command = trackedCommand;
}
}
}

View File

@ -5,11 +5,11 @@ namespace NzbDrone.Core.Messaging.Events
{
public class CommandExecutedEvent : IEvent
{
public Command Command { get; private set; }
public CommandModel Command { get; private set; }
public CommandExecutedEvent(Command trackedCommand)
public CommandExecutedEvent(CommandModel command)
{
Command = trackedCommand;
Command = command;
}
}
}

View File

@ -156,9 +156,11 @@
<Compile Include="Datastore\Converters\BooleanIntConverter.cs" />
<Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" />
<Compile Include="Datastore\Converters\EnumIntConverter.cs" />
<Compile Include="Datastore\Converters\TimeSpanConverter.cs" />
<Compile Include="Datastore\Converters\Int32Converter.cs" />
<Compile Include="Datastore\Converters\GuidConverter.cs" />
<Compile Include="Datastore\Converters\OsPathConverter.cs" />
<Compile Include="Datastore\Converters\CommandConverter.cs" />
<Compile Include="Datastore\Converters\ProviderSettingConverter.cs" />
<Compile Include="Datastore\Converters\QualityIntConverter.cs" />
<Compile Include="Datastore\Converters\UtcConverter.cs" />
@ -235,6 +237,7 @@
<Compile Include="Datastore\Migration\060_remove_enable_from_indexers.cs" />
<Compile Include="Datastore\Migration\062_convert_quality_models.cs" />
<Compile Include="Datastore\Migration\065_make_scene_numbering_nullable.cs" />
<Compile Include="Datastore\Migration\078_add_commands_table.cs" />
<Compile Include="Datastore\Migration\068_add_release_restrictions.cs" />
<Compile Include="Datastore\Migration\066_add_tags.cs" />
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
@ -424,6 +427,7 @@
<Compile Include="History\HistoryRepository.cs" />
<Compile Include="History\HistoryService.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupCommandQueue.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
@ -433,6 +437,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleases.cs" />
<Compile Include="Housekeeping\Housekeepers\DeleteBadMediaCovers.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
<Compile Include="Housekeeping\Housekeepers\TrimLogDatabase.cs" />
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
<Compile Include="Housekeeping\HousekeepingService.cs" />
@ -525,14 +530,13 @@
<Compile Include="Instrumentation\Commands\ClearLogCommand.cs" />
<Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" />
<Compile Include="Instrumentation\Commands\DeleteUpdateLogFilesCommand.cs" />
<Compile Include="Instrumentation\Commands\TrimLogCommand.cs" />
<Compile Include="Instrumentation\DatabaseTarget.cs" />
<Compile Include="Instrumentation\DeleteLogFilesService.cs" />
<Compile Include="Instrumentation\Log.cs" />
<Compile Include="Instrumentation\LogRepository.cs" />
<Compile Include="Instrumentation\LogService.cs" />
<Compile Include="Instrumentation\ReconfigureLogging.cs" />
<Compile Include="Jobs\JobRepository.cs" />
<Compile Include="Jobs\ScheduledTaskRepository.cs" />
<Compile Include="Jobs\ScheduledTask.cs" />
<Compile Include="Jobs\Scheduler.cs" />
<Compile Include="Jobs\TaskManager.cs" />
@ -547,7 +551,6 @@
<Compile Include="MediaCover\MediaCoverService.cs" />
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
<Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" />
<Compile Include="MediaFiles\Commands\CleanMediaFileDb.cs" />
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" />
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
@ -603,18 +606,24 @@
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
<Compile Include="MediaFiles\UpgradeMediaFileService.cs" />
<Compile Include="Messaging\Commands\BackendCommandAttribute.cs" />
<Compile Include="Messaging\Commands\CleanupCommandMessagingService.cs" />
<Compile Include="Messaging\Commands\Command.cs" />
<Compile Include="Messaging\Commands\CommandEqualityComparer.cs" />
<Compile Include="Messaging\Commands\CommandExecutor.cs" />
<Compile Include="Messaging\Commands\ICommandExecutor.cs" />
<Compile Include="Messaging\Commands\CommandFailedException.cs" />
<Compile Include="Messaging\Commands\MessagingCleanupCommand.cs" />
<Compile Include="Messaging\Commands\CommandModel.cs" />
<Compile Include="Messaging\Commands\CommandPriority.cs" />
<Compile Include="Messaging\Commands\CommandNotFoundException.cs" />
<Compile Include="Messaging\Commands\CommandQueue.cs" />
<Compile Include="Messaging\Commands\CommandStatus.cs" />
<Compile Include="Messaging\Commands\CommandRepository.cs" />
<Compile Include="Messaging\Commands\CommandQueueManager.cs" />
<Compile Include="Messaging\Commands\CommandTrigger.cs" />
<Compile Include="Messaging\Commands\IExecute.cs" />
<Compile Include="Messaging\Commands\RequeueQueuedCommands.cs" />
<Compile Include="Messaging\Commands\TestCommand.cs" />
<Compile Include="Messaging\Commands\TestCommandExecutor.cs" />
<Compile Include="Messaging\Commands\Tracking\CommandStatus.cs" />
<Compile Include="Messaging\Commands\Tracking\CommandTrackingService.cs" />
<Compile Include="Messaging\Commands\Tracking\ExistingCommand.cs" />
<Compile Include="Messaging\Commands\Tracking\TrackedCommandCleanupCommand.cs" />
<Compile Include="Messaging\Events\CommandCreatedEvent.cs" />
<Compile Include="Messaging\Events\CommandExecutedEvent.cs" />
<Compile Include="Messaging\Events\EventAggregator.cs" />
<Compile Include="Messaging\Events\IEventAggregator.cs" />
@ -853,6 +862,7 @@
<Compile Include="Tv\RefreshSeriesService.cs" />
<Compile Include="Tv\Season.cs" />
<Compile Include="Tv\Series.cs" />
<Compile Include="Tv\SeriesAddedHandler.cs" />
<Compile Include="Tv\SeriesScannedHandler.cs" />
<Compile Include="Tv\SeriesEditedService.cs" />
<Compile Include="Tv\SeriesRepository.cs" />

View File

@ -5,9 +5,9 @@ namespace NzbDrone.Core.ProgressMessaging
{
public class CommandUpdatedEvent : IEvent
{
public Command Command { get; set; }
public CommandModel Command { get; set; }
public CommandUpdatedEvent(Command command)
public CommandUpdatedEvent(CommandModel command)
{
Command = command;
}

View File

@ -1,9 +1,9 @@
using NLog.Config;
using System;
using NLog.Config;
using NLog;
using NLog.Targets;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.ProgressMessaging
@ -11,13 +11,13 @@ namespace NzbDrone.Core.ProgressMessaging
public class ProgressMessageTarget : Target, IHandle<ApplicationStartedEvent>
{
private readonly IEventAggregator _eventAggregator;
private readonly ITrackCommands _trackCommands;
private readonly IManageCommandQueue _commandQueueManager;
private static LoggingRule _rule;
public ProgressMessageTarget(IEventAggregator eventAggregator, ITrackCommands trackCommands)
public ProgressMessageTarget(IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager)
{
_eventAggregator = eventAggregator;
_trackCommands = trackCommands;
_commandQueueManager = commandQueueManager;
}
protected override void Write(LogEventInfo logEvent)
@ -26,27 +26,26 @@ protected override void Write(LogEventInfo logEvent)
if (IsClientMessage(logEvent, command))
{
command.SetMessage(logEvent.FormattedMessage);
_commandQueueManager.SetMessage(command, logEvent.FormattedMessage);
_eventAggregator.PublishEvent(new CommandUpdatedEvent(command));
}
}
private Command GetCurrentCommand()
private CommandModel GetCurrentCommand()
{
var commandId = MappedDiagnosticsContext.Get("CommandId");
if (string.IsNullOrWhiteSpace(commandId))
if (String.IsNullOrWhiteSpace(commandId))
{
return null;
}
return _trackCommands.GetById(commandId);
return _commandQueueManager.Get(Convert.ToInt32(commandId));
}
private bool IsClientMessage(LogEventInfo logEvent, Command command)
private bool IsClientMessage(LogEventInfo logEvent, CommandModel command)
{
if (command == null || !command.SendUpdatesToClient)
if (command == null || !command.Body.SendUpdatesToClient)
{
return false;
}
@ -63,4 +62,4 @@ public void Handle(ApplicationStartedEvent message)
LogManager.ReconfigExistingLoggers();
}
}
}
}

View File

@ -11,4 +11,4 @@ public SeriesAddedEvent(Series series)
Series = series;
}
}
}
}

View File

@ -15,7 +15,7 @@
namespace NzbDrone.Core.Tv
{
public class RefreshSeriesService : IExecute<RefreshSeriesCommand>, IHandleAsync<SeriesAddedEvent>
public class RefreshSeriesService : IExecute<RefreshSeriesCommand>
{
private readonly IProvideSeriesInfo _seriesInfo;
private readonly ISeriesService _seriesService;
@ -138,7 +138,7 @@ public void Execute(RefreshSeriesCommand message)
foreach (var series in allSeries)
{
if (message.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
if (message.Trigger == CommandTrigger.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
{
try
{
@ -165,10 +165,5 @@ public void Execute(RefreshSeriesCommand message)
}
}
}
public void HandleAsync(SeriesAddedEvent message)
{
RefreshSeriesInfo(message.Series);
}
}
}

View File

@ -0,0 +1,22 @@
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Commands;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Tv
{
public class SeriesAddedHandler : IHandle<SeriesAddedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
public SeriesAddedHandler(IManageCommandQueue commandQueueManager)
{
_commandQueueManager = commandQueueManager;
}
public void Handle(SeriesAddedEvent message)
{
_commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id));
}
}
}

View File

@ -7,18 +7,18 @@ namespace NzbDrone.Core.Tv
{
public class SeriesEditedService : IHandle<SeriesEditedEvent>
{
private readonly CommandExecutor _commandExecutor;
private readonly IManageCommandQueue _commandQueueManager;
public SeriesEditedService(CommandExecutor commandExecutor)
public SeriesEditedService(IManageCommandQueue commandQueueManager)
{
_commandExecutor = commandExecutor;
_commandQueueManager = commandQueueManager;
}
public void Handle(SeriesEditedEvent message)
{
if (message.Series.SeriesType != message.OldSeries.SeriesType)
{
_commandExecutor.PublishCommandAsync(new RefreshSeriesCommand(message.Series.Id));
_commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id));
}
}
}

View File

@ -15,18 +15,18 @@ public class SeriesScannedHandler : IHandle<SeriesScannedEvent>,
{
private readonly ISeriesService _seriesService;
private readonly IEpisodeService _episodeService;
private readonly ICommandExecutor _commandExecutor;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
public SeriesScannedHandler(ISeriesService seriesService,
IEpisodeService episodeService,
ICommandExecutor commandExecutor,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_seriesService = seriesService;
_episodeService = episodeService;
_commandExecutor = commandExecutor;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
@ -82,7 +82,7 @@ private void HandleScanEvents(Series series)
if (series.AddOptions.SearchForMissingEpisodes)
{
_commandExecutor.PublishCommand(new MissingEpisodeSearchCommand(series.Id));
_commandQueueManager.Push(new MissingEpisodeSearchCommand(series.Id));
}
series.AddOptions = null;

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;

View File

@ -11,5 +11,13 @@ public override bool SendUpdatesToClient
return true;
}
}
public override string CompletionMessage
{
get
{
return "Restarting Sonarr to apply updates";
}
}
}
}
}

View File

@ -191,7 +191,7 @@ public void Execute(ApplicationUpdateCommand message)
return;
}
if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && !message.Manual)
if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && message.Trigger != CommandTrigger.Manual)
{
_logger.ProgressDebug("Auto-update not enabled, not installing available update.");
return;
@ -200,23 +200,21 @@ public void Execute(ApplicationUpdateCommand message)
try
{
InstallUpdate(latestAvailable);
message.Completed("Restarting Sonarr to apply updates");
}
catch (UpdateFolderNotWritableException ex)
{
_logger.ErrorException("Update process failed", ex);
message.Failed(ex, string.Format("Startup folder not writable by user '{0}'", Environment.UserName));
throw new CommandFailedException("Startup folder not writable by user '{0}'", ex, Environment.UserName);
}
catch (UpdateVerificationFailedException ex)
{
_logger.ErrorException("Update process failed", ex);
message.Failed(ex, "Downloaded update package is corrupt");
throw new CommandFailedException("Downloaded update package is corrupt", ex);
}
catch (UpdateFailedException ex)
{
_logger.ErrorException("Update process failed", ex);
message.Failed(ex);
throw new CommandFailedException(ex);
}
}
}

View File

@ -74,7 +74,7 @@ var singleton = function() {
return;
}
model.bind('change:state', function(model) {
model.bind('change:status', function(model) {
if (!model.isActive()) {
options.element.stopSpin();

View File

@ -6,6 +6,14 @@ module.exports = Backbone.Model.extend({
parse : function(response) {
response.name = response.name.toLocaleLowerCase();
response.body.name = response.body.name.toLocaleLowerCase();
for (var key in response.body) {
response[key] = response.body[key];
}
delete response.body;
return response;
},
@ -33,10 +41,10 @@ module.exports = Backbone.Model.extend({
},
isActive : function() {
return this.get('state') !== 'completed' && this.get('state') !== 'failed';
return this.get('status') !== 'completed' && this.get('status') !== 'failed';
},
isComplete : function() {
return this.get('state') === 'completed';
return this.get('status') === 'completed';
}
});