e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
306 lines
11 KiB
C#
306 lines
11 KiB
C#
//-----------------------------------------------------------------------------
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
namespace System.Activities.Statements
|
|
{
|
|
using System;
|
|
using System.Activities;
|
|
using System.Runtime;
|
|
using System.ComponentModel;
|
|
using System.Activities.Persistence;
|
|
using System.Collections.Generic;
|
|
using System.Xml.Linq;
|
|
using System.Activities.Hosting;
|
|
using System.Threading;
|
|
|
|
[Fx.Tag.XamlVisible(false)]
|
|
public class DurableTimerExtension : TimerExtension, IWorkflowInstanceExtension, IDisposable, ICancelable
|
|
{
|
|
WorkflowInstanceProxy instance;
|
|
TimerTable registeredTimers;
|
|
Action<object> onTimerFiredCallback;
|
|
TimerPersistenceParticipant timerPersistenceParticipant;
|
|
static AsyncCallback onResumeBookmarkComplete = Fx.ThunkCallback(new AsyncCallback(OnResumeBookmarkComplete));
|
|
|
|
static readonly XName timerTableName = XNamespace.Get("urn:schemas-microsoft-com:System.Activities/4.0/properties").GetName("RegisteredTimers");
|
|
static readonly XName timerExpirationTimeName = XNamespace.Get("urn:schemas-microsoft-com:System.Activities/4.0/properties").GetName("TimerExpirationTime");
|
|
bool isDisposed;
|
|
|
|
[Fx.Tag.SynchronizationObject()]
|
|
object thisLock;
|
|
|
|
public DurableTimerExtension()
|
|
: base()
|
|
{
|
|
this.onTimerFiredCallback = new Action<object>(this.OnTimerFired);
|
|
this.thisLock = new object();
|
|
this.timerPersistenceParticipant = new TimerPersistenceParticipant(this);
|
|
this.isDisposed = false;
|
|
}
|
|
|
|
object ThisLock
|
|
{
|
|
get
|
|
{
|
|
return this.thisLock;
|
|
}
|
|
}
|
|
|
|
internal Action<object> OnTimerFiredCallback
|
|
{
|
|
get
|
|
{
|
|
return this.onTimerFiredCallback;
|
|
}
|
|
}
|
|
|
|
internal TimerTable RegisteredTimers
|
|
{
|
|
get
|
|
{
|
|
if (this.registeredTimers == null)
|
|
{
|
|
this.registeredTimers = new TimerTable(this);
|
|
}
|
|
return this.registeredTimers;
|
|
}
|
|
}
|
|
|
|
public virtual IEnumerable<object> GetAdditionalExtensions()
|
|
{
|
|
yield return this.timerPersistenceParticipant;
|
|
}
|
|
|
|
public virtual void SetInstance(WorkflowInstanceProxy instance)
|
|
{
|
|
if (this.instance != null && instance != null)
|
|
{
|
|
throw FxTrace.Exception.AsError(new InvalidOperationException(SR.TimerExtensionAlreadyAttached));
|
|
}
|
|
|
|
this.instance = instance;
|
|
}
|
|
|
|
protected override void OnRegisterTimer(TimeSpan timeout, Bookmark bookmark)
|
|
{
|
|
// This lock is to synchronize with the Timer callback
|
|
if (timeout < TimeSpan.MaxValue)
|
|
{
|
|
lock (this.ThisLock)
|
|
{
|
|
Fx.Assert(!this.isDisposed, "DurableTimerExtension is already disposed, it cannot be used to register a new timer.");
|
|
this.RegisteredTimers.AddTimer(timeout, bookmark);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnCancelTimer(Bookmark bookmark)
|
|
{
|
|
// This lock is to synchronize with the Timer callback
|
|
lock (this.ThisLock)
|
|
{
|
|
this.RegisteredTimers.RemoveTimer(bookmark);
|
|
}
|
|
}
|
|
|
|
internal void OnSave(out IDictionary<XName, object> readWriteValues, out IDictionary<XName, object> writeOnlyValues)
|
|
{
|
|
readWriteValues = null;
|
|
writeOnlyValues = null;
|
|
|
|
// Using a lock here to prevent the timer firing back without us being ready
|
|
lock (this.ThisLock)
|
|
{
|
|
this.RegisteredTimers.MarkAsImmutable();
|
|
if (this.registeredTimers != null && this.registeredTimers.Count > 0)
|
|
{
|
|
readWriteValues = new Dictionary<XName, object>(1);
|
|
writeOnlyValues = new Dictionary<XName, object>(1);
|
|
readWriteValues.Add(timerTableName, this.registeredTimers);
|
|
writeOnlyValues.Add(timerExpirationTimeName, this.registeredTimers.GetNextDueTime());
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void PersistenceDone()
|
|
{
|
|
lock (this.ThisLock)
|
|
{
|
|
this.RegisteredTimers.MarkAsMutable();
|
|
}
|
|
}
|
|
|
|
internal void OnLoad(IDictionary<XName, object> readWriteValues)
|
|
{
|
|
lock (this.ThisLock)
|
|
{
|
|
object timerTable;
|
|
if (readWriteValues != null && readWriteValues.TryGetValue(timerTableName, out timerTable))
|
|
{
|
|
this.registeredTimers = timerTable as TimerTable;
|
|
Fx.Assert(this.RegisteredTimers != null, "Timer Table cannot be null");
|
|
this.RegisteredTimers.OnLoad(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnTimerFired(object state)
|
|
{
|
|
Bookmark timerBookmark = state as Bookmark;
|
|
|
|
WorkflowInstanceProxy targetInstance = this.instance;
|
|
// it's possible that we've been unloaded while the timer was in the process of firing, in
|
|
// which case targetInstance will be null
|
|
if (targetInstance != null)
|
|
{
|
|
BookmarkResumptionResult resumptionResult;
|
|
IAsyncResult result = null;
|
|
bool completed = false;
|
|
|
|
result = targetInstance.BeginResumeBookmark(timerBookmark, null, TimeSpan.MaxValue,
|
|
onResumeBookmarkComplete, new BookmarkResumptionState(timerBookmark, this, targetInstance));
|
|
completed = result.CompletedSynchronously;
|
|
|
|
if (completed && result != null)
|
|
{
|
|
try
|
|
{
|
|
resumptionResult = targetInstance.EndResumeBookmark(result);
|
|
ProcessBookmarkResumptionResult(timerBookmark, resumptionResult);
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
ProcessBookmarkResumptionResult(timerBookmark, BookmarkResumptionResult.NotReady);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void OnResumeBookmarkComplete(IAsyncResult result)
|
|
{
|
|
if (result.CompletedSynchronously)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BookmarkResumptionState state = (BookmarkResumptionState)result.AsyncState;
|
|
|
|
BookmarkResumptionResult resumptionResult = state.Instance.EndResumeBookmark(result);
|
|
state.TimerExtension.ProcessBookmarkResumptionResult(state.TimerBookmark, resumptionResult);
|
|
}
|
|
|
|
|
|
void ProcessBookmarkResumptionResult(Bookmark timerBookmark, BookmarkResumptionResult result)
|
|
{
|
|
switch (result)
|
|
{
|
|
case BookmarkResumptionResult.NotFound:
|
|
case BookmarkResumptionResult.Success:
|
|
// The bookmark is removed maybe due to WF cancel, abort or the bookmark succeeds
|
|
// no need to keep the timer around
|
|
lock (this.ThisLock)
|
|
{
|
|
if (!this.isDisposed)
|
|
{
|
|
this.RegisteredTimers.RemoveTimer(timerBookmark);
|
|
}
|
|
}
|
|
break;
|
|
case BookmarkResumptionResult.NotReady:
|
|
// The workflow maybe in one of these states: Completed, Aborted, Abandoned, unloading, Suspended
|
|
// In the first 3 cases, we will let TimerExtension.CancelTimer take care of the cleanup.
|
|
// In the 4th case, we want the timer to retry when it is loaded back, in all 4 cases we don't need to delete the timer
|
|
// In the 5th case, we want the timer to retry until it succeeds.
|
|
// Retry:
|
|
lock (this.ThisLock)
|
|
{
|
|
this.RegisteredTimers.RetryTimer(timerBookmark);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (this.registeredTimers != null)
|
|
{
|
|
lock (this.ThisLock)
|
|
{
|
|
this.isDisposed = true;
|
|
if (this.registeredTimers != null)
|
|
{
|
|
this.registeredTimers.Dispose();
|
|
}
|
|
}
|
|
}
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
void ICancelable.Cancel()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
class BookmarkResumptionState
|
|
{
|
|
public BookmarkResumptionState(Bookmark timerBookmark, DurableTimerExtension timerExtension, WorkflowInstanceProxy instance)
|
|
{
|
|
this.TimerBookmark = timerBookmark;
|
|
this.TimerExtension = timerExtension;
|
|
this.Instance = instance;
|
|
}
|
|
|
|
public Bookmark TimerBookmark
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public DurableTimerExtension TimerExtension
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public WorkflowInstanceProxy Instance
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
}
|
|
|
|
class TimerPersistenceParticipant : PersistenceIOParticipant
|
|
{
|
|
DurableTimerExtension defaultTimerExtension;
|
|
|
|
public TimerPersistenceParticipant(DurableTimerExtension timerExtension)
|
|
: base(false, false)
|
|
{
|
|
this.defaultTimerExtension = timerExtension;
|
|
}
|
|
|
|
protected override void CollectValues(out IDictionary<XName, object> readWriteValues, out IDictionary<XName, object> writeOnlyValues)
|
|
{
|
|
this.defaultTimerExtension.OnSave(out readWriteValues, out writeOnlyValues);
|
|
}
|
|
|
|
protected override void PublishValues(IDictionary<XName, object> readWriteValues)
|
|
{
|
|
this.defaultTimerExtension.OnLoad(readWriteValues);
|
|
}
|
|
|
|
protected override IAsyncResult BeginOnSave(IDictionary<XName, object> readWriteValues, IDictionary<XName, object> writeOnlyValues, TimeSpan timeout, AsyncCallback callback, object state)
|
|
{
|
|
this.defaultTimerExtension.PersistenceDone();
|
|
return base.BeginOnSave(readWriteValues, writeOnlyValues, timeout, callback, state);
|
|
}
|
|
|
|
protected override void Abort()
|
|
{
|
|
this.defaultTimerExtension.PersistenceDone();
|
|
}
|
|
}
|
|
}
|
|
}
|