Priming cache.app
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWatch closely: our production site is super slow! It takes a few seconds to load! What!? It's especially weird because, locally in the dev environment, it's way faster: just a few hundred milliseconds!
Why? Open src/AppBundle/Controller/DefaultController.php
:
// ... lines 1 - 9 | |
class DefaultController extends Controller | |
{ | |
// ... lines 12 - 14 | |
public function indexAction() | |
{ | |
// ... lines 17 - 20 | |
// Caching | |
$uploadsItem = $this->getAppCache()->getItem('total_video_uploads_count'); | |
if (!$uploadsItem->isHit()) { | |
$uploadsItem->set($this->countTotalVideoUploads()); | |
$uploadsItem->expiresAfter(60); | |
// defer cache item saving | |
$this->getAppCache()->saveDeferred($uploadsItem); | |
} | |
$totalVideoUploadsCount = $uploadsItem->get(); | |
$viewsItem = $this->getAppCache()->getItem('total_video_views_count'); | |
if (!$viewsItem->isHit()) { | |
$viewsItem->set($this->countTotalVideoViews()); | |
$viewsItem->expiresAfter(60); | |
// defer cache item saving | |
$this->getAppCache()->saveDeferred($viewsItem); | |
} | |
$totalVideoViewsCount = $viewsItem->get(); | |
// save all deferred cache items | |
$this->getAppCache()->commit(); | |
// ... lines 42 - 48 | |
} | |
// ... lines 50 - 132 | |
} |
On the homepage, we show the total number of videos and the total number of video views. To get these, we first look inside a cache: we look for total_video_uploads_count
and total_video_views_count
. If they are not in the cache, then we calculate those and store them in the cache.
To calculate the number of videos, we call $this->countTotalVideoUploads()
:
// ... lines 1 - 9 | |
class DefaultController extends Controller | |
{ | |
// ... lines 12 - 93 | |
/** | |
* @return int | |
*/ | |
private function countTotalVideoUploads() | |
{ | |
sleep(1); // simulating a long computation: waiting for 1s | |
$fakedCount = intval(date('Hms') . rand(1, 9)); | |
return $fakedCount; | |
} | |
// ... lines 105 - 132 | |
} |
That's a private method in this controller. It generates a random number... but has a sleep()
in it! I added this to simulate a slow query. The countTotalVideoViews()
also has a sleep()
:
// ... lines 1 - 9 | |
class DefaultController extends Controller | |
{ | |
// ... lines 12 - 105 | |
/** | |
* @return int | |
*/ | |
private function countTotalVideoViews() | |
{ | |
sleep(1); // simulating a long computation: waiting for 1s | |
$fakedCount = intval(date('Hms') . rand(1, 9)) * 111; | |
return $fakedCount; | |
} | |
// ... lines 117 - 132 | |
} |
So why is our site so slow? Because I put a sleep()
in our code! I'm sabotaging us! But more importantly, for some reason, it seems like the cache system is failing. Let's find out why!
Hello cache.app
First, look at the getAppCache()
method:
// ... lines 1 - 9 | |
class DefaultController extends Controller | |
{ | |
// ... lines 12 - 125 | |
/** | |
* @return AdapterInterface | |
*/ | |
private function getAppCache() | |
{ | |
return $this->get('cache.app'); | |
} | |
} |
To cache things, we're using a service called cache.app
. This service is awesome. We already know about the system.cache
service: an internal service that's used to cache things that make the site functional. The cache.app
service is for us: we can use it to cache whatever we want! And unlike system.cache
, it is not cleared on each deploy.
So why is this service failing? Because, by default, it tries to cache to the filesystem, in a var/cache/prod/pools
directory:
ls var/cache/prod/pools
On production, we know that this directory is not writable. So actually, I'm surprised the site isn't broken! This service should not be able to write its cache!
Caching Failing is not Critical
To understand what's going on, lets mimic the issue locally. First, run:
bin/console cache:clear
This will clear and warm up the dev
cache. Then, run:
sudo chmod -R 000 var/cache/dev/pools
Now, our local site won't be able to cache either.
Let's see what happens. Refresh! Huh... the site works... but it's slow. And the web debug toolbar is reporting a few warnings. Click to see those.
Woh! There are two warnings:
Failed to save key
total_video_uploads_count
and
Failed to save key
total_video_views_count
Of course! If caching fails, it's not fatal... it just makes our site slow. This is what's happening on production.
Let's fix the permissions for that directory:
sudo chmod -R 777 var/cache/dev/pools
Production Caching with Redis
So how can we fix this on production? We could make that directory writable, but there's a much better way: change cache.app
to not use the file system! We already installed Redis during provision, so let's use that!
How? Open app/config/config.yml
. Actually, use config_prod.yml
, to only use this in production. Add framework
, cache
and app
set to cache.adapter.redis
:
// ... lines 1 - 3 | |
framework: | |
cache: | |
app: cache.adapter.redis | |
// ... lines 7 - 30 |
cache.adapter.redis
is the id of a service that Symfony automatically makes available. You can also use cache.adapter.filesystem
- which is the default - doctrine
, apcu
, memcached
or create your own service. If you need to configure Redis, use the default_redis_provider
key under app, set to redis://
and then your connection info:
# app/config/config_prod.yml
framework:
cache:
app: cache.adapter.redis
default_redis_provider: redis://ConnectionInfo
There are similar config keys to control the other cache adapters.
Since we just changed our code, commit that change:
git add -u
git commit -m "using Redis cache"
Then, push and dance!
git push origin master
And then deploy!
ansible-playbook ansible/deploy.yml -i ansible/hosts.ini --ask-vault-pass
When the deploy finishes... try it! The first refresh should be slow: it's creating the cache. Yep... slow... Try again. Fast! Super fast! Our cache system is fixed!
Do Not Clear cache.app on Deploy
As we talked about earlier, the cache.system
cache is effectively cleared on each deploy automatically. But cache.app
is not cleared on deploy... and that's good! We're caching items that we do not want to automatically remove.
Actually... in Symfony 3.3, that's not true: when you run cache:clear
, this does empty the cache.app
cache. This is actually a bug, and it's fixed in Symfony 3.4. If you need to fix it for Symfony 3.3, open app/config/services.yml
and override a core service:
// ... lines 1 - 5 | |
services: | |
// ... lines 7 - 36 | |
# Prevents cache.app from being cleared on cache:clear | |
# this bug is fixed in Symfony 3.4 | |
cache.app_clearer: | |
class: Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer |
The details of this aren't important, and if you're using Symfony 3.4 or higher, you don't need this.
Oh, and if you do want to clear cache.app
, use:
bin/console cache:pool:clear cache.app
What's the default value of query_cache_driver, and is it better/faster to set it to apcu or redis ! By default, does doctrine set the result of repository queries under cache ?