From 2bc4e0290337b78ae68b444049eddc3e27b2d69b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 22 Oct 2025 17:32:10 +0200 Subject: [PATCH 1/8] fix(cache): improve support for Valkey Signed-off-by: Benjamin Janssens --- .changeset/sour-lines-taste.md | 5 + docs/backend-system/core-services/cache.md | 5 +- packages/backend-defaults/config.d.ts | 22 +--- .../entrypoints/cache/CacheManager.test.ts | 70 ++++++++--- .../src/entrypoints/cache/CacheManager.ts | 119 +++++++++++++----- .../src/entrypoints/cache/types.ts | 15 ++- 6 files changed, 166 insertions(+), 70 deletions(-) create mode 100644 .changeset/sour-lines-taste.md diff --git a/.changeset/sour-lines-taste.md b/.changeset/sour-lines-taste.md new file mode 100644 index 0000000000..f9a316a800 --- /dev/null +++ b/.changeset/sour-lines-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Improved support for Valkey diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index ad4d04b50a..120cc0c510 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -27,6 +27,9 @@ backend: # Other Redis-specific options... clearBatchSize: 1000 useUnlink: false + valkey: + # Optional: Global namespace prefix for all cache keys (including separator used between namespace and plugin ID) + keyPrefix: 'my-app:' ``` ### Namespace Configuration @@ -36,7 +39,7 @@ For Redis and Valkey stores, you can configure a global namespace that will be p - **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 `:`). +For Redis, `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. diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index f0632a37bb..84212cbe6a 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -710,27 +710,9 @@ export interface Config { */ client?: { /** - * Namespace for the current instance. + * Namespace and separator used for prefixing keys. */ - namespace?: string; - /** - * Separator to use between namespace and key. - */ - keyPrefixSeparator?: string; - /** - * Number of keys to delete in a single batch. - */ - clearBatchSize?: number; - /** - * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions. - */ - useUnlink?: boolean; - /** - * Whether to allow clearing all keys when no namespace is set. - * If set to true and no namespace is set, iterate() will return all keys. - * Defaults to `false`. - */ - noNamespaceAffectsAll?: boolean; + keyPrefix?: string; }; /** * An optional Valkey cluster (redis cluster under the hood) configuration. diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index a308caa222..6b300e1b60 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -336,11 +336,17 @@ describe('CacheManager store options', () => { it('correctly applies namespace configuration to redis and valkey stores', () => { const testCases = [ - { store: 'redis', namespace: 'test1', separator: ':' }, - { store: 'valkey', namespace: 'test2', separator: '!' }, + { + store: 'redis', + client: { + namespace: 'my-app', + keyPrefixSeparator: ':', + }, + }, + { store: 'valkey', client: { keyPrefix: 'my-app:' } }, ]; - testCases.forEach(({ store, namespace, separator }) => { + testCases.forEach(({ store, client }) => { const manager = CacheManager.fromConfig( mockServices.rootConfig({ data: { @@ -349,10 +355,7 @@ describe('CacheManager store options', () => { store, connection: 'redis://localhost:6379', [store]: { - client: { - namespace, - keyPrefixSeparator: separator, - }, + client, }, }, }, @@ -364,16 +367,16 @@ describe('CacheManager store options', () => { if (store === 'redis') { // eslint-disable-next-line jest/no-conditional-expect - expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { - namespace, - keyPrefixSeparator: separator, - }); + expect(KeyvRedis).toHaveBeenCalledWith( + 'redis://localhost:6379', + client, + ); } else if (store === 'valkey') { // eslint-disable-next-line jest/no-conditional-expect - expect(KeyvValkey).toHaveBeenCalledWith('redis://localhost:6379', { - namespace, - keyPrefixSeparator: separator, - }); + expect(KeyvValkey).toHaveBeenCalledWith( + 'redis://localhost:6379', + client, + ); } }); }); @@ -408,8 +411,9 @@ describe('CacheManager store options', () => { expect(result).toBe('testPlugin'); }); - it('returns pluginId when store options have no namespace', () => { + it('returns pluginId when store options have no namespace (redis)', () => { const storeOptions = { + type: 'redis', client: { keyPrefixSeparator: ':', }, @@ -421,8 +425,9 @@ describe('CacheManager store options', () => { expect(result).toBe('testPlugin'); }); - it('combines namespace and pluginId with default separator', () => { + it('combines namespace and pluginId with default separator (redis)', () => { const storeOptions = { + type: 'redis', client: { namespace: 'my-app', keyPrefixSeparator: ':', @@ -435,8 +440,9 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app:testPlugin'); }); - it('combines namespace and pluginId with custom separator', () => { + it('combines namespace and pluginId with custom separator (redis)', () => { const storeOptions = { + type: 'redis', client: { namespace: 'my-app', keyPrefixSeparator: '-', @@ -449,8 +455,9 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app-testPlugin'); }); - it('uses default separator when keyPrefixSeparator is not provided', () => { + it('uses default separator when keyPrefixSeparator is not provided (redis)', () => { const storeOptions = { + type: 'redis', client: { namespace: 'my-app', }, @@ -462,6 +469,31 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app:testPlugin'); }); + it('returns pluginId when store options have no keyPrefix (valkey)', () => { + const storeOptions = { + type: 'valkey', + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + + it('uses keyPrefix (valkey)', () => { + const storeOptions = { + type: 'valkey', + client: { + keyPrefix: '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: { diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 06b2b0c57b..4e57fcc8d0 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -29,6 +29,7 @@ import { RedisCacheStoreOptions, InfinispanClientBehaviorOptions, InfinispanServerConfig, + ValkeyCacheStoreOptions, } from './types'; import { InfinispanOptionsMapper } from './providers/infinispan/InfinispanOptionsMapper'; import { durationToMilliseconds } from '@backstage/types'; @@ -140,37 +141,43 @@ export class CacheManager { ); } - if (store === 'redis' || store === 'valkey') { - return CacheManager.parseRedisOptions( - store, - storeConfigPath, - config, - logger, - ); + switch (store) { + case 'redis': + return CacheManager.parseRedisOptions( + store, + storeConfigPath, + config, + logger, + ); + case 'valkey': + return CacheManager.parseValkeyOptions( + store, + storeConfigPath, + config, + logger, + ); + case 'infinispan': + return InfinispanOptionsMapper.parseInfinispanOptions( + storeConfigPath, + config, + logger, + ); + default: + return undefined; } - - if (store === 'infinispan') { - return InfinispanOptionsMapper.parseInfinispanOptions( - storeConfigPath, - config, - logger, - ); - } - - return undefined; } /** * Parse Redis-specific options from configuration. */ private static parseRedisOptions( - store: string, + store: 'redis', storeConfigPath: string, config: RootConfigService, logger?: LoggerService, ): RedisCacheStoreOptions { const redisOptions: RedisCacheStoreOptions = { - type: store as 'redis' | 'valkey', + type: store, }; const redisConfig = @@ -213,6 +220,52 @@ export class CacheManager { return redisOptions; } + /** + * Parse Valkey-specific options from configuration. + */ + private static parseValkeyOptions( + store: 'valkey', + storeConfigPath: string, + config: RootConfigService, + logger?: LoggerService, + ): ValkeyCacheStoreOptions { + const valkeyOptions: ValkeyCacheStoreOptions = { + type: store, + }; + + const valkeyConfig = + config.getOptionalConfig(storeConfigPath) ?? new ConfigReader({}); + + valkeyOptions.client = { + keyPrefix: valkeyConfig.getOptionalString('client.keyPrefix'), + }; + + if (valkeyConfig.has('cluster')) { + const clusterConfig = valkeyConfig.getConfig('cluster'); + + if (!clusterConfig.has('rootNodes')) { + logger?.warn( + `Redis cluster config has no 'rootNodes' key, defaulting to non-clustered mode`, + ); + return valkeyOptions; + } + + valkeyOptions.cluster = { + rootNodes: clusterConfig.get('rootNodes'), + defaults: clusterConfig.getOptional('defaults'), + minimizeConnections: clusterConfig.getOptionalBoolean( + 'minimizeConnections', + ), + useReplicas: clusterConfig.getOptionalBoolean('useReplicas'), + maxCommandRedirections: clusterConfig.getOptionalNumber( + 'maxCommandRedirections', + ), + }; + } + + return valkeyOptions; + } + /** * Construct the full namespace based on the options and pluginId. * @@ -222,13 +275,23 @@ export class CacheManager { */ private static constructNamespace( pluginId: string, - storeOptions: RedisCacheStoreOptions | undefined, + storeOptions: RedisCacheStoreOptions | ValkeyCacheStoreOptions | undefined, ): string { - const prefix = storeOptions?.client?.namespace - ? `${storeOptions.client.namespace}${ - storeOptions.client.keyPrefixSeparator ?? ':' - }` - : ''; + let prefix: string; + switch (storeOptions?.type) { + case 'redis': + prefix = storeOptions?.client?.namespace + ? `${storeOptions.client.namespace}${ + storeOptions.client.keyPrefixSeparator ?? ':' + }` + : ''; + break; + case 'valkey': + prefix = storeOptions.client?.keyPrefix ?? ''; + break; + default: + prefix = ''; + } return `${prefix}${pluginId}`; } @@ -317,7 +380,7 @@ export class CacheManager { private createValkeyStoreFactory(): StoreFactory { const KeyvValkey = require('@keyv/valkey').default; - const { createCluster } = require('@keyv/valkey'); + const { createCluster } = require('@keyv/redis'); const stores: Record = {}; return (pluginId, defaultTtl) => { @@ -327,9 +390,7 @@ export class CacheManager { ); } if (!stores[pluginId]) { - const valkeyOptions = this.storeOptions?.client || { - keyPrefixSeparator: ':', - }; + const valkeyOptions = this.storeOptions?.client; if (this.storeOptions?.cluster) { // Create a Valkey cluster (Redis cluster under the hood) const cluster = createCluster(this.storeOptions?.cluster); diff --git a/packages/backend-defaults/src/entrypoints/cache/types.ts b/packages/backend-defaults/src/entrypoints/cache/types.ts index 522ab2ac3e..60e32e8618 100644 --- a/packages/backend-defaults/src/entrypoints/cache/types.ts +++ b/packages/backend-defaults/src/entrypoints/cache/types.ts @@ -17,6 +17,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { HumanDuration, durationToMilliseconds } from '@backstage/types'; import { RedisClusterOptions, KeyvRedisOptions } from '@keyv/redis'; +import { KeyvValkeyOptions } from '@keyv/valkey'; /** * Options for Redis cache store. @@ -24,11 +25,22 @@ import { RedisClusterOptions, KeyvRedisOptions } from '@keyv/redis'; * @public */ export type RedisCacheStoreOptions = { - type: 'redis' | 'valkey'; + type: 'redis'; client?: KeyvRedisOptions; cluster?: RedisClusterOptions; }; +/** + * Options for Valkey cache store. + * + * @public + */ +export type ValkeyCacheStoreOptions = { + type: 'valkey'; + client?: KeyvValkeyOptions; + cluster?: RedisClusterOptions; +}; + /** * Union type of all cache store options. * @@ -36,6 +48,7 @@ export type RedisCacheStoreOptions = { */ export type CacheStoreOptions = | RedisCacheStoreOptions + | ValkeyCacheStoreOptions | InfinispanCacheStoreOptions; /** From 0f5a6af4b1d32ca423727608b5340fdbf7249317 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 24 Oct 2025 17:31:34 +0200 Subject: [PATCH 2/8] test(cache): combine tests Signed-off-by: Benjamin Janssens --- .../entrypoints/cache/CacheManager.test.ts | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index 6b300e1b60..323ca881c5 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -411,21 +411,29 @@ describe('CacheManager store options', () => { expect(result).toBe('testPlugin'); }); - it('returns pluginId when store options have no namespace (redis)', () => { - const storeOptions = { + it.each([ + { type: 'redis', - client: { - keyPrefixSeparator: ':', - }, - }; - const result = (CacheManager as any).constructNamespace( - 'testPlugin', - storeOptions, - ); - expect(result).toBe('testPlugin'); - }); + field: 'namespace', + client: { keyPrefixSeparator: ':' }, + }, + { type: 'valkey', field: 'keyPrefix', client: {} }, + ])( + 'returns pluginId when store options have no $field for $type', + ({ type, client }) => { + const storeOptions = { + type, + client, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }, + ); - it('combines namespace and pluginId with default separator (redis)', () => { + it('combines namespace and pluginId with default separator for redis', () => { const storeOptions = { type: 'redis', client: { @@ -440,7 +448,7 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app:testPlugin'); }); - it('combines namespace and pluginId with custom separator (redis)', () => { + it('combines namespace and pluginId with custom separator for redis', () => { const storeOptions = { type: 'redis', client: { @@ -455,7 +463,7 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app-testPlugin'); }); - it('uses default separator when keyPrefixSeparator is not provided (redis)', () => { + it('uses default separator when keyPrefixSeparator is not provided for redis', () => { const storeOptions = { type: 'redis', client: { @@ -469,18 +477,7 @@ describe('CacheManager store options', () => { expect(result).toBe('my-app:testPlugin'); }); - it('returns pluginId when store options have no keyPrefix (valkey)', () => { - const storeOptions = { - type: 'valkey', - }; - const result = (CacheManager as any).constructNamespace( - 'testPlugin', - storeOptions, - ); - expect(result).toBe('testPlugin'); - }); - - it('uses keyPrefix (valkey)', () => { + it('uses keyPrefix for valkey', () => { const storeOptions = { type: 'valkey', client: { From ad96fb1ed76b0cd9b2aab67f0bb1b7db6e5f1fea Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Nov 2025 14:11:52 +0100 Subject: [PATCH 3/8] refactor(cache): remove store parameter from parseRedisOptions and parseValkeyOptions; improve log message for Valkey Signed-off-by: Benjamin Janssens --- .../src/entrypoints/cache/CacheManager.ts | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 4e57fcc8d0..8724e64e24 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -143,19 +143,9 @@ export class CacheManager { switch (store) { case 'redis': - return CacheManager.parseRedisOptions( - store, - storeConfigPath, - config, - logger, - ); + return CacheManager.parseRedisOptions(storeConfigPath, config, logger); case 'valkey': - return CacheManager.parseValkeyOptions( - store, - storeConfigPath, - config, - logger, - ); + return CacheManager.parseValkeyOptions(storeConfigPath, config, logger); case 'infinispan': return InfinispanOptionsMapper.parseInfinispanOptions( storeConfigPath, @@ -171,13 +161,12 @@ export class CacheManager { * Parse Redis-specific options from configuration. */ private static parseRedisOptions( - store: 'redis', storeConfigPath: string, config: RootConfigService, logger?: LoggerService, ): RedisCacheStoreOptions { const redisOptions: RedisCacheStoreOptions = { - type: store, + type: 'redis', }; const redisConfig = @@ -224,13 +213,12 @@ export class CacheManager { * Parse Valkey-specific options from configuration. */ private static parseValkeyOptions( - store: 'valkey', storeConfigPath: string, config: RootConfigService, logger?: LoggerService, ): ValkeyCacheStoreOptions { const valkeyOptions: ValkeyCacheStoreOptions = { - type: store, + type: 'valkey', }; const valkeyConfig = @@ -245,7 +233,7 @@ export class CacheManager { if (!clusterConfig.has('rootNodes')) { logger?.warn( - `Redis cluster config has no 'rootNodes' key, defaulting to non-clustered mode`, + `Valkey cluster config has no 'rootNodes' key, defaulting to non-clustered mode`, ); return valkeyOptions; } From 6e69c407cd16bcf63554fcefbb2eaf192ec7097c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Nov 2025 14:57:24 +0100 Subject: [PATCH 4/8] docs(cache): add store-specific config Signed-off-by: Benjamin Janssens --- docs/backend-system/core-services/cache.md | 70 ++++++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index 120cc0c510..188ae0791a 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -11,27 +11,84 @@ This service lets your plugin interact with a cache. It is bound to your plugin The cache service can be configured using the `backend.cache` section in your `app-config.yaml`: +### In-Memory (default) + ```yaml backend: cache: - store: redis # or 'valkey', 'memcache', 'memory' + store: memory +``` + +### Memcache + +```yaml +backend: + cache: + store: memcache + connection: user:pass@cache.example.com:11211 +``` + +### Redis + +```yaml +backend: + cache: + store: redis connection: redis://localhost:6379 - # Store-specific configuration (Redis/Valkey only) + # Store-specific configuration (optional) redis: client: - # Optional: Global namespace prefix for all cache keys + # Global namespace prefix for all cache keys namespace: 'my-app' - # Optional: Separator used between namespace and plugin ID (default: ':') + # Separator used between namespace and plugin ID (default: ':') keyPrefixSeparator: ':' # Other Redis-specific options... clearBatchSize: 1000 useUnlink: false +``` + +### Valkey + +```yaml +backend: + cache: + store: valkey + connection: redis://localhost:6379 + + # Store-specific configuration (optional) valkey: - # Optional: Global namespace prefix for all cache keys (including separator used between namespace and plugin ID) + # Global namespace prefix for all cache keys (including separator used between namespace and plugin ID) keyPrefix: 'my-app:' ``` +### Infinispan + +```yaml +backend: + cache: + store: infinispan + + # Store-specific configuration (optional) + infinispan: + servers: + # IP address or hostname of the server (default: '127.0.0.1') + - host: 127.0.0.1 + # Port number of the server (default: '11222') + port: 11222 + # Name of the cache (default: 'cache') + cacheName: cache + mediaType: application/json + authentication: + # Whether authentication is enabled (default: 'false') + enabled: true + userName: yourusername + password: yourpassword + saslMechanism: PLAIN +``` + +A full list of configuration items is available [here](https://docs.jboss.org/infinispan/hotrod-clients/javascript/1.0/apidocs/module-infinispan.html), including support for backup clusters. + ### Namespace Configuration For Redis and Valkey stores, you can configure a global namespace that will be prefixed to all cache keys: @@ -40,8 +97,9 @@ For Redis and Valkey stores, you can configure a global namespace that will be p - **With namespace**: Cache keys use the format `namespace:pluginId:key` (e.g., `my-app:catalog:some-key`) For Redis, `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). +For Valkey, you set the full `keyPrefix` including the separator. -**Note**: Memory and Memcache stores do not support namespace configuration and will always use the plugin ID directly. +**Note**: In-memory, Memcache and Infinispan stores do not support namespace configuration and will always use the plugin ID directly. ## Using the service From d857bc43888937f384bfd9e764c778bffdb1e450 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Nov 2025 15:00:36 +0100 Subject: [PATCH 5/8] docs(cache): improve docs Signed-off-by: Benjamin Janssens --- docs/backend-system/core-services/cache.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index 188ae0791a..a23332409b 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -96,8 +96,8 @@ For Redis and Valkey stores, you can configure a global namespace that will be p - **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`) -For Redis, `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). -For Valkey, you set the full `keyPrefix` including the separator. +For **Redis**, `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). +For **Valkey**, you set the full `keyPrefix` including the separator. **Note**: In-memory, Memcache and Infinispan stores do not support namespace configuration and will always use the plugin ID directly. From 1a51cd99b0c97ec41d7be56e24b7a5ca5d19c7c7 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Nov 2025 15:16:57 +0100 Subject: [PATCH 6/8] chore(cache): improve changeset Signed-off-by: Benjamin Janssens --- .changeset/sour-lines-taste.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.changeset/sour-lines-taste.md b/.changeset/sour-lines-taste.md index f9a316a800..baed435419 100644 --- a/.changeset/sour-lines-taste.md +++ b/.changeset/sour-lines-taste.md @@ -1,5 +1,21 @@ --- -'@backstage/backend-defaults': patch +'@backstage/backend-defaults': minor --- -Improved support for Valkey +**BREAKING** The correct configuration options for Valkey are now being used. + +These changes are **required** to `app-config.yaml`: + +```diff +backend: + cache: + valkey: + client: +- namespace: 'my-app' +- keyPrefixSeparator: ':' ++ keyPrefix: 'my-app:' +- clearBatchSize: 1000 +- useUnlink: false +``` + +In comparison to Redis, Valkey requires the full `keyPrefix` including the separator to be specified instead of separate `namespace` and `keyPrefixSeparator` options. Also, Valkey does not support the `clearBatchSize` and `useUnlink` options. From 4ed29d5b7ff38b7cf8bd81f9b5179657471d9f0b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Nov 2025 16:36:13 +0100 Subject: [PATCH 7/8] chore(cache): add comment regarding usage of createCluster for Valkey Signed-off-by: Benjamin Janssens --- packages/backend-defaults/src/entrypoints/cache/CacheManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 8724e64e24..f65a2187c0 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -368,6 +368,8 @@ export class CacheManager { private createValkeyStoreFactory(): StoreFactory { const KeyvValkey = require('@keyv/valkey').default; + // `@keyv/valkey` doesn't export a `createCluster` function, but is compatible with the one from `@keyv/redis` + // See https://keyv.org/docs/storage-adapters/valkey const { createCluster } = require('@keyv/redis'); const stores: Record = {}; From 907f98285169ede6d4ae31ced948599f40435226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 15 Dec 2025 22:02:15 +0100 Subject: [PATCH 8/8] Update .changeset/sour-lines-taste.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/sour-lines-taste.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.changeset/sour-lines-taste.md b/.changeset/sour-lines-taste.md index baed435419..83868d6a57 100644 --- a/.changeset/sour-lines-taste.md +++ b/.changeset/sour-lines-taste.md @@ -9,13 +9,14 @@ These changes are **required** to `app-config.yaml`: ```diff backend: cache: - valkey: - client: -- namespace: 'my-app' -- keyPrefixSeparator: ':' -+ keyPrefix: 'my-app:' -- clearBatchSize: 1000 -- useUnlink: false + store: valkey + connection: ... + client: +- namespace: 'my-app' +- keyPrefixSeparator: ':' ++ keyPrefix: 'my-app:' +- clearBatchSize: 1000 +- useUnlink: false ``` In comparison to Redis, Valkey requires the full `keyPrefix` including the separator to be specified instead of separate `namespace` and `keyPrefixSeparator` options. Also, Valkey does not support the `clearBatchSize` and `useUnlink` options.