dashboard/shell/utils/gc/gc.ts

283 lines
8.1 KiB
TypeScript

import { SETTING } from '@shell/config/settings';
import { COUNT, MANAGEMENT } from '@shell/config/types';
import { GC_DEFAULTS, GC_PREFERENCES } from './gc-types';
class GarbageCollect {
private static ENABLE_DEBUG_LOGGING = false;
/**
* Don't run GC if it's been run within 5 seconds
*/
private static GC_RE_RUN_GAP = 1000 * 5;
private cachedGcPrefs: GC_PREFERENCES = GC_DEFAULTS;
private cachedGcPrefsStamp = '';
private debugLog = (...args: any) => {
if (GarbageCollect.ENABLE_DEBUG_LOGGING) {
console.debug('GC', ...args); // eslint-disable-line no-console
}
};
/**
* Time the GC last ran
*/
private gcLastRun: number | null = 0;
/**
* To avoid JSON.parse on the `ui-performance` setting keep a local cache
*/
private getUiPerfGarbageCollection = (rootState: any) => {
const uiPerfSetting = rootState.management.types[MANAGEMENT.SETTING]?.list?.find((s: any) => s.id === SETTING.UI_PERFORMANCE);
if (!uiPerfSetting || !uiPerfSetting.value) {
// Could be in the process of logging out
return undefined;
}
const stamp = `${ uiPerfSetting.metadata.generation }-${ uiPerfSetting.metadata.resourceVersion }`;
if (this.cachedGcPrefsStamp !== stamp) {
this.debugLog(`Updating ${ SETTING.UI_PERFORMANCE } cache`);
this.cachedGcPrefsStamp = stamp;
const uiPerfSettingParsed = JSON.parse(uiPerfSetting.value);
this.cachedGcPrefs = uiPerfSettingParsed.garbageCollection;
}
return this.cachedGcPrefs;
};
/**
* The last time a resource was accessed by either find or getters style functions
*/
private lastAccessedCache: {
[store: string]: {
[type: string]: number;
}
} = {}
/**
* Track when a logged in route changes.
*/
private lastRouteChange = 0;
// ------------- GC Enabled ---------------------
gcEnabledAll(pseudoCtx: any, type: string) {
return this.gcEnabledForStore(pseudoCtx.state) && this.gcEnabledSetting(pseudoCtx) && this.gcEnabledForType(pseudoCtx, type);
}
gcEnabledSetting(pseudoCtx: any) {
const { rootState } = pseudoCtx;
// Don't use a getter... as we'll end up triggering ourselves again
const uiPerfGarbageCollection = this.getUiPerfGarbageCollection(rootState);
return uiPerfGarbageCollection?.enabled;
}
gcEnabledForStore(state: any) {
return state?.config?.supportsGc;
}
/**
* Store can require certain types are not GC'd (for example `cluster` and `schema`s, `counts`, etc)
*/
gcEnabledForType(pseudoCtx: any, type: string) {
const { getters } = pseudoCtx;
if (!type || getters.gcIgnoreTypes[type]) {
return false;
}
return true;
}
gcEnabledInterval(pseudoCtx: any) {
const { rootState } = pseudoCtx;
// Don't use a getter... as we'll end up triggering ourselves again
const uiPerfGarbageCollection = this.getUiPerfGarbageCollection(rootState);
return {
enabled: uiPerfGarbageCollection?.enabledInterval,
interval: uiPerfGarbageCollection?.interval || 0
};
}
gcEnabledRoute(pseudoCtx: any) {
const { rootState } = pseudoCtx;
// Don't use a getter... as we'll end up triggering ourselves again
const uiPerfGarbageCollection = this.getUiPerfGarbageCollection(rootState);
return uiPerfGarbageCollection?.enabledOnNavigate;
}
// ------------- GC (actual) ---------------------
/**
* Remove stale resource types from the store and stop watching them for changes
*/
garbageCollect(ctx: any, ignoreTypes: {[type: string]: boolean} = {}) {
const now = new Date().getTime();
// Is gc currently running OR has run in the past GC_RE_RUN_GAP return early
if (this.gcLastRun === null || now - this.gcLastRun < GarbageCollect.GC_RE_RUN_GAP) {
this.debugLog('Skipping (running or recently run)', this.gcLastRun ? new Date(this.gcLastRun) : 'running');
return;
}
this.gcLastRun = null;
const gcd: {[type: string]: number} = {};
try {
const { getters, rootState, dispatch } = ctx;
if (!rootState.clusterReady) {
this.debugLog('Skipping (cluster not ready)');
this.gcLastRun = new Date().getTime();
return ;
}
this.debugLog(`------ Started ------`);
const uiPerfGarbageCollection = this.getUiPerfGarbageCollection(rootState);
if (!uiPerfGarbageCollection) {
return ;
}
const maxAge = uiPerfGarbageCollection.ageThreshold * 1000;
const maxCount = uiPerfGarbageCollection.countThreshold;
this.debugLog(`Max Age: ${ maxAge }. Max Count: ${ maxCount }`);// , 'Cache', this.lastAccessedCache
const store = ctx.state.config.namespace;
// this.debugLog('Cache', this.lastAccessedCache);
Object.entries((this.lastAccessedCache[store] || {})).forEach(([type, lastAccessed]) => {
if (!lastAccessed) {
// There's no last accessed time... gc is probably disabled in another way
this.debugLog(`${ type }: Skipping (no accessed time)`);
return;
}
if (!this.gcEnabledForType(ctx, type)) {
// This specific store is telling us to ignore the type (for example `cluster` store will not GC schema's, counts, etc)
this.debugLog(`${ type }: Skipping (type ignored by store)`);
return;
}
if (ignoreTypes[type]) {
// We're going to a place that needs the resource
this.debugLog(`${ type }: Skipping (navigating to type)`);
return;
}
if (now - lastAccessed <= maxAge) {
// The resource was recently accessed
this.debugLog(`${ type }: Skipping (recently accessed)`);
return;
}
if (this.lastRouteChange !== undefined && this.lastRouteChange < lastAccessed ) {
// The resource is being used in the current route/page
this.debugLog(`${ type }: Skipping (used in current route/page)`);
return;
}
const countFromResource = getters.all(COUNT)[0].counts[type]?.summary.count;
const currentCount = countFromResource ?? 0;
if (currentCount === undefined || currentCount < maxCount) {
// There's too few resources, we might as well keep them to avoid a network request when we need them again
this.debugLog(`${ type }: Skipping (too few of resource)`);
return;
}
this.debugLog(`${ type }: Removing from store`);
dispatch('forgetType', type);
gcd[type] = currentCount;
});
this.gcLastRun = new Date().getTime();
} catch (e) {
this.debugLog(`: Error`, e);
this.gcLastRun = new Date().getTime();
}
if (Object.keys(gcd).length > 0) {
console.info('Garbage Collected Resources', gcd); // eslint-disable-line no-console
}
this.debugLog(`------ Finished ------`);
}
// ------------- GC Update local cache ---------------------
/**
* Update the time that the resource type was accessed
* This needs to run after any type initialisation (aka registerType)
*/
gcUpdateLastAccessed(pseudoCtx: any, type: string) {
if (!this.gcEnabledAll(pseudoCtx, type)) {
return;
}
const store = pseudoCtx.state.config.namespace;
if (!this.lastAccessedCache[store]) {
this.lastAccessedCache[store] = {};
}
this.lastAccessedCache[store][type] = new Date().getTime();
}
/**
* Update the time the user last changed routes
*/
gcUpdateRouteChanged() {
this.lastRouteChange = new Date().getTime();
}
// ------------- GC reset ---------------------
/**
* Remove all cached access times for the given store
*/
gcResetStore(state: any) {
const store = state.config.namespace;
delete this.lastAccessedCache[store];
this.debugLog('Forgetting Store:', store);
}
/**
* Remove cached access time for the given resource type
*/
gcResetType(state: any, type: string) {
const store = state.config.namespace;
if (!this.lastAccessedCache[store]) {
return;
}
delete this.lastAccessedCache[store][type];
this.debugLog('Forgetting Type:', store, type);
}
}
const gc = new GarbageCollect();
export default gc;