CakePHP: Why is my cached file causing huge spikes when it expires? - cakephp

I'm using cake 2.1.3 and currently have a page that is getting hundreds of views per second and so I have utelized caching in order to handle the load better. The problem is, that once the cache expires, I get a spike in my server resources as well as hundreds of mysql connections.
I'm wondering if I'm going about this the wrong way and if I should be running a cron to cache the page instead of how I'm currently doing it or if there's another technique I'm not thinking of.
here's what my function looks like in my controller:
public function index() {
$this->layout = 'ajax';
if (isset($this->params['url']['callback'])) {
$callback = $this->params['url']['callback'];
}else{
$callback = 'callback';
}
$this->set('callback',$callback);
$today = date("Y-m-d");
$end_date = strtotime ('+1 day' , strtotime($today)) ;
$end_date = date ( 'Y-m-d' , $end_date);
$start_date = strtotime ('-1 day' , strtotime($today)) ;
$start_date = date ( 'Y-m-d' , $start_date);
$total = Cache::read('popular_stories', 'short');
if (!$total) {
$total = $this->TrackStoryView->find('all', array(
'fields' => array('COUNT(story_id) AS theCount', 'headline', 'url'),
'conditions' => array('date BETWEEN ? AND ?' => array($start_date,$end_date)),
'group' => 'story_id',
'order' => array('theCount DESC'),
'limit' => 20,
));
Cache::write('popular_stories', $total, 'short');
}
$this->set('story', $total);
}
Here's what my Cache config looks like in my bootstrap.php file:
Cache::config('short', array(
'engine' => 'File',
'duration' => '+60 minutes',
'path' => CACHE,
'prefix' => 'cake_short_'
));
This is what's in my view file:
<?php
echo $callback . '('.json_encode($story).')';
?>
I was hoping that once the cached file expires, as soon as the first person accessed it, it would craete a new cached file and serve that up for everyone, however because hundreds of people are hitting it per second, it seems like this method isn't working for me and that maybe I should be caching the view view a cron somehow instead or maybe there's a different way to cache that I'm not utelizing.

It sounds like you have the answer more or less figured out (create the cache automatically, not triggered by a user request).
To do this, look into cake's AppShell class, book talks about it here. You can then link this to a cron job. If you create the file thru Cache::write, cake should be aware that it is a new cache file and read it transparently. You might want to leave the "if cache not found" block in there just in case your cronjob fails.
Shells & Tasks in cake are fun and allow you to free your application from using the request/response model exclusively.

TLDR: It's not ideal to force a user to break the cache for you. Use a chron job or a trigger on data change.
Explanation:
"hundreds of views per second" is the problem. When it expires, there are "hundreds of views" during the time it's trying to create the cache file.
The first person hits it, it starts creating the cache, and in the meantime, another hundred+ people hit it, and it looks, and can't yet find a cache file...etc.
If you can manage, try to manually create the cache when an item(s) is updated, or run a chron job that creates a new cache every X minutes as opposed to having it create for a user.
Cake has lots of cool triggers like afterSave() that you can use to trigger this kind of thing. If that doesn't make sense in your case though, a chron job should be fine for you.

I think the answer lies by working out how long this query takes:
$total = $this->TrackStoryView->find('all', array(
'fields' => array('COUNT(story_id) AS theCount', 'headline', 'url'),
'conditions' => array('date BETWEEN ? AND ?' => array($start_date,$end_date)),
'group' => 'story_id',
'order' => array('theCount DESC'),
'limit' => 20,
));
Lets say it takes 500ms.
You are getting 100 hits a second, so when the cache clears the first request makes the find call and then 50 other people also make the find call before the first request completes.
One alternative solution:
Make the cached content never expire. Set up a cron task that overwrites the cache by calling a different action which runs:
Cache::write('popular_stories', $total, 'short');
To overwrite the cached content.
This way, the 100s of users per second will ALWAYS read from cache

Related

Ways to use array in cakephp

Hello I am having a tought time figuring out how to use arrays in cakephp. right now i have a view with 2 columns, active and startYear. i need to grab the start years for all of the columns in the view and sho i have this code.
public function initialize(array $config)
{
$this->setTable('odb.SchoolYear');
}
controller
public function index()
{
$deleteTable = $this->loadModel('DeletedTranscripts');
$this->$deleteTable->find('all', array(
'conditions' => array(
'field' => 500,
'status' => 'Confirmed'
),
'order' => 'ASC'
));
$this->set('startYear',$deleteTable );
}
once i have the array captured and put into lets say startYear can in input a statement like this into my dropdown list to populate it?
<div class="dropdown-menu">
<a class="dropdown-item" href="#"><?= $delete->startYear; ?></a>
</div>
i have been looking for answers for quite awhile any help would be awesome.
Couple of things:
Loading Tables in CakePHP
For this line:
$deleteTable = $this->loadModel('DeletedTranscripts');
While you can get a table this way, there's really no reason to set the return of loadModel to a variable. This function sets a property of the same name on the Controller, which almost correctly used on the next line. Just use:
$this->loadModel('DeletedTranscripts');
Then you can start referencing this Table with:
$this->DeletedTranscripts
Additionally, if you're in say the DeletedTranscriptsController - the corresponding Table is loaded for you automatically, this call might be unnecessary entirely.
Getting Query Results
Next, you're close on the query part, you've can start to build a new Query with:
$this->DeletedTranscripts->find('all', array(
'conditions' => array(
'field' => 500,
'status' => 'Confirmed'
),
'order' => 'ASC'
));
But note that the find() function does not immediately return results - it's just building a query. You can continue to modify this query with additional functions (like ->where() or ->contain()).
To get results from a query you need to call something like toArray() to get all results or first() to get a single one, like so:
$deletedTranscriptsList = $this->DeletedTranscripts->find('all', array(
'conditions' => array(
'field' => 500,
'status' => 'Confirmed'
),
'order' => 'ASC'
))->toArray();
Sending data to the view
Now that you've got the list, set that so it's available in your view as an array:
$this->set('startYear', $deletedTranscriptsList );
See also:
Using Finders to Load Data
Setting View Variables
I also noticed you've had a few other related questions recently - CakePHP's docs are really good overall, it does cover these systems pretty well. I'd encourage you to read up as much as possible on Controller's & View's.
I'd also maybe suggest running through the CMS Tutorial if you've not done so already, the section covering Controllers might help explain a number of CakePHP concepts related here & has some great working examples.
Hope this helps!

cakephp paginator extremely slow

I have a cakephp application, 2.4, and I'm having issues with the Paginator component. First off, it's not the database, it's definitely the execution of parsing the query results. I have DebugKit installed and can see that my mysql query for the paginated data takes a whole 2 ms. The table has 2.5 million records of messages, and 500,000 users. Obviously proper indexing is in place. But, the controller action is taking 6167.82 ms. So, here's my controller action:
$this->Paginator->settings = array(
'Message' => array(
'fields' => array(
'Recipient.username',
'Recipient.profile_photo',
'Recipient.id',
'Message.*'
),
'joins' => array(array(
'table' => 'users',
'alias' => 'Recipient',
'type' => 'LEFT',
'conditions' => array(
'Recipient.id = `Message`.`recipient_id`'
)
)),
'conditions' => array(
'Message.sender_id' => $this->Auth->user('id'),
'Message.deleted_by_sender' => '0'
),
'limit' => 10,
'order' => 'Message.id DESC',
'recursive' => -1
)
);
$sents = $this->Paginator->paginate( 'Message' );
$this->set( 'sents', $sents );
$this->view = 'index';
I've google this and searched stack overflow. The majority of the responses are for poor mysql optimization which isn't my case. The other half of the responses suggest containable. So, I tried containable. Using contain was actually slower because it tried to grab even more data from the user's field than just the username, photo, and id. Then when cake built the array from the query results it executed nearly 500 ms slower with containable because of the extra user data I'm assuming.
I'm going to now dig into the cake Paginator component and see why it's taking so long to build the response. I'm hoping someone beats me to it and has a good solution to help speed this up.
My web server is running ubuntu 12.04 with 3gb ram, apache and mod_php with apc installed and working for the model and core cache. The database is on a separate server. I also have a redis server persisting other user data and the cake session data. There is plenty of power here to parse 10 records from a mysql query containing about a dozen rows.
EDIT: ANSWER
As suggested first by Ilie Pandia there was something else happening, such as a callback, that was slowing down the pagination. This was actually unrelated to the pagination component. The Recipient model had a behavior that loaded an sdk in the setup callback for a 3rd party service. That service was taking several seconds to respond. This happened when the linkedModel in the query was loaded to filter the results. Hopefully anyone else looking for reasons why cake might be performing poorly will also look at the callbacks on models in the application and plugins.
I see no reason for this to run slow at all.
So this suggests that there are some callback installed (either in the model or the controller) that do additional processing and inflate the action time so much.
That is assuming that there is nothing else in the controller but what your wrote.
You could actually measure the time of the paginate call itself and I think you will find that it is very fast. So the bottle neck is elsewhere in the code.
PS: You could also try to disable DebugKit for a while. Introspection may take very long for some particular cases.
Install DebugKit for your application.
And inspect which query is taking too much time. From there, you should be able to track the bottleneck.

CakePHP: caching with APC still creates cache files, no performance benefit

My problem:
I am making Apache Benchmark test to see if CakePHP APC engine works. However, if I setup Cake's caching configuration to use APC engine, the cache files with serialized cached data are still being created in tmp folder, which make me think that file caching is being used.
I also get no performance benefit: using APC and File engines, test results are ~ 4 sec. If I hardcode plain apc_add() and apc_fetch functions in my controller, the test result gets better: ~3.5 sec.
SO the APC is working, but Cake somewhy can't use it.
My setup:
bootstrap.php:
/*Cache::config('default', array(
'engine' => 'File',
'duration'=> '+999 days',
'prefix' => 'file_',
));*/
Cache::config('default', array(
'engine' => 'Apc',
'duration'=> '+999 days',
'prefix' => 'apc_',
));
controller:
$catalogsLatest = Cache::read('catalogsLatest');
if(!$catalogsLatest){
$catalogsLatest = $this->Catalog->getCatalogs('latest', 5, array('Upload'));
Cache::write('catalogsLatest', $catalogsLatest);
}
php.ini:
[APC]
apc.enabled = 1
apc.enable_cli = 1
apc.max_file_size = 64M
If I check Cache::settings() in controller before or after cache executuon, I get these results:
Array
(
[engine] => Apc
[path] => E:\wamp\www\cat\app\tmp\cache\
[prefix] => apc_
[lock] => 1
[serialize] =>
[isWindows] => 1
[mask] => 436
[duration] => 86313600
[probability] => 100
[groups] => Array
(
)
)
I am using CakePHP 2.2.4.
Yes, of course APC cache will boost up your cakephp powered application performance So let's check your settings from my following instructions and let me know after following the instruction do a benchmark test and tell me the result.
You can cache whole your HTML view file in cache with APC cache engine in CakePHP.
Cake's CacheHelper will do that job for you. Suppose you have a PostsController and you want to cache all your view files related this controller. In this case first of all you have to define the following code in your controller.
class PostsController extends AppController {
public $helpers = array('Cache');
}
And in your bootstrap.php file you have to add the CacheDispatcher.
Configure::write('Dispatcher.filters', array(
'CacheDispatcher'
)
);
And now again in your PostsController you have to tell about the cache files.
public $cacheAction = array(
'view' => 36000,
'index' => 48000
);
This will cache the view action 10 hours, and the index action 13 hours.
Let me know your apache benchmark tool test result. I think the mostly similar question are being discussed on another thread https://stackoverflow.com/a/18916692/1431786 check it out.
Thanks.

CakePHP not loading associated properties with model on production server

This is a weird one.
I have a local server on which I develop apps. A product review app I developed works flawlessly on it, and utilizes Cake's associative modeling ($hasMany, $belongsTo, et. al.).
After pushing this app up to a production server, it fails. Gives me an error message:
Notice (8): Undefined property: AppModel::$Product [APP/controllers/reviews_controller.php, line 46]
ReviewsController::home() - APP/controllers/reviews_controller.php, line 46
Dispatcher::_invoke() - CORE/cake/dispatcher.php, line 204
Dispatcher::dispatch() - CORE/cake/dispatcher.php, line 171
[main] - APP/webroot/index.php, line 83
I've debug()'d $this and it shows, plain as day, that, while the local server is loading the associated models, the production server is not. The databases are mirror duplicates (literally, the production server was imported from the dev db), and I can manually load models, which tells me it's connecting to the DB just fine.
What on Earth is going on?
UPDATE
The sql query from the production server is this:
SELECT `Review`.`id`, `Review`.`title`, `Review`.`product_id`, `Review`.`score`, `Review`.`submitted`, `Review`.`reviewed`, `Review`.`right`, `Review`.`wrong`, `Review`.`user_id`, `Review`.`goals`
FROM `reviews`
AS `Review`
WHERE 1 = 1
ORDER BY `Review`.`submitted` desc LIMIT 10
The sql query from the dev server is this:
SELECT `Review`.`id`, `Review`.`title`, `Review`.`product_id`, `Review`.`score`, `Review`.`submitted`, `Review`.`reviewed`, `Review`.`right`, `Review`.`wrong`, `Review`.`user_id`, `Review`.`goals`, `User`.`id`, `User`.`username`, `Product`.`id`, `Product`.`name`
FROM `reviews`
AS `Review`
LEFT JOIN `users` AS `User` ON (`Review`.`user_id` = `User`.`id`)
LEFT JOIN `products` AS `Product` ON (`Review`.`product_id` = `Product`.`id`)
WHERE 1 = 1
ORDER BY `Review`.`submitted` desc LIMIT 10
UPDATE 2
Here's some of the code the errors point to:
$title = $this->Review->Product->find( 'first', array( 'fields' => array( 'Product.name' ), 'conditions' => array( 'Product.id' => $filter ) ) );
UPDATE 3
<?php
class Review extends AppModel {
var $name = 'Review';
var $displayField = 'title';
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Product' => array(
'className' => 'Product',
'foreignKey' => 'product_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
?>
I had this problem, and for me it was due to a missing field in one of the database tables. I'd triple-check to make sure both DB's are exactly the same (although you said they were...): feel free to use this 7-year-old app to check them :D http://www.mysqldiff.org/
Other people with this issue talked about filename issues and that all files should be lowercased, so that may be something to check as well...
Actually - from a quick glance it might be worth using containable to make sure your data calls are consistent.
If you don't want to go through the hassle of adding containable (but I would urge you to do so - it is one my favourite features of cakephp), you may want to set recursive in your find() call just to make sure the associated models are loaded.
Do you have a way of looking at the files on the server without going through ftp? I had a problem similar to this where the timestamps were messed up on the files and the server would not update the file. I had to delete the files, and then re upload them. You may have already tried this but I just thought I would suggest the possibility. Maybe some of those files are outdated on the server.
Have a great day!
Can you pastebin the Review model, Product model, AppModel, AppController, and the controller you are getting the error from.
The line:
Notice (8): Undefined property: AppModel::$Product [APP/controllers/reviews_controller.php, line 46]
Seems to indicate the Review Model is loading the AppModel and not the file you want it to. In that case the Review model won't have a Product association.
you can print the stacktrace out to see, here's some code I snatch from php.net
echo "<div>Stack<br /><table border='1'>";
$aCallstack=debug_backtrace();
echo "<thead><tr><th>file</th><th>line</th><th>function</th><th>args</th></tr></thead>";
foreach($aCallstack as $aCall)
{
if (!isset($aCall['file'])) $aCall['file'] = '[PHP Kernel]';
if (!isset($aCall['line'])) $aCall['line'] = '';
echo "<tr><td>{$aCall['file']}</td><td>{$aCall['line']}".
"</td><td>{$aCall['function']}</td><td>";
debug (($aCall['arg']));
echo "</td></tr>";
}
echo "</table></div>";
die();
It's gonna be hard looking through all that though.

Cakephp cache only caching one file per action

I have a songs controller. Within the songs controller i have a 'view' action which get's passed an id, eg
/songs/view/1
/songs/view/5
/songs/view/500
When a user visits /songs/view/1, the file is cached correctly and saved as 'songs_view_1.php'
Now for the problem, when a user hit's a different song, eg /songs/view/2, the 'songs_view_1.php' is deleted and '/songs/view/2.php' is in it's place.
The cahced files will stay there for a day if I don't visit a different url, and visiting a different action will not affect any other action's cached file.
I've tried replacing my 'cake' folder (from 1.2 to 1.2.6), but that didn't do anything. I get no error messages at all and nothing in the logs.
Here's my code, I've tried umpteen variations all ending up with the same problem.
var $helpers = array('Cache');
var $cacheAction = array(
'view/' => '+1 day'
);
Any ideas?
EDIT:
After some more testing, this code
var $cacheAction = array(
'view/1' => "1 day",
'view/2' => "1 day"
);
will cache 'view/1' or 'view/2', but delete the previous page as before. If I visit '/view/3' it will delete the cached page from before... sigh
EDIT:
Having the same issue on another server with same code...
After working hours on this, I finally figure out the reason why the cache keep being deleted, the REASON is because you had some operations that update your 'song' record in the database after you view the 'song'. For my case, I keep a column in my database called 'Hits' to store the number of hits/reads, and it updates it everytime it read the record.
Cakephp has a feature to aumotically detect changes to your database and clear the cache for you.
Try remove any operations that update your 'song' record and the cacheaction should be working properly.
An alternative is to redefine the clearcache function in your 'song' model... it will disable the function to auto-clear off the cache.. but then remember to manually clear the cache yourself when an update is performed.
function _clearCache($type = null) {
}
After working hours on this, I finally figured out the reason why the cache keeps on being deleted. The reason is because you had some operations that update your 'song' record in the database after you view the 'song'. For my case, I keep a column in my database called 'Hits' to store the number of hits/reads, and it updates it everytime it read the record.
Cakephp has a feature to automatically detect changes to your database and clear the cache for you.
Try remove any operations that update your 'song' record and the cacheaction should work properly.
After fixing it, there will be another issue. Let's say you cache many of your records, for example song/1, song/5, song/100...etc, if there is any update for any 1 of the record... all of the caches for song/1, song/5, song/100 will be deleted. This makes cacheaction useless for frequently update website.
The solution to this is to redefine the clearcache function in your 'song' model... it will disable the function to auto-clear off the cache.. so if there is any update, none of the caches will be deleted. But then remember to manually clear the cache yourself when an update is performed.
function _clearCache($type = null) {
}
to remove cache manually, you could use
#unlink(CACHE.'views'.DS.'website_songs_view_50.php');
I think that kind of caching method is depreceted. Perhaps you should use Cache:
$song = Cache::read('songs/view/'.$id, 'cache_time');
if(empty($song)){
$song = $this->Song->findById($id);
Cache::write('songs/view/'.$id, $song, 'cache_time');
}
cache_time is a variable you define in core.php:
Cache::config('cache_time', array('engine' => 'File', 'duration' => 60*60*24));
Hope it helps.
Check some setting in the config.php file. Do you have the following setting enabled?
Configure::write('debug', 0);
//Configure::write('Cache.disable', true);
Configure::write('Cache.check', true);
Cache::config('default', array('engine' => 'File'));

Resources