4 // Copyright (C) 2004-2005 Novell, Inc.
8 // Permission is hereby granted, free of charge, to any person obtaining a
9 // copy of this software and associated documentation files (the "Software"),
10 // to deal in the Software without restriction, including without limitation
11 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 // and/or sell copies of the Software, and to permit persons to whom the
13 // Software is furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 // DEALINGS IN THE SOFTWARE.
28 using System
.Collections
;
30 using System
.Threading
;
32 namespace Beagle
.Util
{
34 public class Scheduler
{
36 static public bool Debug
= false;
38 public enum Priority
{
39 Shutdown
= 0, // Do it on shutdown
40 Idle
= 1, // Do it when the system is idle
41 Delayed
= 2, // Do it soon
42 Immediate
= 3, // Do it right now
45 public delegate void Hook ();
46 public delegate void TaskHook (Task task
);
48 //////////////////////////////////////////////////////////////////////////////
50 public abstract class Task
: IComparable
{
52 private string tag
= null;
53 private Priority priority
= Priority
.Delayed
;
54 private int sub_priority
= 0;
55 private DateTime trigger_time
= DateTime
.MinValue
;
56 private DateTime timestamp
; // when added to the scheduler
59 public string Creator
;
60 public string Description
;
62 public ITaskCollector Collector
= null;
63 public double Weight
= 1.0;
65 public bool Reschedule
= false;
67 private ArrayList task_groups
= null;
68 private TaskGroupPrivate child_task_group
= null;
70 ///////////////////////////////
73 // The tag is the task's unique identifier
79 // Don't allow us to change the tag of a scheduled task
80 if (tag
== null || scheduler
== null)
83 throw new Exception ("Can't change tag of " + tag
+ "!");
87 public Priority Priority
{
89 get { return priority; }
92 if (priority
!= value) {
99 public int SubPriority
{
101 get { return sub_priority; }
104 if (sub_priority
!= value) {
105 sub_priority
= value;
111 public DateTime TriggerTime
{
113 get { return trigger_time; }
116 if (trigger_time
!= value) {
117 trigger_time
= value;
123 public DateTime Timestamp
{
124 get { return timestamp; }
127 ///////////////////////////////
129 public void AddTaskGroup (TaskGroup
group)
131 if (task_groups
== null)
132 task_groups
= new ArrayList ();
133 task_groups
.Add (group);
136 private void IncrementAllTaskGroups ()
138 if (task_groups
!= null) {
139 foreach (TaskGroupPrivate
group in task_groups
) {
140 if (! group.Finished
)
146 private void DecrementAllTaskGroups ()
148 if (task_groups
!= null) {
149 foreach (TaskGroupPrivate
group in task_groups
) {
150 if (! group.Finished
)
156 private void TouchAllTaskGroups ()
158 if (task_groups
!= null) {
159 foreach (TaskGroupPrivate
group in task_groups
) {
160 if (! group.Finished
)
166 ///////////////////////////////
168 private Scheduler scheduler
= null;
170 public void Schedule (Scheduler scheduler
)
172 // Increment the task groups the first
173 // time a task is scheduled.
174 if (this.scheduler
== null)
175 IncrementAllTaskGroups ();
176 this.timestamp
= DateTime
.Now
;
177 this.scheduler
= scheduler
;
178 this.cancelled
= false;
181 private void Recompute ()
183 if (scheduler
!= null)
187 ///////////////////////////////
189 // A blocked task will not execute.
190 public bool Blocked
{
192 // Block the task if we have unexecuted children
193 return child_task_group
!= null && ! child_task_group
.Finished
;
197 ///////////////////////////////
199 private bool cancelled
= false;
201 public bool Cancelled
{
202 get { return cancelled; }
205 public void Cancel ()
208 DecrementAllTaskGroups ();
212 ///////////////////////////////
214 // The Task's count keeps track of how many
215 // times it has been executed.
217 private int count
= 0;
220 get { return count; }
223 ///////////////////////////////
225 public void SpawnChild (Task child_task
)
227 if (child_task_group
== null)
228 child_task_group
= new TaskGroupPrivate ("Children of " + Tag
, null, null);
229 child_task
.AddTaskGroup (child_task_group
);
230 scheduler
.Add (child_task
);
233 ///////////////////////////////
235 public void DoTask ()
239 Logger
.Log
.Debug ("Starting task {0}", Tag
);
240 child_task_group
= null;
242 TouchAllTaskGroups ();
244 Stopwatch sw
= new Stopwatch ();
249 } catch (Exception ex
) {
250 Logger
.Log
.Warn ("Caught exception in DoTaskReal");
251 Logger
.Log
.Warn (" Tag: {0}", Tag
);
252 Logger
.Log
.Warn (" Creator: {0}", Creator
);
253 Logger
.Log
.Warn ("Description: {0}", Description
);
254 Logger
.Log
.Warn (" Priority: {0} ({1})", Priority
, SubPriority
);
255 Logger
.Log
.Warn (ex
);
259 Logger
.Log
.Debug ("Finished task {0} in {1}", Tag
, sw
);
263 scheduler
.Add (this); // re-add ourselves
265 DecrementAllTaskGroups ();
271 protected abstract void DoTaskReal ();
273 ///////////////////////////////
275 // Sort from lowest to highest priority
276 public int CompareTo (object obj
)
278 Task other
= obj
as Task
;
283 cmp
= this.Priority
.CompareTo (other
.Priority
);
287 cmp
= this.SubPriority
.CompareTo (other
.SubPriority
);
291 // Tasks that were added to the scheduler earlier take
292 // precedence over those that were added later.
293 cmp
= DateTime
.Compare (other
.Timestamp
, this.Timestamp
);
297 // Try to break any ties
298 return this.GetHashCode () - other
.GetHashCode ();
301 public override string ToString ()
303 StringBuilder sb
= new StringBuilder ();
305 sb
.AppendFormat ("{0} {1} ({2})\n", Priority
, SubPriority
, Timestamp
);
307 sb
.Append (Tag
+ "\n");
309 double t
= (TriggerTime
- DateTime
.Now
).TotalSeconds
;
312 sb
.AppendFormat ("Hold for {0:0.00} seconds\n", t
);
314 sb
.AppendFormat ("Hold until {0}\n", TriggerTime
);
318 sb
.AppendFormat ("Creator: {0}\n", Creator
);
320 if (Description
!= null)
321 sb
.Append (Description
+ "\n");
323 return sb
.ToString ();
327 private class TaskHookWrapper
: Task
{
331 public TaskHookWrapper (TaskHook hook
)
336 protected override void DoTaskReal ()
343 public static Task
TaskFromHook (TaskHook hook
)
345 return new TaskHookWrapper (hook
);
348 //////////////////////////////////////////////////////////////////////////////
354 public static TaskGroup
NewTaskGroup (string name
, Hook pre_hook
, Hook post_hook
)
356 return new TaskGroupPrivate (name
, pre_hook
, post_hook
);
359 // The TaskGroup we hand back to the user is an interface that
360 // exposes minimal functionality.
361 public interface TaskGroup
{
363 bool Finished { get; }
366 private class TaskGroupPrivate
: TaskGroup
{
368 private int task_count
= 0;
369 private bool touched
= false;
370 private bool finished
= false;
371 private Hook pre_hook
;
372 private Hook post_hook
;
374 public TaskGroupPrivate (string name
,
379 this.pre_hook
= pre_hook
;
380 this.post_hook
= post_hook
;
387 public bool Finished
{
388 get { return finished; }
391 // Call this when a task is added to the task group.
392 public void Increment ()
395 throw new Exception ("Tried to increment a finished TaskGroup");
399 // Call this when we execute a task in the task group.
403 throw new Exception ("Tried to touch a finished TaskGroup");
406 if (pre_hook
!= null) {
409 } catch (Exception ex
) {
410 Logger
.Log
.Warn ("Caught exception in pre_hook of task group '{0}'", Name
);
411 Logger
.Log
.Warn (ex
);
418 // Call this after a task in the task group is complete.
419 public void Decrement ()
422 throw new Exception ("Tried to decrement a finished TaskGroup");
425 // Only fire our post-hook if the pre-hook fired
426 // (or would have fired, had it been non-null)
427 if (task_count
== 0 && touched
) {
428 if (post_hook
!= null) {
431 } catch (Exception ex
) {
432 Logger
.Log
.Warn ("Caught exception in post_hook of task group '{0}'", Name
);
433 Logger
.Log
.Warn (ex
);
441 //////////////////////////////////////////////////////////////////////////////
446 // This is a mechanism for executing tasks in sets, possibly outside of
450 public interface ITaskCollector
{
452 double GetMaximumWeight ();
455 void PostTaskHook ();
458 //////////////////////////////////////////////////////////////////////////////
460 static private double global_delay
= -1.0;
465 exercise
= Environment
.GetEnvironmentVariable ("BEAGLE_EXERCISE_THE_DOG");
467 if (exercise
!= null) {
468 if (exercise
.Length
> 2 && exercise
[0] == 't')
469 global_delay
= Double
.Parse (exercise
.Substring (1));
475 //////////////////////////////////////////////////////////////////////////////
477 static private Scheduler
global = new Scheduler ();
479 static public Scheduler Global
{
480 get { return global; }
483 //////////////////////////////////////////////////////////////////////////////
485 private object big_lock
= new object ();
487 // FIXME: shutdown tasks should probably be ordered by something
488 private Queue shutdown_task_queue
= new Queue ();
490 private Hashtable tasks_by_tag
= new Hashtable ();
491 private int executed_task_count
= 0;
493 public void Add (Task task
)
498 Task old_task
= null;
502 // Keep track of when immediate priority tasks are
503 // added so that we can throttle if the scheduler
504 // is being slammed with them.
505 if (task
.Priority
== Priority
.Immediate
) {
506 // Shift our times down by one
507 Array
.Copy (last_immediate_times
, 1, last_immediate_times
, 0, 4);
508 last_immediate_times
[4] = DateTime
.Now
;
511 // Re-adding the same task is a no-op
512 old_task
= tasks_by_tag
[task
.Tag
] as Task
;
513 if (old_task
== task
)
517 Logger
.Log
.Debug ("Adding task");
518 Logger
.Log
.Debug ("Tag: {0}", task
.Tag
);
519 if (task
.Description
!= null)
520 Logger
.Log
.Debug ("Desc: {0}", task
.Description
);
523 task
.Schedule (this);
525 if (task
.Priority
== Priority
.Shutdown
)
526 shutdown_task_queue
.Enqueue (task
);
528 tasks_by_tag
[task
.Tag
] = task
;
530 Monitor
.Pulse (big_lock
);
533 // If we clobbered another task, call cancel on it.
534 // This happens after we release the lock, since
535 // cancellation could result in a task group post-hook
537 if (old_task
!= null)
541 public Task
GetByTag (string tag
)
544 return tasks_by_tag
[tag
] as Task
;
547 public bool ContainsByTag (string tag
)
549 Task task
= GetByTag (tag
);
550 return task
!= null && !task
.Cancelled
;
553 public void Recompute ()
556 Monitor
.Pulse (big_lock
);
559 //////////////////////////////////////////////////////////////////////////////
561 Thread thread
= null;
562 public bool running
= false;
570 thread
= new Thread (new ThreadStart (Worker
));
580 Monitor
.Pulse (big_lock
);
586 // Delay Computations
588 // This code controls how we space out tasks
591 // FIXME: random magic constants
592 const double idle_threshold
= 5.314159 * 60; // probably should be longer
593 const double idle_ramp_up_time
= 5.271828 * 60; // probably should be longer
594 const double default_delayed_rate_factor
= 9.03; // work about 1/10th of the time
595 const double default_idle_rate_factor
= 2.097; // work about 1/3rd of the time
596 const double maximum_delay
= 20; // never wait for more than 20s
597 const double min_throttled_delay
= 1.5; // never wait less than this when throttled
599 DateTime
[] last_immediate_times
= new DateTime
[5];
601 // The return value and duration_of_previous_task are both measured in seconds.
602 private double ComputeDelay (Priority priority_of_next_task
,
603 double duration_of_previous_task
)
605 if (global_delay
>= 0.0)
612 // Do everything faster the longer we are idle.
613 double idle_time
= SystemInformation
.InputIdleTime
;
614 double idle_scale
= 1.0;
615 bool is_idle
= false;
616 bool need_throttle
= false;
618 // Never speed up if we are using the battery.
619 if (idle_time
> idle_threshold
&& ! SystemInformation
.UsingBattery
) {
621 double t
= (idle_time
- idle_threshold
) / idle_ramp_up_time
;
622 idle_scale
= (1 - Math
.Min (t
, 1.0));
625 switch (priority_of_next_task
) {
627 case Priority
.Immediate
:
630 if (last_immediate_times
[0] != DateTime
.MinValue
) {
631 TimeSpan last_add_delta
= DateTime
.Now
.Subtract (last_immediate_times
[4]);
633 // If less than a second has gone by since the
634 // last immediate task was added, there is
635 // still a torrent of events coming in, and we
636 // may need to throttle.
637 if (last_add_delta
.Seconds
<= 1) {
638 TimeSpan between_add_delta
= last_immediate_times
[4].Subtract (last_immediate_times
[0]);
640 // At least 5 immediate tasks have been
641 // added in the last second. We
642 // definitely need to throttle.
643 if (between_add_delta
.Seconds
<= 1) {
644 Logger
.Log
.Debug ("Thottling immediate priority tasks");
645 need_throttle
= true;
646 rate_factor
= idle_scale
* default_idle_rate_factor
;
653 case Priority
.Delayed
:
654 rate_factor
= idle_scale
* default_delayed_rate_factor
;
658 rate_factor
= idle_scale
* default_idle_rate_factor
;
662 // FIXME: we should do something more sophisticated than this
663 // with the load average.
664 // Random numbers galore!
665 double load_average
= SystemInformation
.LoadAverageOneMinute
;
666 if (load_average
> 3.001)
667 rate_factor
*= 5.002;
668 else if (load_average
> 1.5003)
669 rate_factor
*= 2.004;
671 double delay
= rate_factor
* duration_of_previous_task
;
673 // space out delayed tasks a bit when we aren't idle
675 && priority_of_next_task
== Priority
.Delayed
679 if (delay
> maximum_delay
)
680 delay
= maximum_delay
;
682 // If we need to throttle, make sure we don't delay less than
683 // a second and some.
684 if (need_throttle
&& delay
< min_throttled_delay
)
685 delay
= min_throttled_delay
;
694 // A convenience function. There should be a
695 // constructor to TimeSpan that does this.
696 private static TimeSpan
TimeSpanFromSeconds (double t
)
698 // Wait barfs if you hand it a negative TimeSpan,
699 // so we are paranoid;
703 // 1 tick = 100 nanoseconds
704 long ticks
= (long) (t
* 1.0e+7);
705 return new TimeSpan (ticks
);
708 private string status_str
= null;
710 private void Worker ()
712 DateTime end_time_of_previous_task
= DateTime
.MinValue
;
713 double duration_of_previous_task
= 0.0;
715 Hook pre_hook
= null;
716 Hook post_hook
= null;
717 ArrayList to_be_executed
= new ArrayList ();
721 status_str
= "Finding next task to execute";
725 // If there are no pending tasks, wait
726 // on our lock and then re-start our
728 if (tasks_by_tag
.Count
== 0) {
729 status_str
= "Waiting on empty queue";
730 Monitor
.Wait (big_lock
);
734 // Walk across our list of tasks and find
735 // the next one to execute.
736 DateTime now
= DateTime
.Now
;
737 DateTime next_trigger_time
= DateTime
.MaxValue
;
738 Task next_task
= null;
739 foreach (Task task
in tasks_by_tag
.Values
) {
742 if (task
.TriggerTime
< now
) {
743 if (next_task
== null || next_task
.CompareTo (task
) < 0)
745 } else if (task
.TriggerTime
< next_trigger_time
)
746 next_trigger_time
= task
.TriggerTime
;
749 // If we didn't find a task, wait for the next trigger-time
750 // and then re-start our while loop.
751 if (next_task
== null) {
752 if (next_trigger_time
== DateTime
.MaxValue
) {
753 status_str
= "Waiting for an unblocked task";
754 Monitor
.Wait (big_lock
);
756 status_str
= "Waiting for the next trigger time";
757 Monitor
.Wait (big_lock
, next_trigger_time
- now
);
762 // If we did find a task, do we want to execute it right now?
763 // Or should we wait a bit?
765 // How should we space things out?
767 delay
= ComputeDelay (next_task
.Priority
, duration_of_previous_task
);
768 delay
= Math
.Min (delay
, (next_trigger_time
- now
).TotalSeconds
);
770 // Adjust by the time that has actually elapsed since the
772 delay
-= (now
- end_time_of_previous_task
).TotalSeconds
;
774 // If we still need to wait a bit longer, wait for the appropriate
775 // amount of time and then re-start our while loop.
777 status_str
= "Waiting for next task.";
778 Monitor
.Wait (big_lock
, TimeSpanFromSeconds (delay
));
783 // If we've made it to this point, it is time to start
784 // executing our selected task.
787 to_be_executed
.Clear ();
789 if (next_task
.Collector
== null) {
791 to_be_executed
.Add (next_task
);
795 pre_hook
= new Hook (next_task
.Collector
.PreTaskHook
);
796 post_hook
= new Hook (next_task
.Collector
.PostTaskHook
);
798 // Find all eligible tasks with the same collector,
799 // and add them to the collection list.
801 foreach (Task task
in tasks_by_tag
.Values
)
802 if (task
!= next_task
803 && task
.Collector
== next_task
.Collector
805 && task
.TriggerTime
< now
)
806 to_be_executed
.Add (task
);
808 // Order the tasks from highest to lowest priority.
809 // Our original task will always be the first item
810 // in the resulting array.
811 to_be_executed
.Sort ();
812 to_be_executed
.Add (next_task
);
813 to_be_executed
.Reverse ();
815 // Now find how many tasks can be executed before we
816 // exceed the collector's maximum weight. If necessary,
817 // prune the list of tasks.
818 double remaining_weight
;
819 remaining_weight
= next_task
.Collector
.GetMaximumWeight ();
821 while (i
< to_be_executed
.Count
&& remaining_weight
> 0) {
823 task
= to_be_executed
[i
] as Task
;
824 remaining_weight
-= task
.Weight
;
827 if (i
< to_be_executed
.Count
)
828 to_be_executed
.RemoveRange (i
, to_be_executed
.Count
- i
);
831 // Remove the tasks we are about to execute from our
833 foreach (Task task
in to_be_executed
)
834 tasks_by_tag
.Remove (task
.Tag
);
836 // Pulse our lock, in case anyone is waiting for it.
837 Monitor
.Pulse (big_lock
);
840 // Now actually execute the set of tasks we found.
842 status_str
= "Executing tasks";
844 DateTime start_time
= DateTime
.Now
;
845 if (pre_hook
!= null) {
848 } catch (Exception ex
) {
849 Logger
.Log
.Error ("Caught exception in pre_hook '{0}'", pre_hook
);
850 Logger
.Log
.Error (ex
);
853 foreach (Task task
in to_be_executed
) {
855 ++executed_task_count
;
857 if (post_hook
!= null) {
860 } catch (Exception ex
) {
861 Logger
.Log
.Error ("Caught exception in post_hook '{0}'", post_hook
);
862 Logger
.Log
.Error (ex
);
866 end_time_of_previous_task
= DateTime
.Now
;
867 duration_of_previous_task
= (end_time_of_previous_task
- start_time
).TotalSeconds
;
870 // Execute all shutdown tasks
871 foreach (Task task
in shutdown_task_queue
)
872 if (! task
.Cancelled
&& ! task
.Blocked
)
876 Logger
.Log
.Debug ("Scheduler.Worker finished");
879 //////////////////////////////////////////////////////////////////////////////
881 public string GetHumanReadableStatus ()
883 StringBuilder sb
= new StringBuilder ();
887 ArrayList blocked_tasks
= new ArrayList ();
888 ArrayList future_tasks
= new ArrayList ();
889 ArrayList pending_tasks
= new ArrayList ();
891 DateTime now
= DateTime
.Now
;
892 foreach (Task task
in tasks_by_tag
.Values
) {
894 blocked_tasks
.Add (task
);
895 else if (task
.TriggerTime
> now
)
896 future_tasks
.Add (task
);
898 pending_tasks
.Add (task
);
901 blocked_tasks
.Sort ();
902 blocked_tasks
.Reverse ();
904 future_tasks
.Sort ();
905 future_tasks
.Reverse ();
907 pending_tasks
.Sort ();
908 pending_tasks
.Reverse ();
910 sb
.Append ("Scheduler:\n");
911 sb
.AppendFormat ("Count: {0}\n", executed_task_count
);
913 if (status_str
!= null)
914 sb
.AppendFormat ("Status: {0}\n", status_str
);
917 sb
.Append ("\nPending Tasks:\n");
918 foreach (Task task
in pending_tasks
) {
919 sb
.AppendFormat ("{0} {1}\n", pos
, task
);
923 sb
.Append ("Scheduler queue is empty.\n");
926 if (future_tasks
.Count
> 0) {
927 sb
.Append ("\nFuture Tasks:\n");
928 foreach (Task task
in future_tasks
)
929 sb
.AppendFormat ("{0}\n", task
);
932 if (blocked_tasks
.Count
> 0) {
933 sb
.Append ("\nBlocked Tasks:\n");
934 foreach (Task task
in blocked_tasks
)
935 sb
.AppendFormat ("{0}\n", task
);
940 return sb
.ToString ();
945 class TestTask
: Scheduler
.Task
{
947 private class TestCollector
: Scheduler
.ITaskCollector
{
949 public double GetMinimumWeight ()
954 public double GetMaximumWeight ()
959 public void PreTaskHook ()
961 Console
.WriteLine ("+++ Pre-Task Hook");
964 public void PostTaskHook ()
966 Console
.WriteLine ("+++ Post-Task Hook");
970 protected override void DoTaskReal ()
972 Console
.WriteLine ("Doing task '{0}' at {1}", Tag
, DateTime
.Now
);
978 static void BeginTaskGroup ()
980 Console
.WriteLine ("--- Begin Task Group!");
983 static void EndTaskGroup ()
985 Console
.WriteLine ("--- End Task Group!");
990 Scheduler sched
= Scheduler
.Global
;
992 Scheduler
.TaskGroup tg
= Scheduler
.NewTaskGroup ("foo",
993 new Scheduler
.Hook (BeginTaskGroup
),
994 new Scheduler
.Hook (EndTaskGroup
));
1000 task
= new TestTask ();
1002 task
.AddTaskGroup (tg
);
1003 task
.Priority
= Scheduler
.Priority
.Delayed
;
1004 task
.TriggerTime
= DateTime
.Now
.AddSeconds (7);
1007 task
= new TestTask ();
1009 task
.AddTaskGroup (tg
);
1010 task
.Priority
= Scheduler
.Priority
.Delayed
;
1013 Scheduler
.ITaskCollector collector
= null;
1014 for (int i
= 0; i
< 20; ++i
) {
1016 collector
= new TestCollector ();
1017 task
= new TestTask ();
1018 task
.Tag
= String
.Format ("Baboon {0}", i
);
1019 task
.Collector
= collector
;
1020 task
.Priority
= Scheduler
.Priority
.Delayed
;
1025 Thread
.Sleep (1000);