Add new when-expression operators.
[whenjobs.git] / daemon / daemon.ml
index fba3ae5..eca0218 100644 (file)
@@ -36,6 +36,9 @@ let dependencies = ref StringMap.empty
  *)
 let variables : variables ref = ref StringMap.empty
 
+(* Last time that an every job ran.  See schedule_next_everyjob below. *)
+let last_t = ref (time ())
+
 (* $HOME/.whenjobs *)
 let jobsdir = ref ""
 
@@ -43,6 +46,7 @@ let jobsdir = ref ""
 let debug = ref false
 
 let esys = Unixqueue.standard_event_system ()
+let timer_group = ref None
 
 let rec init j d =
   jobsdir := j;
@@ -80,8 +84,7 @@ and proc_set_variable (name, value) =
 
   (* Which jobs need to be re-evaluated? *)
   let jobnames = try StringMap.find name !dependencies with Not_found -> [] in
-  let jobs = reevaluate_jobs jobnames in
-  run_jobs jobs
+  reevaluate_whenjobs jobnames
 
 and proc_get_variable name =
   if !debug then Syslog.notice "remote call: get_variable %s" name;
@@ -142,14 +145,18 @@ and reload_file () =
     ) StringMap.empty js in
     dependencies := map in
 
-  (* Re-evaluate all jobs. *)
-  let jobs = reevaluate_jobs (StringMap.keys !jobs) in
-  run_jobs jobs
+  (* Re-evaluate all when jobs. *)
+  reevaluate_whenjobs (StringMap.keys !jobs);
+
+  (* Schedule the next every job to run. *)
+  last_t := time ();
+  schedule_next_everyjob ()
 
-(* Re-evaluate each named job, in a loop until we reach a fixpoint.
- * Return the names of all the jobs that need to be run.
+(* Re-evaluate each named when-statement job, in a loop until we reach
+ * a fixpoint.  Run those that need to be run.  every-statement jobs
+ * are ignored here.
  *)
-and reevaluate_jobs jobnames =
+and reevaluate_whenjobs jobnames =
   let rec loop set jobnames =
     let set' =
       List.fold_left (
@@ -159,7 +166,13 @@ and reevaluate_jobs jobnames =
             with Not_found -> assert false in
           assert (jobname = job.job_name);
 
-          let r, job' = job_evaluate job !variables in
+          let r, job' =
+            try job_evaluate job !variables
+            with Invalid_argument err | Failure err ->
+              Syslog.error "error evaluating job %s (at %s): %s"
+                jobname (Camlp4.PreCast.Ast.Loc.to_string job.job_loc) err;
+              false, job in
+
           jobs := StringMap.add jobname job' !jobs;
 
           if !debug then
@@ -173,16 +186,102 @@ and reevaluate_jobs jobnames =
       set'
   in
   let set = loop StringSet.empty jobnames in
-  StringSet.elements set
+  let jobnames = StringSet.elements set in
+  (* Ensure the jobs always run in predictable (name) order. *)
+  let jobnames = List.sort compare jobnames in
+  List.iter run_job
+    (List.map (fun jobname -> StringMap.find jobname !jobs) jobnames)
 
-and run_jobs jobnames =
-  let run_job job =
-    Syslog.notice "running %s" job.job_name;
-    () (* XXX *)
+(* Schedule the next every-statement job to run, if there is one.  We
+ * look at the every jobs, work out the time that each must run at,
+ * pick the job(s) which must run soonest, and schedule a timer to run
+ * them.  When the timer fires, it runs those jobs, then call this
+ * function again.  'last_t' is the base time used for scheduling (or
+ * the time that the file was last reloaded).
+ *)
+and schedule_next_everyjob () =
+  (* Get only everyjobs. *)
+  let jobs = StringMap.values !jobs in
+  let jobs = filter_map (
+    function
+    | { job_cond = Every_job period } as job -> Some (job, period)
+    | { job_cond = When_job _ } -> None
+  ) jobs in
+
+  (* Map everyjob to next time it must run. *)
+  let jobs = List.map (
+    fun (job, period) ->
+      let t' = next_periodexpr !last_t period in
+      assert (t' > !last_t); (* serious bug in next_periodexpr if false *)
+      job, t'
+  ) jobs in
+
+  (* Sort, soonest first. *)
+  let jobs = List.sort (fun (_,a) (_,b) -> compare a b) jobs in
+
+  if !debug then (
+    List.iter (
+      fun (job, t) ->
+        Syslog.notice "%s: next scheduled run at %s"
+          job.job_name (string_of_time_t t)
+    ) jobs
+  );
+
+  (* Pick the job(s) which run soonest. *)
+  let rec pick = function
+    | [] -> 0., []
+    | [j, t] -> t, [j]
+    | (j1, t) :: (j2, t') :: _ when t < t' -> t, [j1]
+    | (j1, t) :: (((j2, t') :: _) as rest) -> t, (j1 :: snd (pick rest))
   in
+  let t, jobs = pick jobs in
+
+  if t > 0. then (
+    last_t := t;
+
+    if jobs <> [] then (
+      if !debug then
+        Syslog.notice "scheduling job(s) %s to run at %s"
+          (String.concat ", " (List.map (fun { job_name = name } -> name) jobs))
+          (string_of_time_t t);
+
+      (* Schedule them to run at time t. *)
+      let g = new_timer_group () in
+      let w = Unixqueue.new_wait_id esys in
+      let t_diff = t -. Unix.time () in
+      let t_diff = if t_diff < 0. then 0. else t_diff in
+      Unixqueue.add_resource esys g (Unixqueue.Wait w, t_diff);
+      let run_jobs _ _ _ =
+        List.iter run_job jobs;
+        delete_timer ();
+        schedule_next_everyjob ();
+      in
+      Unixqueue.add_handler esys g run_jobs;
+    )
+  )
 
-  List.iter run_job
-    (List.map (fun jobname -> StringMap.find jobname !jobs) jobnames)
+and string_of_time_t t =
+  let tm = gmtime t in
+  sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC"
+    (1900+tm.tm_year) (1+tm.tm_mon) tm.tm_mday
+    tm.tm_hour tm.tm_min tm.tm_sec
+
+and new_timer_group () =
+  delete_timer ();
+  let g = Unixqueue.new_group esys in
+  timer_group := Some g;
+  g
+
+and delete_timer () =
+  match !timer_group with
+  | None -> ()
+  | Some g ->
+    Unixqueue.clear esys g;
+    timer_group := None
+
+and run_job job =
+  Syslog.notice "running %s" job.job_name;
+  () (* XXX *)
 
 let main_loop () =
   Unixqueue.run esys