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.
- 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
Running a controller action in the background does not work
I have been trying for hours to get ERunActions::runAction to execute a controller action set up in the same way as actionTimeConsumingProcess and this function does not work.
First there is a bug when calling ERunActions::runAction from within a controller that has just had a _POST submitted with array data.
In ERunActionsHttpClient.php(87) the exception "urlencode() expects parameter 1 to be string, array given" is thrown. The code is:
82 if (isset($postData)) 83 { 84 if (is_array($postData)) 85 { 86 foreach ($postData as $k => $v) 87 $postdata_str .= urlencode($k) .'='. urlencode($v) .'&'; 88 89 $postdata_str = substr($postdata_str, 0, -1); 90 } 91 else 92 $postdata_str = is_string($postData) ? $postData : serialize($postData); 93 }
I got around this by first redirecting to a backgroundTask action, and from within that, calling ERunActions::runAction. This at least gets around the urlencode exception, but for some reason the action I want to execute in the background is not running.
This is essentially what I am doing:
public function actionBackgroundRunner($id) { ERunActions::runAction("/admin/designs/transferToS3/id/{$id}", array('id'=>$id), true, false, true); $this->redirect(array('view', 'id'=>$id)); } public function actionTransferToS3($id) { Yii::log('###### actionTransferToS3() called'); if (ERunActions::runBackground()) { Yii::log('###### Starting background task: actionTransferToS3...'); //... time-consuming code here //Inform the user if 'hasFlash' is implemented in all views //Yii::app()->user->setFlash('runbackground','Process finished'); Yii::log('###### Background task: actionTransferToS3 completed'); } else { //$this->redirect(array('view', 'id'=>$id)); } }
I've done a little digging through ERunActions.php, and have determined that the $action->RunWithParams is recieving the correct parameters and returning true, meaning that it thinks all is okay. However, actionTransferToS3 is never called, at least I do not see any log messages.
At this point I'm lost. I need to be able to transfer large files to S3 from the server, and I'd like to be able to do it in the background so that the user isn't held up, and it would be good to update the database at the same time. ERunActions appeared to offer an elegant solution. If this simply isn't going to work, I need an alternative. Any suggestions?
actionBackgroundRunner
Why don't you call 'actionTransferToS3' directly?
Did you test like below by calling the url 'HOSTPATH/admin/designs/transfertos3/id/123'?
public function actionTransferToS3($id) { if (ERunActions::runBackground()) { Yii::log('###### actionTransferToS3 in background...'); //transfer your file here } else { $this->redirect(array('view', 'id'=>$id)); } }
What's about the accessRules of 'actionTransferToS3'?
You use 'ERunActions::runAction' in actionBackgroundRunner with $ignoreBeforeAfterAction=false.
I don't know what is implemented in your onBefore-onAfterAction of the controller so I can't reproduce your problems.
Note: You should use 'createAbsoluteUrl' when using touchUrl or touchUrlExt;
public function actionBackgroundRunner($id) { $url = Yii::app()->createAbsoluteUrl('/admin/designs/transferToS3',array('id'=>$id)); Yii::log('actionBackgroundRunner - url:'.$url); ERunActions::touchUrl($url); $this->redirect(array('view', 'id'=>$id)); } public function actionTransferToS3($id) { Yii::log('###### tranfer to S3'); //transfer your file here }
All sample
I want to run controller action as background process. Where i can see full sample for this action ?
No sample
There is no sample, because it depends on what someone wants to do on background task.
A short example - add a controller action like this:
public function actionTest() { if (ERunActions::runBackground()) { $siteContent = file_get_contents('http://www.yiiframework.com'); Yii::log($siteContent); Yii::app()->end(); } echo 'Take a look at the logfile'; }
You should see the message on the screen and find the content of the website in your application log.
Other sample
Sorry, can i see full sample to use your ext ?
Example
Full example, here it's, in the cron.php from extension folder
<?php return array( ERunActions::TYPE_ACTION => array( 'route' => 'routePath', //for example site/someAction 'interval' => 3600, ), );
Then, SiteController, put action there
public function someAction() { if ( ERunActions::runBackground() ) { //some stuff here; } }
and last is widget in view main.php
$this->widget('ext.runactions.ERunActions');
runactions
how to include your ext. when i want to use ERunActions::runAction from exists controller ?
import
You have to use 'ext.runactions.components.ERunAction'.
In the documentation above the path is wrong ('ext.runactions.ERunAction').
Yii::import('ext.runactions.components.ERunAction'); ERunActions::runAction('controllerid/route');
@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 ,
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'.
@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
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.
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"; }
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, ), ), );
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
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, ) ); }
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.
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
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.
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.
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)
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.
Dependency
Is there any Dependency in the php version because this ext it doesn't work in my server while locally works fine.
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; }
@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.
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 } }
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?
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
EHttpClient
There seems to be an undocumented dependancy on something called "EHttpClient" - whats that about?
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.
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); }
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:
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)
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?
answers
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.
I use logging for that ¬¬
See previous answer.
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.
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?
Of course you can use the wrench, but I'll always choose the hammer.
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.
Install
You have to import ERunActions in the top of the controller code, where you use the component:
Yii::import('ext.runactions.components.ERunActions');
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
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.
Not Working
I didn't get any errors, but background process is not working. I tried touchUrl and runBackground options, nothing is working. Did i miss anything in these steps
Yii::import('ext.runactions.components.ERunActions'); <br> class DefaultController extends Controller<br> { .... }<br>
public function actionGetSalesCTWAchStats()<br> {<br>
$url="http://localhost/myproject/admin/default/sendTestMail";
ERunActions::touchUrl($url,$postData=null,$contentType=null);
// and also this action
if(ERunActions::runBackground()){
MailFunction::sendMail();
Yii::log("test this action");
}
else{ ... } // it executed well without any error
not working
Maybe your url "http://localhost/myproject/admin/default/sendTestMail" needs to authenticate as an admin. This will not work, only public url will work.
Try to make the url public for all in the accessrules.
Try to debug to see whats happened.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.