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!

  • 2018-02-27 Victor Bocharsky

    Yo Cesar,

    Webpack Encore is going to be completely released in a few weeks, as you might notice we're working on it right now and trying to release a new video every day. And Webpack Encore course will be FREE for everyone! But if you're looking for buying something, take a look at paid Webpack course which is already completely released :p

    Cheers!

  • 2018-02-27 Cesar

    Thanks Victor. When are you calculating to finish the Webpack Encore course? I am going to buy it or sure.

  • 2018-02-27 Victor Bocharsky

    Hey Cesar,

    Do you mean watching Webpack course before Webpack Encore? Actually Encore is just a simple wrapper around Webpack which makes your work with Webpack library drastically simpler. So, you can start directly with Encore course, but if you need to dive deeper into Webpack - you need the Webpack course. However, I'd recommend to see Webpack course to understand Webpack and Webpack Encore better.

    If you mean AsseticBundle - Webpack Encore is the tool which completely replaces this bundle and adds you more more features out of the box. So if you're going to use Encore - forget about AsseticBundle at all.

    About HTTP cache: it depends on your project, if you have a simple website with a lot of public content (not user-specific pages or user-specific blocks on each page) - it makes sense. Otherwise it could be even overhead in some cases. Anyway, HTTP cache complicates things a lot and you need to start thinking about cache invalidation on deploys. I'd recommend you to look at Symfony Cache Component first that are enough in most cases and that is much simpler.

    Cheers!

  • 2018-02-26 Cesar

    Thanks Ryan for answering. That's exactly my issue with CSS and JS. I would like to buy your tutorial when it's complete. Do I need to watch first another tutorial before to enter in this one?. Also, meanwhile, do you recommend me to use ascetic or http-cache?

  • 2018-02-26 weaverryan

    Hey Cesar!

    Interesting! So.... the answer depends on what you mean exactly by "some of my users are still watching the previous version". On a PHP level, during deploy, 100% of the users are still using the original PHP code path. After deploy, 100% of the users are using the NEW PHP code path (as soon as the symbolic link changes, all requests use the new code). However, if you're referring to JavaScript or CSS, then it's more interesting :). If you make an update to a JS or CSS file and deploy, the new JS or CSS file will be deployed just like any other file. However, the user's browser might still use the old, *cached* version.

    Is this your issue? If so, you need to use a cache busting strategy. I *highly* recommend using Webpack Encore, as it will handle cache busting / versioning automatically. We're creating a screencast about this right now - https://knpuniversity.com/s... - but you can find details here: https://symfony.com/doc/cur...

    Cheers!

  • 2018-02-26 Cesar

    Hi. I am using ansistrano to deploy my symfony app and it's working very well. However, sometimes when I release a new version, some of my users are still watching there previous version. I think this is related to the cache service. Do I need to do any configuration for that? I want that everybody can watch the new release immediately after the deploy. I hope you can help me.