开发 ac# 应用程序,该应用程序将根据用户上传的文件运行一系列任务;这可能需要几秒钟到几天的时间。如果进程中断,我计划实施某种日志系统以恢复工作。什么是中断/恢复日志,我在哪里可以了解有关现有实现的更多信息?
编辑:
到目前为止我发现的有用的东西:
日志文件系统将使用相同的基本过程,但需要一些额外的步骤。就像是:
- 日记条目:将文件从 A 移动到 B
- 将旧文件物理复制到新位置
- 更新新驱动器上的目录条目
- 从旧驱动器中删除目录条目
- 旧驱动器上的可用空间
- 日记条目:完成将文件从 A 移动到 B
来自https://serverfault.com/questions/173176/what-is-a-journaling-file-system
检查点:将日志元数据和数据写入其固定位置的过程称为检查点。当超过各种阈值时触发检查点,例如,当文件系统缓冲区空间不足时,当日志中剩余的可用空间很少时,或者当计时器到期时。
崩溃恢复:在 ext3 中,崩溃恢复很简单(就像在许多日志文件系统中一样);使用重做日志的基本形式。因为新的更新(无论是数据还是元数据)被写入日志,恢复就地文件系统结构的过程很容易。在恢复期间,文件系统扫描日志以查找已提交的完整事务;不完整的交易被丢弃。已完成事务中的每次更新都会简单地重播到固定位置的 ext2 结构中。
来自http://research.cs.wisc.edu/adsl/Publications/sba-usenix05.pdf(第 5 页)
编辑2:
与我第一次发布这个问题时相比,我觉得我对容错的了解并不多,但这里是我实现的概要。
Main 首先尝试让作业管理器从文件加载任何现有的已保存状态,或者创建一个新的空管理器。
public static void Main(string[] args)
{
try
{
_jxman = JobManager<Job>.Load(Properties.Settings.Default.JobJournalFilename);
}
catch
{
_jxman = new JobManager<Job>(Properties.Settings.Default.JobJournalFilename);
}
...
_jxman.Start();
...
}
JobManager 类看起来像这样
public sealed class JobManager<T> : WorkGroupBase<JobBase>, IWorkSerializable where T : IJob
{
#region Fields
/// <summary>
/// Hash goes here in file
/// </summary>
private const string _hashHeading = "SHA-256";
/// <summary>
/// Flag to know whether to update the journal
/// </summary>
private bool _isDirty = false;
/// <summary>
/// Last time the journal was written to disk
/// </summary>
private DateTime _lastSaveTime = DateTime.MinValue;
/// <summary>
/// Minimum time to wait before writing journal to disk again
/// </summary>
private TimeSpan _minTimeToSave = new TimeSpan(0,0,60);
/// <summary>
/// Threading object for lock
/// </summary>
private object _lock = new object();
/// <summary>
/// Thread to monitor status
/// </summary>
private Thread _watchDirtyFlag;
#endregion
#region Properties
/// <summary>
/// journal file to track changes
/// </summary>
public string Filename
{
get;
private set;
}
#endregion
#region Constructors
/// <summary>
/// default constructor
/// </summary>
/// <param name="filename">Path to filename to write journal file</param>
public JobManager(string filename) : base()
{
ConstructorHelper();
Filename = filename;
}
/// <summary>
/// Parses XML element to recreate the item
/// </summary>
/// <param name="xe">XML element used to create object</param>
public JobManager(XElement xe)
: base(xe)
{
// Checksum validation before doing anything else.
// Will throw exception on failure.
ValidateChecksum(xe);
ConstructorHelper();
string myName = "JobManager";
XElement myself;
try
{
myself = xe.DescendantsAndSelf(myName).First();
}
catch
{
throw new ArgumentException("Attempting to instantiate object, but no relevant information was found in the XML element");
}
Filename = myself.FirstElementValue("Filename");
// Load up all the jobs
XElement[] wq = myself.Descendants("WorkQueue").Elements().ToArray();
foreach (XElement x in wq)
{
try
{
IJob blarg = (IJob)Activator.CreateInstance(typeof(T), x);
if (blarg != null)
WorkQueue.Enqueue((JobBase)blarg);
}
catch
{ }
}
}
/// <summary>
/// Helper for common constructing
/// </summary>
private void ConstructorHelper()
{
// need to wait for the base constructor to finish before attempting to
// hook events there
base.QueueChanged += new EventHandler(JobManager_QueueChanged);
base.HookQueueChangedEvents();
_watchDirtyFlag = new Thread(WatchDirtyFlag);
_watchDirtyFlag.Start();
}
#endregion
#region Methods
/// <summary>
/// Saves the state of the JobManager to Filename using XML
/// </summary>
public void Save()
{
TextWriter writer = null;
try
{
writer = new StreamWriter(Filename);
writer.Write(this.ToXElement());
}
catch (Exception ex)
{
throw ex;
}
finally
{
writer.Close();
}
}
/// <summary>
/// Loads the filename and attempts to parse it as XML to
/// create a JobManager. Pass the type of job to manage.
/// </summary>
/// <param name="filename">File storing the JobManager as XML</param>
/// <returns>JobManager with values loaded from file</returns>
public static JobManager<T> Load(string filename)
{
if (filename == "")
throw new ArgumentException("Can not load JobManager: Filename not set");
TextReader reader = null;
string text;
try
{
reader = new StreamReader(filename);
text = reader.ReadToEnd();
}
catch (Exception ex)
{
throw ex;
}
finally
{
reader.Close();
}
XElement loadFrom = null;
try
{
loadFrom = XElement.Parse(text);
}
catch //(Exception ex)
{
//throw ex;
loadFrom = new XElement("empty");
}
JobManager<T> output = new JobManager<T>(loadFrom);
output.Filename = filename;
return output;
}
/// <summary>
/// Converts the item to an XML element
/// </summary>
/// <returns></returns>
new public XElement ToXElement()
{
XElement bxe = base.ToXElement();
//string myName = this.GetType().Name;
string myName = "JobManager";
XElement wq = new XElement("WorkQueue");
foreach (IWorkSerializable t in WorkQueue.ToArray())
{
XElement addee = t.ToXElement();
wq.Add(addee);
}
bxe.Add(wq);
XElement xe = new XElement(myName,
bxe,
new XElement("Filename", Filename)
);
xe.Add(
new XElement(_hashHeading, Generic.ComputeSha256Hash(xe.ToString()))
);
return xe;
}
/// <summary>
/// Validates the checksum for the current xelement. Throws exceptions on failure
/// </summary>
/// <param name="xe">XML tree of the itme to validate</param>
private void ValidateChecksum(XElement xe)
{
XElement checksum;
try
{
checksum = xe.DescendantsAndSelf(_hashHeading).First();
}
catch (Exception ex)
{
throw new Exception("Unable to find checksum node", ex);
}
XElement withoutChecksum = new XElement(xe);
withoutChecksum.Elements(_hashHeading).Remove();
string computedChecksum = Generic.ComputeSha256Hash(withoutChecksum.ToString());
if (computedChecksum != checksum.Value)
throw new Exception("Checksum from XML element and checksum from contents of XML element do not match: \n" + xe.Value);
}
/// <summary>
/// This thread will watch the dirty flag, which is set everytime the
/// queues are changed. Every _minTimeToSave the flag is checked, and
/// if the flag is set, Save() is called.
/// </summary>
private void WatchDirtyFlag()
{
while (true)
{
// sleep until there's something to update
while (_isDirty == false)
{
Thread.Sleep(_minTimeToSave);
}
// but don't update too frequently
if (DateTime.Now.Subtract(_lastSaveTime) > _minTimeToSave)
{
// save first ...
this.Save();
// then update items ...
_lastSaveTime = DateTime.Now;
lock (_lock)
{
_isDirty = false;
}
}
}
}
#endregion
#region Event Handlers
/// <summary>
/// updates flag when any underlying queue changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void JobManager_QueueChanged(object sender, EventArgs e)
{
lock (_lock)
{
_isDirty = true;
}
}
#endregion
}
注意事项:
- 这是不完整的代码,以防有人试图复制它(缺少基类和东西)
- 常规(二进制)和 XML 序列化从未完全正常工作,因此我实现了将对象保存为 XML 的自定义序列化。这是
ToXElement()
方法,也是接受XElement
参数的构造函数。 - 序列化的顶层 (JobManager) 包含一个校验和 (SHA-256)。当从 实例化新对象时
XElement
,将保存的序列化对象的校验和与文件中的校验和进行比较。 - 有一个静态方法
.Load(file)
通过读取文件并尝试反序列化内容来返回一个新的 JobManager 对象。 - 此处未显示自定义 ConcurrentQueue 类。这是 MSDN ConcurrentQueue类的包装器,但添加了一个事件以在队列更改时进行通知。
- 这个 JobManager 类实现了一个带有上述 ConcurrentQueue 的基类;那些队列更改事件挂在构造函数助手中
- 当事件触发时,JobManager 设置一个标志
_isDirty
- JobManager 在实例化时启动一个线程,它监视
_isDirty
标志。大部分时间都花在睡眠上,但如果至少_minTimeToSave
已经过去,JobManager 的内容将被序列化到磁盘。这应该可以防止 JobManager 过于频繁地写入磁盘。
未解决的注意事项:
- 线程真的是观看
_isDirty
标志的正确解决方案吗? - JobManager(单线程)管理包含任务的作业(一次一个,但不同的线程);序列化时没有类到基类同步来锁定状态
- 旧的已完成作业被序列化到磁盘,然后重新加载