diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index 032f91bead..09b150f5f5 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -386,233 +386,226 @@ async function get_app (options) { * @returns {Promise>} */ const get_apps = spanify('get_apps', async (specifiers, options = {}) => { - const start = Date.now(); - console.log('Entering Get apps at: ', start); - try { - if ( ! Array.isArray(specifiers) ) { - specifiers = [specifiers]; - } + if ( ! Array.isArray(specifiers) ) { + specifiers = [specifiers]; + } - const rawIcon = Boolean(options.rawIcon); - const cacheNamespace = rawIcon ? 'apps' : 'apps:lite'; - const pendingNamespace = rawIcon ? 'pending_app' : 'pending_app_lite'; - const decorateApp = (app) => (rawIcon ? app : withAppIconUrl(app)); - const cacheApp = (app) => { - if ( ! app ) return; - const cached_app = { ...app }; - kv.set(`${cacheNamespace}:uid:${cached_app.uid}`, cached_app, { EX: 60 }); - kv.set(`${cacheNamespace}:name:${cached_app.name}`, cached_app, { EX: 60 }); - kv.set(`${cacheNamespace}:id:${cached_app.id}`, cached_app, { EX: 60 }); - }; + const rawIcon = Boolean(options.rawIcon); + const cacheNamespace = rawIcon ? 'apps' : 'apps:lite'; + const pendingNamespace = rawIcon ? 'pending_app' : 'pending_app_lite'; + const decorateApp = (app) => (rawIcon ? app : withAppIconUrl(app)); + const cacheApp = (app) => { + if ( ! app ) return; + const cached_app = { ...app }; + kv.set(`${cacheNamespace}:uid:${cached_app.uid}`, cached_app, { EX: 60 }); + kv.set(`${cacheNamespace}:name:${cached_app.name}`, cached_app, { EX: 60 }); + kv.set(`${cacheNamespace}:id:${cached_app.id}`, cached_app, { EX: 60 }); + }; - const APP_COLUMNS_NO_ICON = [ - 'id', - 'uid', - 'owner_user_id', - 'name', - 'title', - 'description', - 'godmode', - 'maximize_on_start', - 'index_url', - 'approved_for_listing', - 'approved_for_opening_items', - 'approved_for_incentive_program', - 'timestamp', - 'last_review', - 'tags', - 'app_owner', - 'metadata', - 'protected', - 'background', - ].map(column => `\`${column}\``).join(', '); - - const normalized = specifiers.map(spec => spec ? { ...spec } : {}); - - if ( options.follow_old_names ) { - const svc_oldAppName = _servicesHolder.services.get('old-app-name'); - for ( const spec of normalized ) { - if ( spec.uid || !spec.name ) continue; - const old_name = await svc_oldAppName.check_app_name(spec.name); - if ( old_name ) { - spec.uid = old_name.app_uid; - delete spec.name; - } + const APP_COLUMNS_NO_ICON = [ + 'id', + 'uid', + 'owner_user_id', + 'name', + 'title', + 'description', + 'godmode', + 'maximize_on_start', + 'index_url', + 'approved_for_listing', + 'approved_for_opening_items', + 'approved_for_incentive_program', + 'timestamp', + 'last_review', + 'tags', + 'app_owner', + 'metadata', + 'protected', + 'background', + ].map(column => `\`${column}\``).join(', '); + + const normalized = specifiers.map(spec => spec ? { ...spec } : {}); + + if ( options.follow_old_names ) { + const svc_oldAppName = _servicesHolder.services.get('old-app-name'); + for ( const spec of normalized ) { + if ( spec.uid || !spec.name ) continue; + const old_name = await svc_oldAppName.check_app_name(spec.name); + if ( old_name ) { + spec.uid = old_name.app_uid; + delete spec.name; } } + } - const appByUid = new Map(); - const appByName = new Map(); - const appById = new Map(); + const appByUid = new Map(); + const appByName = new Map(); + const appById = new Map(); - const addApp = (app) => { - if ( ! app ) return; - appByUid.set(app.uid, app); - appByName.set(app.name, app); - appById.set(app.id, app); - }; + const addApp = (app) => { + if ( ! app ) return; + appByUid.set(app.uid, app); + appByName.set(app.name, app); + appById.set(app.id, app); + }; - const pendingLookups = new Map(); - const pendingToResolve = new Map(); - const queryUids = new Set(); - const queryNames = new Set(); - const queryIds = new Set(); + const pendingLookups = new Map(); + const pendingToResolve = new Map(); + const queryUids = new Set(); + const queryNames = new Set(); + const queryIds = new Set(); - const queueMissing = (type, value) => { - const queryKey = `${type}:${value}`; - if ( pendingToResolve.has(queryKey) || pendingLookups.has(queryKey) ) { - return; - } + const queueMissing = (type, value) => { + const queryKey = `${type}:${value}`; + if ( pendingToResolve.has(queryKey) || pendingLookups.has(queryKey) ) { + return; + } - const pendingKey = `${pendingNamespace}:${queryKey}`; - const pending = kv.get(pendingKey); - if ( pending ) { - pendingLookups.set(queryKey, pending); - return; - } + const pendingKey = `${pendingNamespace}:${queryKey}`; + const pending = kv.get(pendingKey); + if ( pending ) { + pendingLookups.set(queryKey, pending); + return; + } - let resolveQuery; - let rejectQuery; - const queryPromise = new Promise((resolve, reject) => { - resolveQuery = resolve; - rejectQuery = reject; - }); - kv.set(pendingKey, queryPromise, { EX: PENDING_QUERY_TTL }); - pendingToResolve.set(queryKey, { resolveQuery, rejectQuery, pendingKey }); - - if ( type === 'uid' ) { - queryUids.add(value); - } else if ( type === 'name' ) { - queryNames.add(value); - } else if ( type === 'id' ) { - queryIds.add(value); - } - }; + let resolveQuery; + let rejectQuery; + const queryPromise = new Promise((resolve, reject) => { + resolveQuery = resolve; + rejectQuery = reject; + }); + kv.set(pendingKey, queryPromise, { EX: PENDING_QUERY_TTL }); + pendingToResolve.set(queryKey, { resolveQuery, rejectQuery, pendingKey }); + + if ( type === 'uid' ) { + queryUids.add(value); + } else if ( type === 'name' ) { + queryNames.add(value); + } else if ( type === 'id' ) { + queryIds.add(value); + } + }; - for ( const spec of normalized ) { - if ( spec.uid ) { - const cached = kv.get(`${cacheNamespace}:uid:${spec.uid}`); - if ( cached ) { - addApp(decorateApp(cached)); - } else { - queueMissing('uid', spec.uid); - } - continue; + for ( const spec of normalized ) { + if ( spec.uid ) { + const cached = kv.get(`${cacheNamespace}:uid:${spec.uid}`); + if ( cached ) { + addApp(decorateApp(cached)); + } else { + queueMissing('uid', spec.uid); } - if ( spec.name ) { - const cached = kv.get(`${cacheNamespace}:name:${spec.name}`); - if ( cached ) { - addApp(decorateApp(cached)); - } else { - queueMissing('name', spec.name); - } - continue; + continue; + } + if ( spec.name ) { + const cached = kv.get(`${cacheNamespace}:name:${spec.name}`); + if ( cached ) { + addApp(decorateApp(cached)); + } else { + queueMissing('name', spec.name); } - if ( spec.id ) { - const cached = kv.get(`${cacheNamespace}:id:${spec.id}`); - if ( cached ) { - addApp(decorateApp(cached)); - } else { - queueMissing('id', spec.id); - } + continue; + } + if ( spec.id ) { + const cached = kv.get(`${cacheNamespace}:id:${spec.id}`); + if ( cached ) { + addApp(decorateApp(cached)); + } else { + queueMissing('id', spec.id); } } + } - const pendingResultsPromise = pendingLookups.size - ? Promise.all(Array.from(pendingLookups.values())) - : Promise.resolve([]); + const pendingResultsPromise = pendingLookups.size + ? Promise.all(Array.from(pendingLookups.values())) + : Promise.resolve([]); - if ( queryUids.size || queryNames.size || queryIds.size ) { + if ( queryUids.size || queryNames.size || queryIds.size ) { /** @type BaseDatabaseAccessService */ - const db = _servicesHolder.services.get('database').get(DB_READ, 'apps'); - - const clauses = []; - const params = []; + const db = _servicesHolder.services.get('database').get(DB_READ, 'apps'); - if ( queryUids.size ) { - const uids = Array.from(queryUids); - clauses.push(`uid IN (${uids.map(() => '?').join(', ')})`); - params.push(...uids); - } - if ( queryNames.size ) { - const names = Array.from(queryNames); - clauses.push(`name IN (${names.map(() => '?').join(', ')})`); - params.push(...names); - } - if ( queryIds.size ) { - const ids = Array.from(queryIds); - clauses.push(`id IN (${ids.map(() => '?').join(', ')})`); - params.push(...ids); - } + const clauses = []; + const params = []; - let rows = []; - const resolvedKeys = new Set(); - try { - const select_columns = rawIcon ? '*' : APP_COLUMNS_NO_ICON; - rows = await db.read(`SELECT ${select_columns} FROM \`apps\` WHERE ${clauses.join(' OR ')}`, - params); - for ( const app of rows ) { - const decorated_app = decorateApp(app); - cacheApp(decorated_app); - addApp(decorated_app); - - const uidKey = `uid:${decorated_app.uid}`; - const nameKey = `name:${decorated_app.name}`; - const idKey = `id:${decorated_app.id}`; - - if ( pendingToResolve.has(uidKey) ) { - pendingToResolve.get(uidKey).resolveQuery(decorated_app); - resolvedKeys.add(uidKey); - } - if ( pendingToResolve.has(nameKey) ) { - pendingToResolve.get(nameKey).resolveQuery(decorated_app); - resolvedKeys.add(nameKey); - } - if ( pendingToResolve.has(idKey) ) { - pendingToResolve.get(idKey).resolveQuery(decorated_app); - resolvedKeys.add(idKey); - } - } + if ( queryUids.size ) { + const uids = Array.from(queryUids); + clauses.push(`uid IN (${uids.map(() => '?').join(', ')})`); + params.push(...uids); + } + if ( queryNames.size ) { + const names = Array.from(queryNames); + clauses.push(`name IN (${names.map(() => '?').join(', ')})`); + params.push(...names); + } + if ( queryIds.size ) { + const ids = Array.from(queryIds); + clauses.push(`id IN (${ids.map(() => '?').join(', ')})`); + params.push(...ids); + } - for ( const [key, { resolveQuery }] of pendingToResolve.entries() ) { - if ( ! resolvedKeys.has(key) ) { - resolveQuery(null); - } + let rows = []; + const resolvedKeys = new Set(); + try { + const select_columns = rawIcon ? '*' : APP_COLUMNS_NO_ICON; + rows = await db.read(`SELECT ${select_columns} FROM \`apps\` WHERE ${clauses.join(' OR ')}`, + params); + for ( const app of rows ) { + const decorated_app = decorateApp(app); + cacheApp(decorated_app); + addApp(decorated_app); + + const uidKey = `uid:${decorated_app.uid}`; + const nameKey = `name:${decorated_app.name}`; + const idKey = `id:${decorated_app.id}`; + + if ( pendingToResolve.has(uidKey) ) { + pendingToResolve.get(uidKey).resolveQuery(decorated_app); + resolvedKeys.add(uidKey); } - } catch ( err ) { - for ( const { rejectQuery } of pendingToResolve.values() ) { - rejectQuery(err); + if ( pendingToResolve.has(nameKey) ) { + pendingToResolve.get(nameKey).resolveQuery(decorated_app); + resolvedKeys.add(nameKey); } - throw err; - } finally { - for ( const { pendingKey } of pendingToResolve.values() ) { - kv.del(pendingKey); + if ( pendingToResolve.has(idKey) ) { + pendingToResolve.get(idKey).resolveQuery(decorated_app); + resolvedKeys.add(idKey); } } + for ( const [key, { resolveQuery }] of pendingToResolve.entries() ) { + if ( ! resolvedKeys.has(key) ) { + resolveQuery(null); + } + } + } catch ( err ) { + for ( const { rejectQuery } of pendingToResolve.values() ) { + rejectQuery(err); + } + throw err; + } finally { + for ( const { pendingKey } of pendingToResolve.values() ) { + kv.del(pendingKey); + } } - const pendingResults = await pendingResultsPromise; - for ( const app of pendingResults ) { - addApp(decorateApp(app)); - } + } - return normalized.map(spec => { - let app; - if ( spec.uid ) { - app = appByUid.get(spec.uid); - } else if ( spec.name ) { - app = appByName.get(spec.name); - } else if ( spec.id ) { - app = appById.get(spec.id); - } - return app ? { ...app } : null; - }); - } finally { - const end = Date.now(); - console.log('Exiting at: ', end); - console.log(`Total time taken for get_apps(): ${end - start}`); + const pendingResults = await pendingResultsPromise; + for ( const app of pendingResults ) { + addApp(decorateApp(app)); } + + return normalized.map(spec => { + let app; + if ( spec.uid ) { + app = appByUid.get(spec.uid); + } else if ( spec.name ) { + app = appByName.get(spec.name); + } else if ( spec.id ) { + app = appById.get(spec.id); + } + return app ? { ...app } : null; + }); + }); /** diff --git a/src/backend/src/services/repositories/DDBClient.ts b/src/backend/src/services/repositories/DDBClient.ts index 6c1a7e05cb..39cd376dba 100644 --- a/src/backend/src/services/repositories/DDBClient.ts +++ b/src/backend/src/services/repositories/DDBClient.ts @@ -49,7 +49,7 @@ export class DDBClient { const port = (typeof address === 'object' && address ? address.port : undefined) || 4567; const dynamoEndpoint = `http://127.0.0.1:${port}`; - return new DynamoDBClient({ + const client = new DynamoDBClient({ credentials: { accessKeyId: 'fake', secretAccessKey: 'fake', @@ -63,9 +63,11 @@ export class DDBClient { endpoint: dynamoEndpoint, region: 'us-west-2', }); + console.log(`Dynalite DynamoDB client created with region ${await client.config.region()}`); + return client; } - return new DynamoDBClient({ + const client = new DynamoDBClient({ credentials: { accessKeyId: this.config.aws.access_key, secretAccessKey: this.config.aws.secret_key, @@ -79,10 +81,11 @@ export class DDBClient { ...(this.config.endpoint ? { endpoint: this.config.endpoint } : {}), region: this.config.aws.region || 'us-west-2', }); + console.log(`Dynalite DynamoDB client created with region ${await client.config.region()}`); + return client; } async get >(table: string, key: T, consistentRead = false) { - const command = new GetCommand({ TableName: table, Key: key, diff --git a/src/backend/src/services/sla/RateLimitService.js b/src/backend/src/services/sla/RateLimitService.js index 4865a70534..c40fde99f9 100644 --- a/src/backend/src/services/sla/RateLimitService.js +++ b/src/backend/src/services/sla/RateLimitService.js @@ -93,7 +93,7 @@ class RateLimitService extends BaseService { kv.set(`${kvkey}:window_start`, window_start); kv.set(`${kvkey}:count`, 0); - await this.db.write('INSERT INTO `rl_usage_fixed_window` (`key`, `window_start`, `count`) VALUES (?, ?, ?)', + this.db.write('INSERT INTO `rl_usage_fixed_window` (`key`, `window_start`, `count`) VALUES (?, ?, ?)', [dbkey, ts_to_sql(window_start), 0]); this.log.debug('CREATE window_start and count', @@ -105,7 +105,7 @@ class RateLimitService extends BaseService { kv.set(`${kvkey}:window_start`, window_start); kv.set(`${kvkey}:count`, 0); - await this.db.write('UPDATE `rl_usage_fixed_window` SET `window_start` = ?, `count` = ? WHERE `key` = ?', + this.db.write('UPDATE `rl_usage_fixed_window` SET `window_start` = ?, `count` = ? WHERE `key` = ?', [ts_to_sql(window_start), 0, dbkey]); } @@ -118,7 +118,7 @@ class RateLimitService extends BaseService { } kv.incr(`${kvkey}:count`); - await this.db.write('UPDATE `rl_usage_fixed_window` SET `count` = `count` + 1 WHERE `key` = ?', + this.db.write('UPDATE `rl_usage_fixed_window` SET `count` = `count` + 1 WHERE `key` = ?', [dbkey]); } diff --git a/src/backend/src/util/otelutil.js b/src/backend/src/util/otelutil.js index 7cf9406df2..02a9d22122 100644 --- a/src/backend/src/util/otelutil.js +++ b/src/backend/src/util/otelutil.js @@ -21,9 +21,9 @@ // to create spans correctly. The path of least resistance should // be the correct path, not a way to shoot yourself in the foot. -const { context, trace, SpanStatusCode } = require('@opentelemetry/api'); -const { Context } = require('./context'); -const { TeePromise } = require('@heyputer/putility').libs.promise; +import { context, trace, SpanStatusCode } from '@opentelemetry/api'; +import { Context } from './context.js'; +import { TeePromise } from '@heyputer/putility/src/libs/promise.js'; /* parallel span example from GPT-4: @@ -43,7 +43,7 @@ promises.push(tracer.startActiveSpan(`job:${job.id}`, (span) => { */ /** @type {(label:string, fn:T, tracer?: unknown)=> T} */ -const spanify = (label, fn, tracer) => async function (...args) { +export const spanify = (label, fn, tracer) => async function (...args) { const context = Context.get(); if ( ! context ) { // We don't use the proper logger here because we would normally @@ -51,24 +51,36 @@ const spanify = (label, fn, tracer) => async function (...args) { console.error('spanify failed', new Error('missing context')); } - tracer = tracer ?? context.get('services').get('traceService').tracer; + tracer = tracer ?? context?.get('services')?.get('traceService')?.tracer; + if ( ! tracer ) { + console.error('spanify failed', new Error('missing tracer or services')); + // eslint-disable-next-line no-invalid-this + return await fn.apply(this, args); + } let result; - await tracer.startActiveSpan(label, async span => { + return await tracer.startActiveSpan(label, async span => { + try { // eslint-disable-next-line no-invalid-this - result = await fn.apply(this, args); - span.end(); + result = await fn.apply(this, args); + span.setStatus({ code: SpanStatusCode.OK }); + return result; + } catch (e) { + span.recordException(e); + span.setStatus({ code: SpanStatusCode.ERROR, message: e.message }); + } finally { + span.end(); + } }); - return result; }; /** @type {(label: string, tracer?: unknown) => MethodDecorator} */ -const Span = (label, tracer) => (_target, _propertyKey, descriptor) => { +export const Span = (label, tracer) => (_target, _propertyKey, descriptor) => { if ( !descriptor || typeof descriptor.value !== 'function' ) return descriptor; descriptor.value = spanify(label, descriptor.value, tracer); return descriptor; }; -const abtest = async (label, impls) => { +export const abtest = async (label, impls) => { const context = Context.get(); if ( ! context ) { // We don't use the proper logger here because we would normally @@ -91,7 +103,7 @@ const abtest = async (label, impls) => { return result; }; -class ParallelTasks { +export class ParallelTasks { constructor ({ tracer, max } = {}) { this.tracer = tracer; this.max = max ?? Infinity; @@ -154,11 +166,4 @@ class ParallelTasks { throw new AggregateError(errors); } } -} - -module.exports = { - ParallelTasks, - spanify, - Span, - abtest, -}; +} \ No newline at end of file