runactions

Helper for running controller actions in background, cron and more.
43 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

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

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

#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)

#7332 report it
vgfitzger at 2012/03/14 08:06pm
Access Rules

How can ERunActions::runBackground() get around yii's access rules? It wont work unless I use it in a action that allows all users.

#7305 report it
Joblo at 2012/03/13 07:38am
httpclient

Thanks for your remark.

As referenced in the source the internal httpclient is a (quick) port from this code.

I will change the code to your bugfix in the next release.

#7304 report it
jcsmesquita at 2012/03/13 06:52am
passing $_POST and $_GET (?) arrays

If you're trying to use ERunActions::runBackground() inside an action which deals with form data. Most likely your $_POST will be a nested array. In this case an exception occurs "urlencode() expects parameter 1 to be string, array given".

I got around this by using PHP's native http_build_query() in components/ERunActionsHttpClient.php

if (isset($postData))
{
        if (is_array($postData))
        {
            $postdata_str = http_build_query($postData);
                foreach ($postData as $k => $v){
                        $postdata_str .= urlencode($k) .'='. urlencode(serialize($v)) .'&';
                    else
                        $postdata_str .= urlencode($k) .'='. urlencode($v) .'&';
                }
 
                $postdata_str = substr($postdata_str, 0, -1);
        }
        else
                $postdata_str = is_string($postData) ? $postData : serialize($postData);
}

I replaced it with this:

if (isset($postData))
{
        if (is_array($postData))
        {
            $postdata_str = http_build_query($postData);
        }
        else
            $postdata_str = is_string($postData) ? $postData : serialize($postData);
}

I suspect that by using this function you won't need the is_array conditional. And I'm also guessing it'll work for $_GET.

Anyway I've only tested for nested $_POST arrays and it's working for me.

Cheers, jc

#6649 report it
Nafania at 2012/01/25 09:35am
Yii 1.1.9

Thanks for your answer. Another question - does runactions works with yii 1.1.9? Just upgraded, but seems it's not working. I have lines at log, that runactions executed succefully, but seems to be not, cause any data that must be changed by actions not changed.

#6648 report it
Joblo at 2012/01/25 07:00am
Multiple actions

You can create a cron.php that runs a script and in the script you can run the actions.

cron.php

return array(
   ERunActions::TYPE_SCRIPT  => array('script' => 'cronscript'),  //execute cronscript.php
);

runactions/config/scripts/cronscript.php

ERunactions::runAction(....);
  ERunactions::runAction(....);
  ERunactions::runAction(....);
  ....

You cannot set a interval in the runAction method only for the runactions widget. If you need different intervals you have to generate multiple urls/cronscripts (take a look at RunActionsController.php).

You can add different actions to your CronController like

public function actionCron1()
    {
        $this->widget('ERunActions',
        array(
               'config'=>'cron1', 
               'interval'=>3600, 
              )
        );
    }
 
  public function actionCron2()
    {
        $this->widget('ERunActions',
        array(
               'config'=>'cron2',
               'interval'=>1800, 
              )
        );
    }
#6646 report it
Nafania at 2012/01/25 05:03am
Multiple actions of one type

Hello. Thanks for useful extension. I have a question: How to add multiple actions of one type in config file. I.e. i want to do something like this in my cron.php

return array(
    ERunActions::TYPE_ACTION  => array(
        array(
            'route' => 'route1',
            'interval' => interval1,
        ),
        array(
            'route' => 'route2',
            'interval' => interval2,
        ),
    ),
);
#6433 report it
Joblo at 2012/01/10 03:43am
How runBackground works

Please see this topic.

If you have problems with 'runBackground' add an action like below to a controller and check if you can find the log item in your logfile:

public function actionTest ()
{
        Yii::import('ext.runactions.components.ERunActions');
 
        if (ERunActions::runBackground())
        {
           Yii::log('Running actionTest in Background',CLogger::LEVEL_ERROR);
 
           Yii::app()->end(); 
        }
 
        echo "Do this now";
}
#6032 report it
WTF at 2011/12/07 02:31am
How it's work

After reading extension documentation very difficult to understand how to use it. But really it's simple and useful. After hour of search found this forum topic where explained "How it's works" :)

Thanks for good extension. Save a lot of time.

#5790 report it
yiqing95 at 2011/11/13 10:58pm
@joblo: thanks for fast reply, i will test different adapters !

the article you provided i v read , fsockopen limitation in that document may be the max size . it may only occur in loop or iteration environment. when using while or for statement it will cause that happens. if the script is long live it will cause some bad things happening such as memory leak ,memory exhaust, treasure handler waste (you see the db connection ,file handler ,socket are). i just use it to do async call in methods of controller or model not in console environment and no loop .

       thanks again , hope you will share your test result here when accomplish your tests .

another thing: Yii::import('ext.runactions.components.ERunAction'); should be Yii::import('ext.runactions.components.ERunActions'); if run as cron , are there some trick to stop the cron job ? i do the cron example but in the log file give me this error info: [error] [runactions] Invalid time interval - Last call: Allowed interval: 3600

#5787 report it
Joblo at 2011/11/13 12:02pm
limitations

runactions itself has no limitations. Touchurl does only a httprequest to the own or another http-server (without reading data).

A limitation can be the server performance, configured maxclients in apache ... etc. If your service (for example converting image, videos ...) is running at the same server the memory-limit can be a problem too.

You have to check about issues about the different request adapters of ehttpclient. For example, the internal httpclient or the default ehttpclient uses 'fsockopen'. There is an issue about this published (April, 2005) in the fsockopen PHP manual.

But when using touchUrlExt you can test with other adapters (curl,...) too.

I would set up a test-application with 'touchUrl'/'touchUrlExt' (sleep, log profiling....) in a loop and test different adapters from ehttpclient. The internal httpclient of the exentsion has no 'overhead' and should be the 'fastest', but only uses 'fsockopen'.

#5782 report it
yiqing95 at 2011/11/13 06:38am
@joblo when run in async mode , if it has some limits such as connections?

joblo : firstly great job! i want to use the async feature , but don't clear if it has connection limits , if many users call this code at same time:

  ERunActions::touchUrlExt(app()->createAbsoluteUrl('site/someAction'));

so what's your advice ? does it lose action call when workload is too much? i 'v used the gearman for this purpose now ! but if i can use this extension it will be better . thanks for this great extension ,

Leave a comment

Please to leave your comment.

Create extension