Parse-server: Implementing Redis Cache functionality for Parse-server

Created on 8 May 2018  Â·  19Comments  Â·  Source: parse-community/parse-server

Issue Description

I would like to implement a Redis Cache for a user-specified class list. I know that Parse-server has a Redis cache but my understanding is that is only used for caching certain things (Roles, session info etc). My idea is to allow the developer to specify which classes they wish to cache on the Redis server as a start up item on the parse server. Something like this:

    var api = new ParseServer({
        databaseURI: databaseUri,
        cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',
        appId: process.env.APP_ID,
        masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret!
        serverURL: process.env.SERVER_URL,  // Don't forget to change to https if needed
        fileKey: process.env.S3_FILE_KEY,
        publicServerURL: process.env.SERVER_URL,
                cacheAdapter:{
                   classNames:["vendor", "location"]
                }
    });

When a REST query is executed on one of those classes then a check is first made to see if the data is all ready cached on the REDIS server. If it is then return it. If not, get it from the DB and then write it to the REDIS server.

We will also need to find a mechanism where the developer can invalidate the cache. We might be able to use the afterSave triggers but I am open to ideas on this.

I also suppose we should not develop specific for REDIS, rather we developer a framework and a REDIS adapter. That way someone else could easily add a MemCache adapter and so on.

Anyone willing to design/develop this with me?

Most helpful comment

Any news on this ?

All 19 comments

@araskin That's a great idea, if you have any questions, feel free to ping us. Also, this could probably be hooked in the beforeFind hook where a user may respond with cached data.

What do you think?

Thanks @flovilmart . I think beforeFind and the other hooks would be very useful. The issue I have with beforeFind is that right now it seems to be used for modifying the query object as well as the possibility of returning a NEW query object. If we started returning a ParseObject would that not cause issues for those that have used this trigger to return a new Query object?

The second issue I am struggling with his how to 'invalidate' the cache (redis entry)? There are so many different options that I think it will probably be easier to let the developer implement their own logic... Just need to find a way to make it simple to do....I need to chew over this a bit more....

This is where it gets tricky, caching is quite complex and depending on your application you may wanna have different levels of caching, from none to very long be cause your data is quite stale and refreshing every 10 minutes is OK. I'm really open to investigate different kind of options, perhaps we could leverage the query serialization from LiveQuery server and use those query hashs as key/value stores keys are they are 'stable'.

The main issue comes then when one needs to invalidate or define a particular duration for the cache. Letting parse-server do it himself, would be quite harmful, we do it for users and roles but only for short amounts of time and yet this should be improved to .

Fantastic idea, I'd be wiling to help out with this. We struggle ourselves with caching to Redis in our parse-server. We've done a lot of manual methods for caching in our cloud code functions, but it sometimes feels like a dirty solution. Implementing it at the parse-server query level would be a lot cleaner.

Thanks @johnnydimas !! I would definitely love some guidance from someone with more experience. I have never enhanced a 'community project' before so would be great to work with someone on this.

Shall we connect directly to discuss design/issues. etc? I can send you my Skype details if you like...

I was struggling with finding the best way to cache to Redis. I felt that using hashes was optimal for saving large data sets, but it becomes kind of a chore to convert Parse Objects to hash sets to save to Redis and converting hash sets back into Parse Objects for consumption. Maybe this isn't an issue if doing this at the query level though.

The degree of caching our data varies a lot as well. It can change rapidly at times so I would agree that having the developer determine the time to live sounds like the best approach.

@johnnydimas

Yes I implemented a quick "hack" in RestQuery.js for 'ParseQuery.get' calls (simplest scenario) which is the point just before it calls the DB and made my call to Redis there. If Redis found a value it would be returned back. If not it would do the DB call and then write the result to Redis. In this simple scenario (GET call) there would only be 0 or 1 records so was no issue. We will also need to consider ParseQuery.Find and ParseQuery.each calls.

We also need to consider security. Does the current User have the correct auth to query this class? I haven't really looked into it so perhaps the auth has all ready been checked before it gets to RestQuery.js . Just need to confirm.

Regarding invalidation of cache (TTL), I think all we will need to do is provide some sort of method which can be called in the _beforeSave_ event which will allow the developer to get a list of queries stored in Redis for this className. Then they could call _expire_ method (or something like that) which would cause Redis to delete those entries.

@araskin for all the reasons you enumerate; I would recommend to add a way to bypass the DB find at beforeFind level. I’d rather have a simple implementation at first than a bloated one that tries to do too many smart things :)

@flovilmart OK.

So basically here is how I see it play out... (roughly)

    var api = new ParseServer({
        databaseURI: databaseUri,
        cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',
        appId: process.env.APP_ID,
        masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret!
        serverURL: process.env.SERVER_URL,  // Don't forget to change to https if needed
        fileKey: process.env.S3_FILE_KEY,
        publicServerURL: process.env.SERVER_URL,
                classCache:{
                   classNames:["vendor", "location"]
                }
    });

By providing a list of classes, we will attach an 'internal' callback when the beforeFind is triggered (I assume this trigger will get called during a Query.get and Query.find). In that 'callback' we will call the REDIS server and see if there is a response all ready cached for the query based on the where clause provided. IF there is not we return nothing and the query will proceed as usual. Once the query on the DB is complete we will add it to our REDIS cache so subsequent calls will get the data.

If there is data in the REDIS cache, we will take the data back from the REDIS server, convert it back to ParseObject and return it as a result of the query. In that scenario we will need to enhance the code to ensure that the call to the DB does not proceed. Maybe we can check on teh return type of the beforeFind. If it is of type ParseObject we do not do the DB query but just proceed as usual.

We should probably also provide a method call to on ParseObject (or maybe ParseQuery) which allows the developer to see the keys of all the cached data for a particular class. That way they can choose to invalidate the cache for a particular class based on their own logic. They could invalidate the cache based on

  • the data changing (afterSave event

  • periodic time (ie. batch job running that invalidates the cache every X hours)

  • cloud code (user clicks on something in the UI which calls a cloud code function to invalidate the cache)

What ever the 'trigger' for the cache invalidation we have to make sure that the developer has an easy way of clearing the cache for a particular class.

Shall I create the fork of the project or does @johnnydimas want to do it? I dont mind either way.

@araskin I had a fork already created for another PR. I went ahead and added you as a collaborator, feel free to create a branch for the task. :)

I'm not sure if this direction which require major changes and probably flaky behaviors is really what we're looking for. For now, I'd probably prefer if the user of parse-server would have the ability to provide the cached data in the beforeFind.

We could let objects alongside queries to be returned from this hook and then the developer would be able to invalidate it's cache by himself.

There are 2 advantages to this approach:

  1. You need this in any case, to bypass the DB runs
  2. You can write your whole logic externally.

As it stands with your proposal, I will not follow through nor take the code in if you decide to bake everything into parse-server.

However, I can very well see it working as an addon:

const parseServerCache = require('parse-server-cache');

Parse.Cloud.beforeFind('MyClass', (req, res) => {
   parseServerCache.beforeFind('MyClass', req, res);
});

Parse.Cloud.afterSave('MyClass', (req) => {
   parseServerCache.afterSave('MyClass', req, res);
});

// Or something like

// This would register the functions.
parseServerCache.setup('MyClass');

the main benefit of the addon approach would be that you retain control on the evoltion of this project geared towards caching.

But wouldn't we have to change the beforeFind trigger to allow the user to return the ParseObject and abort the execution of the query? this will require changes to the core parse-server code. No? Sent from my BlackBerry - the most secure mobile device From: [email protected]: May 9, 2018 10:44 AMTo: [email protected]: [email protected]: [email protected]; [email protected]: Re: [parse-community/parse-server] Implementing Redis Cache functionality for Parse-server (#4750) I'm not sure if this direction which require major changes and probably flaky behaviors is really what we're looking for. For now, I'd probably prefer if the user of parse-server would have the ability to provide the cached data in the beforeFind.
We could let objects alongside queries to be returned from this hook and then the developer would be able to invalidate it's cache by himself.
There are 2 advantages to this approach:
You need this in any case, to bypass the DB runsYou can write your whole logic externally.
As it stands with your proposal, I will not follow through nor take the code in if you decide to bake everything into parse-server.
However, I can very well see it working as an addon:
const parseServerCache = require('parse-server-cache');

Parse.Cloud.beforeFind('MyClass', (req, res) => {
   parseServerCache.beforeFind('MyClass', req, res);
});

Parse.Cloud.afterSave('MyClass', (req) => {
   parseServerCache.afterSave('MyClass', req, res);
});

the main benefit of the addon approach would be that you retain control on the evoltion of this project geared towards caching.

—You are receiving this because you were mentioned.Reply to this email directly, view it on GitHub, or mute the thread.

Yes, but this is minimal, compared to a whole cache infrastructure :)

OK @flovilmart so do we wait for you guys to make this change? I am not really sure how to proceed.

Feel free to open a pull request and add the appropriate tests for it (letting someone return one/many objects in beforeFind. No need to wait for anything.

On May 9, 2018, 12:32 -0400, Alon Raskin notifications@github.com, wrote:

OK @flovilmart so do we wait for you guys to make this change? I am not really sure how to proceed.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

Any news on this ?

Has it been any progress with this?

Hello guys,

Very interesting topic and I really enjoy reading it :) A few months ago when I was working on something similar I found this paper:

And the concept of an Expiring Bloom Filter data structure:

Hope it will help this discussion :)

What do you think?

Closing due to lack of activity, please update to latest parse-server version and open a new issue if the issue persist.

Don't forget to include your current:

  • node version
  • npm version
  • parse-server version
  • any relevant logs (VERBOSE=1 will enable verbose logging)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dcdspace picture dcdspace  Â·  3Comments

lorki picture lorki  Â·  3Comments

ShawnBaek picture ShawnBaek  Â·  4Comments

dpaid picture dpaid  Â·  3Comments

ugo-geronimo picture ugo-geronimo  Â·  3Comments