//----------------------------------------------------------------------------- // 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 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(this.OnTimerFired); this.thisLock = new object(); this.timerPersistenceParticipant = new TimerPersistenceParticipant(this); this.isDisposed = false; } object ThisLock { get { return this.thisLock; } } internal Action OnTimerFiredCallback { get { return this.onTimerFiredCallback; } } internal TimerTable RegisteredTimers { get { if (this.registeredTimers == null) { this.registeredTimers = new TimerTable(this); } return this.registeredTimers; } } public virtual IEnumerable 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 readWriteValues, out IDictionary 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(1); writeOnlyValues = new Dictionary(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 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 readWriteValues, out IDictionary writeOnlyValues) { this.defaultTimerExtension.OnSave(out readWriteValues, out writeOnlyValues); } protected override void PublishValues(IDictionary readWriteValues) { this.defaultTimerExtension.OnLoad(readWriteValues); } protected override IAsyncResult BeginOnSave(IDictionary readWriteValues, IDictionary 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(); } } } }