-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Implement redis cache plugin
Relates to #3043
- Loading branch information
1 parent
382e314
commit 9d99593
Showing
10 changed files
with
308 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { CacheService, mergeConfig, RedisCachePlugin } from '@vendure/core'; | ||
import { createTestEnvironment } from '@vendure/testing'; | ||
import path from 'path'; | ||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; | ||
|
||
import { initialData } from '../../../e2e-common/e2e-initial-data'; | ||
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; | ||
|
||
import { | ||
deletesAKey, | ||
evictsTheOldestKeyWhenCacheIsFull, | ||
getReturnsUndefinedForNonExistentKey, | ||
invalidatesALargeNumberOfKeysByTag, | ||
invalidatesByMultipleTags, | ||
invalidatesBySingleTag, | ||
invalidatesManyByMultipleTags, | ||
setsAKey, | ||
setsAKeyWithTtl, | ||
setsArrayOfObjects, | ||
setsArrayValue, | ||
setsObjectValue, | ||
} from './fixtures/cache-service-shared-tests'; | ||
|
||
describe('CacheService with RedisCachePlugin', () => { | ||
let cacheService: CacheService; | ||
const { server, adminClient } = createTestEnvironment( | ||
mergeConfig(testConfig(), { | ||
plugins: [ | ||
RedisCachePlugin.init({ | ||
redisOptions: { | ||
host: '127.0.0.1', | ||
port: process.env.CI ? +(process.env.E2E_REDIS_PORT || 6379) : 6379, | ||
}, | ||
}), | ||
], | ||
}), | ||
); | ||
|
||
beforeAll(async () => { | ||
await server.init({ | ||
initialData, | ||
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'), | ||
customerCount: 1, | ||
}); | ||
await adminClient.asSuperAdmin(); | ||
cacheService = server.app.get(CacheService); | ||
}, TEST_SETUP_TIMEOUT_MS); | ||
|
||
afterAll(async () => { | ||
await server.destroy(); | ||
}); | ||
|
||
it('get returns undefined for non-existent key', () => | ||
getReturnsUndefinedForNonExistentKey(cacheService)); | ||
|
||
it('sets a key', () => setsAKey(cacheService)); | ||
|
||
it('sets an object value', () => setsObjectValue(cacheService)); | ||
|
||
it('sets an array value', () => setsArrayValue(cacheService)); | ||
|
||
it('sets an array of objects', () => setsArrayOfObjects(cacheService)); | ||
|
||
it('deletes a key', () => deletesAKey(cacheService)); | ||
|
||
it('invalidates by single tag', () => invalidatesBySingleTag(cacheService)); | ||
|
||
it('invalidates by multiple tags', () => invalidatesByMultipleTags(cacheService)); | ||
|
||
it('invalidates many by multiple tags', () => invalidatesManyByMultipleTags(cacheService)); | ||
|
||
it('invalidates a large number of keys by tag', () => invalidatesALargeNumberOfKeysByTag(cacheService)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const PLUGIN_INIT_OPTIONS = Symbol('PLUGIN_INIT_OPTIONS'); | ||
export const loggerCtx = 'RedisCacheStrategy'; | ||
export const DEFAULT_NAMESPACE = 'vendure-cache'; | ||
export const DEFAULT_TTL = 86400 * 30; |
28 changes: 28 additions & 0 deletions
28
packages/core/src/plugin/redis-cache-plugin/redis-cache-plugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { PluginCommonModule } from '../plugin-common.module'; | ||
import { VendurePlugin } from '../vendure-plugin'; | ||
|
||
import { PLUGIN_INIT_OPTIONS } from './constants'; | ||
import { RedisCacheStrategy } from './redis-cache-strategy'; | ||
import { RedisCachePluginInitOptions } from './types'; | ||
|
||
@VendurePlugin({ | ||
imports: [PluginCommonModule], | ||
providers: [{ provide: PLUGIN_INIT_OPTIONS, useFactory: () => RedisCachePlugin.options }], | ||
configuration: config => { | ||
config.systemOptions.cacheStrategy = new RedisCacheStrategy(RedisCachePlugin.options); | ||
return config; | ||
}, | ||
compatibility: '>0.0.0', | ||
}) | ||
export class RedisCachePlugin { | ||
static options: RedisCachePluginInitOptions = { | ||
maxItemSizeInBytes: 128_000, | ||
redisOptions: {}, | ||
namespace: 'vendure-cache', | ||
}; | ||
|
||
static init(options: RedisCachePluginInitOptions) { | ||
this.options = options; | ||
return RedisCachePlugin; | ||
} | ||
} |
107 changes: 107 additions & 0 deletions
107
packages/core/src/plugin/redis-cache-plugin/redis-cache-strategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { JsonCompatible } from '@vendure/common/lib/shared-types'; | ||
|
||
import { Logger } from '../../config/logger/vendure-logger'; | ||
import { CacheStrategy, SetCacheKeyOptions } from '../../config/system/cache-strategy'; | ||
|
||
import { DEFAULT_NAMESPACE, DEFAULT_TTL, loggerCtx } from './constants'; | ||
import { RedisCachePluginInitOptions } from './types'; | ||
|
||
export class RedisCacheStrategy implements CacheStrategy { | ||
private client: import('ioredis').Redis; | ||
|
||
constructor(private options: RedisCachePluginInitOptions) {} | ||
|
||
async init() { | ||
const IORedis = await import('ioredis').then(m => m.default); | ||
this.client = new IORedis.Redis(this.options.redisOptions ?? {}); | ||
this.client.on('error', err => Logger.error(err.message, loggerCtx, err.stack)); | ||
} | ||
async destroy() { | ||
await this.client.quit(); | ||
} | ||
|
||
async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> { | ||
try { | ||
const retrieved = await this.client.get(this.namespace(key)); | ||
if (retrieved) { | ||
try { | ||
return JSON.parse(retrieved); | ||
} catch (e: any) { | ||
Logger.error(`Could not parse cache item ${key}: ${e.message as string}`, loggerCtx); | ||
} | ||
} | ||
} catch (e: any) { | ||
Logger.error(`Could not get cache item ${key}: ${e.message as string}`, loggerCtx); | ||
} | ||
} | ||
async set<T extends JsonCompatible<T>>( | ||
key: string, | ||
value: T, | ||
options?: SetCacheKeyOptions, | ||
): Promise<void> { | ||
try { | ||
const multi = this.client.multi(); | ||
const ttl = options?.ttl ? options.ttl / 1000 : DEFAULT_TTL; | ||
const namedspacedKey = this.namespace(key); | ||
const serializedValue = JSON.stringify(value); | ||
if (this.options.maxItemSizeInBytes) { | ||
if (Buffer.byteLength(serializedValue) > this.options.maxItemSizeInBytes) { | ||
Logger.error( | ||
`Could not set cache item ${key}: item size of ${Buffer.byteLength( | ||
serializedValue, | ||
)} bytes exceeds maxItemSizeInBytes of ${this.options.maxItemSizeInBytes} bytes`, | ||
loggerCtx, | ||
); | ||
return; | ||
} | ||
} | ||
multi.set(namedspacedKey, JSON.stringify(value), 'EX', ttl); | ||
if (options?.tags) { | ||
for (const tag of options.tags) { | ||
multi.sadd(this.tagNamespace(tag), namedspacedKey); | ||
} | ||
} | ||
await multi.exec(); | ||
} catch (e: any) { | ||
Logger.error(`Could not set cache item ${key}: ${e.message as string}`, loggerCtx); | ||
} | ||
} | ||
|
||
async delete(key: string): Promise<void> { | ||
try { | ||
await this.client.del(this.namespace(key)); | ||
} catch (e: any) { | ||
Logger.error(`Could not delete cache item ${key}: ${e.message as string}`, loggerCtx); | ||
} | ||
} | ||
|
||
async invalidateTags(tags: string[]): Promise<void> { | ||
try { | ||
const keys = [ | ||
...(await Promise.all(tags.map(tag => this.client.smembers(this.tagNamespace(tag))))), | ||
]; | ||
const pipeline = this.client.pipeline(); | ||
|
||
keys.forEach(key => { | ||
pipeline.del(key); | ||
}); | ||
|
||
tags.forEach(tag => { | ||
const namespacedTag = this.tagNamespace(tag); | ||
pipeline.del(namespacedTag); | ||
}); | ||
|
||
await pipeline.exec(); | ||
} catch (err) { | ||
return Promise.reject(err); | ||
} | ||
} | ||
|
||
private namespace(key: string) { | ||
return `${this.options.namespace ?? DEFAULT_NAMESPACE}:${key}`; | ||
} | ||
|
||
private tagNamespace(tag: string) { | ||
return `${this.options.namespace ?? DEFAULT_NAMESPACE}:tag:${tag}`; | ||
} | ||
} |
Oops, something went wrong.