diff --git a/docsite/docs/configuration/configuration.mdx b/docsite/docs/configuration/configuration.mdx index 13ce858a..9ffb4941 100644 --- a/docsite/docs/configuration/configuration.mdx +++ b/docsite/docs/configuration/configuration.mdx @@ -245,11 +245,8 @@ WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECR -## Application Options -These options affect multi-scrobbler's behavior and are not specific to any source/client. - -### Base URL +## Base URL Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints. @@ -280,185 +277,82 @@ Useful when running with [docker](../installation/installation.mdx#docker) so th -### Caching - -Multi-scrobbler caches some activities to persist important data across restarts, reduce external API calls, and make some actions faster. - -All of the activities below are **always** cached **in-memory** with an optional, configurable [**secondary** store](#secondary-caching-configuration) for persistence. - - - - -**Queued** and **Failed** Scrobbles are cached so that any un-scrobbled data you have is persisted across restarts of multi-scrobbler. - -:::tip - -By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically. - -If you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this. - -::: - -##### Configuration - -Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values: - - - - -| Environmental Variable | Required? | Default | Description | -| :--------------------- | --------- | --------- | --------------------- | -| `CACHE_SCROBBLE` | No | `file` | The cache type to use | -| `CACHE_SCROBBLE_CONN` | No | `/config` | | +## Caching - +Multi-scrobbler implements caching to persist important data across restarts, reduce external API calls, and make some actions faster. - +A default **in-memory** cache store is used so that you always benefit from some caching. An optional, [**secondary** store](#secondary-caching) can be configured for greater caching capabilities. -```json5 title="config.json" -{ - "cache": { - "scrobble": { - "provider": "file", - "connection": "/config" - } - }, - // ... -} -``` - - + - + - Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts. - -:::tip - -By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically. - -If you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this. - -::: - -##### Configuration - -Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values: - - - - -| Environmental Variable | Required? | Default | Description | -| :--------------------- | --------- | --------- | --------------------- | -| `CACHE_AUTH` | No | `file` | The cache type to use | -| `CACHE_AUTH_CONN` | No | `/config` | | - - - - -```json5 title="config.json" -{ - "cache": { - "auth": { - "provider": "file", - "connection": "/config" - } - }, - // ... -} -``` - - - + +The results of [transform rules](/configuration/transforms) are cached so that if a scrobble with identical data (track/artists/album) is identified and it has the same set of transform rules then the cached transform results can be applied. - API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations. - -By default, these calls are only cached in memory. If you wish for cached calls to be persisted across restarts then setup [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration). - -##### Configuration - -Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values: - - - - -| Environmental Variable | Required? | Default | Description | -| :--------------------- | --------- | ------- | --------------------- | -| `CACHE_METADATA` | No | | The cache type to use | -| `CACHE_METADATA_CONN` | No | | | - - - - - -```json5 title="config.json" -{ - "cache": { - "metadata": { - "provider": "valkey", - "connection": "yourConnectionStringHere" - } - }, - // ... -} -``` - - + -#### Secondary Caching Configuration + -The type of cache used, and its connection properties, can be configured through **ENV** or **AIO** config. +Auth caching defaults to a **file** that is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-auth.cache`. - +This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)). - +If you wish to use the [secondary store](#secondary-caching) for caching Auth you must explicitly configure it. This is because valkey can potentially be *ephemeral* if you do not provide a volume for its data directory. -**File** cache is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-[cacheName].cache`. +To explicitly configure auth to use the secondary store: - -Example - -| Environmental Variable | Required? | Default | Description | -| :--------------------- | --------- | --------- | ------------------------------------------------------------ | -| `CACHE_SCROBBLE` | No | `file` | The cache type to use | -| `CACHE_SCROBBLE_CONN` | No | `/config` | The directory, within the container, to store the cache file | +```yaml title="compose.yaml" +services: + multi-scrobbler: + # ... + environment: + // highlight-start + - CACHE_AUTH=valkey + // highlight-end + # ... +``` -Example - ```json5 title="config.json" { "cache": { - "scrobble": { - "provider": "file", - "connection": "/config" + "auth": { + "provider": "valkey" } }, // ... } ``` - - - - + + +### Secondary Caching Store {#secondary-caching} + +Using a secondary store enables: -[**Valkey**](https://valkey.io/) is an open-source fork of Redis. +* persistence of cached data across restarts +* a larger store (more data is saved) +* a longer time-to-live in the store (cached data is fetchable for a longer period) + +These benefits are particularly beneficial when using [transforms](/configuration/transforms) like Musicbrainz and it is **strongly recommended** for these scenarios. + +Currently, Multi-scrobbler only supports [**Valkey**](https://valkey.io/), an open-source fork of Redis, as a secondary store.
@@ -466,11 +360,13 @@ Example A valkey container can be added to the [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker): + + + ```yaml title="docker-compose.yml" services: multi-scrobbler: # ... - # adding everything below // highlight-start valkey: image: valkey/valkey @@ -483,6 +379,25 @@ volumes: // highlight-end ``` + + + +```yaml title="docker-compose.yml" +services: + multi-scrobbler: + # ... + // highlight-start + valkey: + image: valkey/valkey + volumes: + - ./valkeyData:/data + // highlight-end +``` + + + + + Use `redis://valkey:6379` as the connection string in the configurations below.
@@ -497,12 +412,16 @@ redis://HOST_IP:HOST_PORT -Example - -| Environmental Variable | Required? | Default | Description | -| :--------------------- | --------- | -------- | ------------------------------------------------------------------- | -| `CACHE_METADATA` | Yes | `valkey` | The cache type to use | -| `CACHE_METADATA_CONN` | Yes | | The host/IP and port to connect to EX: `redis://192.168.0.120:6379` | +```yaml title="compose.yaml" +services: + multi-scrobbler: + # ... + environment: + // highlight-start + - CACHE_VALKEY=redis://192.168.0.120:6379 + // highlight-end + # ... +``` @@ -513,10 +432,7 @@ Example ```json5 title="config.json" { "cache": { - "metadata": { - "provider": "valkey", - "connection": "redis://192.168.0.120:6379" - } + "valkey": "redis://192.168.0.120:6379" }, // ... } @@ -524,12 +440,9 @@ Example -
- - -### Debug Mode +## Debug Mode Turning on Debug Mode will @@ -550,7 +463,7 @@ To set debug mode either add it to [AIO `config.json`](./?configType=aio#configu or set the [ENV](./?configType=env#configuration-types) `DEBUG_MODE=true` -### Disable Web +## Disable Web If you do not need the dashboard and/or ingress sources, or have security concerns about ingress and cannot control your hosting environment, the web server and API can be disabled. diff --git a/docsite/docs/configuration/transforms/transforms.mdx b/docsite/docs/configuration/transforms/transforms.mdx index be1a3bca..b5780c69 100644 --- a/docsite/docs/configuration/transforms/transforms.mdx +++ b/docsite/docs/configuration/transforms/transforms.mdx @@ -591,7 +591,7 @@ The output shows the diff between the previous stage (or original Play) and the MS uses [caching](/configuration/#caching) to reduce the number of API calls needed for stages like [Musicbrainz](/configuration/transforms/musicbrainz) and to speed up all transforms by caching steps and results. However, the default caching strategy uses a small cache size and very short [TTLs](https://en.wikipedia.org/wiki/Time_to_live) because it is *in-memory*. -**If you are using any Transform stages you should configure [secondary caching with Valkey for Metadata](/configuration/?cacheType=valkey&cachedThings=metadata#secondary-caching-configuration)** to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS. +**If you are using any Transform stages you should configure [secondary caching](/configuration#secondary-caching)** to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS.
diff --git a/docsite/docs/installation/installation.mdx b/docsite/docs/installation/installation.mdx index 83f6f9bb..93a29296 100644 --- a/docsite/docs/installation/installation.mdx +++ b/docsite/docs/installation/installation.mdx @@ -132,7 +132,7 @@ services: -**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration/?cacheType=valkey#secondary-caching-configuration) to take advantage of faster performance and reduced memory usage. +**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration#secondary-caching) to take advantage of faster performance and reduced memory usage. ```yaml title="docker-compose.yml" services: @@ -142,8 +142,7 @@ services: environment: # ... // highlight-start - CACHE_METADATA=valkey - CACHE_METADATA_CONN=redis://valkey:6379 + CACHE_VALKEY=redis://valkey:6379 // highlight-end # ... diff --git a/docsite/docs/quickstart.mdx b/docsite/docs/quickstart.mdx index 140d106e..f38abac6 100644 --- a/docsite/docs/quickstart.mdx +++ b/docsite/docs/quickstart.mdx @@ -336,7 +336,7 @@ Visit `http://192.168.0.100:9078` to see the dashboard where ## Next Steps * See more advanced docker options as well as other install methods in the [**Installation**](/installation#docker) docs - * Setup [secondary caching](/configuration/?cacheType=valkey#secondary-caching-configuration) with [valkey](/installation?dockerSetting=caching#recommended-settings) for increased performance and reduced memory usage + * Setup [secondary caching](/configuration#secondary-caching) with [valkey](/installation?dockerSetting=caching#recommended-settings) for increased performance and reduced memory usage * Review the [**Configuration**](/configuration) docs * Learn about how to configure multi-scrobbler using files for more complicated Source/Client scenarios * See all available [**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) alongside configuration examples diff --git a/src/backend/common/Cache.ts b/src/backend/common/Cache.ts index a1f19189..b0ed73a6 100644 --- a/src/backend/common/Cache.ts +++ b/src/backend/common/Cache.ts @@ -15,13 +15,14 @@ import path from 'path'; import { cacheFunctions } from "@foxxmd/regex-buddy-core"; import { fileExists, fileOrDirectoryIsWriteable } from '../utils/FSUtils.js'; import { copyFile } from 'fs/promises'; -import { asCacheAuthProvider, asCacheConfig, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js'; +import { asCacheConfig, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheConfigUser, CacheScrobbleProvider } from './infrastructure/Atomic.js'; import { Typeson } from 'typeson'; import { builtin } from 'typeson-registry'; import { loggerNoop } from './MaybeLogger.js'; import { ListenProgressPositional, ListenProgressTS } from '../sources/PlayerState/ListenProgress.js'; const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); import prom, { Gauge } from 'prom-client'; +import { nonEmptyStringOrDefault } from '../../core/StringUtils.js'; dayjs.extend(utc) dayjs.extend(isBetween); @@ -43,6 +44,14 @@ typeson.register({ ListenProgressPositional }); +const unsupportedEnvKeys = [ + 'CACHE_AUTH_CONN', + 'CACHE_METADATA', + 'CACHE_SCROBBLE', + 'CACHE_SCROBBLE_CONN', + 'CACHE_AUTH_CONN' +]; + export class MSCache { config: Required @@ -70,19 +79,19 @@ export class MSCache { const { metadata: { - provider: mProvider = (process.env.CACHE_METADATA as (CacheMetadataProvider | undefined) ?? false), - connection: mConn = process.env.CACHE_METADATA_CONN, - ...restMetadata + provider: mProvider = false, + connection: mConn, + //...restMetadata } = {}, scrobble: { - provider: sProvider = (process.env.CACHE_SCROBBLE as (CacheScrobbleProvider | undefined) ?? 'file'), - connection = (process.env.CACHE_SCROBBLE_CONN ?? configDir), - ...restScrobble + provider: sProvider = false, + connection: sConnection, + //...restScrobble } = {}, auth: { - provider: aProvider = (process.env.CACHE_AUTH as (CacheAuthProvider | undefined) ?? 'file'), - connection: aConn = (process.env.CACHE_AUTH_CONN ?? configDir), - ...restAuth + provider: aProvider = false, + connection: aConn, + //...restAuth } = {}, regex = 200, } = config; @@ -91,17 +100,14 @@ export class MSCache { metadata: { provider: mProvider, connection: mConn, - ...restMetadata, }, scrobble: { provider: sProvider, - connection, - ...restScrobble + connection: sConnection, }, auth: { provider: aProvider, connection: aConn, - ...restAuth }, regex }; @@ -275,10 +281,10 @@ export class MSCache { } } if (config.provider === 'file') { - logger.debug(`Building file cache from ${path.join(config.connection, `${namespace}.cache`)}`); + logger.debug(`Building file cache from ${path.join(config.connection ?? configDir, `${namespace}.cache`)}`); try { - const [keyvFile] = await initFileCache({ ...config, cacheDir: config.connection, cacheId: `${namespace}.cache` }, {ttl: config.ttl}, logger); + const [keyvFile] = await initFileCache({ ...config, cacheDir: config.connection ?? configDir, cacheId: `${namespace}.cache` }, {ttl: config.ttl}, logger); return keyvFile; } catch (e) { throw e; @@ -368,8 +374,8 @@ export const flatCacheCreate = (opts: FlatCacheOptions) => { return new FlatCache({ ttl: 0, lruSize: 2000, - cacheDir: opts.cacheDir ?? configDir, - cacheId: opts.cacheId ?? 'scrobble.cache', + cacheDir: opts.cacheDir, + cacheId: opts.cacheId ?? 'ms.cache', persistInterval: 1 * 1000 * 10, expirationInterval: 1 * 1000 * 10, // 10 seconds ...opts @@ -507,4 +513,86 @@ const noopKeyv: KeyvStoreAdapter = { delete: (_) => undefined, clear: () => Promise.resolve(), on: (_, __) => undefined +} + +export const parseUserConfig = (config: CacheConfigUser = {}, parentLogger: Logger = loggerNoop): CacheConfigOptions => { + const logger = childLogger(parentLogger, 'Cache'); + + let valkeyEnvVal: string | undefined = nonEmptyStringOrDefault(process.env.CACHE_VALKEY); + if(valkeyEnvVal === undefined) { + valkeyEnvVal = nonEmptyStringOrDefault(process.env.CACHE_METADATA_CONN); + if(valkeyEnvVal !== undefined) { + logger.warn('ENV CACHE_METADATA_CONN is deprecated! Replace it with CACHE_VALKEY'); + } + } + + for(const key of unsupportedEnvKeys) { + if(nonEmptyStringOrDefault(process.env[key]) !== undefined) { + logger.warn(`ENV ${key} is no longer supported. Refer to the Caching docs.`); + } + } + + const { + valkey = valkeyEnvVal, + // metadata: { + // provider: mProvider = (process.env.CACHE_METADATA as (CacheMetadataProvider | undefined) ?? false), + // connection: mConn = process.env.CACHE_METADATA_CONN, + // //...restMetadata + // } = {}, + scrobble: { + provider: sProvider = (process.env.CACHE_SCROBBLE as (CacheScrobbleProvider | undefined) ?? 'file'), + connection = (process.env.CACHE_SCROBBLE_CONN ?? configDir), + ...restScrobble + } = {}, + auth: { + provider: aProvider = (process.env.CACHE_AUTH as (CacheAuthProvider | undefined) ?? 'file'), + //...restAuth + } = {}, + regex = 200, + } = config; + + if(config.metadata !== undefined) { + logger.warn('Configuring cache.metadata is no longer supported. Refer to the Caching docs.'); + } + if(config.scrobble !== undefined) { + logger.warn('Configuring cache.scrobble is no longer supported. Refer to the Caching docs.'); + } + if(config.auth?.connection !== undefined) { + logger.warn('Configuring cache.auth.connection is no longer supported. Refer to the Caching docs.'); + } + + let authConn: string, + authProvider = aProvider; + if(authProvider === 'valkey') { + if(valkey === undefined) { + logger.warn(`Auth Provider set to 'valkey' but not valkey connection string was not provided, falling back to file.`); + authConn = configDir; + authProvider = 'file'; + } else { + authConn = valkey; + } + } else { + if(authProvider !== 'file') { + logger.warn(`Unsupported provider given for auth: ${authProvider}`); + } + authConn = configDir; + authProvider = 'file'; + } + + return { + metadata: { + provider: valkey !== undefined ? 'valkey' : false, + connection: valkey, + }, + scrobble: { + provider: sProvider, + connection, + ...restScrobble + }, + auth: { + provider: authProvider, + connection: authConn, + }, + regex + }; } \ No newline at end of file diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts index b5f869f3..eb607941 100644 --- a/src/backend/common/infrastructure/Atomic.ts +++ b/src/backend/common/infrastructure/Atomic.ts @@ -296,6 +296,21 @@ export interface CacheConfigOptions { regex?: number } +export interface CacheConfigUser { + auth?: { + provider: 'valkey' | 'file', + [key: string]: any + }; + valkey?: string + /** Number of regex functions to cache (LRU) + * + * @default 200 + */ + regex?: number + // to allow deprecated scrobble config without having it show up in schema docs + [key: string]: any +} + export interface MusicbrainzApiConfigData { url?: string contact: string, diff --git a/src/backend/common/infrastructure/config/aioConfig.ts b/src/backend/common/infrastructure/config/aioConfig.ts index 51c11778..6b7f9dfd 100644 --- a/src/backend/common/infrastructure/config/aioConfig.ts +++ b/src/backend/common/infrastructure/config/aioConfig.ts @@ -5,7 +5,7 @@ import { RequestRetryOptions } from "./common.js"; import { WebhookConfig } from "./health/webhooks.js"; import { CommonSourceOptions, SourceRetryOptions } from "./source/index.js"; import { SourceAIOConfig } from "./source/sources.js"; -import { CacheConfigOptions, DurationValue } from "../Atomic.js"; +import { CacheConfigOptions, CacheConfigUser, DurationValue } from "../Atomic.js"; import { TransformerCommonConfig } from "../../../../core/Atomic.js"; import { RetentionConfig } from "./database.js"; @@ -67,7 +67,7 @@ export interface AIOConfig { * */ debugMode?: boolean - cache?: CacheConfigOptions + cache?: CacheConfigUser transformers?: TransformerCommonConfig[] diff --git a/src/backend/index.ts b/src/backend/index.ts index d5e9615a..0f1e657c 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -24,6 +24,7 @@ import { Notifiers } from './notifier/Notifiers.js'; import { getDb, performDbMigrationWithBackup } from './common/database/drizzle/drizzleUtils.js'; import { getDbPath } from './common/database/Database.js'; import { createRetentionCleanupTask } from './tasks/retentionCleanup.js'; +import { parseUserConfig } from './common/Cache.js'; dayjs.extend(utc) dayjs.extend(isBetween); @@ -75,6 +76,7 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`) webhooks = [], logging = {}, debugMode, + cache, } = (config || {}) as AIOConfig; if (process.env.DEBUG_MODE === undefined && debugMode !== undefined) { @@ -100,6 +102,7 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`) const root = getRoot({ ...config, + cache: parseUserConfig(cache, logger), logger, loggingConfig: logging, loggerStream: appLoggerStream, diff --git a/src/backend/tests/utils/TransientTestUtils.ts b/src/backend/tests/utils/TransientTestUtils.ts index 3f8f51da..713f8dd4 100644 --- a/src/backend/tests/utils/TransientTestUtils.ts +++ b/src/backend/tests/utils/TransientTestUtils.ts @@ -2,7 +2,7 @@ import { loggerTest } from "@foxxmd/logging"; import { MSCache } from "../../common/Cache.js"; import { getDb, migrateDbSync } from "../../common/database/drizzle/drizzleUtils.js"; -export const transientCache = () => new MSCache(loggerTest, { scrobble: { provider: 'memory' }, auth: { provider: 'memory' }, metadata: { provider: 'memory' } }); +export const transientCache = () => new MSCache(loggerTest); export const transientDb = () => { const db = getDb(':memory:');