I am developing a simple RESTful API using Laravel 4.
I have set a Route that calls a function of my Controller that basically does this:
If information is in the database, pack it in a JSON object and return a response
Else try to download it (html/xml parsing), store it and finally pack the JSON response and send it.
I have noticed that the CPU load while doing a total of 1700 requests, only 2 at a time together, raises to 70-90%.
I am a complete php and laravel beginner and I've made the API following this tutorial, maybe I'm probably doing something wrong or it's just a proof of concept lacking of optimzations. How can I improve this code? (starting function is getGames)
Do you think the root of all problems is Laravel or I should obtain the same result even changing framework/using raw PHP?
UPDATE1 I also set a file Cache, but the CPU load is still ~50%.
UPDATE2 I set the query rate at two each 500ms and the CPU load lowered at 12%, so I guess this code is missing queue handling or something like this.
class GameController extends BaseController{
private static $platforms=array(
"Atari 2600",
"Commodore 64",
"Sega Dreamcast",
"Sega Game Gear",
"Nintendo Game Boy",
"Nintendo Game Boy Color",
"Nintendo Game Boy Advance",
"Atari Lynx",
"M.A.M.E.",
"Sega Mega Drive",
"Colecovision",
"Nintendo 64",
"Nintendo DS",
"Nintendo Entertainment System (NES)",
"Neo Geo Pocket",
"Turbografx 16",
"Sony PSP",
"Sony PlayStation",
"Sega Master System",
"Super Nintendo (SNES)",
"Nintendo Virtualboy",
"Wonderswan");
private function getDataTGDB($name,$platform){
$url = 'http://thegamesdb.net/api/GetGame.php?';
if(null==$name || null==$platform) return NULL;
$url.='name='.urlencode($name);
$xml = simplexml_load_file($url);
$data=new Data;
$data->query=$name;
$resultPlatform = (string)$xml->Game->Platform;
$data->platform=$platform;
$data->save();
foreach($xml->Game as $entry){
$games = Game::where('gameid',(string)$entry->id)->get();
if($games->count()==0){
if(strcasecmp($platform , $entry->Platform)==0 ||
(strcasecmp($platform ,"Sega Mega Drive")==0 &&
($entry->Platform=="Sega Genesis" ||
$entry->Platform=="Sega 32X" ||
$entry->Platform=="Sega CD"))){
$game = new Game;
$game->gameid = (string)$entry->id;
$game->title = (string)$entry->GameTitle;
$game->releasedate = (string)$entry->ReleaseDate;
$genres='';
if(NULL!=$entry->Genres->genre)
foreach($entry->Genres->genre as $genre){
$genres.=$genre.',';
}
$game->genres=$genres;
unset($genres);
$game->description = (string)$entry->Overview;
foreach($entry->Images->boxart as $boxart){
if($boxart["side"]=="front"){
$game->bigcoverurl = (string)$boxart;
$game->coverurl = (string) $boxart["thumb"];
} continue;
}
$game->save();
$data->games()->attach($game->id);
}
}
else foreach($games as $game){
$data->games()->attach($game->id);
}
}
unset($xml);
unset($url);
return $this->printJsonArray($data);
}
private function getArcadeHits($name){
$url = "http://www.arcadehits.net/index.php?p=roms&jeu=";
$url .=urlencode($name);
$html = file_get_html($url);
$data = new Data;
$data->query=$name;
$data->platform='M.A.M.E.';
$data->save();
$games = Game::where('title',$name)->get();
if($games->count()==0){
$game=new Game;
$game->gameid = -1;
$title = $html->find('h4',0)->plaintext;
if("Derniers jeux commentés"==$title)
{
unset($game);
return Response::json(array('status'=>'404'),200);
}
else{
$game->title=$title;
$game->description="(No description.)";
$game->releasedate=$html->find('a[href*=yearz]',0)->plaintext;
$game->genres = $html->find('a[href*=genre]',0)->plaintext;
$minithumb = $html->find('img.minithumb',0);
$game->coverurl = $minithumb->src;
$game->bigcoverurl = str_replace("/thumb/","/jpeg/",$minithumb->src);
$game->save();
$data->games()->attach($game->id);
}
}
unset($html);
unset($url);
return $this->printJsonArray($data);
}
private function printJsonArray($data){
$games = $data->games()->get();
$array_games = array();
foreach($games as $game){
$array_games[]=array(
'GameTitle'=>$game->title,
'ReleaseDate'=>$game->releasedate,
'Genres'=>$game->genres,
'Overview'=>$game->description,
'CoverURL'=>$game->coverurl,
'BigCoverURL'=>$game->bigcoverurl
);
}
$result = Response::json(array(
'status'=>'200',
'Game'=>$array_games
),200);
$key = $data->query.$data->platform;
if(!Cache::has($key))
Cache::put($key,$result,1440);
return $result;
}
private static $baseImgUrl = "";
public function getGames($apikey,$title,$platform){
$key = $title.$platform;
if(Cache::has($key)) return Cache::get($key);
if(!in_array($platform,GameController::$platforms)) return Response::json(array("status"=>"403","message"=>"non valid platform"));
$datas = Data::where('query',$title)
->where('platform',$platform)
->get();
//If this query has already been done we return data,otherwise according to $platform
//we call the proper parser.
if($datas->count()==0){
if("M.A.M.E."==$platform){
return $this->getArcadeHits($title);
}
else{
return $this->getDataTGDB($title,$platform);
}
} else{
else return $this->printJsonArray($datas->first());
}
}
}
?>
You're trying to retrieve data from others' servers. That is putting your CPU "on hold" until the data is fully retrieved. That's what is making your code be so "CPU expensive" (couldn't find other stuff that fits here =/ ), cause your script is waiting until the data is received and then release the script (CPU) work.
I strongly suggest that you make asynchronous calls. That would release your CPU to work on the code, while other part of your system is getting the information you need.
I hope that'll be some help! =D
UPDATE
To make examples, I'd have to re-factor your code (and I'm lazy as anything!). But, I can tell you for sure: If you put your request code, those who make calls to others site's XML, onto a queue you would gain a lot of free CPU time. Every request are redirected for a queue. Once they're ready, you treat them as you wish. Laravel has a beautiful way for dealing with queues.
what I would do first is to use a profiler to find out which parts would need an optimization. You can use for example this:
http://xdebug.org/docs/profiler
As well you didn't specify what kind of cpu is it, how many cores are you using? Is this a problem that your cpu is getting used that high?
you should use Laravel's Queue system along with beanstalkd for example and then monitor the queue (worker) with artisan queue:listen
Related
I can see in the SWF Management Console a Workflow has around 18 events, with the 16th event being my ActivityTaskCompleted event however whenever i poll for decisions i only get up to the 15th event so i never get to call RespondDecisionTaskCompleted with the decision type CompleteWorkflowExecution as such my workflows are always sitting in the Active state until they timeout.
The flow i'm using is from a PHP SWF git i found a while ago, i unfortunately do not have the link to it anymore though.
$response = $this->swf->PollForDecisionTask($opts);
$decision_list = self::_decide(new HistoryEventIterator($this->swf, $opts, $response), $this->swf);
if(count($decision_list) > 0)
{
//Some decisions
}
else
{
//No decisions
}
Where the HistoryEventIterator looks like so
public function __construct(Aws\Swf\SwfClient $swf_client, $original_request, $original_response) {
$this->swf = $swf_client;
$this->events = array();
$this->event_index = 0;
$this->next_page_token = null;
$this->original_poll_request = $original_request;
$this->_process_poll_response($original_response);
$this->workflow_id = $original_response->get('workflowExecution')['workflowId'];
$this->run_id = $original_response->get('workflowExecution')['runId'];
}
protected function _process_poll_response($response) {
if ($response->hasKey("nextPageToken")) {
$this->next_page_token = (string) $response->get("nextPageToken");
} else {
$this->next_page_token = null;
}
$next_events = $response->get("events");
$next_events_object = new ArrayObject($next_events);
$next_events_copy = $next_events_object->getArrayCopy();
$this->events = array_merge($this->events, $next_events);
}
I have omitted error checking and functions of HistoryEventIterator that would not be called in this scenario.
I have output the next_page_token of HistoryEventIterator and found it was always NULL.
Should the RespondDecisionTaskCompleted called from an Activity reach the decider? If so, what could be the cause for mine not? Surely it wouldn't be paging after 15 events, and simply not paging correctly.
I can verify that the Domain, Activity Task List, and Decider Task List are accurate as the Workflow shows up in the SWF Management Console, as does the decisions and the Activity (The Activity even has the status Completed) There is appropriate error checking and Try/Catch blocks and in no cases are there any exceptions.
my web app requires making 7 different soap wsdl api requests to complete one task (I need the users to wait for the result of all the requests). The avg response time is 500 ms to 1.7 second for each request. I need to run all these request in parallel to speed up the process.
What's the best way to do that:
pthreads or
Gearman workers
fork process
curl multi (i have to build the xml soap body)
Well the first thing to say is, it's never really a good idea to create threads in direct response to a web request, think about how far that will actually scale.
If you create 7 threads for everyone that comes along and 100 people turn up, you'll be asking your hardware to execute 700 threads concurrently, which is quite a lot to ask of anything really...
However, scalability is not something I can usefully help you with, so I'll just answer the question.
<?php
/* the first service I could find that worked without authorization */
define("WSDL", "http://www.webservicex.net/uklocation.asmx?WSDL");
class CountyData {
/* this works around simplexmlelements being unsafe (and shit) */
public function __construct(SimpleXMLElement $element) {
$this->town = (string)$element->Town;
$this->code = (string)$element->PostCode;
}
public function run(){}
protected $town;
protected $code;
}
class GetCountyData extends Thread {
public function __construct($county) {
$this->county = $county;
}
public function run() {
$soap = new SoapClient(WSDL);
$result = $soap->getUkLocationByCounty(array(
"County" => $this->county
));
foreach (simplexml_load_string(
$result->GetUKLocationByCountyResult) as $element) {
$this[] = new CountyData($element);
}
}
protected $county;
}
$threads = [];
$thread = 0;
$threaded = true; # change to false to test without threading
$counties = [ # will create as many threads as there are counties
"Buckinghamshire",
"Berkshire",
"Yorkshire",
"London",
"Kent",
"Sussex",
"Essex"
];
while ($thread < count($counties)) {
$threads[$thread] =
new GetCountyData($counties[$thread]);
if ($threaded) {
$threads[$thread]->start();
} else $threads[$thread]->run();
$thread++;
}
if ($threaded)
foreach ($threads as $thread)
$thread->join();
foreach ($threads as $county => $data) {
printf(
"Data for %s %d\n", $counties[$county], count($data));
}
?>
Note that, the SoapClient instance is not, and can not be shared, this may well slow you down, you might want to enable caching of wsdl's ...
We're using Gearman and when do use methods like doLowBackground or doHigh, these all return a jobHandle, but when we do tasks there is no jobHandle object. We get the GearmanTask object, instead of getting the jobHandle, we get string(0) ""
Any ideas what could cause this?
Thank you!
EDIT: Here is the code and additional info:
// $client = \GearmanClient; // servers added, all that jazz
// $workload = 'string';
$arguments = array(
'handleJob',
$workload
);
$task = call_user_func_array(array($client, $method), $arguments);
if ($task instanceof GearmanTask) {
$handles[] = $task->jobHandle();
}
$data = $client->runTasks();
The tasks run correctly but $handle is being populated with empty strings (one for each task added)
EDIT: This is the response we get:
object(GearmanTask)#294 (0) {
}
I've dumped every PECL gearman object, nothing ever displays, here's the client, populated with servers, options, etc
object(GearmanClient)#291 (0) {
}
Doesn't show anything.
A job handle is not assigned to a task until the task is received and queued by the Gearman job server.
However, you can use GearmanClient::setCreatedCallback() to get the handle once it has been queued. This must be done before both adding and running the tasks:
$client = new \GearmanClient();
$client->addServer('127.0.0.1');
$handles = array();
$client->setCreatedCallback(function (\GearmanTask $task) use (&$handles) {
$handles[] = $task->jobHandle();
});
$client->addTask('functionName', 'workload'); // ...
$client->runTasks();
I am trying to build an configuration parser for my application I installed APC today, but everytime I try to put an serialized object in the store, it does not get in there and does not. (I am checking with apc.php for my version[3.1.8-dev] on PHP 5.3.16 [My Dev Environment], so I am sure that the data is not in the cache). this is how I pass the data to the cacher:
// The data before the caching
array (
'key' => md5($this->filename),
'value' => serialize($this->cfg)
);
// The caching interface
function($argc){
$key = $argc['key'];
Cache\APC::getInstance()->set($key,$argc['value']);
}
// The caching method described above
public function set($key, $val) {
if (apc_exists($key)) {
apc_delete ($key);
return apc_store($key, $val);
}
else
return false;
}
// the constructor of the configuration class.
// It 1st looks for the configuration in
// the cache if it is not present performs the reading from the file.
public function __construct($filename = '/application/config/application.ini',
$type = self::CONFIG_INI)
{
if (defined('SYSTEM_CACHE') && SYSTEM_CACHE === 'APC'){
$key = md5($filename);
$cfg = APC::getInstance()->get($key);
if (!empty($cfg)) {
print "From Cache";
$this->cfg = unserialize($cfg);
return;
} else {
print "From File";
}
}
}
I did a few tests and there is not a problem with the MD5() key (which I thought while writing this question) nor with APC itself. I am really stuck on this one, nothing odd in the logs, so if anyone can give me at least some directions will be very appreciated.
Thanks in advance!
The problem is was in my code:\
public function set($key, $val) {
/*
*
* If the key exists in the cache delete it and store it again,
* but how could it exist when the else clause is like that...
*/
if (apc_exists($key)) {
apc_delete ($key);
return apc_store($key, $val);
}
// This is very wrong in the current case
// cuz the function will never store and the if will
// never match..
else
return false;
}
NOTE:
Always think and keep your eyes open, if you still can't find anything get off the PC and give yourself a rest. Get back after 10-15 minutes and pown the code. It helps! :D
Question
I want to know if it is possible to asynchronously invoke a Yii controller method from one of its actions while the action renders a view, leaving the method to complete a long running operation. I would love to do something like the code below and I don't need to return a result from my_long_running_func.
public function actionCreate() {
$model = new Vacancies;
if (isset($_POST['Vacancies'])) {
$model->setAttributes($_POST['Vacancies']);
$model->save();
//I wish :)
call_user_func_async('my_long_running_func',$model);
}
$this->render('create', array( 'model' => $model));
}
Problem
I am trying to write a controller action in Yii that posts a vacancy and notifies interested subscribers of the post. The problem is that it takes a long time to execute the notification query.
Now I am searching for a way to asynchronously run the query so the poster sees his response in as little time as possible while the query runs in the background in a way similar to C# delegates or events.
The solutions I googled up performed asynchronous request(s) during the course of the controller action but all I want to do is to run a method of the controller asynchronously and the action had to wait till the request(s) were completed.
Attempted
I have tried the following methods but the query is still slow for my test data of about 1500 users.
Yii ActiveRecord
if ($vacancy->save()) {
if($vacancy->is_active == 1) {
$url = Yii::app()->createUrl('vacancies/view',array('id'=>$model->id));
$trainees = YumUser::getUsersByRole('Trainees');
if($trainees!=null) {
foreach($trainees as $trainee){
$message = new YumMessage;
$message->from_user_id = Yii::app()->user->id;
$message->title = 'Vacancy Notification: '.date('M j, Y');
$message->message = "A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$message->to_user_id = $trainee->id;
$message->save();
}
}
}
}
Yii Data Access Objects
if ($vacancy->save()) {
if($vacancy->is_active == 1) {
$url = Yii::app()->createAbsoluteUrl('vacancies/view',array('id'=>$model->id));
$trainee_ids=Yii::app()->db->createCommand()->select('user_id')->from('trainee')->queryColumn();
$fid=Yii::app()->user->id;
$msg="A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$ts = time();
$tt = 'Vacancy Notification: '.date('M j, Y');
if($trainee_ids!=null) {
foreach($trainee_ids as $trainee_id){
Yii::app()->db->createCommand()
->insert('message',array('timestamp'=>$ts,'from_user_id'=>$fid,'to_user_id'=>$tid,'title'=>$tt,'message'=>$msg));
}
}
}
}
Prepared Statements
if ($vacancy->save()) {
if($vacancy->is_active == 1) {
$url = Yii::app()->createUrl('vacancies/view',array('id'=>$model->id));
$trainee_ids=Yii::app()->db->createCommand()->select('user_id')->from('trainee')->queryColumn();
$fu=Yii::app()->user->id;
$msg="A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$ts = time();
$tt = 'Vacancy Notification: '.date('M j, Y');
$sql="INSERT INTO message (timestamp,from_user_id,title,message,to_user_id) VALUES (:ts,:fu,:tt,:msg,:tu)";
if($trainee_ids!=null) {
foreach($trainee_ids as $trainee_id){
$command=Yii::app()->db->createCommand($sql);
$command->bindParam(":ts",$ts,PDO::PARAM_INT);
$command->bindParam(":fu",$fu,PDO::PARAM_INT);
$command->bindParam(":tt",$tt,PDO::PARAM_STR);
$command->bindParam(":msg",$msg,PDO::PARAM_STR);
$command->bindParam(":tu",$trainee_id,PDO::PARAM_INT);
$command->execute();
}
}
}
}
Research
I have also checked the following websites (I'm only allowed to post two links) but they either require the action to wait for the request to be completed or need curl (which I don't have access to on the deployment server) or need an external library. I was hoping for a native PHP implementation.
PHP Simulated Multi-Threading
Multithreading in php
Asynchronous PHP calls?
Asynchronous processing in PHP
Edit
I was able to decrease response time considerably by rewriting my query in this way (moving the user loop to the database layer):
public function actionCreate() {
$user=YumUser::model()->findByPk(Yii::app()->user->id);
$model = new Vacancies;
$model->corporate_id=$user->professional->institution->corporate->id;
$model->date_posted=date('Y-m-d');
$model->last_modified=date('Y-m-d H:i:s');
if (isset($_POST['Vacancies'])) {
$model->setAttributes($_POST['Vacancies']);
if ($model->save()) {
if($model->is_active == 1) {
$url = Yii::app()->createAbsoluteUrl('vacancies/view',array('id'=>$model->id));
$fu=Yii::app()->user->id;
$msg="A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$ts = time();
$tt = 'New Vacancy: '.$model->title;
$sql='INSERT INTO message (timestamp,from_user_id,title,message,to_user_id) SELECT :ts,:fu,:tt,:msg,t.user_id FROM trainee t';
Yii::app()->db->createCommand($sql)->execute(array(':ts'=>$ts,':fu'=>$fu,':tt'=>$tt,':msg'=>$msg));
}
if (Yii::app()->getRequest()->getIsAjaxRequest())
Yii::app()->end();
else
$this->redirect(array('view', 'id' => $model->id));
}
}
$this->render('create', array( 'model' => $model));
}
Notwithstanding, it would be nice if someone could post a way to call functions asynchronously.
Typically, the solution for these kind of problems would be to integrate a message-bus in your system. You could consider a product like Beanstalkd. This requires installing software on your server.
I suppose this suggestion would be called "using an external library".
If you can access the deployment server and you can add cronjob (or maybe a sysadmin can) you could consider a cronjob that does a php-cli call to a script that reads jobs from a job queue in your database which is filled by the controller method.
If you cannot install software on the server you're running, you could consider using a SAAS solution like Iron.io to host the bus functionality for you. Iron.io is using what is called a push queue. With a push queue the message bus actively performs a request (push) to the registered listeners with the message content. This might work since it doesn't require you to do a curl request.
If none of the above is possible, your hands are tied. Another post which is quite relevant on the subject: Scalable, Delayed PHP Processing
I would try this, though I'm not 100% that Yii will work properly, but its relatively simple and worth a go:
public function actionCreate() {
$model = new Vacancies;
if (isset($_POST['Vacancies'])) {
$model->setAttributes($_POST['Vacancies']);
$model->save();
//I wish :)
}
HttpResponse::setContentType('text/html');
HttpResponse::setData($this->render('create', array( 'model' => $model), true);
HttpResponse::send();
flush(); // writes the response out to the client
if (isset($_POST['Vacancies'])) {
call_user_func_async('my_long_running_func',$model);
}
}
Here's an entirely different type of suggestion. What about registering for the onEndRequest event that is fired by CWebApplication's end() function?
public function end($status=0, $exit=true)
{
if($this->hasEventHandler('onEndRequest'))
$this->onEndRequest(new CEvent($this));
if($exit)
exit($status);
}
You'd need to register for the event and figure out how to pass your model in somehow, but the code would properly run after all the data has been flushed to the browser ...