mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-21 19:34:40 +08:00
RecipeJournal implementation.
- IStorageProvider based journal implementation for the recipe pipeline. - Was needed as the existing reporting capability was found insufficient. --HG-- branch : recipe
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
@@ -68,6 +67,7 @@ namespace Orchard.Tests.Modules.Recipes.Services {
|
|||||||
builder.RegisterType<RecipeHarvester>().As<IRecipeHarvester>();
|
builder.RegisterType<RecipeHarvester>().As<IRecipeHarvester>();
|
||||||
builder.RegisterType<RecipeStepExecutor>().As<IRecipeStepExecutor>();
|
builder.RegisterType<RecipeStepExecutor>().As<IRecipeStepExecutor>();
|
||||||
builder.RegisterType<StubStepQueue>().As<IRecipeStepQueue>().InstancePerLifetimeScope();
|
builder.RegisterType<StubStepQueue>().As<IRecipeStepQueue>().InstancePerLifetimeScope();
|
||||||
|
builder.RegisterType<StubRecipeJournal>().As<IRecipeJournal>();
|
||||||
builder.RegisterType<StubRecipeScheduler>().As<IRecipeScheduler>();
|
builder.RegisterType<StubRecipeScheduler>().As<IRecipeScheduler>();
|
||||||
builder.RegisterType<ExtensionManager>().As<IExtensionManager>();
|
builder.RegisterType<ExtensionManager>().As<IExtensionManager>();
|
||||||
builder.RegisterType<StubAppDataFolder>().As<IAppDataFolder>();
|
builder.RegisterType<StubAppDataFolder>().As<IAppDataFolder>();
|
||||||
@@ -145,6 +145,28 @@ namespace Orchard.Tests.Modules.Recipes.Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class StubRecipeJournal : IRecipeJournal {
|
||||||
|
public void ExecutionStart(string executionId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecutionComplete(string executionId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecutionFailed(string executionId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteJournalEntry(string executionId, string message) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecipeJournal GetRecipeJournal(string executionId) {
|
||||||
|
return new RecipeJournal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecipeStatus GetRecipeStatus(string executionId) {
|
||||||
|
return RecipeStatus.Complete;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class StubStepQueue : IRecipeStepQueue {
|
public class StubStepQueue : IRecipeStepQueue {
|
||||||
readonly Queue<RecipeStep> _queue = new Queue<RecipeStep>();
|
readonly Queue<RecipeStep> _queue = new Queue<RecipeStep>();
|
||||||
|
|
||||||
@@ -171,8 +193,12 @@ namespace Orchard.Tests.Modules.Recipes.Services {
|
|||||||
|
|
||||||
public class CustomRecipeHandler : IRecipeHandler {
|
public class CustomRecipeHandler : IRecipeHandler {
|
||||||
public static string AttributeValue;
|
public static string AttributeValue;
|
||||||
|
public string[] _handles = {"Module", "Theme", "Migration", "CleanUpInactive", "Custom1", "Custom2", "Command", "Metadata", "Feature", "Settings"};
|
||||||
|
|
||||||
public void ExecuteRecipeStep(RecipeContext recipeContext) {
|
public void ExecuteRecipeStep(RecipeContext recipeContext) {
|
||||||
|
if (_handles.Contains(recipeContext.RecipeStep.Name)) {
|
||||||
|
recipeContext.Executed = true;
|
||||||
|
}
|
||||||
if (recipeContext.RecipeStep.Name == "Custom1") {
|
if (recipeContext.RecipeStep.Name == "Custom1") {
|
||||||
foreach (var attribute in recipeContext.RecipeStep.Step.Attributes().Where(attribute => attribute.Name == "attr1")) {
|
foreach (var attribute in recipeContext.RecipeStep.Step.Attributes().Where(attribute => attribute.Name == "attr1")) {
|
||||||
AttributeValue = attribute.Value;
|
AttributeValue = attribute.Value;
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Xml.Linq;
|
||||||
using Orchard.FileSystems.Media;
|
using Orchard.FileSystems.Media;
|
||||||
using Orchard.Localization;
|
using Orchard.Localization;
|
||||||
using Orchard.Logging;
|
using Orchard.Logging;
|
||||||
@@ -7,6 +10,7 @@ using Orchard.Recipes.Models;
|
|||||||
namespace Orchard.Recipes.Services {
|
namespace Orchard.Recipes.Services {
|
||||||
public class RecipeJournalManager : IRecipeJournal {
|
public class RecipeJournalManager : IRecipeJournal {
|
||||||
private readonly IStorageProvider _storageProvider;
|
private readonly IStorageProvider _storageProvider;
|
||||||
|
private readonly string _recipeJournalFolder = "RecipeJournal" + Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
public RecipeJournalManager(IStorageProvider storageProvider) {
|
public RecipeJournalManager(IStorageProvider storageProvider) {
|
||||||
_storageProvider = storageProvider;
|
_storageProvider = storageProvider;
|
||||||
@@ -18,36 +22,103 @@ namespace Orchard.Recipes.Services {
|
|||||||
public Localizer T { get; set; }
|
public Localizer T { get; set; }
|
||||||
ILogger Logger { get; set; }
|
ILogger Logger { get; set; }
|
||||||
|
|
||||||
public void StartExecution(string executionId) {
|
public void ExecutionStart(string executionId) {
|
||||||
throw new NotImplementedException();
|
var executionJournal = GetJournalFile(executionId);
|
||||||
|
var xElement = XElement.Parse(ReadJournal(executionJournal));
|
||||||
|
xElement.Element("Status").Value = "Started";
|
||||||
|
WriteJournal(executionJournal, xElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExecutionComplete(string executionId) {
|
public void ExecutionComplete(string executionId) {
|
||||||
throw new NotImplementedException();
|
var executionJournal = GetJournalFile(executionId);
|
||||||
|
var xElement = XElement.Parse(ReadJournal(executionJournal));
|
||||||
|
xElement.Element("Status").Value = "Complete";
|
||||||
|
WriteJournal(executionJournal, xElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExecutionFailed(string executionId) {
|
public void ExecutionFailed(string executionId) {
|
||||||
throw new NotImplementedException();
|
var executionJournal = GetJournalFile(executionId);
|
||||||
|
var xElement = XElement.Parse(ReadJournal(executionJournal));
|
||||||
|
xElement.Element("Status").Value = "Failed";
|
||||||
|
WriteJournal(executionJournal, xElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteJournalEntry(string executionId, string message) {
|
public void WriteJournalEntry(string executionId, string message) {
|
||||||
throw new NotImplementedException();
|
var executionJournal = GetJournalFile(executionId);
|
||||||
|
var xElement = XElement.Parse(ReadJournal(executionJournal));
|
||||||
|
var journalEntry = new XElement("Message", message);
|
||||||
|
xElement.Add(journalEntry);
|
||||||
|
WriteJournal(executionJournal, xElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipeJournal GetRecipeJournal(string executionId) {
|
public RecipeJournal GetRecipeJournal(string executionId) {
|
||||||
throw new NotImplementedException();
|
var executionJournal = GetJournalFile(executionId);
|
||||||
|
var xElement = XElement.Parse(ReadJournal(executionJournal));
|
||||||
|
|
||||||
|
var journal = new RecipeJournal { ExecutionId = executionId };
|
||||||
|
var messages = new List<JournalMessage>();
|
||||||
|
|
||||||
|
journal.Status = ReadStatusFromJournal(xElement);
|
||||||
|
foreach (var message in xElement.Elements("Message")) {
|
||||||
|
messages.Add(new JournalMessage {Message = message.Value});
|
||||||
|
}
|
||||||
|
journal.Messages = messages;
|
||||||
|
|
||||||
|
return journal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecipeStatus GetRecipeStatus(string executionId) {
|
||||||
|
var executionJournal = GetJournalFile(executionId);
|
||||||
|
var xElement = XElement.Parse(ReadJournal(executionJournal));
|
||||||
|
|
||||||
|
return ReadStatusFromJournal(xElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IStorageFile GetJournalFile(string executionId) {
|
private IStorageFile GetJournalFile(string executionId) {
|
||||||
IStorageFile journalFile;
|
IStorageFile journalFile;
|
||||||
|
var journalPath = Path.Combine(_recipeJournalFolder, executionId);
|
||||||
try {
|
try {
|
||||||
journalFile = _storageProvider.GetFile(executionId);
|
_storageProvider.TryCreateFolder(_recipeJournalFolder);
|
||||||
|
journalFile = _storageProvider.GetFile(journalPath);
|
||||||
}
|
}
|
||||||
catch (ArgumentException) {
|
catch (ArgumentException) {
|
||||||
journalFile = _storageProvider.CreateFile(executionId);
|
journalFile = _storageProvider.CreateFile(journalPath);
|
||||||
|
var recipeStepElement = new XElement("RecipeJournal");
|
||||||
|
recipeStepElement.Add(new XElement("Status", "Unknown"));
|
||||||
|
WriteJournal(journalFile, recipeStepElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return journalFile;
|
return journalFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ReadJournal(IStorageFile executionJournal) {
|
||||||
|
using (var stream = executionJournal.OpenRead()) {
|
||||||
|
using (var streamReader = new StreamReader(stream)) {
|
||||||
|
return streamReader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteJournal(IStorageFile journalFile, XElement journal) {
|
||||||
|
string content = journal.ToString();
|
||||||
|
using (var stream = journalFile.OpenWrite()) {
|
||||||
|
using (var tw = new StreamWriter(stream)) {
|
||||||
|
tw.Write(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RecipeStatus ReadStatusFromJournal(XElement xElement) {
|
||||||
|
switch (xElement.Element("Status").Value) {
|
||||||
|
case "Started":
|
||||||
|
return RecipeStatus.Started;
|
||||||
|
case "Complete":
|
||||||
|
return RecipeStatus.Complete;
|
||||||
|
case "Failed":
|
||||||
|
return RecipeStatus.Failed;
|
||||||
|
default:
|
||||||
|
return RecipeStatus.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -7,10 +7,12 @@ namespace Orchard.Recipes.Services {
|
|||||||
public class RecipeManager : IRecipeManager {
|
public class RecipeManager : IRecipeManager {
|
||||||
private readonly IRecipeStepQueue _recipeStepQueue;
|
private readonly IRecipeStepQueue _recipeStepQueue;
|
||||||
private readonly IRecipeScheduler _recipeScheduler;
|
private readonly IRecipeScheduler _recipeScheduler;
|
||||||
|
private readonly IRecipeJournal _recipeJournal;
|
||||||
|
|
||||||
public RecipeManager(IRecipeStepQueue recipeStepQueue, IRecipeScheduler recipeScheduler) {
|
public RecipeManager(IRecipeStepQueue recipeStepQueue, IRecipeScheduler recipeScheduler, IRecipeJournal recipeJournal) {
|
||||||
_recipeStepQueue = recipeStepQueue;
|
_recipeStepQueue = recipeStepQueue;
|
||||||
_recipeScheduler = recipeScheduler;
|
_recipeScheduler = recipeScheduler;
|
||||||
|
_recipeJournal = recipeJournal;
|
||||||
|
|
||||||
Logger = NullLogger.Instance;
|
Logger = NullLogger.Instance;
|
||||||
T = NullLocalizer.Instance;
|
T = NullLocalizer.Instance;
|
||||||
@@ -24,7 +26,8 @@ namespace Orchard.Recipes.Services {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var executionId = Guid.NewGuid().ToString("n");
|
var executionId = Guid.NewGuid().ToString("n");
|
||||||
// TODO: Run each step inside a transaction boundary.
|
_recipeJournal.ExecutionStart(executionId);
|
||||||
|
|
||||||
foreach (var recipeStep in recipe.RecipeSteps) {
|
foreach (var recipeStep in recipe.RecipeSteps) {
|
||||||
_recipeStepQueue.Enqueue(executionId, recipeStep);
|
_recipeStepQueue.Enqueue(executionId, recipeStep);
|
||||||
}
|
}
|
||||||
|
@@ -7,10 +7,12 @@ using Orchard.Recipes.Models;
|
|||||||
namespace Orchard.Recipes.Services {
|
namespace Orchard.Recipes.Services {
|
||||||
public class RecipeStepExecutor : IRecipeStepExecutor {
|
public class RecipeStepExecutor : IRecipeStepExecutor {
|
||||||
private readonly IRecipeStepQueue _recipeStepQueue;
|
private readonly IRecipeStepQueue _recipeStepQueue;
|
||||||
|
private readonly IRecipeJournal _recipeJournal;
|
||||||
private readonly IEnumerable<IRecipeHandler> _recipeHandlers;
|
private readonly IEnumerable<IRecipeHandler> _recipeHandlers;
|
||||||
|
|
||||||
public RecipeStepExecutor(IRecipeStepQueue recipeStepQueue, IEnumerable<IRecipeHandler> recipeHandlers) {
|
public RecipeStepExecutor(IRecipeStepQueue recipeStepQueue, IRecipeJournal recipeJournal, IEnumerable<IRecipeHandler> recipeHandlers) {
|
||||||
_recipeStepQueue = recipeStepQueue;
|
_recipeStepQueue = recipeStepQueue;
|
||||||
|
_recipeJournal = recipeJournal;
|
||||||
_recipeHandlers = recipeHandlers;
|
_recipeHandlers = recipeHandlers;
|
||||||
|
|
||||||
Logger = NullLogger.Instance;
|
Logger = NullLogger.Instance;
|
||||||
@@ -23,8 +25,10 @@ namespace Orchard.Recipes.Services {
|
|||||||
public bool ExecuteNextStep(string executionId) {
|
public bool ExecuteNextStep(string executionId) {
|
||||||
var nextRecipeStep= _recipeStepQueue.Dequeue(executionId);
|
var nextRecipeStep= _recipeStepQueue.Dequeue(executionId);
|
||||||
if (nextRecipeStep == null) {
|
if (nextRecipeStep == null) {
|
||||||
|
_recipeJournal.ExecutionComplete(executionId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
_recipeJournal.WriteJournalEntry(executionId, string.Format("Executing step {0}.", nextRecipeStep.Name));
|
||||||
var recipeContext = new RecipeContext { RecipeStep = nextRecipeStep, Executed = false };
|
var recipeContext = new RecipeContext { RecipeStep = nextRecipeStep, Executed = false };
|
||||||
try {
|
try {
|
||||||
foreach (var recipeHandler in _recipeHandlers) {
|
foreach (var recipeHandler in _recipeHandlers) {
|
||||||
@@ -34,11 +38,15 @@ namespace Orchard.Recipes.Services {
|
|||||||
catch(Exception exception) {
|
catch(Exception exception) {
|
||||||
Logger.Error(exception, "Recipe execution {0} was cancelled because a step failed to execute", executionId);
|
Logger.Error(exception, "Recipe execution {0} was cancelled because a step failed to execute", executionId);
|
||||||
while (_recipeStepQueue.Dequeue(executionId) != null) ;
|
while (_recipeStepQueue.Dequeue(executionId) != null) ;
|
||||||
|
_recipeJournal.ExecutionFailed(executionId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!recipeContext.Executed) {
|
if (!recipeContext.Executed) {
|
||||||
Logger.Error("Could not execute recipe step '{0}' because the recipe handler was not found.", recipeContext.RecipeStep.Name);
|
Logger.Error("Could not execute recipe step '{0}' because the recipe handler was not found.", recipeContext.RecipeStep.Name);
|
||||||
|
while (_recipeStepQueue.Dequeue(executionId) != null) ;
|
||||||
|
_recipeJournal.ExecutionFailed(executionId);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
namespace Orchard.Recipes.Models {
|
namespace Orchard.Recipes.Models {
|
||||||
public class RecipeJournal {
|
public class RecipeJournal {
|
||||||
public string ExecutionId { get; set; }
|
public string ExecutionId { get; set; }
|
||||||
public RecipeJournalStatus Status { get; set; }
|
public RecipeStatus Status { get; set; }
|
||||||
public IEnumerable<JournalMessage> Messages { get; set; }
|
public IEnumerable<JournalMessage> Messages { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,8 +11,9 @@ namespace Orchard.Recipes.Models {
|
|||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RecipeJournalStatus {
|
public enum RecipeStatus {
|
||||||
Running,
|
Unknown,
|
||||||
|
Started,
|
||||||
Complete,
|
Complete,
|
||||||
Failed
|
Failed
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace Orchard.Recipes.Services {
|
namespace Orchard.Recipes.Services {
|
||||||
public interface IRecipeJournal : IDependency {
|
public interface IRecipeJournal : IDependency {
|
||||||
void StartExecution(string executionId);
|
void ExecutionStart(string executionId);
|
||||||
void ExecutionComplete(string executionId);
|
void ExecutionComplete(string executionId);
|
||||||
void ExecutionFailed(string executionId);
|
void ExecutionFailed(string executionId);
|
||||||
void WriteJournalEntry(string executionId, string message);
|
void WriteJournalEntry(string executionId, string message);
|
||||||
RecipeJournal GetRecipeJournal(string executionId);
|
RecipeJournal GetRecipeJournal(string executionId);
|
||||||
|
RecipeStatus GetRecipeStatus(string executionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user