jaredwray / cache-manager

Cache module for Node.JS
MIT License
1.4k stars 152 forks source link

Malformed cache data #707

Closed rivatove closed 1 month ago

rivatove commented 1 month ago

Describe the bug Data from the cache comes in the form of { v: DATA } instead of DATA. So it's getting wrapped in an object under a v key for some reason. It obviously breaks all functionality. It seems to be a regression, as it did not happen earlier.

How To Reproduce (best to provide workable code or tests!) Unfortunately, I only encountered the issue in production (where both the affected and unaffected versions used the same cache store). I wasn't able to replicate it locally.

Package versions affected:

Package versions free of the issue:

I don't know exactly which package is responsible for the problem.

jaredwray commented 1 month ago

@rivatove - we upgraded to telejson a while back which could store the objects that way but should not affect you retrieving the value. Can you give an example of how you see this happening? Do you have code example to reproduce?

rivatove commented 1 month ago

@rivatove - we upgraded to telejson a while back which could store the objects that way but should not affect you retrieving the value. Can you give an example of how you see this happening? Do you have code example to reproduce?

Unfortunately I don't have more detailed examples as it happened in production.

Most important info:

francescosalvi commented 1 month ago

I have a working repro, and the explanation for the behavior (and why it appears transient):

package.json (install latest version of cache-manager and cache-manager-ioredis-yet alongside the last former working versions, in my specific case 5.5.3/2.0.4)

{
  "name": "cache-manager-fiddle",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "cache-manager-prev": "npm:cache-manager@5.5.3",
    "cache-manager-ioredis-yet-prev": "npm:cache-manager-ioredis-yet@2.0.4",
    "cache-manager": "5.6.1",
    "cache-manager-ioredis-yet": "2.1.1"
  }
}

index.mjs

import { redisStore as createRedisStoreCurr } from 'cache-manager-ioredis-yet';
import { redisStore as createRedisStorePrev } from 'cache-manager-ioredis-yet-prev';
import { caching as createCacheCurr } from 'cache-manager';
import { caching as createCachePrev } from 'cache-manager-prev';

const connOptions = { host: 'localhost', port: 6379 };
const cachePrev = await createCachePrev(await createRedisStorePrev(connOptions));

const key = 'someKey';

await cachePrev.set(key, 'hello cache', 5000);
console.log(await cachePrev.get(key)); // outputs: hello cache

const cacheCurr = await createCacheCurr(await createRedisStoreCurr(connOptions));

console.log(await cacheCurr.get(key)); // outputs: { v: 'hello cache' }

process.exit(0);

Hence what we are abserving is a breaking change the internal expectations cache-manager-ioredis-yet makes about the schema it uses. In other words, all is fine if the cache store gets completely wiped an repopulated across package upgrade, but all data previously present becomes invalid.

Kauhsa commented 1 month ago

I suspect https://github.com/jaredwray/cache-manager/pull/681 has something to do with this.

jaredwray commented 1 month ago

I have a working repro, and the explanation for the behavior (and why it appears transient):

package.json (install latest version of cache-manager and cache-manager-ioredis-yet alongside the last former working versions, in my specific case 5.5.3/2.0.4)

{
  "name": "cache-manager-fiddle",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "cache-manager-prev": "npm:cache-manager@5.5.3",
    "cache-manager-ioredis-yet-prev": "npm:cache-manager-ioredis-yet@2.0.4",
    "cache-manager": "5.6.1",
    "cache-manager-ioredis-yet": "2.1.1"
  }
}

index.mjs

import { redisStore as createRedisStoreCurr } from 'cache-manager-ioredis-yet';
import { redisStore as createRedisStorePrev } from 'cache-manager-ioredis-yet-prev';
import { caching as createCacheCurr } from 'cache-manager';
import { caching as createCachePrev } from 'cache-manager-prev';

const connOptions = { host: 'localhost', port: 6379 };
const cachePrev = await createCachePrev(await createRedisStorePrev(connOptions));

const key = 'someKey';

await cachePrev.set(key, 'hello cache', 5000);
console.log(await cachePrev.get(key)); // outputs: hello cache

const cacheCurr = await createCacheCurr(await createRedisStoreCurr(connOptions));

console.log(await cacheCurr.get(key)); // outputs: { v: 'hello cache' }

process.exit(0);

Hence what we are abserving is a breaking change the internal expectations cache-manager-ioredis-yet makes about the schema it uses. In other words, all is fine if the cache store gets completely wiped an repopulated across package upgrade, but all data previously present becomes invalid.

Thanks for doing this. Can you tell me what the schema looks like in previous as maybe we can just handle an conversion for it in the new code base?

francescosalvi commented 1 month ago

@jaredwray no problem!

Can you tell me what the schema looks like in previous

If I do a "raw" get using ioredis right after the set using 5.5.3/2.1.1 in my snippet above, I get back {"v":"hello cache"}, so it appears that the wrapping of the user/business payload with the v field was already in place in the previous version.

This is confirmed If I diff the implementations ("prev" -> "curr"): image

What's changed is that previously stringify used to include the "wrapping" parts in redis, while in the new version they are stripped using slice.

I am thinking that even by implementing a heuristic that handles both the old and new format (like: "if it's an object with strictly one property named v, then it must come from version < xxx and we should unwrap it"), there probably isn't a way to 100% exclude false positives (users could have a v as part of their business payload ?).

jaredwray commented 1 month ago

Yep. I think the guidance is to flush the cache when moving to this and that resolves it. Going to close this as there isnt a good way to handle it.

evenisse commented 1 month ago

Yep. I think the guidance is to flush the cache when moving to this and that resolves it. Going to close this as there isnt a good way to handle it.

Hello, It's not a solution. With 2 applications sharing the same cache data, if one application can't be updated, it's impossible to upgrade the other to the new version of cache-manager. Data storage must be independent of the library used.

rvitaliy commented 1 month ago

agree with @evenisse

another problem what is see is that two different applications can use different approach/libraries to store and get values, the contract of what i put and what i get at the level of the storage MUST be respected! Libraries like this that have the purpose to be a wrapper that allow easier change the storage driver or just hide implementation detail MUST not alterate input and output datas at the storage level.

i mean:

NodeJSApplication: cacheManager.put('ping', 'pong');

storage: [{'ping': {'v': 'pong'}}]  // expected result storage: [{'ping': 'pong'}]

PHPApplication: redisDirectImplementation.get('ping') > result {'v': 'pong'} // ERROR!
GolangApplication: redisDirectImplementation.get('ping') > result {'v': 'pong'} // ERROR!

// and viceversa

GolangApplication|PHPApplication.put('key1', 'value1');
NodeJSApplication.cacheManager.get('key1') > result 'value1' expected {'v': 'value1'}
rvitaliy commented 3 weeks ago

@jaredwray Hi, given all these considerations, do you intend to investigate further to determine if we need to rollback?

IMHO: yes, we should.

jaredwray commented 3 weeks ago

@rvitaliy - I beleive it is not doing this anymore. What version are you on as what is the code example that does this?

evenisse commented 2 weeks ago

My problem is different. It's not between 2 versions of cache-manager but between 2 applications. One application uses a redis library directly (ioredis) and the other uses cache-manager. As cache-manager only accepts object with v field, it isn't possible to set data in the first app with a redis lib and read then in the other app with cache-manager. And reciprocally.

import { caching as createCacheCurr } from 'cache-manager';
import Redis from 'ioredis';
const redis = new Redis();

const connOptions = { host: 'localhost', port: 6379 };
const cacheCurr = await createCacheCurr(await createRedisStoreCurr(connOptions));

const key = 'someKey';

await redis.set(key, 'hello cache', 'PX', 5000);
console.log('From Redis: ', await redis.get(key)); // outputs: hello cache
console.log('From Cache: ', await cacheCurr.get(key)); // error

process.exit(0);

The output is

undefined:1
{"v":hello cache}
     ^

SyntaxError: Unexpected token 'h', "{"v":hello cache}" is not valid JSON
    at JSON.parse (<anonymous>)
    at Object.parse (/home/evenisse/test_redis_cache_manager/node_modules/telejson/dist/index.js:1565:24)
    at parse (/home/evenisse/test_redis_cache_manager/node_modules/cache-manager-ioredis-yet/dist/index.js:33:35)
    at Object.get (/home/evenisse/test_redis_cache_manager/node_modules/cache-manager-ioredis-yet/dist/index.js:61:24)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async file:///home/evenisse/test_redis_cache_manager/index.mjs:13:29

Why do you use an object with a v field and not directly th real value?