mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-24 07:46:41 +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';
|
} from './lib/errors';
|
||||||
import { pathExistsOnHost } from './lib/fs-utils';
|
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 { ServiceManager } from './compose/service-manager';
|
||||||
import { Service } from './compose/service';
|
import { Service } from './compose/service';
|
||||||
@ -185,8 +185,6 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
this._targetVolatilePerImageId = {};
|
this._targetVolatilePerImageId = {};
|
||||||
this._containerStarted = {};
|
this._containerStarted = {};
|
||||||
|
|
||||||
this.targetStateWrapper = new TargetStateAccessor(this);
|
|
||||||
|
|
||||||
this.actionExecutors = compositionSteps.getExecutors({
|
this.actionExecutors = compositionSteps.getExecutors({
|
||||||
lockFn: this._lockingIfNecessary,
|
lockFn: this._lockingIfNecessary,
|
||||||
services: this.services,
|
services: this.services,
|
||||||
@ -225,34 +223,28 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
return this.emit('change', data);
|
return this.emit('change', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
return Images.initialized
|
await Images.initialized;
|
||||||
.then(() => Images.cleanupDatabase())
|
await Images.cleanupDatabase();
|
||||||
.then(() => {
|
const cleanup = () => {
|
||||||
const cleanup = () => {
|
return docker.listContainers({ all: true }).then((containers) => {
|
||||||
return docker.listContainers({ all: true }).then((containers) => {
|
return logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
// 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
|
// Returns the status of applications and their services
|
||||||
@ -409,7 +401,7 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTargetApp(appId) {
|
getTargetApp(appId) {
|
||||||
return this.targetStateWrapper.getTargetApp(appId).then((app) => {
|
return targetStateCache.getTargetApp(appId).then((app) => {
|
||||||
if (app == null) {
|
if (app == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1129,7 +1121,7 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
return Promise.map(appsArray, this.normaliseAppForDB)
|
return Promise.map(appsArray, this.normaliseAppForDB)
|
||||||
.then((appsForDB) => {
|
.then((appsForDB) => {
|
||||||
return this.targetStateWrapper.setTargetApps(appsForDB, trx);
|
return targetStateCache.setTargetApps(appsForDB, trx);
|
||||||
})
|
})
|
||||||
.then(() =>
|
.then(() =>
|
||||||
trx('app')
|
trx('app')
|
||||||
@ -1219,7 +1211,7 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
|
|
||||||
getTargetApps() {
|
getTargetApps() {
|
||||||
return Promise.map(
|
return Promise.map(
|
||||||
this.targetStateWrapper.getTargetApps(),
|
targetStateCache.getTargetApps(),
|
||||||
this.normaliseAndExtendAppFromDB,
|
this.normaliseAndExtendAppFromDB,
|
||||||
)
|
)
|
||||||
.map((app) => {
|
.map((app) => {
|
||||||
|
@ -4,71 +4,78 @@ import { ApplicationManager } from '../application-manager';
|
|||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
|
|
||||||
// Once we have correct types for both applications and the
|
// We omit the id (which does appear in the db) in this type, as we don't use it
|
||||||
// incoming target state this should be changed
|
// at all, and we can use the below type for both insertion and retrieval.
|
||||||
export type DatabaseApp = Dictionary<any>;
|
export interface DatabaseApp {
|
||||||
|
name: string;
|
||||||
|
releaseId: number;
|
||||||
|
commit: string;
|
||||||
|
appId: number;
|
||||||
|
services: string;
|
||||||
|
networks: string;
|
||||||
|
volumes: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
export type DatabaseApps = DatabaseApp[];
|
export type DatabaseApps = DatabaseApp[];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This class is a wrapper around the database setting and
|
* This module is a wrapper around the database fetching and retrieving of
|
||||||
* receiving of target state. Because the target state can
|
* target state. Because the target state can only be set only be set from a
|
||||||
* only be set from a single place, but several workflows
|
* single place, but several workflows rely on getting the target state at one
|
||||||
* rely on getting the target state at one point or another,
|
* point or another, we cache the values using this class. Accessing the
|
||||||
* we cache the values using this class. Accessing the
|
* database is inherently expensive, and for example the local log backend
|
||||||
* database is inherently expensive, and for example the
|
* accesses the target state for every log line. This can very quickly cause
|
||||||
* local log backend accesses the target state for every log
|
* serious memory problems and database connection timeouts.
|
||||||
* line. This can very quickly cause serious memory problems
|
|
||||||
* and database connection timeouts.
|
|
||||||
*/
|
*/
|
||||||
export class TargetStateAccessor {
|
let targetState: DatabaseApps | undefined;
|
||||||
private targetState?: DatabaseApps;
|
|
||||||
|
|
||||||
public constructor(protected applications: ApplicationManager) {
|
export const initialized = (async () => {
|
||||||
// If we switch backend, the target state also needs to
|
await db.initialized;
|
||||||
// be invalidated (this includes switching to and from
|
await config.initialized;
|
||||||
// local mode)
|
// If we switch backend, the target state also needs to
|
||||||
config.on('change', (conf) => {
|
// be invalidated (this includes switching to and from
|
||||||
if (conf.apiEndpoint != null || conf.localMode != null) {
|
// local mode)
|
||||||
this.targetState = undefined;
|
config.on('change', (conf) => {
|
||||||
}
|
if (conf.apiEndpoint != null || conf.localMode != null) {
|
||||||
});
|
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);
|
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> {
|
return _.find(targetState, (app) => app.appId === appId);
|
||||||
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)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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