Introducing caching

In the previous section, we talked about databases and wiring one up to our user store example. However, in applications, a database will often be the main performance bottleneck in the technology stack. The queries we used in the previous section were quite simple, but as an application grows, so does the complexity of a typical database query. Query complexity, as well as a database growing in size will increase the length of time it takes for a single query to run, making for a poor experience for users of our application.

The best strategy for tackling this is to introduce caching. Fortunately, hapi actually has built-in support for server-side caching through the catbox module. catbox is a key-value-based object store which has extensions for different storage engines via adapters, such as an in-memory adapter (added by default when we create a server), Redis, and memcached.

As this is quite an advanced part of application development, I won't go into too much detail here, but would like to just make you aware of the support that hapi provides for it. Caching in hapi is provided by a low-level client and a higher level policy, but in reality, you will likely never need to use either, as hapi's server methods provide a great abstraction for adding caching to the existing functionality. Let's take a quick look at server methods first, and then we can look at using them to add some caching to our applications.

Server methods

Server methods are functions you attach to the server, which are available everywhere you have a reference to your server object. These, like plugins, are easy to use and very useful for sharing functionality throughout your applications, and as I mentioned, make it very easy to add caching. Let's take a look at an example of how to register a server method, and use it now.

To demonstrate server methods, let's see what a simple function for returning a hello message and registering it as a server method looks like:

const getHello = function (name, next) {
  return next(null, `hello ${name} on ${new Date()}`);
};
server.method('getHello', getHello);

So that's not too difficult; just be conscious that the callback to a server method has the following signature:

next(err, value)

The importance of this is that if we return the value as the first parameter, we are in fact returning an error instead of the value. When it comes to later parts such as caching, errors won't be cached, and you might be left wondering why your caching isn't working.

In the preceding example, we've created a server method that, when called, will return the hello message to a name, along with the current date. We can then call this anywhere we have a reference to the server object with, as follows:

server.methods.getHello('world', (err, message) => {
  console.log(message); // prints 'hello world' with date
});

Note that if the name used to register contains a . character, the method will be registered as a nested object. This can be useful in sandboxing methods. Consider the following example, noting the . between get and hello:

…
server.method('get.hello', getHello);
…

This would then actually be called by:

server.methods.get.hello('world', (err, message) => {
  console.log(message); // prints 'hello world' with date
});

This covers the basics of server methods and the most general of their use cases; if you want to learn more, you can read about them in the API documentation at http://hapijs.com/api#servermethodname-method-options.

Caching with server methods

Now that you've seen server methods, let's use them to add some caching to an application. For this example, we will use the default in-memory adapter. Conveniently, this is created by default for us when we create our hapi server.

Modifying the getHello function from the previous example, this time when we create our server method, we'll add the extra server method options object with some cache configuration:

…
server.method('getHello', getHello, {
  cache: {
    expiresIn: 30 * 1000,
    generateTimeout: 10
  }
});
…

We now have a cache built for the getHello server method by providing a configuration object with our cache information. expiresIn specifies how long this response will live for in the cache in milliseconds; in this case, it will cache the getHello response for 30 seconds. generateTimeout here specifies how long it should take for the server method to generate the response before sending a timeout error back to the client that initiated the request. This would obviously need to be increased if we were doing anything more complicated than simply returning a string.

Let's see how we could use this in a route handler now:

…
server.route({
  path: '/hello/{name}',
  method: 'GET',
  handler: function (request, reply) {
    const name = request.params.name;
    return server.methods.getHello(name, reply);
  }
});
…

Here we call the server method and pass the reply as the server method callback, which means not too much extra complexity has been added to the route handler in order to add an in-memory server-side cache here.

The huge advantage of this is now that if an in-memory cache wasn't suitable for your needs, you could add a different default cache engine using some of the other catbox adapters, and you wouldn't need to update any server methods or route handler code.

It's worth noting here that in order to test or use your cache, unlike earlier where we could test with server.inject(), you must have started your server or at least called server.initialize(). This can be a common source of frustration when adding or testing the cache functionality.

Let's now look at a more real-world example. Let's modify our user store example so that for the GET route where we retrieve a user from the database, it will now cache this in memory so that all future requests for the same user will be retrieved from the cache instead of querying the database every time.

As we saw with the previous example, adding the caching functionality doesn't look too complicated. Here, we will register a server method with cache configuration for the getUser function, then update our route handler to call the server method instead of the getUser function directly. Let's look at what this looks like now. First, we will add the server method for the getUser function:

…
server.method('getUser', getUser, {
  cache: {
    expiresIn: 30 * 1000,
    generateTimeout: 1000
  }
});
…

Next we make the update to the GET route handler for calling our new server method:

…
handler: function (request, reply) {
  const userId = request.params.userId;
  server.methods.getUser(userId, (err, user) => {
    if (!user) {
      return reply(Boom.notFound());
    }
    return reply(user);
  });
},
…

So, as we saw with the previous example, it's not very difficult to add some simple caching to our applications, even for more real-world examples. The complete example can be found in the examples for this chapter in the Getting Started with hapi.js repository on GitHub (https://github.com/johnbrett/Getting-Started-with-hapi.js). I encourage you to run this one and test it out, as I've added some console.log() messages to the example to show which requests actually hit the database and which are returned just from the in-memory cache.

While this example is much closer to how caching would be used in the real world, there are still some problems that are left unsolved. If you think about adding UPDATE or DELETE routes to this user store API, you would need methods to invalidate the cache for a user. Without those, we would return out-of-date content for the duration for which the response was in the cache.

For this, we use the drop() method that is added when we create a server method. In a sample UPDATE or DELETE route handler, this would work something like the following:

…
handler: function (request, reply) {
  const userId = request.params.userId;
  // perform update or delete logic
  server.methods.getUser.cache.drop(userId, () => {
    console.log('user removed from cache ... ');
    return reply({ message: 'user removed from cache!' });
  });
},
…

Cache invalidation can actually become a very complex topic in applications, as there is no silver-bullet solution that can be used for every application. While my goal was not to cover caching completely here, as it is quite a large topic on its own, I hope I have given you enough to think about here so that when you build your applications, you'll have a good starting point for adding a layer of server-side caching with hapi.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.232.11