Memcache cache Adapter for Parse Server on Google App Engine

July 24, 2017

Increasing performance and reducing costs is a constant goal in cloud applications, and caching is often an effective way to make improvements.

The Parse server supports a cache adapter which caches commonly queries such as database schema, users, roles and sessions.

Out of the box Parse comes with a cache adapter for Redis, which is a top choice when it comes to caching.

Our full stack Ionic and Parse app runs on the Google App Engine, which has a Memcached service. Leveraging this would minimize having to setup and manage a Redis node or cluster.

At this time Memcache for the Node.js flexible environment on App Engine is currently in Alpha and requires completing a signup form to request access

While we’re waiting for access to the Memcached service we can get started on our Parse Memcached adapter.

The existing Redis cache adapter will be the starting point, and as it almost not surprisingly turns out, only minimal changes are required to updated the usage of the Redis API to the Memcached API, using the memcached module.

Additionally in the constructor we check if we are running in the Google App Engine, and auto-configure the Memcache URL

If you use this code don’t forget to run npm install memcached to install the memcached module.

/**
 * MemCache backed CacheAdapter, with auto-configuration for Google App Engine Memcached serivce
 */

var Memcached = require('memcached');
import logger from '../../logger';

const DEFAULT_MEMCACHE_TTL = 30 * 1000; // 30 seconds in milliseconds

function debug() {
    logger.debug.apply(logger, ['MemcacheCacheAdapter', ...arguments]);
}

export class MemcacheCacheAdapter {

    constructor(memcacheConfig, ttl = DEFAULT_MEMCACHE_TTL) {

        // Environment variables are defined in app.yaml.
        let MEMCACHE_URL = process.env.MEMCACHE_URL || '127.0.0.1:11211';

        if (process.env.GAE_MEMCACHE_HOST) {
            MEMCACHE_URL = `${process.env.GAE_MEMCACHE_HOST}:${process.env.GAE_MEMCACHE_PORT}`;
        }

        this.client = new Memcached(MEMCACHE_URL);

        this.p = Promise.resolve();
        this.ttl = ttl;
    }

    get(key) {
        debug('get', key);
        this.p = this.p.then(() => {
            return new Promise((resolve) => {
                this.client.get('key', function (err, res) {
                    debug('-> get', key, res);
                    if(!res) {
                        return resolve(null);
                    }
                    resolve(JSON.parse(res));
                });
            });
        });
        return this.p;
    }

    put(key, value, ttl = this.ttl) {
        value = JSON.stringify(value);
        debug('put', key, value, ttl);
        if (ttl === 0) {
            return this.p; // ttl of zero is a logical no-op
        }
        if (ttl < 0 || isNaN(ttl)) {
            ttl = DEFAULT_MEMCACHE_TTL;
        }
        this.p = this.p.then(() => {
            return new Promise((resolve) => {
                if (ttl === Infinity) {
                    this.client.set(key, value, 0, function () {
                        resolve();
                    });
                } else {
                    this.client.set(key, ttl, value, function() {
                        resolve();
                    });
                }
            });
        });
        return this.p;
    }

    del(key) {
        debug('del', key);
        this.p = this.p.then(() => {
            return new Promise((resolve) => {
                this.client.del(key, function() {
                    resolve();
                });
            });
        });
        return this.p;
    }

    clear() {
        debug('clear');
        this.p = this.p.then(() => {
            return new Promise((resolve) => {
                this.client.flush(function() {
                    resolve();
                });
            });
        });
        return this.p;
    }
}

export default MemcacheCacheAdapter;