Add a cache around the database application target state

Change-type: minor
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2019-09-26 15:03:36 +01:00
parent 6027556150
commit 7239b93f4a
2 changed files with 87 additions and 10 deletions

View File

@ -16,6 +16,8 @@ updateLock = require './lib/update-lock'
{ NotFoundError } = require './lib/errors'
{ pathExistsOnHost } = require './lib/fs-utils'
{ ApplicationTargetStateWrapper } = require './target-state'
{ ServiceManager } = require './compose/service-manager'
{ Service } = require './compose/service'
{ Images } = require './compose/images'
@ -79,6 +81,8 @@ module.exports = class ApplicationManager extends EventEmitter
@_targetVolatilePerImageId = {}
@_containerStarted = {}
@targetStateWrapper = new ApplicationTargetStateWrapper(this, @config, @db)
@config.on 'change', (changedConfig) =>
if changedConfig.appUpdatePollInterval
@images.appUpdatePollInterval = changedConfig.appUpdatePollInterval
@ -251,9 +255,7 @@ module.exports = class ApplicationManager extends EventEmitter
).get(appId)
getTargetApp: (appId) =>
@config.get('apiEndpoint').then (endpoint) ->
@db.models('app').where({ appId, source: endpoint }).select()
.then ([ app ]) =>
@targetStateWrapper.getTargetApp(appId).then (app) =>
if !app?
return
@normaliseAndExtendAppFromDB(app)
@ -690,8 +692,7 @@ module.exports = class ApplicationManager extends EventEmitter
return appClone
Promise.map(appsArray, @normaliseAppForDB)
.tap (appsForDB) =>
Promise.map appsForDB, (app) =>
@db.upsertModel('app', app, { appId: app.appId }, trx)
@targetStateWrapper.setTargetApps(appsForDB, trx)
.then (appsForDB) ->
trx('app').where({ source }).whereNotIn('appId', _.map(appsForDB, 'appId')).del()
.then =>
@ -714,11 +715,7 @@ module.exports = class ApplicationManager extends EventEmitter
@_targetVolatilePerImageId[imageId] = {}
getTargetApps: =>
@config.getMany(['apiEndpoint', 'localMode']). then ({ apiEndpoint, localMode }) =>
source = apiEndpoint
if localMode
source = 'local'
Promise.map(@db.models('app').where({ source }), @normaliseAndExtendAppFromDB)
Promise.map(@targetStateWrapper.getTargetApps(), @normaliseAndExtendAppFromDB)
.map (app) =>
if !_.isEmpty(app.services)
app.services = _.map app.services, (service) =>

80
src/target-state.ts Normal file
View File

@ -0,0 +1,80 @@
import * as _ from 'lodash';
import ApplicationManager from './application-manager';
import Config from './config';
import Database, { Transaction } from './db';
// Once we have correct types for both applications and the
// incoming target state this should be changed
export type DatabaseApp = Dictionary<any>;
export type DatabaseApps = DatabaseApp[];
/*
* This class is a wrapper around the database setting and
* receiving of target state. Because the target state can
* only be set from a single place, but several workflows
* rely on getting the target state at one point or another,
* we cache the values using this class. Accessing the
* database is inherently expensive, and for example the
* local log backend accesses the target state for every log
* line. This can very quickly cause serious memory problems
* and database connection timeouts.
*/
export class ApplicationTargetStateWrapper {
private targetState?: DatabaseApps;
public constructor(
protected applications: ApplicationManager,
protected config: Config,
protected db: Database,
) {
// If we switch backend, the target state also needs to
// be invalidated (this includes switching to and from
// local mode)
this.config.on('change', conf => {
if (conf.apiEndpoint != null || conf.localMode != null) {
this.targetState = undefined;
}
});
}
public async getTargetApp(appId: number): Promise<DatabaseApp | undefined> {
if (this.targetState == null) {
// TODO: Perhaps only fetch a single application from
// the DB, at the expense of repeating code
await this.getTargetApps();
}
return _.find(this.targetState, app => app.appId === appId);
}
public async getTargetApps(): Promise<DatabaseApp> {
if (this.targetState == null) {
const { apiEndpoint, localMode } = await this.config.getMany([
'apiEndpoint',
'localMode',
]);
const source = localMode ? 'local' : apiEndpoint;
this.targetState = await this.db.models('app').where({ source });
}
return this.targetState!;
}
public async setTargetApps(
apps: DatabaseApps,
trx: Transaction,
): Promise<void> {
// We can't cache the value here, as it could be for a
// different source
this.targetState = undefined;
await Promise.all(
apps.map(app =>
this.db.upsertModel('app', app, { appId: app.appId }, trx),
),
);
}
}
export default ApplicationTargetStateWrapper;