CWIS Developer Documentation
AFTaskManager.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: AFTaskManager.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2009-2018 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
15 {
16  # ---- PUBLIC INTERFACE --------------------------------------------------
17 
22  public function __construct()
23  {
24  # set up our internal environment
25  $this->DB = new Database();
26 
27  # load our settings from database
28  $this->LoadSettings();
29  }
33  const PRIORITY_HIGH = 1;
35  const PRIORITY_MEDIUM = 2;
37  const PRIORITY_LOW = 3;
40 
54  public function QueueTask($Callback, $Parameters = NULL,
55  $Priority = self::PRIORITY_LOW, $Description = "")
56  {
57  # make sure priority is within bounds
58  $Priority = min(self::PRIORITY_BACKGROUND,
59  max(self::PRIORITY_HIGH, $Priority));
60 
61  # pack task info and write to database
62  if ($Parameters === NULL) { $Parameters = array(); }
63  $this->DB->Query("INSERT INTO TaskQueue"
64  ." (Callback, Parameters, Priority, Description)"
65  ." VALUES ('".addslashes(serialize($Callback))."', '"
66  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
67  .addslashes($Description)."')");
68  }
69 
88  public function QueueUniqueTask($Callback, $Parameters = NULL,
89  $Priority = self::PRIORITY_LOW, $Description = "")
90  {
91  if ($this->TaskIsInQueue($Callback, $Parameters))
92  {
93  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
94  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
95  .($Parameters ? " AND Parameters = '"
96  .addslashes(serialize($Parameters))."'" : ""));
97  if ($QueryResult !== FALSE)
98  {
99  # make sure priority is within bounds
100  $Priority = min(self::PRIORITY_BACKGROUND,
101  max(self::PRIORITY_HIGH, $Priority));
102 
103  $Record = $this->DB->FetchRow();
104  if ($Record["Priority"] > $Priority)
105  {
106  $this->DB->Query("UPDATE TaskQueue"
107  ." SET Priority = ".intval($Priority)
108  ." WHERE TaskId = ".intval($Record["TaskId"]));
109  }
110  }
111  return FALSE;
112  }
113  else
114  {
115  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
116  return TRUE;
117  }
118  }
119 
129  public function TaskIsInQueue($Callback, $Parameters = NULL)
130  {
131  $QueuedCount = $this->DB->Query(
132  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
133  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
134  .($Parameters ? " AND Parameters = '"
135  .addslashes(serialize($Parameters))."'" : ""),
136  "FoundCount");
137  $RunningCount = $this->DB->Query(
138  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
139  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
140  .($Parameters ? " AND Parameters = '"
141  .addslashes(serialize($Parameters))."'" : ""),
142  "FoundCount");
143  $FoundCount = $QueuedCount + $RunningCount;
144  return ($FoundCount ? TRUE : FALSE);
145  }
146 
152  public function GetTaskQueueSize($Priority = NULL)
153  {
154  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
155  }
156 
164  public function GetQueuedTaskList($Count = 100, $Offset = 0)
165  {
166  return $this->GetTaskList("SELECT * FROM TaskQueue"
167  ." ORDER BY Priority, TaskId ", $Count, $Offset);
168  }
169 
183  public function GetQueuedTaskCount($Callback = NULL, $Parameters = NULL,
184  $Priority = NULL, $Description = NULL)
185  {
186  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
187  $Sep = " WHERE";
188  if ($Callback !== NULL)
189  {
190  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
191  $Sep = " AND";
192  }
193  if ($Parameters !== NULL)
194  {
195  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
196  $Sep = " AND";
197  }
198  if ($Priority !== NULL)
199  {
200  $Query .= $Sep." Priority = ".intval($Priority);
201  $Sep = " AND";
202  }
203  if ($Description !== NULL)
204  {
205  $Query .= $Sep." Description = '".addslashes($Description)."'";
206  }
207  return $this->DB->Query($Query, "TaskCount");
208  }
209 
217  public function GetRunningTaskList($Count = 100, $Offset = 0)
218  {
219  return $this->GetTaskList("SELECT * FROM RunningTasks"
220  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
221  (time() - $GLOBALS["AF"]->MaxExecutionTime()))."'"
222  ." ORDER BY StartedAt", $Count, $Offset);
223  }
224 
229  public function GetRunningTaskCount()
230  {
231  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
232  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
233  (time() - $GLOBALS["AF"]->MaxExecutionTime()))."'",
234  "Count");
235  }
236 
244  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
245  {
246  return $this->GetTaskList("SELECT * FROM RunningTasks"
247  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
248  (time() - $GLOBALS["AF"]->MaxExecutionTime()))."'"
249  ." ORDER BY StartedAt", $Count, $Offset);
250  }
251 
256  public function GetOrphanedTaskCount()
257  {
258  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
259  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
260  (time() - $GLOBALS["AF"]->MaxExecutionTime()))."'",
261  "Count");
262  }
263 
269  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
270  {
271  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
272  $this->DB->Query("INSERT INTO TaskQueue"
273  ." (Callback,Parameters,Priority,Description) "
274  ."SELECT Callback, Parameters, Priority, Description"
275  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
276  if ($NewPriority !== NULL)
277  {
278  $NewTaskId = $this->DB->LastInsertId();
279  $this->DB->Query("UPDATE TaskQueue SET Priority = "
280  .intval($NewPriority)
281  ." WHERE TaskId = ".intval($NewTaskId));
282  }
283  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
284  $this->DB->Query("UNLOCK TABLES");
285  }
286 
293  public function RequeueCurrentTask($NewValue = TRUE)
294  {
295  $this->RequeueCurrentTask = $NewValue;
296  }
297 
303  public function DeleteTask($TaskId)
304  {
305  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
306  $TasksRemoved = $this->DB->NumRowsAffected();
307  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
308  $TasksRemoved += $this->DB->NumRowsAffected();
309  return $TasksRemoved;
310  }
311 
319  public function GetTask($TaskId)
320  {
321  # assume task will not be found
322  $Task = NULL;
323 
324  # look for task in task queue
325  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
326 
327  # if task was not found in queue
328  if (!$this->DB->NumRowsSelected())
329  {
330  # look for task in running task list
331  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
332  .intval($TaskId));
333  }
334 
335  # if task was found
336  if ($this->DB->NumRowsSelected())
337  {
338  # if task was periodic
339  $Row = $this->DB->FetchRow();
340  if ($Row["Callback"] ==
341  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
342  {
343  # unpack periodic task callback
344  $WrappedCallback = unserialize($Row["Parameters"]);
345  $Task["Callback"] = $WrappedCallback[1];
346  $Task["Parameters"] = $WrappedCallback[2];
347  }
348  else
349  {
350  # unpack task callback and parameters
351  $Task["Callback"] = unserialize($Row["Callback"]);
352  $Task["Parameters"] = unserialize($Row["Parameters"]);
353  }
354  }
355 
356  # return task to caller
357  return $Task;
358  }
359 
370  public function TaskExecutionEnabled(
371  $NewValue = DB_NOVALUE, $Persistent = FALSE)
372  {
373  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
374  }
375 
384  public function MaxTasks($NewValue = DB_NOVALUE, $Persistent = FALSE)
385  {
386  return $this->UpdateSetting("MaxTasksRunning", $NewValue, $Persistent);
387  }
388 
396  public static function GetTaskCallbackSynopsis(array $TaskInfo)
397  {
398  # if task callback is function use function name
399  $Callback = $TaskInfo["Callback"];
400  $Name = "";
401  if (!is_array($Callback))
402  {
403  $Name = $Callback;
404  }
405  else
406  {
407  # if task callback is object
408  if (is_object($Callback[0]))
409  {
410  # if task callback is encapsulated ask encapsulation for name
411  if (method_exists($Callback[0], "GetCallbackAsText"))
412  {
413  $Name = $Callback[0]->GetCallbackAsText();
414  }
415  # else assemble name from object
416  else
417  {
418  $Name = get_class($Callback[0]) . "::" . $Callback[1];
419  }
420  }
421  # else assemble name from supplied info
422  else
423  {
424  $Name= $Callback[0] . "::" . $Callback[1];
425  }
426  }
427 
428  # if parameter array was supplied
429  $Parameters = $TaskInfo["Parameters"];
430  $ParameterString = "";
431  if (is_array($Parameters))
432  {
433  # assemble parameter string
434  $Separator = "";
435  foreach ($Parameters as $Parameter)
436  {
437  $ParameterString .= $Separator;
438  if (is_int($Parameter) || is_float($Parameter))
439  {
440  $ParameterString .= $Parameter;
441  }
442  else if (is_string($Parameter))
443  {
444  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
445  }
446  else if (is_array($Parameter))
447  {
448  $ParameterString .= "ARRAY";
449  }
450  else if (is_object($Parameter))
451  {
452  $ParameterString .= "OBJECT";
453  }
454  else if (is_null($Parameter))
455  {
456  $ParameterString .= "NULL";
457  }
458  else if (is_bool($Parameter))
459  {
460  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
461  }
462  else if (is_resource($Parameter))
463  {
464  $ParameterString .= get_resource_type($Parameter);
465  }
466  else
467  {
468  $ParameterString .= "????";
469  }
470  $Separator = ", ";
471  }
472  }
473 
474  # assemble name and parameters and return result to caller
475  return $Name."(".$ParameterString.")";
476  }
477 
484  {
485  return isset($this->RunningTask)
486  ? $this->RunningTask["Priority"] : NULL;
487  }
488 
498  public function GetNextHigherBackgroundPriority($Priority = NULL)
499  {
500  if ($Priority === NULL)
501  {
502  $Priority = $this->GetCurrentBackgroundPriority();
503  if ($Priority === NULL)
504  {
505  return NULL;
506  }
507  }
508  return ($Priority > self::PRIORITY_HIGH)
509  ? ($Priority - 1) : self::PRIORITY_HIGH;
510  }
511 
521  public function GetNextLowerBackgroundPriority($Priority = NULL)
522  {
523  if ($Priority === NULL)
524  {
525  $Priority = $this->GetCurrentBackgroundPriority();
526  if ($Priority === NULL)
527  {
528  return NULL;
529  }
530  }
531  return ($Priority < self::PRIORITY_BACKGROUND)
532  ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
533  }
534 
539  public function RunQueuedTasks()
540  {
541  # if there are tasks in the queue
542  if ($this->GetTaskQueueSize())
543  {
544  # tell PHP to garbage collect to give as much memory as possible for tasks
545  gc_collect_cycles();
546 
547  # turn on output buffering to (hopefully) record any crash output
548  ob_start();
549 
550  # lock tables to prevent anyone else from running a task
551  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
552 
553  # while there is time and memory left
554  # and a task to run
555  # and an open slot to run it in
556  $MinimumTimeToRunAnotherTask = 65;
557  while (($GLOBALS["AF"]->GetSecondsBeforeTimeout()
558  > $MinimumTimeToRunAnotherTask)
560  > $this->BackgroundTaskMinFreeMemPercent)
561  && $this->GetTaskQueueSize()
562  && ($this->GetRunningTaskCount() < $this->MaxTasks()))
563  {
564  # look for task at head of queue
565  $this->DB->Query("SELECT * FROM TaskQueue"
566  ." ORDER BY Priority, TaskId LIMIT 1");
567  $Task = $this->DB->FetchRow();
568 
569  # move task from queued list to running tasks list
570  $this->DB->Query("INSERT INTO RunningTasks "
571  ."(TaskId,Callback,Parameters,Priority,Description) "
572  ."SELECT * FROM TaskQueue WHERE TaskId = "
573  .intval($Task["TaskId"]));
574  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
575  .intval($Task["TaskId"]));
576 
577  # release table locks to again allow other sessions to run tasks
578  $this->DB->Query("UNLOCK TABLES");
579 
580  # update the "last run" time
581  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
582  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
583 
584  # run task
585  $this->RunTask($Task);
586 
587  # lock tables to prevent anyone else from running a task
588  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
589  }
590 
591  $this->ResetTaskIdGeneratorIfNecessary();
592 
593  # make sure tables are released
594  $this->DB->Query("UNLOCK TABLES");
595  }
596  }
597 
598 
599  # ---- PRIVATE INTERFACE -------------------------------------------------
600 
601  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
602  private $BackgroundTaskMinFreeMemPercent = 25;
603  private $DB;
604  private $MaxRunningTasksToTrack = 250;
605  private $RequeueCurrentTask;
606  private $RunningTask;
607  private $Settings;
608 
613  private $NoTSR = FALSE;
614 
619  private function LoadSettings()
620  {
621  # read settings in from database
622  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
623  $this->Settings = $this->DB->FetchRow();
624 
625  # if settings were not previously initialized
626  if ($this->Settings === FALSE)
627  {
628  # initialize settings in database
629  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
630  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
631 
632  # read new settings in from database
633  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
634  $this->Settings = $this->DB->FetchRow();
635 
636  # bail out if reloading new settings failed
637  if ($this->Settings === FALSE)
638  {
639  throw new Exception(
640  "Unable to load application framework settings.");
641  }
642  }
643  }
644 
654  private function GetTaskList($DBQuery, $Count, $Offset)
655  {
656  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
657  $Tasks = array();
658  while ($Row = $this->DB->FetchRow())
659  {
660  $Tasks[$Row["TaskId"]] = $Row;
661  if ($Row["Callback"] ==
662  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
663  {
664  $WrappedCallback = unserialize($Row["Parameters"]);
665  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
666  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
667  }
668  else
669  {
670  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
671  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
672  }
673  }
674  return $Tasks;
675  }
676 
681  private function RunTask($Task)
682  {
683  # unpack stored task info
684  $TaskId = $Task["TaskId"];
685  $Callback = unserialize($Task["Callback"]);
686  $Parameters = unserialize($Task["Parameters"]);
687 
688  # attempt to load task callback if not already available
689  $GLOBALS["AF"]->LoadFunction($Callback);
690 
691  # clear task requeue flag
692  $this->RequeueCurrentTask = FALSE;
693 
694  # save amount of free memory for later comparison
695  $BeforeFreeMem = ApplicationFramework::GetFreeMemory();
696 
697  # run task
698  $this->RunningTask = $Task;
699  if ($Parameters)
700  {
701  call_user_func_array($Callback, $Parameters);
702  }
703  else
704  {
705  call_user_func($Callback);
706  }
707  unset($this->RunningTask);
708 
709  # log if task leaked significant memory
710  $this->LogTaskMemoryLeakIfAny($TaskId, $BeforeFreeMem);
711 
712  # if task requeue requested
713  if ($this->RequeueCurrentTask)
714  {
715  # move task from running tasks list to queue
716  $this->RequeueRunningTask($TaskId);
717  }
718  else
719  {
720  # remove task from running tasks list
721  $this->DB->Query("DELETE FROM RunningTasks"
722  ." WHERE TaskId = ".intval($TaskId));
723  }
724 
725  # prune running tasks list if necessary
726  $RunningTasksCount = $this->DB->Query(
727  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
728  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
729  {
730  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
731  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
732  }
733  }
734 
740  private function RequeueRunningTask($TaskId)
741  {
742  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
743  $this->DB->Query("INSERT INTO TaskQueue"
744  ." (Callback,Parameters,Priority,Description)"
745  ." SELECT Callback,Parameters,Priority,Description"
746  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
747  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
748  $this->DB->Query("UNLOCK TABLES");
749  }
750 
756  private function ResetTaskIdGeneratorIfNecessary()
757  {
758  $this->DB->Query("LOCK TABLES TaskQueue WRITE");
759  if (($GLOBALS["AF"]->GetSecondsBeforeTimeout() > 30)
760  && ($this->GetTaskQueueSize() == 0)
761  && ($this->DB->GetNextInsertId("TaskQueue")
762  > (Database::INT_MAX_VALUE * 0.90)))
763  {
764  $this->DB->Query("TRUNCATE TABLE TaskQueue");
765  }
766  $this->DB->Query("UNLOCK TABLES");
767  }
768 
775  private function LogTaskMemoryLeakIfAny($TaskId, $StartingFreeMemory)
776  {
777  # tell PHP to garbage collect to free up any memory no longer used
778  gc_collect_cycles();
779 
780  # calculate the logging threshold
781  $LeakThreshold = ApplicationFramework::GetPhpMemoryLimit()
782  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
783 
784  # calculate the amount of memory used by task
785  $EndingFreeMemory = ApplicationFramework::GetFreeMemory();
786  $MemoryUsed = $StartingFreeMemory - $EndingFreeMemory;
787 
788  # if amount of memory used is over threshold
789  if ($MemoryUsed > $LeakThreshold)
790  {
791  # log memory leak
792  $TaskSynopsis = self::GetTaskCallbackSynopsis($this->GetTask($TaskId));
793  $GLOBALS["AF"]->LogError(ApplicationFramework::LOGLVL_DEBUG,
794  "Task ".$TaskSynopsis." leaked "
795  .number_format($MemoryUsed)." bytes.");
796  }
797  }
798 
808  private function UpdateSetting(
809  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
810  {
811  static $LocalSettings;
812  if ($NewValue !== DB_NOVALUE)
813  {
814  if ($Persistent)
815  {
816  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
817  "ApplicationFrameworkSettings",
818  $FieldName, $NewValue, NULL, $this->Settings);
819  }
820  else
821  {
822  $LocalSettings[$FieldName] = $NewValue;
823  }
824  }
825  elseif (!isset($LocalSettings[$FieldName]))
826  {
827  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
828  "ApplicationFrameworkSettings",
829  $FieldName, $NewValue, NULL, $this->Settings);
830  }
831  return $LocalSettings[$FieldName];
832  }
833 }
GetCurrentBackgroundPriority()
Determine current priority if running in background.
const PRIORITY_BACKGROUND
Lowest priority.
QueueTask($Callback, $Parameters=NULL, $Priority=self::PRIORITY_LOW, $Description="")
Add task to queue.
GetOrphanedTaskList($Count=100, $Offset=0)
Retrieve list of tasks currently orphaned.
TaskExecutionEnabled($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether automatic task execution is enabled.
SQL database abstraction object with smart query caching.
Definition: Database.php:22
const LOGLVL_DEBUG
DEBUG error logging level.
const PRIORITY_LOW
Lower priority.
GetNextLowerBackgroundPriority($Priority=NULL)
Get next lower possible background task priority.
RunQueuedTasks()
Run any queued background tasks until either remaining PHP execution time or available memory run too...
const PRIORITY_MEDIUM
Medium (default) priority.
const INT_MAX_VALUE
Definition: Database.php:1316
GetNextHigherBackgroundPriority($Priority=NULL)
Get next higher possible background task priority.
const PRIORITY_HIGH
Highest priority.
GetTask($TaskId)
Retrieve task info from queue (either running or queued tasks).
static GetPercentFreeMemory()
Get current percentage of free memory.
GetOrphanedTaskCount()
Retrieve current number of orphaned tasks.
GetQueuedTaskCount($Callback=NULL, $Parameters=NULL, $Priority=NULL, $Description=NULL)
Get number of queued tasks that match supplied values.
const DB_NOVALUE
Definition: Database.php:1784
ReQueueOrphanedTask($TaskId, $NewPriority=NULL)
Move orphaned task back into queue.
RequeueCurrentTask($NewValue=TRUE)
Set whether to requeue the currently-running background task when it completes.
TaskIsInQueue($Callback, $Parameters=NULL)
Check if task is already in queue or currently running.
static GetFreeMemory()
Get current amount of free memory.
Task manager component of top-level framework for web applications.
DeleteTask($TaskId)
Remove task from task queues.
MaxTasks($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set maximum number of tasks to have running simultaneously.
GetQueuedTaskList($Count=100, $Offset=0)
Retrieve list of tasks currently in queue.
static GetTaskCallbackSynopsis(array $TaskInfo)
Get printable synopsis for task callback.
static GetPhpMemoryLimit()
Get PHP memory limit in bytes.
GetRunningTaskList($Count=100, $Offset=0)
Retrieve list of tasks currently running.
GetTaskQueueSize($Priority=NULL)
Retrieve current number of tasks in queue.
QueueUniqueTask($Callback, $Parameters=NULL, $Priority=self::PRIORITY_LOW, $Description="")
Add task to queue if not already in queue or currently running.
GetRunningTaskCount()
Retrieve count of tasks currently running.