Background task with Ajax

You are viewing revision #10 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#11) »

  1. Introduction
  2. Approaches
  3. Example with ajaxSubmitButton
  4. Variations

Introduction

Web applications normally execute almost immediately the user's requests. But some requests, like statistical reports, take long time to execute.

This article discusses how one can run a long task in background in yiiframework 1.1 using Ajax technique.

Approaches

There are different ways to run asynchronously background jobs in yiiframework. Let's see some of them.

Console application

The console application is proper to run periodically tasks using operating system schedulers such as cron. One can, for example, send emails from email queue every 15 minutes or collect statistical information every night.

The console applications can access the same models of the web application reusing most of code. The console applications are appropriate for generic system jobs, but are not suitable for tasks for single users. In fact, there is no web user in console application and hence no role authorizations and row level access limitations normally applied to web users.

BackJob extension

The BackJob extension of Siquo can start actions in background and monitor their execution. It can initiate a controller's action of the Yii web application as current or as an anonymous user. Running an action as current user gives the possibility to execute the code applying usual role authorizations and row level access limitations.

Unfortunately in this approach the yiiframework considers the user's session cookies tampered if the web application uses the cookie validation. In this case the web application is executed with anonymous identity.

Ajax

The Ajax technique discussed in this article gives the possibility to start a controller's action as current user without leaving the web page. This can be used to run long tasks with authorizations given to the user reusing the same Yii controllers and models. One should only intercept the result of the finished task.

Example with ajaxSubmitButton

This is a fictional example of a statistical reporting. The user specifies a parameter in a web form, then invokes the long task. The task produces a file to be downloaded.

The model Example contains a method doTask() executing a long task. This method takes a parameter and yields a blob result. The action runTask in the controller ExampleController renders the web form task and invokes the Example.doTask() method when requested. The form runTask contains one text field, a submit button and an ajaxSubmitButton.

The workflow is as follows. The user calls the runTask form, specifies the parameter's value and presses one of the submit buttons. Each button is designed to call the ExampleController.actionRunTask() action passing the text parameter. The controller in turn calls the Example.doTask() method with the specified parameter. The result of the Example.doTask() method is rendered to the user as file to be downloaded.

When the user presses the usual submit button, he/she should wait for the result. The ajaxSubmitButton instead runs the action in background without leaving the web form; the user is informed that the task is started and that he/she can download the result later returning in the same form.

The result of the background task should be kept in a database table longtask in a blob column. This table keeps one record for each user/task. This example uses also the relative Longtask model and the LongtaskController controller on order to save and retrieve the task's results.

To run this example, execute the action example/runTask.

Table longtask

This table keeps the results of the tasks. Follows the sql script for this table for the MySql database:

[sql]
CREATE TABLE longtask (
	id int(11) NOT NULL AUTO_INCREMENT,
	end_time datetime DEFAULT NULL,
	task text,
	username text,
	filename text,
	mime text,
	cont longblob,
	PRIMARY KEY (id)
);
Longtask model

This model corresponds to the longtask table. Follow essential parts of the model:

class Longtask extends CActiveRecord {
	// Usual staff...

	// Initialize attributes
	public function init() {
		if ($this->scenario <> 'search') {
			$this->end_time = date('Y-m-d H:i:s');
			$this->task = Yii::app()->request->url;
			$this->username = Yii::app()->user->id;
		}
	}

	// Delete this task of this user
	public function cleanMy() {
		$this->deleteAll('task = :task AND username = :username',
			array(':task' => Yii::app()->request->url,
					':username' => Yii::app()->user->id));
	}

	// Permission to view the tasks
	public function defaultScope() {
		$u = Yii::app()->user; // user identity
		if ($u->id == 'admin') // admin sees every record
			return array();
		else // others can see only their own tasks
			return array(
				'condition' => 'username = :username',
				'params' => array(':username' => $u->id));
	}
}
LongtaskController controller

This controller contains the download action used to download the task's result saved in the longtask table:

class LongtaskController extends Controller {
	public function accessRules() {
		return array(
			array('allow', // allow all users to perform 'download' action
				'actions'=>array('download'),
				'users'=>array('*'),
			),
			// other rules
		);
	}

	// Usual staff...

	// Download the document result of a task $id
	public function actionDownload($id) {
		$model = $this->loadModel($id);
		Yii::app()->getRequest()->sendFile(
			$model->filename, $model->cont, $model->mime);
	}
}
Example model

The model Example contains a method doTask() executing the long task. This method takes a parameter and yields a blob result.

class Example extends CFormModel {
	// Usual staff...

	// Long task. Takes parameter $par, yields blob result
	public function doTask($par) {
		sleep (10); // does some long work
		$cont = "This is the result for $par.";
		return $cont; // returns the result
	}
}
ExampleController controller

The action runTask in this controller renders the web form task and invokes the Example.doTask() method when requested.

class ExampleController extends Controller {
	// Usual staff...

	// The action runTask
	public function actionRunTask() {
		$model = new Example; // the model to generate report
		$par = ''; // form parameter

		// Returned from the form
		if (isset($_POST['par'])) {
			$par = $_POST['par']; // parameter specified
			$fn = "rep_$par.txt"; // file name
			$mime = 'text/plain'; // file type
			$cont = $model->doTask($par); // generate report

			// Ajax button
			if (Yii::app()->request->isAjaxRequest) {
				$lt = new Longtask;
				$lt->cleanMy(); // deletes the previous task
				$lt->filename = $fn;
				$lt->cont = $cont;
				$lt->mime = $mime;
				$lt->save(); // creates new task record
				Yii::app()->end(); // end of Ajax initiated process
			}

			// Normal submit button: download file
			else
				Yii::app()->getRequest()->sendFile ($fn, $cont, $mime);
		}

		// Calls the form
		$lt = Longtask::model()->findByAttributes(array(
			'task' => Yii::app()->request->url,
			'username' => Yii::app()->user->id)); // last task
		$this->render('runTask', array('par' => $par, 'lt' => $lt));
	}
}
runTask form

This form contains one text field, a submit button and an ajaxSubmitButton. The same form contains also the link to download the result of the executed background task:

<?php
/* @var $this ExampleController */
/* @var $par string Parameter to be specified in the form */
/* @var $lt Longtask The last task */
?>
<h1>Example of a background task</h1>

<div class="form">

<?php $form = $this->beginWidget('CActiveForm', array(
	'id' => 'runTask-form',
	'enableAjaxValidation' => false,
)); ?>

	<div class="row">
		<?php echo CHtml::label('Report parameter', 'par'); ?>
		<?php echo CHtml::textField('par', $par); ?>
	</div>

	<div class="row buttons">
		<?php echo CHtml::submitButton('Submit'); ?>
		<br>
		<?php
			echo CHtml::ajaxSubmitButton(
				'Submit in background',
				$this->createUrl("runTask"),
				array('type' => 'POST', 'dataType' => 'json'));
		?>
	</div>

<?php $this->endWidget(); ?>

</div><!-- form -->

<?php
	// Link to download the result
	if ($lt && $lt->cont) {
		echo "Result of " . $lt->end_time . ": ";
		echo CHtml::link(CHtml::encode($lt->filename),
			array('longtask/download', 'id' => $lt->id));
	}
?>

Variations

  • One can use this example to run in background different tasks for different users.

  • The application administrator should have access to the longtask table in order to monitor all the background jobs.

  • One can use also ajaxLink in case there are no input parameters requested from the user.

  • One can keep more records per task/user to preserve the history of tasks. In this case it is better for the user to have access to the longtask table via CGridView.

  • More information on the tasks can be specified in the longtask table. For example, it is possible to save the start time and the percentage of the execution. See for example the BackJob extension.

  • If the task yields more then one file, they can be saved in a child table of longtask.

  • The runTask form can give more information on the Ajax execution, such as errors detected. See the blacksheep's example.

  • When the task's result is downloaded, it is reasonable to automatically delete the task's record from the longtask table. When the user has downloaded/deleted from the runTask form, a javascript should immediately hide the download link.

  • When the task takes very long time to execute, it is a good idea to add in the beginning of the task code the set_time_limit() PHP instruction.