diff --git a/src/lib/db/client-metrics-store-v2.ts b/src/lib/db/client-metrics-store-v2.ts index dfc8494f52..357457697b 100644 --- a/src/lib/db/client-metrics-store-v2.ts +++ b/src/lib/db/client-metrics-store-v2.ts @@ -116,9 +116,17 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 { return prev; }, {}); + // Sort the rows to avoid deadlocks + const batchRow = Object.values(batch).sort( + (a, b) => + a.feature_name.localeCompare(b.feature_name) || + a.app_name.localeCompare(b.app_name) || + a.environment.localeCompare(b.environment), + ); + // Consider rewriting to SQL batch! const insert = this.db(TABLE) - .insert(Object.values(batch)) + .insert(batchRow) .toQuery(); const query = `${insert.toString()} ON CONFLICT (feature_name, app_name, environment, timestamp) DO UPDATE SET "yes" = "client_metrics_env"."yes" + EXCLUDED.yes, "no" = "client_metrics_env"."no" + EXCLUDED.no`; diff --git a/src/lib/services/client-metrics/index.ts b/src/lib/services/client-metrics/index.ts index 874261eff4..1b7fd49498 100644 --- a/src/lib/services/client-metrics/index.ts +++ b/src/lib/services/client-metrics/index.ts @@ -22,12 +22,13 @@ import { IMetricsBucket, } from '../../types/model'; import { clientRegisterSchema } from './register-schema'; + import { minutesToMilliseconds, parseISO, secondsToMilliseconds, } from 'date-fns'; -import TTLList = require('./ttl-list'); +import TTLList from './ttl-list'; export default class ClientMetricsService { globalCount = 0; @@ -38,13 +39,13 @@ export default class ClientMetricsService { lastMinuteProjection = new Projection(); - lastHourList = new TTLList({ + lastHourList = new TTLList({ interval: secondsToMilliseconds(10), }); logger = null; - lastMinuteList = new TTLList({ + lastMinuteList = new TTLList({ interval: secondsToMilliseconds(10), expireType: 'minutes', expireAmount: 1, diff --git a/src/lib/services/client-metrics/list.test.js b/src/lib/services/client-metrics/list.test.ts similarity index 97% rename from src/lib/services/client-metrics/list.test.js rename to src/lib/services/client-metrics/list.test.ts index 73a30e45fa..1eb1c0fdf5 100644 --- a/src/lib/services/client-metrics/list.test.js +++ b/src/lib/services/client-metrics/list.test.ts @@ -1,9 +1,7 @@ -'use strict'; - -const List = require('./list'); +import List from './list'; function getList() { - const list = new List(); + const list = new List(); list.add(1); list.add(2); list.add(3); diff --git a/src/lib/services/client-metrics/list.js b/src/lib/services/client-metrics/list.ts similarity index 82% rename from src/lib/services/client-metrics/list.js rename to src/lib/services/client-metrics/list.ts index e0829352de..8a1d55eccf 100644 --- a/src/lib/services/client-metrics/list.js +++ b/src/lib/services/client-metrics/list.ts @@ -1,31 +1,41 @@ /* eslint-disable no-param-reassign */ /* eslint-disable max-classes-per-file */ -'use strict'; +import { EventEmitter } from 'events'; -const { EventEmitter } = require('events'); +class Node { + value: T | null; -class Node { - constructor(value) { + prev: Node | null; + + next: Node | null; + + constructor(value: T) { this.value = value; this.next = null; } - link(next) { + link(next: Node) { this.next = next; next.prev = this; return this; } } -module.exports = class List extends EventEmitter { +type IteratorFn = (cursor: Node) => U; + +export default class List extends EventEmitter { + private start: Node | null; + + private tail: Node | null; + constructor() { super(); this.start = null; this.tail = null; } - add(obj) { + add(obj: T): Node { const node = new Node(obj); if (this.start) { this.start = node.link(this.start); @@ -36,7 +46,7 @@ module.exports = class List extends EventEmitter { return node; } - iterate(fn) { + iterate(fn: IteratorFn): void { if (!this.start) { return; } @@ -51,7 +61,7 @@ module.exports = class List extends EventEmitter { } } - iterateReverse(fn) { + iterateReverse(fn: IteratorFn): void { if (!this.tail) { return; } @@ -66,7 +76,7 @@ module.exports = class List extends EventEmitter { } } - reverseRemoveUntilTrue(fn) { + reverseRemoveUntilTrue(fn: IteratorFn): void { if (!this.tail) { return; } @@ -98,7 +108,7 @@ module.exports = class List extends EventEmitter { } } - toArray() { + toArray(): T[] { const result = []; if (this.start) { @@ -125,4 +135,4 @@ module.exports = class List extends EventEmitter { // return result; // } -}; +} diff --git a/src/lib/services/client-metrics/ttl-list.test.js b/src/lib/services/client-metrics/ttl-list.test.ts similarity index 90% rename from src/lib/services/client-metrics/ttl-list.test.js rename to src/lib/services/client-metrics/ttl-list.test.ts index fc4d39d20b..e2349b2804 100644 --- a/src/lib/services/client-metrics/ttl-list.test.js +++ b/src/lib/services/client-metrics/ttl-list.test.ts @@ -1,11 +1,9 @@ -'use strict'; - -const TTLList = require('./ttl-list'); -const { addMilliseconds } = require('date-fns'); +import { addMilliseconds } from 'date-fns'; +import TTLList from './ttl-list'; test('should emit expire', (done) => { jest.useFakeTimers('modern'); - const list = new TTLList({ + const list = new TTLList<{ n: number }>({ interval: 20, expireAmount: 10, expireType: 'milliseconds', @@ -25,7 +23,7 @@ test('should emit expire', (done) => { test('should slice off list', () => { jest.useFakeTimers('modern'); - const list = new TTLList({ + const list = new TTLList<{ n: string }>({ interval: 10, expireAmount: 10, expireType: 'milliseconds', @@ -69,7 +67,7 @@ test('should slice off list', () => { test('should add item created in the past but expiring in the future', () => { jest.useFakeTimers('modern'); - const list = new TTLList({ + const list = new TTLList<{ n: string }>({ interval: 10, expireAmount: 10, expireType: 'milliseconds', diff --git a/src/lib/services/client-metrics/ttl-list.js b/src/lib/services/client-metrics/ttl-list.ts similarity index 66% rename from src/lib/services/client-metrics/ttl-list.js rename to src/lib/services/client-metrics/ttl-list.ts index e81538cb30..a09bffe9f2 100644 --- a/src/lib/services/client-metrics/ttl-list.js +++ b/src/lib/services/client-metrics/ttl-list.ts @@ -1,21 +1,38 @@ -'use strict'; - -const { EventEmitter } = require('events'); -const List = require('./list'); -const { +import { EventEmitter } from 'events'; +import List from './list'; +import { add, - isFuture, addMilliseconds, secondsToMilliseconds, -} = require('date-fns'); + Duration, + isFuture, +} from 'date-fns'; + +interface ConstructorArgs { + interval: number; + expireAmount: number; + expireType: keyof Duration | 'milliseconds'; +} // this list must have entries with sorted ttl range -module.exports = class TTLList extends EventEmitter { +export default class TTLList extends EventEmitter { + private readonly interval: number; + + private readonly expireAmount: number; + + private readonly expireType: keyof Duration | 'milliseconds'; + + public list: List<{ ttl: Date; value: T }>; + + private timer: NodeJS.Timeout; + + private readonly getExpiryFrom: (timestamp) => Date; + constructor({ interval = secondsToMilliseconds(1), expireAmount = 1, expireType = 'hours', - } = {}) { + }: Partial = {}) { super(); this.interval = interval; this.expireAmount = expireAmount; @@ -37,7 +54,7 @@ module.exports = class TTLList extends EventEmitter { this.startTimer(); } - startTimer() { + startTimer(): void { if (this.list) { this.timer = setTimeout(() => { if (this.list) { @@ -48,7 +65,7 @@ module.exports = class TTLList extends EventEmitter { } } - add(value, timestamp = new Date()) { + add(value: T, timestamp = new Date()): void { const ttl = this.getExpiryFrom(timestamp); if (isFuture(ttl)) { this.list.add({ ttl, value }); @@ -57,14 +74,14 @@ module.exports = class TTLList extends EventEmitter { } } - timedCheck() { + timedCheck(): void { this.list.reverseRemoveUntilTrue(({ value }) => isFuture(value.ttl)); this.startTimer(); } - destroy() { + destroy(): void { clearTimeout(this.timer); this.timer = null; this.list = null; } -}; +} diff --git a/src/test/e2e/api/admin/client-metrics.e2e.test.ts b/src/test/e2e/api/admin/client-metrics.e2e.test.ts index 6558c076a8..f9f2ad7680 100644 --- a/src/test/e2e/api/admin/client-metrics.e2e.test.ts +++ b/src/test/e2e/api/admin/client-metrics.e2e.test.ts @@ -155,14 +155,19 @@ test('should return toggle summary', async () => { .expect('Content-Type', /json/) .expect(200); + const test = demo.lastHourUsage.find((u) => u.environment === 'test'); + const defaultEnv = demo.lastHourUsage.find( + (u) => u.environment === 'default', + ); + expect(demo.featureName).toBe('demo'); expect(demo.lastHourUsage).toHaveLength(2); - expect(demo.lastHourUsage[0].environment).toBe('default'); - expect(demo.lastHourUsage[0].yes).toBe(5); - expect(demo.lastHourUsage[0].no).toBe(4); - expect(demo.lastHourUsage[1].environment).toBe('test'); - expect(demo.lastHourUsage[1].yes).toBe(2); - expect(demo.lastHourUsage[1].no).toBe(6); + expect(test.environment).toBe('test'); + expect(test.yes).toBe(2); + expect(test.no).toBe(6); + expect(defaultEnv.environment).toBe('default'); + expect(defaultEnv.yes).toBe(5); + expect(defaultEnv.no).toBe(4); expect(demo.seenApplications).toStrictEqual(['backend-api', 'web']); }); @@ -219,13 +224,18 @@ test('should only include last hour of metrics return toggle summary', async () .expect('Content-Type', /json/) .expect(200); + const test = demo.lastHourUsage.find((u) => u.environment === 'test'); + const defaultEnv = demo.lastHourUsage.find( + (u) => u.environment === 'default', + ); + expect(demo.featureName).toBe('demo'); expect(demo.lastHourUsage).toHaveLength(2); - expect(demo.lastHourUsage[0].environment).toBe('default'); - expect(demo.lastHourUsage[0].yes).toBe(5); - expect(demo.lastHourUsage[0].no).toBe(4); - expect(demo.lastHourUsage[1].environment).toBe('test'); - expect(demo.lastHourUsage[1].yes).toBe(2); - expect(demo.lastHourUsage[1].no).toBe(6); + expect(defaultEnv.environment).toBe('default'); + expect(defaultEnv.yes).toBe(5); + expect(defaultEnv.no).toBe(4); + expect(test.environment).toBe('test'); + expect(test.yes).toBe(2); + expect(test.no).toBe(6); expect(demo.seenApplications).toStrictEqual(['backend-api', 'web']); }); diff --git a/website/docs/sdks/java.md b/website/docs/sdks/java.md index 5f567a5671..978a92670a 100644 --- a/website/docs/sdks/java.md +++ b/website/docs/sdks/java.md @@ -13,7 +13,7 @@ First we must add Unleash Client SDK as a dependency to your project. Below is a ```xml - no.finn.unleash + io.getunleash unleash-client-java Latest version here