mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-19 19:28:59 +00:00
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:
parent
6027556150
commit
7239b93f4a
@ -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
80
src/target-state.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user