4 // Copyright (C) 2004 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 private double global_delay
= -1.0;
37 static private bool immediate_priority_only
= false;
38 static private string exercise_the_dog
= null;
42 // We used to support the EXERCISE_THE_DOG env variable, but
44 exercise_the_dog
= Environment
.GetEnvironmentVariable ("BEAGLE_EXERCISE_THE_DOG");
45 if (exercise_the_dog
== null &&
46 Environment
.GetEnvironmentVariable ("BEAGLE_EXERCISE_THE_DOG_HARDER") != null)
47 exercise_the_dog
= "harder fallback";
49 if (exercise_the_dog
!= null) {
53 if (exercise_the_dog
.Length
> 2 && exercise_the_dog
[0] == 't')
54 global_delay
= Double
.Parse (exercise_the_dog
.Substring (1));
57 if (Environment
.GetEnvironmentVariable ("BEAGLE_IMMEDIATE_PRIORITY_ONLY") != null)
58 immediate_priority_only
= true;
61 //////////////////////////////////////////////////////////////////////////////
63 static private Scheduler
global = new Scheduler ();
65 static public Scheduler Global
{
66 get { return global; }
69 //////////////////////////////////////////////////////////////////////////////
71 public enum Priority
{
72 Shutdown
= 0, // Do it on shutdown
73 Idle
= 1, // Do it when the system is idle
74 Generator
= 2, // Do it soon, but not *too* soon
75 Delayed
= 3, // Do it soon
76 Immediate
= 4, // Do it right now
79 public delegate void Hook ();
80 public delegate void TaskHook (Task task
);
82 //////////////////////////////////////////////////////////////////////////////
84 public abstract class Task
: IComparable
{
86 // A unique identifier
90 public string Creator
;
91 public string Description
;
93 public Priority Priority
= Priority
.Idle
;
94 public int SubPriority
= 0;
96 public DateTime Timestamp
;
97 public DateTime TriggerTime
= DateTime
.MinValue
;
99 public ITaskCollector Collector
= null;
100 public double Weight
= 1.0;
102 public bool Reschedule
= false;
104 ///////////////////////////////
106 private ArrayList task_groups
= null;
108 public void AddTaskGroup (TaskGroup
group)
110 if (task_groups
== null)
111 task_groups
= new ArrayList ();
112 task_groups
.Add (group);
115 private void IncrementAllTaskGroups ()
117 if (task_groups
!= null) {
118 foreach (TaskGroupPrivate
group in task_groups
) {
119 if (! group.Finished
)
125 private void DecrementAllTaskGroups ()
127 if (task_groups
!= null) {
128 foreach (TaskGroupPrivate
group in task_groups
) {
129 if (! group.Finished
)
135 private void TouchAllTaskGroups ()
137 if (task_groups
!= null) {
138 foreach (TaskGroupPrivate
group in task_groups
) {
139 if (! group.Finished
)
145 ///////////////////////////////
147 private Scheduler scheduler
= null;
149 public Scheduler ThisScheduler
{
150 get { return scheduler; }
153 public void Schedule (Scheduler scheduler
)
155 // Increment the task groups the first
156 // time a task is scheduled.
157 if (this.scheduler
== null)
158 IncrementAllTaskGroups ();
159 this.scheduler
= scheduler
;
162 ///////////////////////////////
164 private bool cancelled
= false;
166 public bool Cancelled
{
167 get { return cancelled; }
170 public void Cancel ()
173 DecrementAllTaskGroups ();
177 ///////////////////////////////
179 // The Task's count keeps track of how many
180 // times it has been executed.
182 private int count
= 0;
185 get { return count; }
188 ///////////////////////////////
190 public void DoTask ()
193 TouchAllTaskGroups ();
195 Logger
.Log
.Debug ("Starting task {0}", Tag
);
196 Stopwatch sw
= new Stopwatch ();
200 Logger
.Log
.Debug ("Finished task {0} in {1}", Tag
, sw
);
201 } catch (Exception ex
) {
202 Logger
.Log
.Warn ("Caught exception in DoTaskReal");
203 Logger
.Log
.Warn (" Tag: {0}", Tag
);
204 Logger
.Log
.Warn (" Creator: {0}", Creator
);
205 Logger
.Log
.Warn ("Description: {0}", Description
);
206 Logger
.Log
.Warn (" Priority: {0} ({1})", Priority
, SubPriority
);
207 Logger
.Log
.Warn (ex
);
212 ThisScheduler
.Add (this);
214 DecrementAllTaskGroups ();
219 protected abstract void DoTaskReal ();
221 ///////////////////////////////
223 // Sort from lowest to highest priority
224 public int CompareTo (object obj
)
226 Task other
= obj
as Task
;
231 cmp
= this.Priority
.CompareTo (other
.Priority
);
235 cmp
= this.SubPriority
.CompareTo (other
.SubPriority
);
239 cmp
= other
.Timestamp
.CompareTo (this.Timestamp
);
243 // Try to break any ties
244 return this.GetHashCode ().CompareTo (other
.GetHashCode ());
247 public override string ToString ()
249 StringBuilder sb
= new StringBuilder ();
251 sb
.AppendFormat ("{0} {1}\n", Priority
, SubPriority
);
253 sb
.Append (Tag
+ "\n");
255 double t
= (TriggerTime
- DateTime
.Now
).TotalSeconds
;
258 sb
.AppendFormat ("Trigger in {0:0.00} seconds\n", t
);
260 sb
.AppendFormat ("Trigger at {0}\n", TriggerTime
);
264 sb
.AppendFormat ("Creator: {0}\n", Creator
);
266 if (Description
!= null)
267 sb
.Append (Description
+ "\n");
269 return sb
.ToString ();
273 private class TaskHookWrapper
: Task
{
277 public TaskHookWrapper (TaskHook hook
)
282 protected override void DoTaskReal ()
289 public static Task
TaskFromHook (TaskHook hook
)
291 return new TaskHookWrapper (hook
);
294 //////////////////////////////////////////////////////////////////////////////
300 public static TaskGroup
NewTaskGroup (string name
, Hook pre_hook
, Hook post_hook
)
302 return new TaskGroupPrivate (name
, pre_hook
, post_hook
);
305 // We split the task group data structure into two parts:
306 // TaskGroup and TaskGroupPrivate. The TaskGroup we hand
307 // back to the user exposes minimal functionality.
308 public abstract class TaskGroup
{
311 protected TaskGroup (string name
) {
319 public abstract bool Finished { get; }
322 private class TaskGroupPrivate
: TaskGroup
{
323 private int task_count
= 0;
324 private bool touched
= false;
325 private bool finished
= false;
326 private Hook pre_hook
;
327 private Hook post_hook
;
329 public TaskGroupPrivate (string name
,
331 Hook post_hook
) : base (name
)
333 this.pre_hook
= pre_hook
;
334 this.post_hook
= post_hook
;
337 public override bool Finished
{
338 get { return finished; }
341 public void Increment ()
344 throw new Exception ("Tried to increment a finished TaskGroup");
351 throw new Exception ("Tried to touch a finished TaskGroup");
354 if (pre_hook
!= null) {
357 } catch (Exception ex
) {
358 Logger
.Log
.Warn ("Caught exception in pre_hook of task group '{0}'", Name
);
359 Logger
.Log
.Warn (ex
);
366 public void Decrement ()
369 throw new Exception ("Tried to decrement a finished TaskGroup");
372 // Only fire our post-hook if the pre-hook fired
373 // (or would have fired, had it been non-null)
374 if (task_count
== 0 && touched
) {
375 if (post_hook
!= null) {
378 } catch (Exception ex
) {
379 Logger
.Log
.Warn ("Caught exception in post_hook of task group '{0}'", Name
);
380 Logger
.Log
.Warn (ex
);
388 //////////////////////////////////////////////////////////////////////////////
393 // This is a mechanism for executing tasks in sets, possibly outside of
397 public interface ITaskCollector
{
399 double GetMinimumWeight ();
400 double GetMaximumWeight ();
403 void PostTaskHook ();
406 //////////////////////////////////////////////////////////////////////////////
408 // FIXME: shutdown tasks should probably be ordered by something
409 private Queue shutdown_task_queue
= new Queue ();
411 private ArrayList task_queue
= new ArrayList ();
412 private Hashtable task_by_tag
= new Hashtable ();
413 private int executed_task_count
= 0;
415 public enum AddType
{
417 OptionallyReplaceExisting
,
422 public bool Add (Task task
, AddType add_type
)
424 Task old_task
= null;
429 if (immediate_priority_only
&& task
.Priority
!= Priority
.Immediate
)
432 // Keep track of when immediate priority tasks are
433 // added so that we can throttle if the scheduler
434 // is being slammed with them.
435 if (task
.Priority
== Priority
.Immediate
) {
436 // Shift our times down by one
437 Array
.Copy (last_immediate_times
, 1, last_immediate_times
, 0, 4);
438 last_immediate_times
[4] = DateTime
.Now
;
441 old_task
= task_by_tag
[task
.Tag
] as Task
;
442 if (old_task
== task
)
445 if (add_type
== AddType
.DeferToExisting
449 if (add_type
== AddType
.OnlyReplaceExisting
454 Logger
.Log
.Debug ("Adding task");
455 Logger
.Log
.Debug ("Tag: {0}", task
.Tag
);
456 if (task
.Description
!= null)
457 Logger
.Log
.Debug ("Desc: {0}", task
.Description
);
460 task
.Timestamp
= DateTime
.Now
;
461 task
.Schedule (this);
463 if (task
.Priority
== Priority
.Shutdown
) {
464 shutdown_task_queue
.Enqueue (task
);
466 int i
= task_queue
.BinarySearch (task
);
469 task_queue
.Insert (i
, task
);
470 task_by_tag
[task
.Tag
] = task
;
474 Monitor
.Pulse (task_queue
);
477 if (old_task
!= null)
483 public bool Add (Task task
)
485 return Add (task
, AddType
.OptionallyReplaceExisting
);
488 public Task
GetByTag (string tag
)
491 return task_by_tag
[tag
] as Task
;
495 public bool ContainsByTag (string tag
)
497 Task task
= GetByTag (tag
);
498 return task
!= null && !task
.Cancelled
;
503 //////////////////////////////////////////////////////////////////////////////
505 private string status_str
= null;
506 private DateTime next_task_time
;
508 public string GetHumanReadableStatus ()
510 StringBuilder sb
= new StringBuilder ();
512 sb
.Append ("Scheduler:\n");
514 sb
.Append (String
.Format ("Count: {0}\n", executed_task_count
));
516 if (next_task_time
.Ticks
> 0)
517 sb
.Append (String
.Format ("Next task in {0:0.00} seconds\n",
518 (next_task_time
- DateTime
.Now
).TotalSeconds
));
520 if (status_str
!= null)
521 sb
.Append ("Status: " + status_str
+ "\n");
525 for (int i
= task_queue
.Count
- 1; i
>= 0; --i
) {
526 Task task
= task_queue
[i
] as Task
;
527 if (task
== null || task
.Cancelled
)
530 sb
.AppendFormat ("{0} ", pos
);
531 sb
.Append (task
.ToString ());
538 sb
.Append ("Scheduler queue is empty.\n");
543 return sb
.ToString ();
546 //////////////////////////////////////////////////////////////////////////////
548 Thread thread
= null;
549 public bool running
= false;
557 thread
= new Thread (new ThreadStart (Worker
));
568 Monitor
.Pulse (task_queue
);
574 // Delay Computations
576 // This code controls how we space out tasks
579 // FIXME: random magic constants
580 const double idle_threshold
= 5.314159 * 60; // probably should be longer
581 const double idle_ramp_up_time
= 5.271828 * 60; // probably should be longer
582 const double default_delayed_rate_factor
= 9.03; // work about 1/10th of the time
583 const double default_idle_rate_factor
= 2.097; // work about 1/3rd of the time
584 const double default_maximum_delay
= 20; // never wait for more than 20s
586 DateTime
[] last_immediate_times
= new DateTime
[5];
588 private double GetIdleTime ()
590 return SystemInformation
.InputIdleTime
;
593 // The return value and duration_of_previous_task are both measured in seconds.
594 private double ComputeDelay (Priority priority_of_next_task
,
595 double duration_of_previous_task
)
597 if (global_delay
>= 0.0)
604 // Do everything faster the longer we are idle.
605 double idle_time
= GetIdleTime ();
606 double idle_scale
= 1.0;
607 bool is_idle
= false;
608 bool need_throttle
= false;
610 // Never speed up if we are using the battery.
611 if (idle_time
> idle_threshold
&& ! SystemInformation
.UsingBattery
) {
613 double t
= (idle_time
- idle_threshold
) / idle_ramp_up_time
;
614 idle_scale
= (1 - Math
.Min (t
, 1.0));
617 switch (priority_of_next_task
) {
619 case Priority
.Immediate
:
622 if (last_immediate_times
[0] != DateTime
.MinValue
) {
623 TimeSpan last_add_delta
= DateTime
.Now
.Subtract (last_immediate_times
[4]);
625 // If less than a second has gone by since the
626 // last immediate task was added, there is
627 // still a torrent of events coming in, and we
628 // may need to throttle.
629 if (last_add_delta
.Seconds
<= 1) {
630 TimeSpan between_add_delta
= last_immediate_times
[4].Subtract (last_immediate_times
[0]);
632 // At least 5 immediate tasks have been
633 // added in the last second. We
634 // definitely need to throttle.
635 if (between_add_delta
.Seconds
<= 1) {
636 Logger
.Log
.Debug ("Thottling immediate priority tasks");
637 need_throttle
= true;
638 rate_factor
= idle_scale
* default_idle_rate_factor
;
645 case Priority
.Generator
:
646 case Priority
.Delayed
:
647 rate_factor
= idle_scale
* default_delayed_rate_factor
;
651 rate_factor
= idle_scale
* default_idle_rate_factor
;
655 // FIXME: we should do something more sophisticated than this
656 // with the load average.
657 // Random numbers galore!
658 double load_average
= SystemInformation
.LoadAverageOneMinute
;
659 if (load_average
> 3.001)
660 rate_factor
*= 5.002;
661 else if (load_average
> 1.5003)
662 rate_factor
*= 2.004;
664 double delay
= rate_factor
* duration_of_previous_task
;
666 // space out delayed tasks a bit when we aren't idle
668 && priority_of_next_task
== Priority
.Delayed
672 if (delay
> default_maximum_delay
)
673 delay
= default_maximum_delay
;
675 // If we need to throttle, make sure we don't delay less than
676 // a second and some.
677 if (need_throttle
&& delay
< 1.25)
687 // A convenience function. There should be a
688 // constructor to TimeSpan that does this.
689 private static TimeSpan
TimeSpanFromSeconds (double t
)
691 // Wait barfs if you hand it a negative TimeSpan,
692 // so we are paranoid;
696 // 1 tick = 100 nanoseconds
697 long ticks
= (long) (t
* 1.0e+7);
698 return new TimeSpan (ticks
);
702 private void DescribeTaskQueue (string note
, int i0
, int i1
)
704 Console
.WriteLine ("----------------------");
705 Console
.WriteLine (note
);
706 for (int i
=i0
; i
<i1
; ++i
) {
707 Task t
= task_queue
[i
] as Task
;
711 else if (t
.Cancelled
)
712 xxx
= t
.Tag
+ " CANCELLED";
715 Console
.WriteLine ("{0}: {1}", i
, xxx
);
717 Console
.WriteLine ("----------------------");
721 // Remove nulls and cancelled tasks from the queue.
722 // Note: this does no locking!
723 private void CleanQueue ()
725 int i
= task_queue
.Count
- 1;
727 Task t
= task_queue
[i
] as Task
;
731 // Remove cancelled items from the tag hash
732 task_by_tag
.Remove (t
.Tag
);
736 if (i
< task_queue
.Count
- 1)
737 task_queue
.RemoveRange (i
+1, task_queue
.Count
- 1 - i
);
740 private void Worker ()
742 DateTime time_of_last_task
= DateTime
.MinValue
;
743 double duration_of_last_task
= 1;
745 Hook pre_hook
= null;
746 Hook post_hook
= null;
747 ArrayList collection
= new ArrayList ();
757 // First, remove any null or cancelled tasks
758 // we find in the task_queue.
761 // If the task queue is now empty, wait on our lock
762 // and then re-start our while loop
763 if (task_queue
.Count
== 0) {
764 next_task_time
= new DateTime ();
765 status_str
= "Waiting on empty queue";
766 Monitor
.Wait (task_queue
);
767 status_str
= "Working";
771 // Find the next event that is past it's trigger time.
772 i
= task_queue
.Count
- 1;
773 DateTime now
= DateTime
.Now
;
774 DateTime next_trigger_time
= DateTime
.MaxValue
;
777 Task t
= task_queue
[i
] as Task
;
778 if (t
!= null && ! t
.Cancelled
) {
779 if (t
.TriggerTime
< now
) {
781 task_i
= i
; // Remember the task's position in the queue.
784 // Keep track of when the next possible trigger time is.
785 if (t
.TriggerTime
< next_trigger_time
)
786 next_trigger_time
= t
.TriggerTime
;
792 // If we didn't find a task, wait for the next trigger-time
793 // and then re-start our while loop.
795 next_task_time
= next_trigger_time
;
796 status_str
= "Waiting for next trigger time.";
797 Monitor
.Wait (task_queue
, next_trigger_time
- now
);
798 next_task_time
= new DateTime ();
799 status_str
= "Working";
803 // If we did find a task, do we want to execute it right now?
804 // Or should we wait a bit?
806 // How should we space things out?
808 delay
= ComputeDelay (task
.Priority
, duration_of_last_task
);
809 delay
= Math
.Min (delay
, (next_trigger_time
- DateTime
.Now
).TotalSeconds
);
811 // Adjust by the time that has actually elapsed since the
813 delay
-= (DateTime
.Now
- time_of_last_task
).TotalSeconds
;
815 // If we still need to wait a bit longer, wait for the appropriate
816 // amount of time and then re-start our while loop.
818 next_task_time
= DateTime
.Now
.AddSeconds (delay
);
819 status_str
= "Waiting for next task.";
820 // Never wait more than 15 seconds.
821 Monitor
.Wait (task_queue
, TimeSpanFromSeconds (Math
.Min (delay
, 15)));
822 next_task_time
= new DateTime ();
823 status_str
= "Working";
827 // Remove this task from the queue
828 task_queue
[task_i
] = null;
829 task_by_tag
.Remove (task
.Tag
);
831 if (task
.Collector
== null) {
835 collection
.Add (task
);
841 pre_hook
= new Hook (task
.Collector
.PreTaskHook
);
842 post_hook
= new Hook (task
.Collector
.PostTaskHook
);
844 double weight
= task
.Weight
;
845 double min_weight
= task
.Collector
.GetMinimumWeight ();
846 double max_weight
= task
.Collector
.GetMaximumWeight ();
848 collection
.Add (task
);
850 // We left i pointing at task
852 while (i
>= 0 && weight
< max_weight
) {
853 Task t
= task_queue
[i
] as Task
;
856 && t
.Collector
== task
.Collector
857 && t
.TriggerTime
< now
) {
859 // Only include differently-prioritized tasks
860 // in the same collection if the total weight so far
861 // is below the minimum.
862 if (t
.Priority
!= task
.Priority
&& weight
> min_weight
)
866 if (weight
> max_weight
)
871 // Remove the task from the queue and clean
872 // up the by-tag hash table.
873 task_queue
[i
] = null;
874 task_by_tag
.Remove (t
.Tag
);
880 // Clean the queue again
881 // (We need to do this to keep rescheduled tasks from blocking
882 // stuff from getting cleaned off the end of the queue)
887 // If we actually found tasks we like, do them now.
888 if (collection
.Count
> 0) {
889 DateTime t1
= DateTime
.Now
;
890 if (pre_hook
!= null) {
893 } catch (Exception ex
) {
894 Logger
.Log
.Error ("Caught exception in pre_hook '{0}'", pre_hook
);
895 Logger
.Log
.Error (ex
);
898 foreach (Task task
in collection
) {
900 ++executed_task_count
;
902 if (post_hook
!= null) {
905 } catch (Exception ex
) {
906 Logger
.Log
.Error ("Caught exception in post_hook '{0}'", post_hook
);
907 Logger
.Log
.Error (ex
);
910 DateTime t2
= DateTime
.Now
;
912 duration_of_last_task
= (t2
- t1
).TotalSeconds
;
913 time_of_last_task
= t2
;
921 // Execute all shutdown tasks
922 while (shutdown_task_queue
.Count
> 0) {
923 Task t
= shutdown_task_queue
.Dequeue () as Task
;
924 if (t
!= null && ! t
.Cancelled
&& t
.Priority
== Priority
.Shutdown
) {
926 // FIXME: Support Pre/Post task hooks
928 } catch (Exception ex
) {
929 Logger
.Log
.Error ("Caught exception while performing shutdown tasks in the scheduler");
930 Logger
.Log
.Error (ex
);
935 Logger
.Log
.Debug ("Scheduler.Worker finished");
940 class TestTask
: Scheduler
.Task
{
942 private class TestCollector
: Scheduler
.ITaskCollector
{
944 public double GetMinimumWeight ()
949 public double GetMaximumWeight ()
954 public void PreTaskHook ()
956 Console
.WriteLine ("+++ Pre-Task Hook");
959 public void PostTaskHook ()
961 Console
.WriteLine ("+++ Post-Task Hook");
965 protected override void DoTaskReal ()
967 Console
.WriteLine ("Doing task '{0}' at {1}", Tag
, DateTime
.Now
);
973 static void BeginTaskGroup ()
975 Console
.WriteLine ("--- Begin Task Group!");
978 static void EndTaskGroup ()
980 Console
.WriteLine ("--- End Task Group!");
985 Scheduler sched
= Scheduler
.Global
;
987 Scheduler
.TaskGroup tg
= Scheduler
.NewTaskGroup ("foo",
988 new Scheduler
.Hook (BeginTaskGroup
),
989 new Scheduler
.Hook (EndTaskGroup
));
995 task
= new TestTask ();
997 task
.AddTaskGroup (tg
);
998 task
.Priority
= Scheduler
.Priority
.Delayed
;
999 task
.TriggerTime
= DateTime
.Now
.AddSeconds (7);
1002 task
= new TestTask ();
1004 task
.AddTaskGroup (tg
);
1005 task
.Priority
= Scheduler
.Priority
.Delayed
;
1008 Scheduler
.ITaskCollector collector
= null;
1009 for (int i
= 0; i
< 20; ++i
) {
1011 collector
= new TestCollector ();
1012 task
= new TestTask ();
1013 task
.Tag
= String
.Format ("Baboon {0}", i
);
1014 task
.Collector
= collector
;
1015 task
.Priority
= Scheduler
.Priority
.Delayed
;
1020 Thread
.Sleep (1000);