Skip to content

Commit

Permalink
Merge pull request #99 from REAN-Foundation/performance_fixes
Browse files Browse the repository at this point in the history
Performance fixes
  • Loading branch information
dattatraya-inflection authored Dec 11, 2024
2 parents e64cbbc + 17ec9a9 commit 2ccb22e
Show file tree
Hide file tree
Showing 21 changed files with 477 additions and 449 deletions.
9 changes: 9 additions & 0 deletions src/lib/server/cache/cache.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

export interface ICache {
set(key: string, value: unknown): Promise<void>;
get(key: string): Promise<unknown | undefined>;
has(key: string): Promise<boolean>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
findAndClear(searchPattern: string): Promise<string[]>;
}
45 changes: 45 additions & 0 deletions src/lib/server/cache/cache.map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

export class CacheMap<V> {

private cache: Map<string, V>;

constructor() {
this.cache = new Map<string, V>();
}

set(key: string, value: V): void {
this.cache.set(key, value);
}

get(key: string): V | undefined {
return this.cache.get(key);
}

has(key: string): boolean {
return this.cache.has(key);
}

delete(key: string): boolean {
return this.cache.delete(key);
}

clear(): void {
this.cache.clear();
}

findAndClear(searchPattern: string): string[] {
let keys: string[] = [];
for (let key of this.cache.keys()) {
if (key.includes(searchPattern)) {
keys.push(key);
}
}
for (let key of keys) {
this.cache.delete(key);
}
return keys;
}

}

////////////////////////////////////////////////////////////////////////////////////////
27 changes: 27 additions & 0 deletions src/lib/server/cache/cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Cache

## Setup KeyDB as a Redis cache
KeyDB is a high-performance fork of Redis with a focus on multithreading and memory efficiency
It is designed to be a drop-in replacement for Redis
KeyDB is fully compatible with Redis and supports all Redis commands

```
docker run \
-d --name keydb \
-p 6379:6379 \
-e "CACHE_PASSWORD=your-password" \
-v /path/to/your/data:/data \
-v /path/to/your/logs:/logs keydb/keydb \
eqalpha/keydb
```

Process to connect with KeyDB is same as Redis.
1. Run the docker container.
2. Set the password by logging into container
a. First run redis-cli as
```# redis-cli```
b. Set the password using
```# auth <your-password>```
3. Create a client and connect to KeyDB.
4. Use the client to perform operations.
5. Close the connection when done.
64 changes: 64 additions & 0 deletions src/lib/server/cache/cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { InMemoryCache } from './inmemory.cache'
import { RedisCache } from './redis.cache';
import type { ICache } from './cache.interface';
import { CACHE_TYPE } from '$env/static/private';
import { building } from '$app/environment';

////////////////////////////////////////////////////////////////////////////////////////

const getCache = () => {
//code should not be executed during the build step.
if (!building) {
if (CACHE_TYPE === 'in-memory') {
return new InMemoryCache();
}
return new RedisCache();
}
};

////////////////////////////////////////////////////////////////////////////////////////

export class CacheService {

static _cache: ICache = getCache();

static get = async (key: string): Promise<any | undefined> => {
console.log('CacheService.get', key);
return CacheService._cache.get(key);
}

static set = async (key: string, value: any): Promise<void> => {
await CacheService._cache.set(key, value);
}

static has = async (key: string): Promise<boolean> => {
return CacheService._cache.has(key);
}

static delete = async (key: string): Promise<boolean> => {
return CacheService._cache.delete(key);
}

static deleteMany = async (keys: string[]): Promise<boolean> => {
let result = true;
for (let key of keys) {
result = result && await CacheService._cache.delete(key);
}
return result;
}

static findAndClear = async (searchPatterns: string[]): Promise<string[]> => {
var keys: string[] = [];
for (var substr of searchPatterns)
{
var removedKeys = await CacheService._cache.findAndClear(substr);
keys.push(...removedKeys);
}
return keys;
}

static clear = async (): Promise<void> => {
await CacheService._cache.clear();
}

}
38 changes: 38 additions & 0 deletions src/lib/server/cache/inmemory.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { CacheMap } from "./cache.map";
import { type ICache } from "./cache.interface";

////////////////////////////////////////////////////////////////////////////////////////

export class InMemoryCache implements ICache {

private cache: CacheMap<any> = new CacheMap<any>();

constructor() {
this.cache = new CacheMap<any>();
}

set = async (key: string, value: any): Promise<void> => {
this.cache.set(key, value);
};

get = async (key: string): Promise<any | undefined> => {
return this.cache.get(key);
};

has = async (key: string): Promise<boolean> => {
return this.cache.has(key);
};

delete = async (key: string): Promise<boolean> => {
return this.cache.delete(key);
};

clear = async (): Promise<void> => {
this.cache.clear();
};

findAndClear = async (searchPattern: string): Promise<string[]> => {
return this.cache.findAndClear(searchPattern);
}

}
120 changes: 120 additions & 0 deletions src/lib/server/cache/redis.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { type ICache } from "./cache.interface";
import { createClient, type RedisClientType } from 'redis';

////////////////////////////////////////////////////////////////////////////////////////
// Using KeyDB as a Redis cache
// KeyDB is a high-performance fork of Redis with a focus on multithreading and memory efficiency
// It is designed to be a drop-in replacement for Redis
// KeyDB is fully compatible with Redis and supports all Redis commands
//
// docker run \
// -d --name keydb \
// -p 6379:6379 \
// -e "CACHE_PASSWORD=your-password" \
// -v /path/to/your/data:/data \
// -v /path/to/your/logs:/logs keydb/keydb \
// eqalpha/keydb

// Process to connect with KeyDB is same as Redis.
// 1. Run the docker container.
// 2. Set the password by logging into container
// a. First run redis-cli as
// ```# redis-cli```
// b. Set the password using
// ```# auth <your-password>```
// 3. Create a client and connect to KeyDB.
// 4. Use the client to perform operations.
// 5. Close the connection when done.
//
////////////////////////////////////////////////////////////////////////////////////////

export class RedisCache implements ICache {

private _client: RedisClientType| null = null;

private _expiry = 60 * 60 * 24 * 2; // 48 hours

constructor() {
// Create a client and connect to KeyDB
var port = process.env.CACHE_PORT ? parseInt(process.env.CACHE_PORT) : 6379;
this._client = createClient({
socket: {
host: process.env.CACHE_HOST || 'localhost',
port: port,
},
password: process.env.CACHE_PASSWORD // if authentication is required
});
(async () => {
if (this._client) await this._client.connect();
})();
}

set = async (key: string, value: any): Promise<void> => {
if (this._client) {
const exists = await this._client.exists(key);
if (exists === 1) {
await this._client.del(key);
}
await this._client.set(key, JSON.stringify(value), {
EX: this._expiry,// 24 hours
});
}
};

get = async (key: string): Promise<any | undefined> => {
if (this._client) {
const val = await this._client.get(key);
if (val) {
const value = JSON.parse(val);
return value;
}
}
return undefined;
};

has = async (key: string): Promise<boolean> => {
if (this._client) {
const exists = await this._client.exists(key);
return exists === 1;
}
return false;
};

delete = async (key: string): Promise<boolean> => {
if (this._client) {
const exists = await this._client.exists(key);
if (exists === 1) {
await this._client.del(key);
return true;
}
}
return false;
};

clear = async (): Promise<void> => {
if (this._client) {
console.log('Clearing cache');
this._client.flushAll();
}
};

// size = async (): Promise<number> => {
// if (this._client) {
// // console.log('Getting cache size');
// this._client.dbSize();
// }
// return 0;
// };

findAndClear = async (searchPattern: string): Promise<string[]> => {
if (this._client) {
const keys = await this._client.keys(searchPattern);
if (keys.length > 0) {
await this._client.del(keys);
}
return keys;
}
return [];
}

}
6 changes: 6 additions & 0 deletions src/lib/utils/time.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ export class TimeHelper {
}
return date.toISOString().split('T')[0];
};

static getYesterdayDate = (): string => {
const today = new Date();
today.setDate(today.getDate() - 1);
return TimeHelper.getDateString(today, DateStringFormat.YYYY_MM_DD);
};
}
36 changes: 34 additions & 2 deletions src/routes/api/services/reancare/statistics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { BACKEND_API_URL } from '$env/static/private';
import { USER_ANALYTICS_API_URL } from '$env/static/private';
import { API_CLIENT_INTERNAL_KEY } from '$env/static/private';
import { CacheService } from '$lib/server/cache/cache.service';
import { DateStringFormat } from '$lib/types/time.types';
import { Helper } from '$lib/utils/helper';
import { TimeHelper } from '$lib/utils/time.helper';
import { SessionManager } from '../../sessions/session.manager';
import { get } from './common.reancare';

Expand Down Expand Up @@ -268,12 +272,40 @@ export const getOverallUsers = async (sessionId: string, searchParams?: any) =>

export const getDailyStatistics = async(sessionId:string)=>{
const url = BACKEND_API_URL + `/daily-stats`;
return await get(sessionId, url, true, API_CLIENT_INTERNAL_KEY);
const today = TimeHelper.getDateString(new Date(), DateStringFormat.YYYY_MM_DD);
const cacheKey = `session-${sessionId}:req-getDailyStatistics:${today}`;
const yesterday = TimeHelper.getYesterdayDate();
const yesterdayCacheKey = `session-${sessionId}:req-getDailyStatistics:${yesterday}`;

if (await CacheService.has(yesterdayCacheKey)) {
await CacheService._cache.delete(yesterdayCacheKey);
console.log(`Cleared old key: ${yesterdayCacheKey}`);
}
if (await CacheService.has(cacheKey)) {
return await CacheService.get(cacheKey);
}
const result = await get(sessionId, url, true, API_CLIENT_INTERNAL_KEY);
await CacheService.set(cacheKey, result);
return result;
}

export const getDailyTenantStatistics = async(sessionId:string, tenantId: string)=>{
const url = BACKEND_API_URL + `/daily-stats/tenants/${tenantId}`;
return await get(sessionId, url, true, API_CLIENT_INTERNAL_KEY);
const today = TimeHelper.getDateString(new Date(), DateStringFormat.YYYY_MM_DD);
const cacheKey = `session-${sessionId}:req-getDailyTenantStatistics:${today}`;
const yesterday = TimeHelper.getYesterdayDate();
const yesterdayCacheKey = `session-${sessionId}:req-getDailyTenantStatistics:${yesterday}`;

if (await CacheService.has(yesterdayCacheKey)) {
await CacheService._cache.delete(yesterdayCacheKey);
console.log(`Cleared old key: ${yesterdayCacheKey}`);
}
if (await CacheService.has(cacheKey)) {
return await CacheService.get(cacheKey);
}
const result = await get(sessionId, url, true, API_CLIENT_INTERNAL_KEY);
await CacheService.set(cacheKey, result);
return result;
}

export const getDailySystemStatistics = async(sessionId:string)=>{
Expand Down
Loading

0 comments on commit 2ccb22e

Please sign in to comment.