Yii 1.1: runactions

Helper for running controller actions in background, cron and more.
54 followers

This extension is a helper class for running actions. It makes controller actions reusable within different contexts.

Features

  • Run controller actions as background tasks
  • Configure cron jobs
  • 'Touch' urls at remote/local servers.
  • Run preconfigured batchjobs or php scripts
  • Use builtin Http client for simple GET and POST requests (since v1.1)
  • Interval filter for controller actions (since v1.1)

Requirements

  • Developed with Yii 1.1.7

  • When using 'touchUrlExt' (see below) you have to install the extension ehttpclient

Usage

  1. Extract the files under .../protected/extensions.
  2. Import the component in the top of controller file, where you are using ERunActions:
Yii::import('ext.runactions.components.ERunActions');
 
class MyController extends CController {
...

This is only a quick overview of the usage. I don't list all configurable properties or methods here. Please take a look at the comments in the code of ERunActions.php

1. 'Touch' a url

Use this static methods to start processes at a remote or the own webserver. A request to the url will be sent, but not waiting for a response.

ERunActions::touchUrl($url,$postData=null,$contentType=null);

uses a simple built in httpclient (fsockopen).

ERunActions::touchUrlExt($url,$postData=null,$contentType=null,$httpClientConfig=array());

uses the extension EHttpClient, if you need support for redirect, proxies, certificates ...

NOTE: - touchUrl only works with absolute urls - Since v1.1 touchUrl works with https too

2. Run a controller action

Similar to CController.forward but with the possibility to suppress output with logging output, skip filters and/or before-afterAction.

ERunActions::runAction($route,$params=array(),$ignoreFilters=true,$ignoreBeforeAfterAction=true,$logOutput=false,$silent=false);

The 'route' is the route to the controller including the action. $params will be added as query params.

You can configure to ignore filters, before and afterAction of the controller, only log the output of the controller if $silent and $logOutput is set to true.

If both $ignoreFilters and $ignoreBeforeAfterAction are set to false, this will be the same as when using the method CController.forward.

3. Run a php script

This is a simple method that includes a script and extract the params as variable. The include file has to be located in runaction/config by default.

ERunActions::runScript($scriptName,$params=array(),$scriptPath=null)

4. Run a controller action as a background task

Use this if you have implemented time-consuming controller actions and the user has not to wait until finished. For example:

  • importing data
  • sending newsletter mails or mails with large attachments
  • cleanup (db-) processes like flush ...
public function actionTimeConsumingProcess()
    {
        if (ERunActions::runBackground())
        {
           //do all the stuff that should work in background
           //mail->send() ....
        }
        else
        {
            //this code will be executed immediately
            //echo 'Time-consuming process has been started'
            //user->setFlash ...render ... redirect,
        }
    }

5. Run preconfigured actions as batchjob

Run the config script 'cron.php' from runactions/config

$this->widget('ext.runactions.ERunActions');

The cron.php should return a batch config array(actiontype => configarray). There are 4 actiontypes (see methods from above) available

  • ERunActions::TYPE_ACTION
  • ERunActions::TYPE_SCRIPT
  • ERunActions::TYPE_TOUCH, ERunActions::TYPE_TOUCHEXT

For example:

return array(
   //execute ImportController actionRun ignoring filters and before- afterAction of the controller
    ERunActions::TYPE_ACTION  => array('route' => '/import/run'),
    ...
 
   //run the php file runaction/config/afterimport.php to do something with the imported data
    ERunActions::TYPE_SCRIPT  => array('script' => 'afterimport'),
    ...
 
   //inform another server that the process is finished
   ERunActions::TYPE_TOUCH => array('url'=>'http://example.com/processfinished');
);

You can override the configure the properties of the widget in the config of the action.

Run the config script 'runactions/config/myscript.php'

$this->widget('ext.runactions.ERunActions',
              'config'=>'myscript',
              'ignoreBeforeAfterAction' => true,
              'interval' => 3600,
              'allowedIps' => array('127.0.0.1'),
);

Content of 'myscript.php'

return array(
    ...
 
    ERunActions::TYPE_ACTION  => array('route' => '/cache/flush'
                                       'ignoreBeforeAfterAction' => false,
                                       ),
    ...
);

6. Use the widget to expose a 'cron' controller action

Add the RunActionsController as 'cron' to the controllerMap in applications config/main.php

'controllerMap' => array(
   'cron' => 'ext.runactions.controllers.RunActionsController',
   ...
 ),

Now you can run the config script runactions/config/cron.php by calling

http://localhost/index.php/cron

or another script by

http://localhost/index.php/cron/run/config/myscript

or running in background so that a HTTP 200 OK will immediatly be returned

http://localhost/index.php/cron/touch/config/myscript

Configure the urls in your crontab by using 'wget'.

7. GET / POST requests

You can use the builtin Http client for simple requests:

echo ERunActions::httpGET('https://example.com',array('type'=>1,'key'=>123));

Will get the content from the url 'https://example.com/?type=1&key=123'

echo ERunActions::httpPOST('https://example.com',array('name'=>'unknown'),null,array('type'=>1,'key'=>123));

Will POST the form variable name='unknown' to the url 'https://example.com/?type=1&key=123

8. Interval filter

You can install the component 'ERunActionsIntervalFilter' (since v1.1) as a filter in a controller. See CController::filters()

public function filters()
 {
   return array(
          ... 
                   array(
                'ext.runactions.components.ERunActionsIntervalFilter + export, import',
                'interval'=>15,  //seconds
                        'perClient'=>true, //default = false
                        //'httpErrorNo' => 403, (=default)
                        //'httpErrorMessage' => 'Forbidden',  (=default) 
            ),
                   ....
    );

This will ensure, that the controller actions 'export' and 'import' can only be executed once withing the time interval of 15 seconds per client (= IP-Address) If the action is called more than once, a CHttpException will be thrown.

Note: There maybe has to be stored a lot of data in the global storage if you set 'perClient' to true.

9. Notes

a) In a controller action executed by 'runAction', 'touchUrl' or a batch script you can use the static methods

  • ERunActions::isRunActionRequest()
  • ERunActions::isBatchMode()
  • ERunActions::isTouchActionRequest()

to switch behavior if the action is called in contexts above.

b) The widget catches all errors (even php errors) and uses Yii::log if an error occurs. So running cron jobs will not display internal errors.

Changelog

  • v.1.1:
    • Modified and fixed bugs in ERunActionsHttpClient
    • Added support for https in ERunActionsHttpClient
    • New static methods httpGET,httpPOST
    • New interval filter ERunActionsIntervalFilter

Total 20 comments

#16681 report it
Joblo at 2014/03/18 11:50am
helper

You should create the helper inside the ERunActions::runBackground() part, because this section will be called by another request. Use Yii::log() there to check if all is ok.

public function actionTimeConsumingProcess()
    {
        if (ERunActions::runBackground())
        {
             ... create your helper here
             Yii::log(...)
        }
        else
        {
            //this code will be executed immediately
        }
    }

It would be a good idea, to create an extra action for the background task

public function actionXY() 
{
   ...The code that should run in background ...
   ... create your helper here
             Yii::log(...)
}
 
public function actionTimeConsumingProcess()
    {
        if (ERunActions::runBackground())
        {
            $this->actionXY();
        }
        else
        {
            //this code will be executed immediately
        }
    }

So you can test the background action without running in background by calling actionXY. If actionXY works fine it also should work in the if(ERunActions::runBackground()) part.

#16671 report it
dodgerid at 2014/03/17 09:43pm
Using a helper

Hi,

If the process i want to run in the background needs to use a helper, do i need to import the helper and just use the function or should it work if the process is calling a new instance of the helper and then the function ?

Thanks

#15383 report it
Joblo at 2013/11/04 02:00am
Install

You have to import ERunActions in the top of the controller code, where you use the component:

Yii::import('ext.runactions.components.ERunActions');
#15380 report it
Nikeware at 2013/11/03 03:10pm
Description need

How to configure this widget? I don't found nothing about it :(

"Extract the files under .../protected/extensions"

It is all?

I need run a controller action as a background task. but I have only:

PHP warning
 
include(ERunActions.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory

To author: Please provide the config instruction more clearly in next time.

#14856 report it
sucotronic at 2013/09/16 03:27am
Good practices

I don't want to start any flame war. Summarizing I would only say, if you have to drive a nail into wood, which tool will you use?

  • a hammer
  • a wrench

Of course you can use the wrench, but I'll always choose the hammer.

#14843 report it
Joblo at 2013/09/14 03:52am
What's the problem?

sucotronic, so I can't see your problem with this extensions.

A thread will be started at the server by a request, but not waiting for a response in the browser (or another client). In the thread you can log the steps executed to see what happens at the server.

It's the same as you would start a process (rest-api delete/post/put) through the browser, but you are not interested in a response, instead you switch to another tab. (Isn't this is a Zombie Request too?)

Later you can take a look at the logging to see if all was working well.

For me this extensions works very well since years to exchange data (import/export) between servers, calling the 'background'-url from a windows-application, a mobile device or from a php admin-backend. The clients only have to call a url where the response will be immediatly available (Message: OK or Process started ...). But this initializes a data-exchange process that can take minutes. The log-file at the server gives all information about a succesful or failure execution.

It's up to you if you decide this is 'fucking ugly' for you. You don't have to use this extension (or another of mine). But I'm missing a statement, where you see a real problem.

#14830 report it
sucotronic at 2013/09/13 04:27am
answers
  • Did you never use the 'ignore_user_abort' function to ensure a process should be finalized even when a user aborts?

Never, and not plan to use it. If I need to start something that don't need user interaction I simply use a queue system with and the queue manager process will take care of executing it.

  • Do you see something in your browser if you call a Yii command or exec only?

I use logging for that ¬¬

  • Are you shure your Yii command app is always working well?

See previous answer.

#14829 report it
Joblo at 2013/09/13 04:12am
thanks

sucotronic, many thanks for the good explanation how this extension works.

If you call a url to start a process (without waiting for a response), of course a 'echo' will go to nowhere, but Yii supports a good logging mechanism.

  • Did you never click the abort-button of your browser or closed it without waiting for a response?

  • Did you never use the 'ignore_user_abort' function to ensure a process should be finalized even when a user aborts?

  • Do you see something in your browser if you call a Yii command or exec only?

  • Are you shure your Yii command app is always working well?
#14824 report it
sucotronic at 2013/09/12 10:33am
Worst way ever to pseudo-background execution

Hey you!! Before installing this extension, and trying to "fix your problem" whatever you're in a hurry or no, please see the code implementation of this "thing", or at least read the following to know how it works.

The flow is the following:

  1. You call 'if(ERunActions::something())' in your code
  2. This is going to take your actual request (URI), append a parameter to it, open a socket with... yourself!! send raw html GET petition through it, will not wait for a reply, close it and call return false (I'm going to call this Zombie Request, ZR)
  3. The if will be skipped
  4. Your server will route the ZR to the same controller and action where the 'if(ERunActions::something())' (well, it depends of your code, but that's the idea)
  5. The ERunActions will detect the extra parameter (added before) and will return true
  6. The code inside the if is executed. If you send something to standard ouput (echo, or similar), it'll go nowhere, because nobody is listening the socket...

So, this extension is a fucking ugly patch that relies on server implementation (hoping it won't kill the execution thread when the other ends closes the socket...) to solve a problem.

Yii have a built'in mechanism to handle 'commands', so please use it, even an 'exec' call will work better than this shit.

(Sorry for bad sounding words, but when you find this in code, this is your reaction)

#14373 report it
Joblo at 2013/08/07 04:46am
Try this

Create a (mail)controller action (not restricted by accessfilter)

public function actionSendMassMmail() 
{
     ... your mailing here ...
     // for first testing: look at the log - this entry should be there
     Yii::log('Sending mails');
 
}

In the model you can 'touch' the controlleraction above on aftersave

protected function afterSave()
{
  parent::afterSave(); 
 
  //url must be an absolute url
  $mailUrl = Yii::app()->createAbsoluteUrl('mailcontroller/sendmassmail');
 
   ERunActions::touchUrl($mailUrl); //start sending mails
 
}

If you need more information in the actionSendMassMmail about the saved record:

Add GET params to the $mailUrl or try something like below and check $_POST in the actionSendMassMail.

protected function afterSave()
{
  parent::afterSave(); 
 
  //url must be an absolute url
   $mailUrl = Yii::app()->createAbsoluteUrl('mailcontroller/sendmassmail');
   $postData = array('id'=>$this->id, .....);
   ERunActions::touchUrl($mailUrl,$postData); 
 
}
#14364 report it
Bertho Joris at 2013/08/06 02:39pm
Runing in model

Hai.. Will I be able to use this extension to model? for example in the case of afterSave(). My case if a data has been inputted will direct mass email sending process. I think this is good if you can use this extension in this process. My application will not wait for it to finish processing the delivery of email if this can be done in the background.

#12042 report it
TimT at 2013/02/22 06:57pm
EHttpClient

There seems to be an undocumented dependancy on something called "EHttpClient" - whats that about?

#9227 report it
fl007 at 2012/07/30 10:47am
Accessing web user

If you need to access the web user that initiated the request when running a background task, you might want to pass on the PHPSESSID cookie with the background request, please see this topic

#8587 report it
Daniel at 2012/06/13 03:49am
runBackground

I tried to use runBackground to read data from fingerprint machine and save to database, but still the server is time out.

Any help on this?

#8553 report it
Joblo at 2012/06/11 05:42pm
How runBackground works

Try to test the touchUrl method at the server.

public function actionA() 
{
   Yii::log('Action A executed');
}
 
public function actionTestActionA ()
{
       ERunActions::touchUrl(Yii::app()->createAbsoluteUrl(... route to actionA ...));
       echo 'Backgroundjob started';
}

This should do a httprequest to actionA. You should be able to debug or log the code. This is how runBackground works, but only one action is used.

As pseudo-code for the runBackground method

public function actionRunBackgroundJob()
{
       if(this action is called by a 'touchUrl' request, means param _runaction_touch isset) 
      {
          do the background job only
      }
      else
      {
          a) do a touchUrl-Request to this action (without fetching html-data)
          b) show the user html code
      } 
 
}
#8373 report it
webservice at 2012/05/29 10:16am
@joblo

I have updated the function and returns the url that it visits. I try to visit this url with wget from inside the server and it returns me to the message of the else statement

if(ERunActions::runBackground()){}else{echo 'error';}

which means the the url works but the code cannot run this method.

so it cannot run the ERunActions::runBackground() for a reason.

#8341 report it
Joblo at 2012/05/27 12:57pm
runBackground: Maybe a hostInfo problem

Please take a look at the runBackground function in ERunActions.php

It 'touches' the same controller action where 'runBackground' is called a second times. As I wrote in the comment of this function, the $request->getHostInfo() there can detect a not 'reachable url' when a server is behind a firewall (maybe '127.0.0.1' or '192.168.x.x is not really reachable...).

So you can try this:

Add a die($url) or Yii::log(...) there to get the url and try to call this url from inside your server/php enviroment (comandline: wget ...).

If this url doesn't work, you can set the param 'internalHostInfo' to a reachable scheme 'http://....'.

public static function runBackground($useHttpClient=false,$httpClientConfig=array(),$internalHostInfo=null)
    {
        if (!self::isTouchActionRequest())
        {
            $request = Yii::app()->request;
 
            $uri = $request->requestUri;
            $port = $request->getPort();
            $host = isset($internalHostInfo) ? $internalHostInfo : $request->getHostInfo();
            $url =  "$host:$port$uri";
 
                        die($url);
 
 
        else
            return true;
    }
#8337 report it
webservice at 2012/05/27 04:59am
Dependency

Is there any Dependency in the php version because this ext it doesn't work in my server while locally works fine.

#7337 report it
jcsmesquita at 2012/03/14 10:43pm
Access Rules workaround (uses touchUrl not runBackground)

For cron/background tasks I typically have a controller written specifically to handle these requests. Inside your controller you add the following:

public function filters() {
    return array(
        'keyAuthentication'
    );
}
 
public function filterKeyAuthentication($filterChain) {
    if (!isset($_GET['key']) OR $_GET['key'] != Yii::app()->params['httpKey']) {
        throw new CHttpException(404, 'The system is unable to find the requested action "' . $this->action->id . '"');
    } else {
        $filterChain->run();
    }
}

where Yii::app()->params['httpKey'] is any security key/password that you can assign (i.e. use a hash algorithm to generate it).

The purpose of this filter is to authenticate anyone trying to access it via a GET key, and this performed before any action in the controller. If authentication fails then it returns a 404 error.

I don't use runBackground for actions which need authentication. Using the implementation above you can setup your touchUrl and add in the key as a GET parameter. (i.e http://www.myblog.com/backgroundtasks/sendemail?key=a123gasj3kasfl...)

That's it, now you can keep away unauthenticated users from triggering your cron/background actions.

#7333 report it
Joblo at 2012/03/14 08:38pm
rules

adhoc answer - not tested:

runBackground calls 'touchUrl', that means, it's a simple httprequest to the same controller action where runBackground is called. But this request is sent from php, not from the browser and therefore - I think - this is an anonymus/guest request (because the php script is not authenticated).

Maybe you can build your rules as you would do, but you have to add an extra rule to allow to execute the action from the php-script (ip = 127.0.0.1 or the internal ip of your server)

Leave a comment

Please to leave your comment.

Create extension