From 3844e28f768c511cb1886ae3c6169da07761f716 Mon Sep 17 00:00:00 2001
From: Anton Vakhrushev <anwinged@ya.ru>
Date: Sat, 2 May 2020 11:48:24 +0300
Subject: [PATCH] Fix statistics records trimming

---
 src/Container.ts                 | 15 ++++++++++-
 src/Executor.ts                  |  6 ++---
 src/Statistics.ts                | 45 ++++++++++++++++++++++++++------
 src/Storage/StatisticsStorage.ts |  4 +--
 src/utils.ts                     |  4 +++
 tests/Queue/StatisticsTest.js.ts | 35 +++++++++++++++++++++++++
 6 files changed, 95 insertions(+), 14 deletions(-)
 create mode 100644 tests/Queue/StatisticsTest.js.ts

diff --git a/src/Container.ts b/src/Container.ts
index 981cb4a..c8787d8 100644
--- a/src/Container.ts
+++ b/src/Container.ts
@@ -5,6 +5,8 @@ import { ActionQueue } from './Queue/ActionQueue';
 import { Executor } from './Executor';
 import { ControlPanel } from './ControlPanel';
 import { DataStorageTaskProvider } from './Queue/DataStorageTaskProvider';
+import { Statistics } from './Statistics';
+import { StatisticsStorage } from './Storage/StatisticsStorage';
 
 export class Container {
     private readonly version: string;
@@ -33,7 +35,7 @@ export class Container {
         this._executor =
             this._executor ||
             (() => {
-                return new Executor(this.version, this.scheduler);
+                return new Executor(this.version, this.scheduler, this.statistics);
             })();
         return this._executor;
     }
@@ -48,4 +50,15 @@ export class Container {
             })();
         return this._controlPanel;
     }
+
+    private _statistics: Statistics | undefined;
+
+    get statistics(): Statistics {
+        this._statistics =
+            this._statistics ||
+            (() => {
+                return new Statistics(new StatisticsStorage());
+            })();
+        return this._statistics;
+    }
 }
diff --git a/src/Executor.ts b/src/Executor.ts
index 6a67dfd..29c6eef 100644
--- a/src/Executor.ts
+++ b/src/Executor.ts
@@ -23,11 +23,11 @@ export class Executor {
     private executionState: ExecutionStorage;
     private logger: Logger;
 
-    constructor(version: string, scheduler: Scheduler) {
+    constructor(version: string, scheduler: Scheduler, statistics: Statistics) {
         this.version = version;
         this.scheduler = scheduler;
         this.grabbers = new GrabberManager();
-        this.statistics = new Statistics();
+        this.statistics = statistics;
         this.executionState = new ExecutionStorage();
         this.logger = new ConsoleLogger(this.constructor.name);
     }
@@ -103,7 +103,7 @@ export class Executor {
             throw new ActionError(`Action task id ${cmd.args.taskId} not equal current task id ${task.id}`);
         }
         if (actionHandler) {
-            this.statistics.incrementAction();
+            this.statistics.incrementAction(timestamp());
             await actionHandler.run(cmd.args, task);
         } else {
             this.logger.warn('ACTION NOT FOUND', cmd.name);
diff --git a/src/Statistics.ts b/src/Statistics.ts
index 8ac8eca..30185f8 100644
--- a/src/Statistics.ts
+++ b/src/Statistics.ts
@@ -1,25 +1,54 @@
-import { StatisticsStorage } from './Storage/StatisticsStorage';
 import * as dateFormat from 'dateformat';
 
+const KEY_FORMAT = 'yyyy-mm-dd-HH';
+const KEEP_RECORD_COUNT = 24;
+
 export interface ActionStatistics {
     [key: string]: number;
 }
 
-export class Statistics {
-    private state: StatisticsStorage;
+export interface StatisticsStorageInterface {
+    getActionStatistics(): ActionStatistics;
+    setActionStatistics(statistics: ActionStatistics): void;
+}
 
-    constructor() {
-        this.state = new StatisticsStorage();
+export class Statistics {
+    private state: StatisticsStorageInterface;
+
+    static readonly keepRecords = KEEP_RECORD_COUNT;
+
+    constructor(storage: StatisticsStorageInterface) {
+        this.state = storage;
     }
 
-    incrementAction(): void {
+    incrementAction(ts: number): void {
         const stat = this.state.getActionStatistics();
-        const key = dateFormat(Date.now(), 'yyyy-mm-dd-HH');
+        const key = dateFormat(ts * 1000, KEY_FORMAT);
         stat[key] = (stat[key] || 0) + 1;
+        this.trimStatistics(stat);
         this.state.setActionStatistics(stat);
     }
 
+    private trimStatistics(stat: ActionStatistics) {
+        const topKeys = this.getTopStatKeys(stat);
+        const statKeys = Object.keys(stat);
+        for (let key of statKeys) {
+            if (!topKeys.includes(key)) {
+                delete stat[key];
+            }
+        }
+        return stat;
+    }
+
+    private getTopStatKeys(stat: ActionStatistics) {
+        const keys = Object.keys(stat);
+        return keys
+            .sort()
+            .reverse()
+            .slice(0, KEEP_RECORD_COUNT);
+    }
+
     getActionStatistics(): ActionStatistics {
-        return {};
+        return this.state.getActionStatistics();
     }
 }
diff --git a/src/Storage/StatisticsStorage.ts b/src/Storage/StatisticsStorage.ts
index 8b89a52..1114bdb 100644
--- a/src/Storage/StatisticsStorage.ts
+++ b/src/Storage/StatisticsStorage.ts
@@ -1,11 +1,11 @@
 import { DataStorage } from '../DataStorage';
-import { ActionStatistics } from '../Statistics';
+import { ActionStatistics, StatisticsStorageInterface } from '../Statistics';
 
 const NAMESPACE = 'statistics.v1';
 
 const ACTION_STATISTICS_KEY = 'actions';
 
-export class StatisticsStorage {
+export class StatisticsStorage implements StatisticsStorageInterface {
     private storage: DataStorage;
     constructor() {
         this.storage = new DataStorage(NAMESPACE);
diff --git a/src/utils.ts b/src/utils.ts
index b608c00..6c98bc5 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -115,6 +115,10 @@ export function notify(msg: string): void {
     setTimeout(() => n && n.close(), 4000);
 }
 
+export interface NowTimeGenerator {
+    (): number;
+}
+
 export function markPage(text: string, version: string) {
     jQuery('body').append(
         '<div style="' +
diff --git a/tests/Queue/StatisticsTest.js.ts b/tests/Queue/StatisticsTest.js.ts
new file mode 100644
index 0000000..c2fdbb7
--- /dev/null
+++ b/tests/Queue/StatisticsTest.js.ts
@@ -0,0 +1,35 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+
+import { ActionStatistics, Statistics, StatisticsStorageInterface } from '../../src/Statistics';
+
+class MemoryStatisticsStorage implements StatisticsStorageInterface {
+    stat: ActionStatistics = {};
+
+    getActionStatistics(): ActionStatistics {
+        return this.stat;
+    }
+
+    setActionStatistics(statistics: ActionStatistics): void {
+        this.stat = statistics;
+    }
+}
+
+describe('Statistics', function() {
+    it('Can save statistics item', function() {
+        const storage = new MemoryStatisticsStorage();
+        const statistics = new Statistics(storage);
+        statistics.incrementAction(1588408294);
+        expect(Object.keys(statistics.getActionStatistics())).to.has.lengthOf(1);
+    });
+
+    it('Can trim statistics', function() {
+        const storage = new MemoryStatisticsStorage();
+        const statistics = new Statistics(storage);
+        const baseTime = 1588408294;
+        for (let i = 0; i < 120; ++i) {
+            statistics.incrementAction(baseTime + 3600 * i);
+        }
+        expect(Object.keys(statistics.getActionStatistics())).to.has.lengthOf(Statistics.keepRecords);
+    });
+});