From 42700cdda1bd71e2921330ace8cd4a99f36b6021 Mon Sep 17 00:00:00 2001 From: Yingyu Cheng Date: Tue, 10 Apr 2018 14:27:19 +0800 Subject: [PATCH] fix(ContextStore): auto eliminate context store to avoid high memory consumsion (#19) --- package-lock.json | 2 +- src/envoy-context-store.ts | 116 ++++++++++++++++++++++++++++++- src/envoy-proto-decorator.ts | 1 + test/envoy-context-store.test.ts | 49 ++++++++++++- 4 files changed, 163 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb0bc8c..beb6eac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "envoy-node", - "version": "1.5.0-development", + "version": "1.6.0-development", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/envoy-context-store.ts b/src/envoy-context-store.ts index d587861..550dd04 100644 --- a/src/envoy-context-store.ts +++ b/src/envoy-context-store.ts @@ -30,8 +30,109 @@ export class NodeInfo { } } -const store = new Map(); +/** + * Eliminate Store + * using two map for context storage, to avoid holding too many data + * it will eliminate the old data + */ +export class EliminateStore { + private old = new Map(); + private current = new Map(); + + private lastEliminateTime = Date.now(); + + /** + * get context + * @param asyncId asyncId + */ + get(asyncId: number) { + const infoFromCurrent = this.current.get(asyncId); + if (infoFromCurrent !== undefined) { + return infoFromCurrent; + } + const infoFromOld = this.old.get(asyncId); + if (infoFromOld !== undefined) { + this.current.set(asyncId, infoFromOld); + } + return infoFromOld; + } + + /** + * set context + * @param asyncId asyncId + * @param info context info + */ + set(asyncId: number, info: NodeInfo) { + this.current.set(asyncId, info); + } + + /** + * delete context + * @param asyncId asyncId + */ + delete(asyncId: number) { + this.current.delete(asyncId); + this.old.delete(asyncId); + } + + /** + * clear all data + */ + clear() { + this.old.clear(); + this.current.clear(); + } + + /** + * eliminate the old data + */ + eliminate() { + this.old = this.current; + this.current = new Map(); + this.lastEliminateTime = Date.now(); + } + + /** + * get last eliminate time + */ + getLastEliminateTime() { + return this.lastEliminateTime; + } + + /** + * the current size + */ + size() { + return this.current.size; + } + + /** + * the old store size + */ + oldSize() { + return this.old.size; + } +} + +const store = new EliminateStore(); let enabled = false; +let eliminateInterval = 300 * 1000; // 300s, 5 mins + +/** + * set the store's eliminate interval, context data older than this and not + * read will be eventually eliminated + * @param interval time in milliseconds + */ +function setEliminateInterval(interval: number) { + eliminateInterval = interval; +} + +/** + * get eliminate interval + */ +function getEliminateInterval() { + return eliminateInterval; +} /** * clean up will decrease the reference count. @@ -53,6 +154,10 @@ function storeCleanUp(asyncId: number) { const asyncHook = asyncHooks.createHook({ init(asyncId, type, triggerAsyncId, resource) { + /* istanbul ignore next */ + if (Date.now() - store.getLastEliminateTime() > eliminateInterval) { + store.eliminate(); + } let triggerInfo = store.get(triggerAsyncId); if (!triggerInfo) { triggerInfo = new NodeInfo(-1); @@ -122,6 +227,7 @@ function set(context: EnvoyContext) { } const asyncId = asyncHooks.executionAsyncId(); const info = store.get(asyncId); + /* istanbul ignore next */ if (info === undefined) { console.trace( "[envoy-node] Cannot find info of current execution, have you enabled the context store correctly?" @@ -138,6 +244,7 @@ function set(context: EnvoyContext) { */ function getContext(asyncId: number): EnvoyContext | undefined { const info = store.get(asyncId); + /* istanbul ignore next */ if (!info) { return undefined; } @@ -157,6 +264,7 @@ function get(): EnvoyContext | undefined { } const asyncId = asyncHooks.executionAsyncId(); const context = getContext(asyncId); + /* istanbul ignore next */ if (context === undefined) { console.trace( "[envoy-node] Cannot find info of current execution, have you enabled and set the context store correctly?" @@ -169,7 +277,7 @@ function isEnabled() { return enabled; } -function getStore() { +function getStoreImpl() { return store; } @@ -179,5 +287,7 @@ export default { set, get, isEnabled, - getStore + getStoreImpl, + getEliminateInterval, + setEliminateInterval }; diff --git a/src/envoy-proto-decorator.ts b/src/envoy-proto-decorator.ts index 3609c87..36af729 100644 --- a/src/envoy-proto-decorator.ts +++ b/src/envoy-proto-decorator.ts @@ -163,6 +163,7 @@ export default function envoyProtoDecorator( } const { originalName }: { originalName?: string } = method; + /* istanbul ignore next */ if (originalName) { // should alway have prototype[originalName] = prototype[name]; diff --git a/test/envoy-context-store.test.ts b/test/envoy-context-store.test.ts index e7e9e3b..b149dfb 100644 --- a/test/envoy-context-store.test.ts +++ b/test/envoy-context-store.test.ts @@ -1,5 +1,5 @@ import EnvoyContext from "../src/envoy-context"; -import store from "../src/envoy-context-store"; +import store, { EliminateStore, NodeInfo } from "../src/envoy-context-store"; import { sleep } from "./lib/utils"; class Data { @@ -66,6 +66,8 @@ describe("Envoy context store", () => { expect(data.inputCtx).toBe(data.fromPromise); expect(data.inputCtx).toBe(data.fromSetTimeout); }); + expect(store.getStoreImpl().size()).toBe(0); + expect(store.getStoreImpl().oldSize()).toBe(0); }); it("should trace error when set context if store is not enabled", () => { @@ -96,4 +98,49 @@ describe("Envoy context store", () => { expect(console.trace).toBeCalled(); console.trace = originalTrace; }); + + it("Eliminate Store should works", () => { + const es = new EliminateStore(); + expect(es.get(1)).toBeUndefined(); + expect(es.size()).toBe(0); + expect(es.oldSize()).toBe(0); + + const info = new NodeInfo(1); + es.set(1, info); + expect(es.get(1)).toBe(info); + expect(es.size()).toBe(1); + expect(es.oldSize()).toBe(0); + + const oldTime = es.getLastEliminateTime(); + es.eliminate(); + expect(es.size()).toBe(0); + expect(es.oldSize()).toBe(1); + expect(es.getLastEliminateTime()).toBeGreaterThan(oldTime); + + const info2 = new NodeInfo(2); + es.set(2, info2); + expect(es.get(2)).toBe(info2); + expect(es.size()).toBe(1); + expect(es.oldSize()).toBe(1); + + expect(es.get(1)).toBe(info); + expect(es.size()).toBe(2); + expect(es.oldSize()).toBe(1); + + es.delete(1); + expect(es.size()).toBe(1); + expect(es.oldSize()).toBe(0); + expect(es.get(1)).toBeUndefined(); + expect(es.get(2)).toBe(info2); + + es.clear(); + expect(es.size()).toBe(0); + expect(es.oldSize()).toBe(0); + }); + + it("should set eliminate interval works", () => { + expect(store.getEliminateInterval()).toBe(300 * 1000); + store.setEliminateInterval(1); + expect(store.getEliminateInterval()).toBe(1); + }); });