From daecae6fdfb5602bc9a18100e9a60fab95ba0fd5 Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Fri, 24 Jul 2015 15:23:30 -0700 Subject: [PATCH 1/8] Call global-clock tick when fast-forwarding time. --- src/mindtouch.dream/system/GlobalClock.cs | 36 +++++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index a0cdc84..ca38fea 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -128,7 +128,13 @@ public static void FastForward(TimeSpan time) { if(time < TimeSpan.Zero) { throw new ArgumentException("time cannot be negative"); } - Interlocked.Add(ref _timeOffset, (int)time.TotalMilliseconds); + var timeMilliseconds = (int)time.TotalMilliseconds; + while(timeMilliseconds >= _intervalMilliseconds) { + Interlocked.Add(ref _timeOffset, _intervalMilliseconds); + MasterTick(UtcNow, TimeSpan.FromMilliseconds(_intervalMilliseconds)); + timeMilliseconds -= _intervalMilliseconds; + } + Interlocked.Add(ref _timeOffset, timeMilliseconds); } internal static bool Shutdown(TimeSpan timeout) { @@ -156,22 +162,26 @@ private static void MasterTickThread() { last = now; // execute all callbacks - lock(_syncRoot) { - var callbacks = _callbacks; - foreach(var callback in callbacks) { - if(callback.Value != null) { - try { - callback.Value(now, elapsed); - } catch(Exception e) { - _log.ErrorExceptionMethodCall(e, "GlobalClock callback failed", callback.Key); - } - } - } - } + MasterTick(now, elapsed); } // indicate that this thread has exited _stopped.Set(); } + + private static void MasterTick(DateTime now, TimeSpan elapsed) { + lock(_syncRoot) { + var callbacks = _callbacks; + foreach(var callback in callbacks) { + if(callback.Value != null) { + try { + callback.Value(now, elapsed); + } catch(Exception e) { + _log.ErrorExceptionMethodCall(e, "GlobalClock callback failed", callback.Key); + } + } + } + } + } } } From afa9c6fcc32733e6879ef5afbef4dc9f3b50692f Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Sat, 25 Jul 2015 08:08:27 -0700 Subject: [PATCH 2/8] GlobalClock.FastForward() now forces firing timers to execute on the current thread. --- src/mindtouch.dream/Tasking/TaskEnv.cs | 128 ------------------ .../Tasking/TaskTimerFactory.cs | 35 ++--- src/mindtouch.dream/Tasking/timer.cs | 4 + .../Threading/DispatchThreadScheduler.cs | 5 +- src/mindtouch.dream/system/GlobalClock.cs | 12 +- src/tests/DreamMisc/TaskTests.cs | 12 -- 6 files changed, 33 insertions(+), 163 deletions(-) diff --git a/src/mindtouch.dream/Tasking/TaskEnv.cs b/src/mindtouch.dream/Tasking/TaskEnv.cs index 4d75a02..086b8bc 100644 --- a/src/mindtouch.dream/Tasking/TaskEnv.cs +++ b/src/mindtouch.dream/Tasking/TaskEnv.cs @@ -525,134 +525,6 @@ public void Invoke(Action handler, T1 arg1) { } } - /// - /// Invoke a two argument action. - /// - /// Type of first argument. - /// Type of second argument. - /// Action to invoke. - /// First argument. - /// Second argument. - public void Invoke(Action handler, T1 arg1, T2 arg2) { - if(handler == null) { - throw new ArgumentNullException("handler"); - } - if(_referenceCount < 1) { - throw new InvalidOperationException("Cannot call invoke an unaquired TaskEnv"); - } -#pragma warning disable 219 - System.Diagnostics.StackTrace stacktrace = DebugUtil.GetStackTrace(); -#pragma warning restore 219 - - // check if handler can be invoked in-place or needs to queued up - if(_dispatchQueue != null) { - _dispatchQueue.QueueWorkItem(() => { - - // store current thread-specific settings - TaskEnv previousEnv = _currentEnv; - try { - - // set thread-specific settings - _currentEnv = this; - - // execute handler - handler(arg1, arg2); - } catch(Exception e) { - _log.WarnExceptionMethodCall(e, "Invoke: unhandled exception in handler"); - } finally { - Release(); - - // restore thread-specific settings - _currentEnv = previousEnv; - } - }); - } else { - - // store current thread-specific settings - TaskEnv previousEnv = _currentEnv; - try { - - // set thread-specific settings - _currentEnv = this; - - // execute handler - handler(arg1, arg2); - } catch(Exception e) { - _log.WarnExceptionMethodCall(e, "Invoke: unhandled exception in handler"); - } finally { - Release(); - - // restore thread-specific settings - _currentEnv = previousEnv; - } - } - } - - /// - /// Invoke a three argument action. - /// - /// Type of first argument. - /// Type of second argument. - /// Type of third argument. - /// Action to invoke. - /// First argument. - /// Second argument. - /// Third argument. - public void Invoke(Action handler, T1 arg1, T2 arg2, T3 arg3) { - if(handler == null) { - throw new ArgumentNullException("handler"); - } - if(_referenceCount < 1) { - throw new InvalidOperationException("Cannot call invoke an unaquired TaskEnv"); - } -#pragma warning disable 219 - System.Diagnostics.StackTrace stacktrace = DebugUtil.GetStackTrace(); -#pragma warning restore 219 - - // check if handler can be invoked in-place or needs to queued up - if(_dispatchQueue != null) { - _dispatchQueue.QueueWorkItem(() => { - - // store current thread-specific settings - TaskEnv previousEnv = _currentEnv; - try { - - // set thread-specific settings - _currentEnv = this; - - // execute handler - handler(arg1, arg2, arg3); - } catch(Exception e) { - _log.WarnExceptionMethodCall(e, "Invoke: unhandled exception in handler"); - } finally { - Release(); - - // restore thread-specific settings - _currentEnv = previousEnv; - } - }); - } else { - - // store current thread-specific settings - TaskEnv previousEnv = _currentEnv; - try { - - // set thread-specific settings - _currentEnv = this; - - // execute handler - handler(arg1, arg2, arg3); - } catch(Exception e) { - _log.WarnExceptionMethodCall(e, "Invoke: unhandled exception in handler"); - } finally { - Release(); - - // restore thread-specific settings - _currentEnv = previousEnv; - } - } - } - /// /// Wrap a method call, delegate or lambda in an environment for later invocation. /// diff --git a/src/mindtouch.dream/Tasking/TaskTimerFactory.cs b/src/mindtouch.dream/Tasking/TaskTimerFactory.cs index e011fef..fc60ef9 100644 --- a/src/mindtouch.dream/Tasking/TaskTimerFactory.cs +++ b/src/mindtouch.dream/Tasking/TaskTimerFactory.cs @@ -306,11 +306,11 @@ public void Shutdown() { // (or maybe this should just be part of the thread clean-up) lock(_queue) { while(_queue.Count > 0) { - TaskTimer timer = _queue.Dequeue(); + var timer = _queue.Dequeue(); if(timer.TryLockPending()) { // retrieve the associated behavior and reset the timer - TaskEnv env = timer.Env; + var env = timer.Env; timer.Env = null; timer.SetStatus(TaskTimerStatus.Done); @@ -321,19 +321,16 @@ public void Shutdown() { } } - // BUGBUGBUG (arnec): we don't actually do anything with timeout, but let every timer take - // an indefinite time. // check if any timers were gathered for immediate execution if(timers != null) { - foreach(KeyValuePair entry in timers) { - entry.Key.Execute(entry.Value); + foreach(var entry in timers) { + entry.Key.ExecuteNow(entry.Value); } } - _running = false; } - private void Tick(DateTime now, TimeSpan elapsed) { + private void Tick(DateTime now, TimeSpan elapsed, bool fastforward) { // ignore ticks that come in after we've initialized a shutdown if(_shutdown) { @@ -348,13 +345,13 @@ private void Tick(DateTime now, TimeSpan elapsed) { // dequeue all timers that are ready to go while((_queue.Count > 0) && (_queue.Peek().When <= now)) { - TaskTimer timer = _queue.Dequeue(); + var timer = _queue.Dequeue(); // check if timer can be transitioned if(timer.TryLockQueued()) { // retrieve the associated behavior and reset the timer - TaskEnv env = timer.Env; + var env = timer.Env; timer.Env = null; timer.SetStatus(TaskTimerStatus.Done); @@ -367,15 +364,15 @@ private void Tick(DateTime now, TimeSpan elapsed) { // check if a maintance run is due if(_maintenance <= now) { _maintenance = now.AddSeconds(TaskTimer.QUEUE_RESCAN); - DateTime horizon = now.AddSeconds(TaskTimer.QUEUE_CUTOFF); + var horizon = now.AddSeconds(TaskTimer.QUEUE_CUTOFF); lock(_pending) { - List activate = new List(); - foreach(TaskTimer timer in _pending.Keys) { + var activate = new List(); + foreach(var timer in _pending.Keys) { if(timer.When <= horizon) { activate.Add(timer); } } - foreach(TaskTimer timer in activate) { + foreach(var timer in activate) { _pending.Remove(timer); if(timer.TryQueuePending()) { _queue.Enqueue(timer); @@ -387,8 +384,14 @@ private void Tick(DateTime now, TimeSpan elapsed) { // run schedule on its own thread to avoid re-entrancy issues if(timers != null) { - foreach(KeyValuePair entry in timers) { - entry.Key.Execute(entry.Value); + if(fastforward) { + foreach(var entry in timers) { + entry.Key.ExecuteNow(entry.Value); + } + } else { + foreach(var entry in timers) { + entry.Key.Execute(entry.Value); + } } } } diff --git a/src/mindtouch.dream/Tasking/timer.cs b/src/mindtouch.dream/Tasking/timer.cs index 317757e..73f1834 100644 --- a/src/mindtouch.dream/Tasking/timer.cs +++ b/src/mindtouch.dream/Tasking/timer.cs @@ -317,6 +317,10 @@ internal void Execute(TaskEnv env) { env.Invoke(_handler, this); } + internal void ExecuteNow(TaskEnv env) { + env.InvokeNow(() => _handler(this)); + } + internal void SetStatus(TaskTimerStatus status) { Interlocked.Exchange(ref _status, (int)status); } diff --git a/src/mindtouch.dream/Threading/DispatchThreadScheduler.cs b/src/mindtouch.dream/Threading/DispatchThreadScheduler.cs index 7b1ca24..1ed9faa 100644 --- a/src/mindtouch.dream/Threading/DispatchThreadScheduler.cs +++ b/src/mindtouch.dream/Threading/DispatchThreadScheduler.cs @@ -188,7 +188,10 @@ public static void UnregisterHost(IDispatchHost host) { } } - private static void Tick(DateTime now, TimeSpan elapsed) { + private static void Tick(DateTime now, TimeSpan elapsed, bool fastforward) { + + // NOTE (2015-07-25, steveb): 'fastforward' is not used by the DispatchThreadScheduler + lock(_syncRoot) { // check if resource manager has been idle for a while diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index ca38fea..b392d4a 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -26,8 +26,8 @@ using MindTouch.Tasking; namespace System { - using ClockCallback = Action; - using NamedClockCallback = KeyValuePair>; + using ClockCallback = Action; + using NamedClockCallback = KeyValuePair>; /// /// Provides a global timing mechanism that accepts registration of callback to be invoked by the clock. In most cases, a @@ -131,7 +131,7 @@ public static void FastForward(TimeSpan time) { var timeMilliseconds = (int)time.TotalMilliseconds; while(timeMilliseconds >= _intervalMilliseconds) { Interlocked.Add(ref _timeOffset, _intervalMilliseconds); - MasterTick(UtcNow, TimeSpan.FromMilliseconds(_intervalMilliseconds)); + MasterTick(UtcNow, TimeSpan.FromMilliseconds(_intervalMilliseconds), true); timeMilliseconds -= _intervalMilliseconds; } Interlocked.Add(ref _timeOffset, timeMilliseconds); @@ -162,20 +162,20 @@ private static void MasterTickThread() { last = now; // execute all callbacks - MasterTick(now, elapsed); + MasterTick(now, elapsed, false); } // indicate that this thread has exited _stopped.Set(); } - private static void MasterTick(DateTime now, TimeSpan elapsed) { + private static void MasterTick(DateTime now, TimeSpan elapsed, bool fastforward) { lock(_syncRoot) { var callbacks = _callbacks; foreach(var callback in callbacks) { if(callback.Value != null) { try { - callback.Value(now, elapsed); + callback.Value(now, elapsed, fastforward); } catch(Exception e) { _log.ErrorExceptionMethodCall(e, "GlobalClock callback failed", callback.Key); } diff --git a/src/tests/DreamMisc/TaskTests.cs b/src/tests/DreamMisc/TaskTests.cs index 43277d7..1fb1bdc 100644 --- a/src/tests/DreamMisc/TaskTests.cs +++ b/src/tests/DreamMisc/TaskTests.cs @@ -374,18 +374,6 @@ public void Invoke1arg_an_unacquired_TaskEnv_throws() { TaskEnv.Current.Invoke(x => _log.Debug("invoke now"), 1); } - [Test] - [ExpectedException(typeof(InvalidOperationException))] - public void Invoke2arg_an_unacquired_TaskEnv_throws() { - TaskEnv.Current.Invoke((x, y) => _log.Debug("invoke now"), 1, 2); - } - - [Test] - [ExpectedException(typeof(InvalidOperationException))] - public void Invoke3arg_an_unacquired_TaskEnv_throws() { - TaskEnv.Current.Invoke((x, y, z) => _log.Debug("invoke now"), 1, 2, 3); - } - [Test] public void Invokenow_does_not_release() { var state = new TaskLifeSpanState("baz"); From 4c813c568a32c7793009b94d405606acfa7d626e Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Sat, 25 Jul 2015 10:14:29 -0700 Subject: [PATCH 3/8] Increase FastForward() resolution. --- src/mindtouch.dream/system/GlobalClock.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index b392d4a..5e7de72 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -129,10 +129,11 @@ public static void FastForward(TimeSpan time) { throw new ArgumentException("time cannot be negative"); } var timeMilliseconds = (int)time.TotalMilliseconds; - while(timeMilliseconds >= _intervalMilliseconds) { - Interlocked.Add(ref _timeOffset, _intervalMilliseconds); - MasterTick(UtcNow, TimeSpan.FromMilliseconds(_intervalMilliseconds), true); - timeMilliseconds -= _intervalMilliseconds; + var intervalMilliseconds = _intervalMilliseconds / 2; + while(timeMilliseconds >= intervalMilliseconds) { + Interlocked.Add(ref _timeOffset, intervalMilliseconds); + MasterTick(UtcNow, TimeSpan.FromMilliseconds(intervalMilliseconds), true); + timeMilliseconds -= intervalMilliseconds; } Interlocked.Add(ref _timeOffset, timeMilliseconds); } From f715706cfb6b1ea4c396cffe26af433280182a9e Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Mon, 27 Jul 2015 14:05:16 -0700 Subject: [PATCH 4/8] GlobalClock.FastForward() can now be canceled based on an optional callback value. --- src/mindtouch.dream/system/GlobalClock.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index 5e7de72..a5beda4 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -123,8 +123,9 @@ public static void RemoveCallback(ClockCallback callback) { /// Fast-forward time for the global clock. /// /// Timespan to fast-forward the global clock (cannot be negative). + /// Optional callback to prematurely cancel the fast-fwoard operation. /// DO NOT USE FOR PRODUCTION CODE!!! - public static void FastForward(TimeSpan time) { + public static void FastForward(TimeSpan time, Func cancelFastForward = null) { if(time < TimeSpan.Zero) { throw new ArgumentException("time cannot be negative"); } @@ -133,6 +134,9 @@ public static void FastForward(TimeSpan time) { while(timeMilliseconds >= intervalMilliseconds) { Interlocked.Add(ref _timeOffset, intervalMilliseconds); MasterTick(UtcNow, TimeSpan.FromMilliseconds(intervalMilliseconds), true); + if((cancelFastForward != null) && cancelFastForward()) { + return; + } timeMilliseconds -= intervalMilliseconds; } Interlocked.Add(ref _timeOffset, timeMilliseconds); From 2d8fa663fed2804fdbc130b74357c009d9f61b69 Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Mon, 27 Jul 2015 14:20:22 -0700 Subject: [PATCH 5/8] Return current time from FastForward(). Useful when FastForward() is canceled by the callback. --- src/mindtouch.dream/system/GlobalClock.cs | 44 ++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index a5beda4..08d8c5a 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -125,21 +125,25 @@ public static void RemoveCallback(ClockCallback callback) { /// Timespan to fast-forward the global clock (cannot be negative). /// Optional callback to prematurely cancel the fast-fwoard operation. /// DO NOT USE FOR PRODUCTION CODE!!! - public static void FastForward(TimeSpan time, Func cancelFastForward = null) { + public static DateTime FastForward(TimeSpan time, Func cancelFastForward = null) { if(time < TimeSpan.Zero) { throw new ArgumentException("time cannot be negative"); } - var timeMilliseconds = (int)time.TotalMilliseconds; - var intervalMilliseconds = _intervalMilliseconds / 2; - while(timeMilliseconds >= intervalMilliseconds) { - Interlocked.Add(ref _timeOffset, intervalMilliseconds); - MasterTick(UtcNow, TimeSpan.FromMilliseconds(intervalMilliseconds), true); - if((cancelFastForward != null) && cancelFastForward()) { - return; + lock(_syncRoot) { + var timeMilliseconds = (int)time.TotalMilliseconds; + var intervalMilliseconds = _intervalMilliseconds / 2; + while(timeMilliseconds >= intervalMilliseconds) { + Interlocked.Add(ref _timeOffset, intervalMilliseconds); + var now = UtcNow; + MasterTick(now, TimeSpan.FromMilliseconds(intervalMilliseconds), true); + if((cancelFastForward != null) && cancelFastForward()) { + return now; + } + timeMilliseconds -= intervalMilliseconds; } - timeMilliseconds -= intervalMilliseconds; + Interlocked.Add(ref _timeOffset, timeMilliseconds); + return UtcNow; } - Interlocked.Add(ref _timeOffset, timeMilliseconds); } internal static bool Shutdown(TimeSpan timeout) { @@ -167,7 +171,9 @@ private static void MasterTickThread() { last = now; // execute all callbacks - MasterTick(now, elapsed, false); + lock(_syncRoot) { + MasterTick(now, elapsed, false); + } } // indicate that this thread has exited @@ -175,15 +181,13 @@ private static void MasterTickThread() { } private static void MasterTick(DateTime now, TimeSpan elapsed, bool fastforward) { - lock(_syncRoot) { - var callbacks = _callbacks; - foreach(var callback in callbacks) { - if(callback.Value != null) { - try { - callback.Value(now, elapsed, fastforward); - } catch(Exception e) { - _log.ErrorExceptionMethodCall(e, "GlobalClock callback failed", callback.Key); - } + var callbacks = _callbacks; + foreach(var callback in callbacks) { + if(callback.Value != null) { + try { + callback.Value(now, elapsed, fastforward); + } catch(Exception e) { + _log.ErrorExceptionMethodCall(e, "GlobalClock callback failed", callback.Key); } } } From 0b5013e7307722229f40d2feeb4d54d48f23ca5a Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Tue, 28 Jul 2015 07:32:13 -0700 Subject: [PATCH 6/8] Added GlobalClock.Suspend() to stop time when needed. --- src/mindtouch.dream/mindtouch.dream.csproj | 1 + src/mindtouch.dream/system/DisposeCallback.cs | 50 +++++++++++++++++++ src/mindtouch.dream/system/GlobalClock.cs | 17 ++++++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/mindtouch.dream/system/DisposeCallback.cs diff --git a/src/mindtouch.dream/mindtouch.dream.csproj b/src/mindtouch.dream/mindtouch.dream.csproj index 760d5bc..219c699 100644 --- a/src/mindtouch.dream/mindtouch.dream.csproj +++ b/src/mindtouch.dream/mindtouch.dream.csproj @@ -134,6 +134,7 @@ + diff --git a/src/mindtouch.dream/system/DisposeCallback.cs b/src/mindtouch.dream/system/DisposeCallback.cs new file mode 100644 index 0000000..4020fa2 --- /dev/null +++ b/src/mindtouch.dream/system/DisposeCallback.cs @@ -0,0 +1,50 @@ +/* + * MindTouch Dream - a distributed REST framework + * Copyright (C) 2006-2014 MindTouch, Inc. + * www.mindtouch.com oss@mindtouch.com + * + * For community documentation and downloads visit mindtouch.com; + * please review the licensing section. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace System { + + /// + /// Class to trigger a callback when Dispose() is called. + /// + public class DisposeCallback : IDisposable { + + //--- Fields --- + private readonly Action _callback; + private bool _disposed; + + //--- Constructors --- + public DisposeCallback(Action callback) { + if(callback == null) { + throw new ArgumentNullException("callback"); + } + _callback = callback; + } + + //--- Methods --- + public void Dispose() { + if(_disposed) { + throw new ObjectDisposedException("DisposeCallback"); + } + _disposed = true; + _callback(); + } + } +} diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index 08d8c5a..bc21a9f 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -46,6 +46,7 @@ public static class GlobalClock { private static readonly ManualResetEvent _stopped = new ManualResetEvent(false); private static NamedClockCallback[] _callbacks = new NamedClockCallback[INITIAL_CALLBACK_CAPACITY]; private static readonly int _intervalMilliseconds; + private static object _suspendedTime; private static int _timeOffset; //--- Class Constructor --- @@ -62,7 +63,7 @@ static GlobalClock() { } //--- Class Properties --- - public static DateTime UtcNow { get { return DateTime.UtcNow + TimeSpan.FromMilliseconds(_timeOffset); } } + public static DateTime UtcNow { get { return (((DateTime?)_suspendedTime) ?? DateTime.UtcNow) + TimeSpan.FromMilliseconds(_timeOffset); } } //--- Class Methods --- @@ -146,6 +147,20 @@ public static DateTime FastForward(TimeSpan time, Func cancelFastForward = } } + /// + /// Suspend the global clock from progressing. + /// + /// Object that when disposed resumes the global clock. + /// DO NOT USE FOR PRODUCTION CODE!!! + public static IDisposable Suspend() { + Monitor.Enter(_syncRoot); + var suspendedTime = Interlocked.Exchange(ref _suspendedTime, UtcNow); + return new DisposeCallback(() => { + Interlocked.Exchange(ref _suspendedTime, suspendedTime); + Monitor.Exit(_syncRoot); + }); + } + internal static bool Shutdown(TimeSpan timeout) { // stop the thread timer From 9f805f495878e175d1b4112ed4af0080775e747a Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Thu, 30 Jul 2015 10:29:22 -0700 Subject: [PATCH 7/8] Code changes from code review. --- .../Tasking/TaskTimerFactory.cs | 4 ++- src/mindtouch.dream/system/GlobalClock.cs | 28 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/mindtouch.dream/Tasking/TaskTimerFactory.cs b/src/mindtouch.dream/Tasking/TaskTimerFactory.cs index fc60ef9..30a4f24 100644 --- a/src/mindtouch.dream/Tasking/TaskTimerFactory.cs +++ b/src/mindtouch.dream/Tasking/TaskTimerFactory.cs @@ -321,10 +321,12 @@ public void Shutdown() { } } + // BUGBUGBUG (arnec): we don't actually do anything with timeout, but let every timer take + // an indefinite time. // check if any timers were gathered for immediate execution if(timers != null) { foreach(var entry in timers) { - entry.Key.ExecuteNow(entry.Value); + entry.Key.Execute(entry.Value); } } _running = false; diff --git a/src/mindtouch.dream/system/GlobalClock.cs b/src/mindtouch.dream/system/GlobalClock.cs index bc21a9f..b0c187f 100644 --- a/src/mindtouch.dream/system/GlobalClock.cs +++ b/src/mindtouch.dream/system/GlobalClock.cs @@ -63,7 +63,7 @@ static GlobalClock() { } //--- Class Properties --- - public static DateTime UtcNow { get { return (((DateTime?)_suspendedTime) ?? DateTime.UtcNow) + TimeSpan.FromMilliseconds(_timeOffset); } } + public static DateTime UtcNow { get { return ((DateTime?)_suspendedTime) ?? (DateTime.UtcNow + TimeSpan.FromMilliseconds(_timeOffset)); } } //--- Class Methods --- @@ -127,22 +127,25 @@ public static void RemoveCallback(ClockCallback callback) { /// Optional callback to prematurely cancel the fast-fwoard operation. /// DO NOT USE FOR PRODUCTION CODE!!! public static DateTime FastForward(TimeSpan time, Func cancelFastForward = null) { - if(time < TimeSpan.Zero) { - throw new ArgumentException("time cannot be negative"); + + // TODO (2015-07-30, steveb): add flag to detect if this code is running in production and throw an exception if it is! + + if(time <= TimeSpan.Zero) { + throw new ArgumentException("time must be positive"); } lock(_syncRoot) { var timeMilliseconds = (int)time.TotalMilliseconds; - var intervalMilliseconds = _intervalMilliseconds / 2; - while(timeMilliseconds >= intervalMilliseconds) { - Interlocked.Add(ref _timeOffset, intervalMilliseconds); + var intervalMilliseconds = _intervalMilliseconds; + do { + var elapsed = Math.Min(timeMilliseconds, intervalMilliseconds); + Interlocked.Add(ref _timeOffset, elapsed); var now = UtcNow; - MasterTick(now, TimeSpan.FromMilliseconds(intervalMilliseconds), true); + MasterTick(now, TimeSpan.FromMilliseconds(elapsed), true); if((cancelFastForward != null) && cancelFastForward()) { return now; } - timeMilliseconds -= intervalMilliseconds; - } - Interlocked.Add(ref _timeOffset, timeMilliseconds); + timeMilliseconds -= elapsed; + } while(timeMilliseconds > 0); return UtcNow; } } @@ -153,8 +156,11 @@ public static DateTime FastForward(TimeSpan time, Func cancelFastForward = /// Object that when disposed resumes the global clock. /// DO NOT USE FOR PRODUCTION CODE!!! public static IDisposable Suspend() { + + // TODO (2015-07-30, steveb): add flag to detect if this code is running in production and throw an exception if it is! + Monitor.Enter(_syncRoot); - var suspendedTime = Interlocked.Exchange(ref _suspendedTime, UtcNow); + var suspendedTime = Interlocked.Exchange(ref _suspendedTime, (DateTime?)UtcNow); return new DisposeCallback(() => { Interlocked.Exchange(ref _suspendedTime, suspendedTime); Monitor.Exit(_syncRoot); From 2e7014293430ee42aa25b5edbedb14d9412a92d0 Mon Sep 17 00:00:00 2001 From: Steve Bjorg Date: Thu, 30 Jul 2015 10:43:53 -0700 Subject: [PATCH 8/8] Copied over methods from Deki that should be available in DReAM instead. --- .../Collections/Generic/DictionaryUtil.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/mindtouch.dream/system/Collections/Generic/DictionaryUtil.cs b/src/mindtouch.dream/system/Collections/Generic/DictionaryUtil.cs index 4b0ce56..7eeca7d 100644 --- a/src/mindtouch.dream/system/Collections/Generic/DictionaryUtil.cs +++ b/src/mindtouch.dream/system/Collections/Generic/DictionaryUtil.cs @@ -39,5 +39,34 @@ public static Dictionary ToDictionaryWithDuplicateErrorCallback(t } return result; } + + public static IEnumerable CollectAllValues(this IDictionary> dictionary, IEnumerable keys, out IEnumerable missing) { + var result = new List(); + var missingResult = new List(); + foreach(var key in keys) { + ICollection value; + if(dictionary.TryGetValue(key, out value)) { + result.AddRange(value); + } else { + missingResult.Add(key); + } + } + missing = missingResult; + return result; + } + + public static Dictionary ToDictionary(this IEnumerable> source, bool overwriteDuplicates = false) { + var result = new Dictionary(); + if(overwriteDuplicates) { + foreach(var pair in source) { + result[pair.Key] = pair.Value; + } + } else { + foreach(var pair in source) { + result.Add(pair.Key, pair.Value); + } + } + return result; + } } }