Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add query cache #9584

Open
mtrezza opened this issue Feb 2, 2025 · 1 comment
Open

Add query cache #9584

mtrezza opened this issue Feb 2, 2025 · 1 comment
Labels
type:feature New feature or improvement of existing feature

Comments

@mtrezza
Copy link
Member

mtrezza commented Feb 2, 2025

New Feature / Enhancement Checklist

Current Limitation

Parse Server has no built-in query cache. A simple caching functionality could significantly reduce database costs.

Feature / Enhancement Description

Add a very simple but versatile query cache.

Scope

  • Only available in Cloud Code.
  • Auto-fallback on database if no cached result.
  • Maintain all Cloud Code triggers to be called agnostically, regardless of data source (DB or cache).
  • Allow to configure cache params (TTL, etc.) per query request for higher versatility.
  • Provide methods for the developer to flush the cache with custom granularity.
  • Optionally restart TTL on cache match; useful for data that does not change over time.
  • Optionally allow to set a custom hash value; this allows to optimize the hash algorithm according to the data set.
  • Cache key composed of hash value calculated from:
    • query conditions, e.g. via the hashed JSON.stringify(query.toJSON()) string.
    • sessionToken or masterKey authentication to preserve ACL, so query result can only be retrieved by session or master key that cached it.

Out of scope:

  • Automatically flush cache, for example on Parse.Object.save, Parse.Object.destroy and related batch functions --> this would require a complex granular determination of what has changed after saving an object, because changing a field that is not a condition of a cached query shouldn't delete the cached entry; flushing the entire class cache because an object has been saved is likely inefficient in most use cases.
  • Cache statistics --> There are existing Redis tools for that.

Suggested Parse.Query.cache() method:

/**
 * @param {Object} options The query cache options.
 * @param {number|undefined} [options.ttl] Time-to-live in milliseconds for the cached query. Optional;
 * default is the Parse Server options cache adapter TTL value.
 * @param {string|undefined} [options.id] The query ID for fine-grained cache invalidation. Optional;
 * default is undefined.
 * @param {string|undefined} [options.hash] The hash value for advanced cache customization. Optional;
 * default is the hash value created from the Parse Query conditions. This allows to override the default
 * hash value and create a hash that omits query conditions to prioritize reduced database load over data
 * freshness.
 * @param {string[]|undefined} [options.indexes] A list of indexes for fine-grained cache invalidation.
 * Each index creates an entry in a set to logically group cache entries. This allows to invalidate all
 * cache entries within a logical groups.
 * 
 * @returns {Parse.Query} The query instance with caching enabled.
 */

Parse Server cache options:

{
  queryCache: {
    // This globally enables or disabled the query cache; when set to `false`, all `Parse.Query.cache()`
    // conditions have no effect and the cache is not used; this allows to quickly disable caching in
    // case of unexpected issues with the cache itself
    enabled: true,
    // The default TTL of cached entries; can be overridden by `Parse.Query.cache({ ttl })`
    ttl: 1_000,
    // The timeout is milliseconds after which the cache request is cancelled and a request is made to
    // the database; can be overridden by `Parse.Query.cache({ timeout })`
    timeout: 10_000,
    // Reset the TTL of a cached entry on cache hit. Default is `false`; can be overridden by
    // `Parse.Query.cache({ resetTtlOnCacheHit })`
    resetTtlOnCacheHit: false,
  }
}

Example Use Case

Simple caching based on class

// Query
const query = new Parse.Query('Purchase');
query.equalTo('user', '<USER_OBJECT_ID>');
query.equalTo('paid', '<PAID_BOOL_VALUE>');
query.cache();
const users = await query.find();

// Cache keys
// parse:query:Purchase:<QUERY_HASH>

// Flush entire query cache
await Parse.Cloud.QueryCache.flush();

// Flush query cache only for specific class
await Parse.Cloud.QueryCache.flush({ class: 'Purchase' });

Caching with query ID for targeted flushing

// Query
const paidQuery = new Parse.Query('Purchase');
paidQuery.equalTo('user', user);
paidQuery.equalTo('paid', true);
paidQuery.cache({ id: 'paid' });
const users = await paidQuery.find();

const invoicedQuery = new Parse.Query('Purchase');
invoicedQuery.equalTo('user', user);
invoicedQuery.equalTo('invoiced', true);
invoicedQuery.cache({ id: 'invoiced' });
const users = await invoicedQuery.find();

// Cache keys
// parse:query:Purchase:paid:<QUERY_HASH>
// parse:query:Purchase:invoiced:<QUERY_HASH>

// Flush query cache only for specific query
await Parse.Cloud.QueryCache.flush({ class: 'Purchase', id: 'paid' });
await Parse.Cloud.QueryCache.flush({ class: 'Purchase', id: 'invoiced' });

Advanced caching with custom hash, indexes for targeted flushing and custom cache TTL

In the example below, the hash value is calculated after removing the paid condition from the query. The purpose of removing the key is to prioritize cost reduction over data freshness. In the example below, Parse Server first tries to find a cached query ignoring the paid condition. If one was found, it returns it, even if it may be outdated. If none was found, it makes a query request to the DB including the paid condition and then caches the result omitting the paid field in the hash value. In order to distinguish a query with an omitted field from a query in which the field is not part of the original query, the id must be set.

// Query
const queryJson = {
    className: 'Purchase',
    where: {
        user: { '__type': 'Pointer', className: '_User', objectId: '<USER_OBJECT_ID>' },
        paid: '<PAID_BOOL_VALUE>'
    },
    limit: 10
};

// Create JSON hash without `paid` key
const hashJson = JSON.parse(JSON.stringify(queryJson));
delete hashJson.where.paid;
const hash = getHashValue(hashJson);

const indexes = [
  'paid:<PAID_BOOL_VALUE>',
  'userId:<USER_OBJECT_ID>'
];

const query = Parse.Query.withJSON(queryJson);
query.cache({ hash, indexes, id: 'state' ttl: 1000 });
const users = await query.find();

// Cache keys
// parse:query:Purchase:state:<QUERY_HASH>

// Created sets of which the cache key above is a member
// parse:query:Purchase:state:paid:<PAID_BOOL_VALUE>
// parse:query:Purchase:state:userId:<USER_OBJECT_ID>

// Flush query cache for all purchase states
await Parse.Cloud.QueryCache.flush({ class: 'Purchase', id: 'state' });

// Flush query cache for all purchase states of a specific user
await Parse.Cloud.QueryCache.flush({ class: 'Purchase', id: 'state', index: '<USER_OBJECT_ID>' });

// Flush query cache for all paid purchases of a specific state
await Parse.Cloud.QueryCache.flush({ class: 'Purchase', id: 'state', index: '<PAID_BOOL_VALUE>' });

References

Copy link

parse-github-assistant bot commented Feb 2, 2025

Thanks for opening this issue!

  • 🎉 We are excited about your ideas for improvement!

@mtrezza mtrezza added the type:feature New feature or improvement of existing feature label Feb 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:feature New feature or improvement of existing feature
Projects
None yet
Development

No branches or pull requests

1 participant