add logic to construct namespace from provided namespace and plugin id

Signed-off-by: Shijun Wang <shijun@unity3d.com>
This commit is contained in:
Shijun Wang
2025-07-29 15:53:19 +03:00
committed by Shijun Wang
parent 6135c091ed
commit 4eda590b6a
4 changed files with 202 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace.
@@ -7,6 +7,39 @@ description: Documentation for the Cache service
This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace.
## Configuration
The cache service can be configured using the `backend.cache` section in your `app-config.yaml`:
```yaml
backend:
cache:
store: redis # or 'valkey', 'memcache', 'memory'
connection: redis://localhost:6379
# Store-specific configuration (Redis/Valkey only)
redis:
client:
# Optional: Global namespace prefix for all cache keys
namespace: 'my-app'
# Optional: Separator used between namespace and plugin ID (default: ':')
keyPrefixSeparator: ':'
# Other Redis-specific options...
clearBatchSize: 1000
useUnlink: false
```
### Namespace Configuration
For Redis and Valkey stores, you can configure a global namespace that will be prefixed to all cache keys:
- **Without namespace**: Cache keys use only the plugin ID (e.g., `catalog:some-key`)
- **With namespace**: Cache keys use the format `namespace:pluginId:key` (e.g., `my-app:catalog:some-key`)
The `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`).
**Note**: Memory and Memcache stores do not support namespace configuration and will always use the plugin ID directly.
## Using the service
The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache.
@@ -333,4 +333,147 @@ describe('CacheManager store options', () => {
keyPrefixSeparator: '!',
});
});
it('correctly applies namespace configuration to redis and valkey stores', () => {
const testCases = [
{ store: 'redis', namespace: 'test1', separator: ':' },
{ store: 'valkey', namespace: 'test2', separator: '!' },
];
testCases.forEach(({ store, namespace, separator }) => {
const manager = CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: {
cache: {
store,
connection: 'redis://localhost:6379',
[store]: {
client: {
namespace,
keyPrefixSeparator: separator,
},
},
},
},
},
}),
);
manager.forPlugin('testPlugin');
if (store === 'redis') {
// eslint-disable-next-line jest/no-conditional-expect
expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', {
namespace,
keyPrefixSeparator: separator,
});
} else if (store === 'valkey') {
// eslint-disable-next-line jest/no-conditional-expect
expect(KeyvValkey).toHaveBeenCalledWith('redis://localhost:6379', {
namespace,
keyPrefixSeparator: separator,
});
}
});
});
it('falls back to pluginId when no namespace is configured', () => {
const manager = CacheManager.fromConfig(
mockServices.rootConfig({
data: {
backend: {
cache: {
store: 'redis',
connection: 'redis://localhost:6379',
},
},
},
}),
);
manager.forPlugin('testPlugin');
expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', {
keyPrefixSeparator: ':',
});
});
describe('Namespace construction', () => {
it('returns pluginId when no store options are provided', () => {
const result = (CacheManager as any).constructNamespace(
'testPlugin',
undefined,
);
expect(result).toBe('testPlugin');
});
it('returns pluginId when store options have no namespace', () => {
const storeOptions = {
client: {
keyPrefixSeparator: ':',
},
};
const result = (CacheManager as any).constructNamespace(
'testPlugin',
storeOptions,
);
expect(result).toBe('testPlugin');
});
it('combines namespace and pluginId with default separator', () => {
const storeOptions = {
client: {
namespace: 'my-app',
keyPrefixSeparator: ':',
},
};
const result = (CacheManager as any).constructNamespace(
'testPlugin',
storeOptions,
);
expect(result).toBe('my-app:testPlugin');
});
it('combines namespace and pluginId with custom separator', () => {
const storeOptions = {
client: {
namespace: 'my-app',
keyPrefixSeparator: '-',
},
};
const result = (CacheManager as any).constructNamespace(
'testPlugin',
storeOptions,
);
expect(result).toBe('my-app-testPlugin');
});
it('uses default separator when keyPrefixSeparator is not provided', () => {
const storeOptions = {
client: {
namespace: 'my-app',
},
};
const result = (CacheManager as any).constructNamespace(
'testPlugin',
storeOptions,
);
expect(result).toBe('my-app:testPlugin');
});
it('handles empty namespace by falling back to pluginId', () => {
const storeOptions = {
client: {
namespace: '',
keyPrefixSeparator: ':',
},
};
const result = (CacheManager as any).constructNamespace(
'testPlugin',
storeOptions,
);
expect(result).toBe('testPlugin');
});
});
});
@@ -213,6 +213,25 @@ export class CacheManager {
return redisOptions;
}
/**
* Construct the full namespace based on the options and pluginId.
*
* @param pluginId - The plugin ID to namespace
* @param storeOptions - Optional cache store configuration options
* @returns The constructed namespace string combining the configured namespace with pluginId
*/
private static constructNamespace(
pluginId: string,
storeOptions: CacheStoreOptions | undefined,
): string {
if (storeOptions?.client?.namespace) {
const separator = storeOptions.client.keyPrefixSeparator ?? ':';
return `${storeOptions.client.namespace}${separator}${pluginId}`;
}
return pluginId;
}
/** @internal */
constructor(
store: string,
@@ -286,7 +305,7 @@ export class CacheManager {
});
}
return new Keyv({
namespace: pluginId,
namespace: CacheManager.constructNamespace(pluginId, this.storeOptions),
ttl: defaultTtl,
store: stores[pluginId],
emitErrors: false,
@@ -326,7 +345,7 @@ export class CacheManager {
});
}
return new Keyv({
namespace: pluginId,
namespace: CacheManager.constructNamespace(pluginId, this.storeOptions),
ttl: defaultTtl,
store: stores[pluginId],
emitErrors: false,