Buy

Watch 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:

134 lines 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():

134 lines src/AppBundle/Controller/DefaultController.php
... 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():

134 lines src/AppBundle/Controller/DefaultController.php
... 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:

134 lines src/AppBundle/Controller/DefaultController.php
... 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:

30 lines app/config/config_prod.yml
... 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:

41 lines app/config/services.yml
... 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

Leave a comment!