Tasks in general can be of 2 types:
- interactive - a progress bar is displayed showing how much % of task is done
- non-interactive - nothing is shown to user
- user triggered - user presses a button (or similar control) to trigger a task
- system triggered - tasks are created on their own
Here is the matrix of what's currently supported by In-Portal:
Interactive | Non-Interactive | |
---|---|---|
User Triggered |
|
|
System Triggered |
|
|
Thoughts:
- interactive tasks are blocking UI and therefore user can't do something else in Admin Console while they are running
- user don't create about real-time status update of interactive tasks in most (but not all) cases, but just wants to know when they're completed
- scheduled tasks (system triggered non-interactive tasks):
- provide limited insight about task execution status
- only retain last task execution status
- e-mail queue:
- successfully executed tasks are removed from queue (the "E-mail Logs" section sort of compensates for that currently)
- no error information is stored within task
Solution
Part 1 (db tables & units)
- create "
ITaskQueueRunner
" interface with "processTaskQueueRun(kDBItem $task_queue_run)
" method - create "
TaskQueue
" database table with following columns:Id
QueuedOn
- when task was queuedQueuedById
- who queued the taskScheduledOn
- when task needs to be processed (set at queuing time)TaskClass
- FQCN of PHP class, which is responsible for processing this task queue record (must implement "ITaskQueueRunner
" interface)TaskData
- JSON-encoded data, that is needed for task execution (e.g. e-mail recipient, IDs of records to be processed)LastStatus
- status from last attempt of this queue record processing - {scheduled
(default),processing
,success
,failed
,timeout
}; when queue processed in parallel, then last ended task statusMaxRetries
- if task fails specified number of times (5 by default), then don't retry itFailedRetries
- failed retry count (number is reset, when task execution was successful)
- create "
TaskQueueRuns
" database table with following columns:Id
TaskQueueId
- associated task queue recordStartedOn
- when task was started executingPercentsCompleted
- "0
" by default, but will be updated as task is being processedFinishedOn
- when task was finished executing (regardless of status)Results
- JSON-encoded results in any format, that be later displayed in human-readable formStatus
- same statuses as for "TaskQueue.LastStatus" column- "
scheduled
" - initially, when record is created; - "
processing
" - when somebody is processing the record; - "
success
" - when execution finished without errors; - "
error
" - when known error happened during processing; - "
timeout
" - when status wasn't updated within 1 day (configurable per-queue record or system-wide, but can't be empty)
- "
ErrorCode
- non-empty when known error happenedErrorMessage
- non-empty when known error happenedProcessId
- the process ID of process that runs this task run (can be a runner process id, or just a regular website visit process); NULL, when not being processed by anybody right now
- create "TaskQueueRunners" database table with following columns:
- Id
- ProcessId - the PID of associated task runner process
- StartedOn - when process was started
- FinishedOn - when process was finished; NULL initially
- create units (the "
task-queue
" and "task-queue-run
", "task-queue-runner
"), that corresponding to above described database tables - in "
TaskQueueRunEventHandler::OnAfterItemUpdate
" aggregate totals from all runs from associated task queue record and update it (task queue record)
Part 2 (adding tasks & runs)
- create "
TaskQueueHelper
" class - add public "
TaskQueueHelper::queueTask($task_class, $task_data, $scheduled_on, $max_retries = null)
" method, that will:- create task queue record with given settings
- consider "
$max_retries
" as "5
" when not given - throw an exception, when specified "
$task_class
" doesn't exist
- add protected "
TaskQueueHelper::synchronizeTaskRunStatus
" method that will:- get all task runs, that are running currently
- get status of their PIDs
- for all task runs which PIDs are dead set their status to "timeout"
- add protected "
TaskQueueHelper::createTaskRun(kDBItem $task_queue)
" method, that:- will create new task run (and return it's ID) for given queue record, when all of following rules aren't violated:
- only 1 active (status = processing) task run can exist at same time (for a given queue record)
- sequential failed task run count (both "error" and "timeout" statuses are considered as failed) can't be more than max allowed retry count
- return "null" otherwise
- will create new task run (and return it's ID) for given queue record, when all of following rules aren't violated:
- add protected "
TaskQueueHelper::createMissingTaskRuns()
" method, that will:- get all records from "
TaskQueue
" table, for which task runs can be created:ScheduledOn
<NOW()
Status
is not "processing
"FailedRetries
<MaxRetries
- call the "
TaskQueueHelper::createTaskRun
" on each of them (method can return "NULL" in some cases, but that's ok)
- get all records from "
- add public "
TaskQueueHelper::createTaskRuns()
" method, that will:- call "
TaskQueueHelper::synchronizeTaskRunStatus
" method - call "
TaskQueueHelper::createMissingTaskRuns
" method
- call "
- add "
TaskQueueEventHandler::OnCreateTaskRuns
" event, that would be called as Scheduled Task on a regular basis (e.g. each 5 minutes)
Part 3 (running runs)
- create new "
TaskQueueRunnerLimit
" system setting set to "8
" by default - add public "
TaskQueueHelper::processTaskRun($task_run_id)
", that will:- load task run by given ID from the database (if failed throw an exception)
- set following fields and save changes to db immediately:
- "
ProcessId
" to current process id - "
Status
" to "processing
"
- "
- create instance of class from "
TaskClass
" field of associated task queue record - call the "
processTaskQueueRun
" (wrapped within try/catch block) on that object providing task run object (was loaded above) as an argument - the above method can update given object fields at will and save to db (e.g. "
PercentsCompleted
" and "Results
") - when exception was caught, then:
- set "
Status
" to "error
" - set "
ErrorCode
" to exception code - set "
ErrorMessage
" to exception message
- set "
- when no exception was caught, then:
- set "
Status
" to "success
" - set "
ErrorCode
" and "ErrorMessage
" to empty value
- set "
- set "
FinishedOn
" to time, when task was finished (with error or not)
- save changes to db
- add protected "
TaskQueueHelper::getQueueTaskRunnerCount()
" method, that will:- get all task queue runners, that are running currently ("
FinishedOn IS NULL
") - get status of their PIDs
for all task queue runners which PIDs are dead set their "FinishedOn" field to NOW()
- return number of running task runners (don't include ones updated above)
- get all task queue runners, that are running currently ("
- add public "
TaskQueueHelper::registerAsTaskQueueRunner()
" method, that will:- get value of "
TaskQueueRunnerLimit
" system setting - call "
TaskQueueHelper::
getTaskQueueRunnerCount
" method - if currently running task queue runner count is larger or equal to allowed count, then return "false"
- add record to "
TaskQueueRunners
" table with current PID - return "
true
"
- get value of "
- add "
TaskQueueEventHandler::OnProcessTaskRun
" event, that will:- call the "
TaskQueueHelper::registerAsTaskQueueRunner
" method - if it returns "false", then do nothing and exit
- acquire WRITE lock "TaskQueueRuns" database table (prevent 2 events executed at same time using same task run)
- pick 1st available run (FIFO logic)
- release above acquired lock
- if none found, then exit
- call "
TaskQueueHelper::
" method with found task run idprocessTaskRun
- call the "
Part 4 (rotation)
- create "
TaskQueueRotationInterval
" setting (same configuration as for e-mail logs) - create "
TaskQueueEventHandler::OnRotate
" event (scheduled task), that would delete old (same concert as for e-mail logs) successful task queue records along with their runs
Part 5 (usage)
configure either of following, but not as scheduled task, because it will block all other scheduled tasks:
command:
/usr/bin/env php /path/to/in-portal/tools/run_event.php task-queue:OnProcessTaskRun password_here
setup "
upstart
" or "supervisord
" or any other tool to ensure presence of X processes powered by above commandadd X records to "crontab" file powered by above command
there won't be any built-in UI for this functionality, because it's too general to be usable by user, but specialized sections (e.g. "E-mail Queue") can read data from these tables to keep user informed
- task can be created through calling "
TaskQueueHelper::queueTask
" method by whoever needs it, e.g.:- user presses a button
- scheduled task decides to offload some work
- etc.