diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index 42a899e7d..8f1133413 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -63,6 +63,43 @@ const safe_json_parse = (value, fallback) => { } }; +const redisGetJsonMany = async (keys) => { + if ( !Array.isArray(keys) || keys.length === 0 ) { + return new Map(); + } + + const uniqueKeys = [...new Set(keys)]; + let valuesByIndex = null; + + // MGET over Redis Cluster can fail for cross-slot keys; use pipelined GETs there. + if ( typeof redisClient.nodes === 'function' ) { + const pipeline = redisClient.pipeline(); + for ( const key of uniqueKeys ) { + pipeline.get(key); + } + const results = await pipeline.exec(); + if ( Array.isArray(results) ) { + valuesByIndex = results.map((item) => { + if ( !Array.isArray(item) || item.length < 2 ) return null; + const [error, value] = item; + return error ? null : value; + }); + } + } else if ( typeof redisClient.mget === 'function' ) { + valuesByIndex = await redisClient.mget(...uniqueKeys); + } + + if ( ! Array.isArray(valuesByIndex) ) { + valuesByIndex = await Promise.all(uniqueKeys.map(key => redisClient.get(key))); + } + + const valuesByKey = new Map(); + for ( let i = 0; i < uniqueKeys.length; i++ ) { + valuesByKey.set(uniqueKeys[i], safe_json_parse(valuesByIndex[i], null)); + } + return valuesByKey; +}; + const buildAppIconUrl = (app_uid, size = DEFAULT_APP_ICON_SIZE) => { if ( ! app_uid ) return null; const uid_string = String(app_uid); @@ -500,44 +537,54 @@ export const get_apps = spanify('get_apps', async (specifiers, options = {}) => } }; - for ( const spec of normalized ) { + const cacheLookupPlan = normalized.map((spec) => { if ( spec.uid ) { - const cached = await AppRedisCacheSpace.getCachedApp({ + return { lookup: 'uid', value: spec.uid, - rawIcon: rawIcon, - }); - if ( cached ) { - addApp(decorateApp(cached)); - } else { - queueMissing('uid', spec.uid); - } - continue; + cacheKey: AppRedisCacheSpace.key({ + lookup: 'uid', + value: spec.uid, + rawIcon, + }), + }; } if ( spec.name ) { - const cached = await AppRedisCacheSpace.getCachedApp({ + return { lookup: 'name', value: spec.name, - rawIcon: rawIcon, - }); - if ( cached ) { - addApp(decorateApp(cached)); - } else { - queueMissing('name', spec.name); - } - continue; + cacheKey: AppRedisCacheSpace.key({ + lookup: 'name', + value: spec.name, + rawIcon, + }), + }; } if ( spec.id ) { - const cached = await AppRedisCacheSpace.getCachedApp({ + return { lookup: 'id', value: spec.id, - rawIcon: rawIcon, - }); - if ( cached ) { - addApp(decorateApp(cached)); - } else { - queueMissing('id', spec.id); - } + cacheKey: AppRedisCacheSpace.key({ + lookup: 'id', + value: spec.id, + rawIcon, + }), + }; + } + return null; + }); + + const cachedAppsByKey = await redisGetJsonMany( + cacheLookupPlan.filter(Boolean).map(item => item.cacheKey), + ); + + for ( const plannedLookup of cacheLookupPlan ) { + if ( ! plannedLookup ) continue; + const cached = cachedAppsByKey.get(plannedLookup.cacheKey); + if ( cached ) { + addApp(decorateApp(cached)); + } else { + queueMissing(plannedLookup.lookup, plannedLookup.value); } } diff --git a/src/backend/src/modules/apps/AppIconService.js b/src/backend/src/modules/apps/AppIconService.js index 4b3e1a24a..6c817561f 100644 --- a/src/backend/src/modules/apps/AppIconService.js +++ b/src/backend/src/modules/apps/AppIconService.js @@ -38,8 +38,8 @@ const DEFAULT_ICON_SIZE = 128; const RAW_BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/; const LEGACY_ICON_FILENAME = ({ appUid, size }) => `${appUid}-${size}.png`; const ORIGINAL_ICON_FILENAME = ({ appUid }) => `${appUid}.png`; -const REDIRECT_MAX_AGE_SIZE = 30 * 24 * 60 * 60; // 1 month -const REDIRECT_MAX_AGE_ORIGINAL = 7 * 24 * 60 * 60; // 1 week +const REDIRECT_MAX_AGE_SIZE = 15 * 60; // 15 min +const REDIRECT_MAX_AGE_ORIGINAL = 60; // 1 min /** * AppIconService handles icon generation and serving for apps. @@ -344,8 +344,10 @@ export class AppIconService extends BaseService { async ensureAppIconsSubdomain ({ dirAppIcons }) { const dbSites = this.services.get('database').get(DB_WRITE, 'sites'); - const existing = await dbSites.read('SELECT * FROM subdomains WHERE subdomain = ? LIMIT 1', - [APP_ICONS_SUBDOMAIN]); + const existing = await dbSites.read( + 'SELECT * FROM subdomains WHERE subdomain = ? LIMIT 1', + [APP_ICONS_SUBDOMAIN], + ); if ( existing[0] ) return existing[0]; const systemUser = await get_user({ username: 'system' }); @@ -362,8 +364,10 @@ export class AppIconService extends BaseService { `sd-${this.modules.uuidv4()}`, ]); - const rows = await dbSites.read('SELECT * FROM subdomains WHERE subdomain = ? LIMIT 1', - [APP_ICONS_SUBDOMAIN]); + const rows = await dbSites.read( + 'SELECT * FROM subdomains WHERE subdomain = ? LIMIT 1', + [APP_ICONS_SUBDOMAIN], + ); return rows[0] ?? null; }