mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 13:47:54 +00:00
Make target-state-cache a singleton
Change-type: patch Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
parent
5512654d1c
commit
c0e170c61f
@ -24,7 +24,7 @@ import {
|
||||
} from './lib/errors';
|
||||
import { pathExistsOnHost } from './lib/fs-utils';
|
||||
|
||||
import { TargetStateAccessor } from './device-state/target-state-cache';
|
||||
import * as targetStateCache from './device-state/target-state-cache';
|
||||
|
||||
import { ServiceManager } from './compose/service-manager';
|
||||
import { Service } from './compose/service';
|
||||
@ -185,8 +185,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
this._targetVolatilePerImageId = {};
|
||||
this._containerStarted = {};
|
||||
|
||||
this.targetStateWrapper = new TargetStateAccessor(this);
|
||||
|
||||
this.actionExecutors = compositionSteps.getExecutors({
|
||||
lockFn: this._lockingIfNecessary,
|
||||
services: this.services,
|
||||
@ -225,34 +223,28 @@ export class ApplicationManager extends EventEmitter {
|
||||
return this.emit('change', data);
|
||||
}
|
||||
|
||||
init() {
|
||||
return Images.initialized
|
||||
.then(() => Images.cleanupDatabase())
|
||||
.then(() => {
|
||||
const cleanup = () => {
|
||||
return docker.listContainers({ all: true }).then((containers) => {
|
||||
return logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
||||
});
|
||||
};
|
||||
// Rather than relying on removing out of date database entries when we're no
|
||||
// longer using them, set a task that runs periodically to clear out the database
|
||||
// This has the advantage that if for some reason a container is removed while the
|
||||
// supervisor is down, we won't have zombie entries in the db
|
||||
|
||||
// Once a day
|
||||
setInterval(cleanup, 1000 * 60 * 60 * 24);
|
||||
// But also run it in on startup
|
||||
return cleanup();
|
||||
})
|
||||
.then(() => {
|
||||
return this.localModeManager.init();
|
||||
})
|
||||
.then(() => {
|
||||
return this.services.attachToRunning();
|
||||
})
|
||||
.then(() => {
|
||||
return this.services.listenToEvents();
|
||||
async init() {
|
||||
await Images.initialized;
|
||||
await Images.cleanupDatabase();
|
||||
const cleanup = () => {
|
||||
return docker.listContainers({ all: true }).then((containers) => {
|
||||
return logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
||||
});
|
||||
};
|
||||
// Rather than relying on removing out of date database entries when we're no
|
||||
// longer using them, set a task that runs periodically to clear out the database
|
||||
// This has the advantage that if for some reason a container is removed while the
|
||||
// supervisor is down, we won't have zombie entries in the db
|
||||
|
||||
// Once a day
|
||||
setInterval(cleanup, 1000 * 60 * 60 * 24);
|
||||
// But also run it in on startup
|
||||
await cleanup();
|
||||
await this.localModeManager.init();
|
||||
await this.services.attachToRunning();
|
||||
this.services.listenToEvents();
|
||||
|
||||
await targetStateCache.initialized;
|
||||
}
|
||||
|
||||
// Returns the status of applications and their services
|
||||
@ -409,7 +401,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
}
|
||||
|
||||
getTargetApp(appId) {
|
||||
return this.targetStateWrapper.getTargetApp(appId).then((app) => {
|
||||
return targetStateCache.getTargetApp(appId).then((app) => {
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
@ -1129,7 +1121,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
});
|
||||
return Promise.map(appsArray, this.normaliseAppForDB)
|
||||
.then((appsForDB) => {
|
||||
return this.targetStateWrapper.setTargetApps(appsForDB, trx);
|
||||
return targetStateCache.setTargetApps(appsForDB, trx);
|
||||
})
|
||||
.then(() =>
|
||||
trx('app')
|
||||
@ -1219,7 +1211,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
|
||||
getTargetApps() {
|
||||
return Promise.map(
|
||||
this.targetStateWrapper.getTargetApps(),
|
||||
targetStateCache.getTargetApps(),
|
||||
this.normaliseAndExtendAppFromDB,
|
||||
)
|
||||
.map((app) => {
|
||||
|
@ -4,71 +4,78 @@ import { ApplicationManager } from '../application-manager';
|
||||
import * as config from '../config';
|
||||
import * as db from '../db';
|
||||
|
||||
// Once we have correct types for both applications and the
|
||||
// incoming target state this should be changed
|
||||
export type DatabaseApp = Dictionary<any>;
|
||||
// We omit the id (which does appear in the db) in this type, as we don't use it
|
||||
// at all, and we can use the below type for both insertion and retrieval.
|
||||
export interface DatabaseApp {
|
||||
name: string;
|
||||
releaseId: number;
|
||||
commit: string;
|
||||
appId: number;
|
||||
services: string;
|
||||
networks: string;
|
||||
volumes: string;
|
||||
source: string;
|
||||
}
|
||||
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.
|
||||
* This module is a wrapper around the database fetching and retrieving of
|
||||
* target state. Because the target state can only be set 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 TargetStateAccessor {
|
||||
private targetState?: DatabaseApps;
|
||||
let targetState: DatabaseApps | undefined;
|
||||
|
||||
public constructor(protected applications: ApplicationManager) {
|
||||
// If we switch backend, the target state also needs to
|
||||
// be invalidated (this includes switching to and from
|
||||
// local mode)
|
||||
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();
|
||||
export const initialized = (async () => {
|
||||
await db.initialized;
|
||||
await config.initialized;
|
||||
// If we switch backend, the target state also needs to
|
||||
// be invalidated (this includes switching to and from
|
||||
// local mode)
|
||||
config.on('change', (conf) => {
|
||||
if (conf.apiEndpoint != null || conf.localMode != null) {
|
||||
targetState = undefined;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
return _.find(this.targetState, (app) => app.appId === appId);
|
||||
export async function getTargetApp(
|
||||
appId: number,
|
||||
): Promise<DatabaseApp | undefined> {
|
||||
if (targetState == null) {
|
||||
// TODO: Perhaps only fetch a single application from
|
||||
// the DB, at the expense of repeating code
|
||||
await getTargetApps();
|
||||
}
|
||||
|
||||
public async getTargetApps(): Promise<DatabaseApps> {
|
||||
if (this.targetState == null) {
|
||||
const { apiEndpoint, localMode } = await config.getMany([
|
||||
'apiEndpoint',
|
||||
'localMode',
|
||||
]);
|
||||
|
||||
const source = localMode ? 'local' : apiEndpoint;
|
||||
this.targetState = await db.models('app').where({ source });
|
||||
}
|
||||
return this.targetState!;
|
||||
}
|
||||
|
||||
public async setTargetApps(
|
||||
apps: DatabaseApps,
|
||||
trx: db.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) => db.upsertModel('app', app, { appId: app.appId }, trx)),
|
||||
);
|
||||
}
|
||||
return _.find(targetState, (app) => app.appId === appId);
|
||||
}
|
||||
|
||||
export default TargetStateAccessor;
|
||||
export async function getTargetApps(): Promise<DatabaseApps> {
|
||||
if (targetState == null) {
|
||||
const { apiEndpoint, localMode } = await config.getMany([
|
||||
'apiEndpoint',
|
||||
'localMode',
|
||||
]);
|
||||
|
||||
const source = localMode ? 'local' : apiEndpoint;
|
||||
targetState = await db.models('app').where({ source });
|
||||
}
|
||||
return targetState!;
|
||||
}
|
||||
|
||||
export async function setTargetApps(
|
||||
apps: DatabaseApps,
|
||||
trx?: db.Transaction,
|
||||
): Promise<void> {
|
||||
// We can't cache the value here, as it could be for a
|
||||
// different source
|
||||
targetState = undefined;
|
||||
|
||||
await Promise.all(
|
||||
apps.map((app) => db.upsertModel('app', app, { appId: app.appId }, trx)),
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user