Caching content in memory for immediate delivery
Directly accessing storage on each client request is not ideal. For this task, we will explore how to enhance server efficiency by accessing the disk only on the first request, caching the data from file for that first request, and serving all further requests out of the process memory.
Getting ready
We are going to improve upon the code from the previous task, so we'll be working with server.js
and in the content
directory, with index.html
, styles.css
, and script.js
.
How to do it...
Let's begin by looking at our following script from the previous recipe Serving static files:
var http = require('http'); var path = require('path'); var fs = require('fs'); var mimeTypes = { '.js' : 'text/javascript', '.html': 'text/html', '.css' : 'text/css' }; http.createServer(function (request, response) { var lookup = path.basename(decodeURI(request.url)) || 'index.html'; var f = 'content/'+lookup; fs.exists(f, function (exists) { if (exists) { fs.readFile(f, function(err,data) { if (err) { response.writeHead(500); response.end('Server Error!'); return; } var headers = {'Content-type': mimeTypes[path.extname(lookup)]}; response.writeHead(200, headers); response.end(data); }); return; } response.writeHead(404); //no such file found! response.end('Page Not Found'); }); }
We need to modify this code to only read the file once, load its contents into memory, and respond to all requests for that file from memory afterwards. To keep things simple and preserve maintainability, we'll extract our cache handling and content delivery into a separate function. So above http.createServer
, and below mimeTypes
, we'll add the following:
var cache = {}; function cacheAndDeliver(f, cb) { if (!cache[f]) { fs.readFile(f, function(err, data) { if (!err) { cache[f] = {content: data} ; } cb(err, data); }); return; } console.log('loading ' + f + ' from cache'); cb(null, cache[f].content); } //http.createServer
A new cache
object and a new function called cacheAndDeliver
have been added to store our files in memory. Our function takes the same parameters as fs.readFile
so we can replace fs.readFile
in the http.createServer
callback while leaving the rest of the code intact as follows:
//...inside http.createServer: fs.exists(f, function (exists) { if (exists) { cacheAndDeliver(f, function(err, data) { if (err) { response.writeHead(500); response.end('Server Error!'); return; } var headers = {'Content-type': mimeTypes[path.extname(f)]}; response.writeHead(200, headers); response.end(data); }); return; } //rest of path exists code (404 handling)...
When we execute our server.js
file and access localhost:8080
twice, consecutively, the second request causes the console to display the following output:
loading content/index.html from cache loading content/styles.css from cache loading content/script.js from cache
How it works...
We defined a function called cacheAndDeliver
, which like fs.readFile
, takes a filename and callback as parameters. This is great because we can pass the exact same callback of fs.readFile
to cacheAndDeliver
, padding the server out with caching logic without adding any extra complexity visually to the inside of the http.createServer
callback.
As it stands, the worth of abstracting our caching logic into an external function is arguable, but the more we build on the server's caching abilities, the more feasible and useful this abstraction becomes. Our cacheAndDeliver
function checks to see if the requested content is already cached. If not, we call fs.readFile
and load the data from disk.
Once we have this data, we may as well hold onto it, so it's placed into the cache
object referenced by its file path (the f
variable). The next time anyone requests the file, cacheAndDeliver
will see that we have the file stored in the cache
object and will issue an alternative callback containing the cached data. Notice that we fill the cache[f]
property with another new object containing a content property. This makes it easier to extend the caching functionality in the future as we would just have to place extra properties into our cache[f]
object and supply logic that interfaces with these properties accordingly.
There's more...
If we were to modify the files we are serving, the changes wouldn't be reflected until we restart the server. We can do something about that.
Reflecting content changes
To detect whether a requested file has changed since we last cached it, we must know when the file was cached and when it was last modified. To record when the file was last cached, let's extend the cache[f]
object as follows:
cache[f] = {content: data,timestamp: Date.now() //store a Unix time stamp };
That was easy! Now let's find out when the file was updated last. The fs.stat
method returns an object as the second parameter of its callback. This object contains the same useful information as the command-line GNU (GNU's Not Unix!) coreutils stat
. The fs.stat
function supplies three time-related properties: last accessed (atime
), last modified (mtime
), and last changed (ctime
). The difference between mtime
and ctime
is that ctime
will reflect any alterations to the file, whereas mtime
will only reflect alterations to the content of the file. Consequently, if we changed the permissions of a file, ctime
would be updated but mtime
would stay the same. We want to pay attention to permission changes as they happen so let's use the ctime
property as shown in the following code:
//requires and mimeType object.... var cache = {}; function cacheAndDeliver(f, cb) { fs.stat(f, function (err, stats) { if (err) { return console.log('Oh no!, Eror', err); } var lastChanged = Date.parse(stats.ctime), isUpdated = (cache[f]) && lastChanged > cache[f].timestamp; if (!cache[f] || isUpdated) { fs.readFile(f, function (err, data) { console.log('loading ' + f + ' from file'); //rest of cacheAndDeliver }); //end of fs.stat }
Note
If we're using Node on Windows, we may have to substitute ctime
with mtime
, since ctime
supports at least Version 0.10.12.
The contents of cacheAndDeliver
have been wrapped in an fs.stat
callback, two variables have been added, and the if(!cache[f])
statement has been modified. We parse the ctime
property of the second parameter dubbed stats
using Date.parse
to convert it to milliseconds since midnight, January 1st, 1970 (the Unix epoch) and assign it to our lastChanged
variable. Then we check whether the requested file's last changed time is greater than when we cached the file (provided the file is indeed cached) and assign the result to our isUpdated
variable. After that, its merely a case of adding the isUpdated
Boolean to the conditional if(!cache[f])
statement via the ||
(or) operator. If the file is newer than our cached version (or if it isn't yet cached), we load the file from disk into the cache
object.
See also
The Optimizing performance with streaming recipe
The Browser-server transmission via AJAX recipe in Chapter 3, Working with Data Serialization
Chapter 4, Interfacing with Databases