2q-cache
Version:
Imlementation of 2Q Cache Algorithm
366 lines (313 loc) • 10.5 kB
text/typescript
import { Cache } from './cache/cache';
import { CacheFactory } from './cache/cache_factory';
import { CacheBuckets } from './buckets/buckets';
export class CacheException extends Error {
constructor(message) {
super(message);
}
}
/**
* @class Cache2Q
* @classdesc 2Q Cache Implementation
* @author ArtemBorisov
*/
export class Cache2Q {
private GLOBAL_TTL : number = 0;
private useStringifyKeys : boolean = false;
private useTtlInMain : boolean = false;
private buckets : CacheBuckets;
private main : Cache;
private mainStorageType : string;
private calculateSizes(size : number) : Array<number> {
return [
Math.floor(Math.abs(0.2 * size)),
Math.floor(Math.abs(0.6 * size)),
Math.floor(Math.abs(0.2 * size)),
];
}
private caclulate(size : number | Array<number>) {
let newSizes : Array<number>;
if (typeof(size) == 'number') {
newSizes = this.calculateSizes(size);
} else if (Array.isArray(size)) {
size = size.filter(el => {
return typeof(el) === 'number';
})
if (size.length === 3) {
size = size.map(el => {
return Math.floor(Math.abs(el));
})
newSizes = size;
} else {
throw new CacheException("Invalid alloc params");
}
} else {
throw new CacheException("Invalid alloc params");
}
return newSizes;
}
constructor(size : number | Array<number>, options : object) {
if (typeof(options) === 'object' && !Array.isArray(options)) {
this.GLOBAL_TTL = this.resolveTtl(options["ttl"]);
this.useStringifyKeys = Boolean(options["stringKeys"]);
this.useTtlInMain = Boolean(options["ttlInMain"]);
this.mainStorageType = options["mainStorageType"] || 'lru';
} else {
this.GLOBAL_TTL = 0;
this.useStringifyKeys = false;
this.useTtlInMain = false;
this.mainStorageType = 'lru';
}
this.buckets = new CacheBuckets(0, 0);
this.main = CacheFactory.makeMainStorage(this.mainStorageType);
this.allocUnsafe(size);
}
/**
* @description Changes max cache capacity in general or in individual buckets.
* Doesn't allow changes to the smaller side
* @params size : number || Array [inSize : number, outSize : number, mainSize : number]
* @throws CacheException
*/
public alloc(size : number | Array<number>) : boolean {
let bucketsSize : Array<number> = this.buckets.getMaxSize();
let [inSize, outSize] = bucketsSize;
let mainSize = this.main.getMaxSize();
let newSizes : Array<number> = this.caclulate(size);
if ((newSizes[0] >= inSize) && (newSizes[1] >= outSize) && (newSizes[2] >= mainSize)) {
this.buckets.setSize(newSizes[0], newSizes[1]);
this.main.setSize(newSizes[2]);
} else {
throw new CacheException("Changes to smaller side not allowed.\nYou should use allocUnsafe()");
}
return true;
}
/**
* @description Changes max cache capacity in general or in individual buckets.
* @params size : number || Array [inSize : number, outSize : number, mainSize : number]
* Allow changes to the smaller side
*/
public allocUnsafe(size : number | Array<number>) : boolean {
let newSizes : Array<number> = this.caclulate(size);
this.buckets.setSize(newSizes[0], newSizes[1]);
this.main.setSize(newSizes[2]);
return true;
}
/**
* @description Add new pair (key, value) into cache
*
* If key already exists, its value will be changed
*
* Else key will be inserted into "in" FIFO bucket
*
* @param key
* @param value
*/
public set(key : any, value : any, ttl : number = 0) : boolean {
key = this.resolveKey(key);
ttl = this.resolveTtl(ttl);
if (this.main.has(key)) {
this.main.update(key, value, ttl);
return true;
}
return this.buckets.set(key, value, ttl);
}
/**
*
* @param objects Array<objects>, where objects :
*
* {
* key : any,
* value : any,
* ttl : number (optional)
* }
*
* @returns Array of insertion results (Array<boolean>)
*/
public mset(objects : Array<object>) : Array<boolean> {
let boolArray = [];
objects.forEach(obj => {
boolArray.push(this.set(obj["key"], obj["value"], obj["ttl"]));
})
return boolArray;
}
/**
* @description Returns value by presented key, if key exists, otherwise returns null
* @param key
*/
public get(key : any) : object {
key = this.resolveKey(key);
let found = this.main.get(key);
if (!found) {
let transport = this.buckets.get(key);
if (transport) {
if (transport.needMove) {
let timeout = this.useTtlInMain ? transport.timeout : 0;
this.main.set(transport.key, transport.value, timeout);
}
return transport.value;
} else {
return null;
}
}
return null;
}
/**
* Returns array of values by presented keys
* @param keys Array of keys
*/
public mget(keys : Array<any>) : Array<any> {
let values : Array<any> = [];
keys.forEach(key => {
values.push(this.get(key));
})
return values;
}
/**
* @description Returns true if key exists, otherwise returns false
* @param key
*/
public has(key : any) : boolean {
key = this.resolveKey(key);
return this.main.has(key) || this.buckets.has(key);
}
/**
* Returns boolean array with results of has(key[i])
* @param keys Array of keys
*/
public mhas(keys : Array<any>) : Array<boolean> {
let boolArray = [];
keys.forEach(key => {
boolArray.push(this.has(key));
})
return boolArray;
}
/**
* @description If key exists, it deletes pair (key, value) from cache and returns true
*
* If key doesn't exists, returns false;
*
* @param key
*/
public delete(key : any) : boolean {
key = this.resolveKey(key);
if (this.main.has(key)) {
this.main.delete(key);
return true;
} else if(this.buckets.has(key)) {
this.buckets.delete(key);
return true;
}
return false;
}
/**
*
* @param keys Array of keys to delete
* @returns Array of boolean - result of each deletion
*/
public mdel(keys : Array<any>) : Array<boolean> {
let boolArray = [];
keys.forEach(key => {
boolArray.push(this.delete(key));
})
return boolArray;
}
/**
* Updates ttl for elem by key
* @param keys Array of objects {key : "any key", ttl : number}
*/
public resetTtl(key : any, ttl : number) {
key = this.resolveKey(key);
ttl = this.resolveTtl(ttl);
if (this.main.has(key)) {
this.main.updateTtl(key, ttl);
} else {
this.buckets.updateTtl(key, ttl)
}
}
/**
* Updates ttl for each key in param array
* @param keys Array of objects {key : "any key", ttl : number}
*/
public mresetTtl(params : Array<object>) {
params.forEach((elem) => {
this.resetTtl(elem["key"], elem["ttl"]);
})
}
private resolveTtl(ttl : number) {
if (typeof(ttl) === 'number' && ttl > 0) {
return ttl;
} else {
return this.GLOBAL_TTL;
}
}
private resolveKey(key : any) {
if (this.useStringifyKeys) {
if (typeof(key) == 'number') {
return String(key);
} else if (typeof(key) == 'string') {
return key;
} else {
try {
return JSON.stringify(key);
} catch(e) {
throw new CacheException(`Can't resolve key to string using JSON.stringify.\n You should use
json-convertable keys or disable 'useStringifyKeys' by method setStringifyKeys(false)`);
}
}
} else {
return key;
}
}
/**
* @description Makes cache empty
*/
public clear() : boolean {
this.buckets.clear();
this.main.clear();
return true;
}
/**
* @description Sets default time to deletion of cache objects.
*
* It will be works only for new objects, not for objects, that already exists in cache
*
* @param ttl
*/
public setDefaultTtl(ttl : number) {
if (typeof(ttl) === 'number' && ttl > 0) {
this.GLOBAL_TTL = ttl;
return true;
}
return false;
}
/**
* @description Sets param useStringifyKeys
*
* It will be works only for new objects, not for objects, that already exists in cache
*
* @param ttl
*/
setStringifyKeys(boolParam : boolean) {
this.useStringifyKeys = boolParam;
}
/**
* @description Disables default time to deletion of cache objects.
*
* It will be works only for new objects, not for objects, that already exists in cache
*
* @param ttl
*/
public disableTtl() : boolean {
this.GLOBAL_TTL = 0;
return true;
}
/**
* @description Returns js - object, that contains currentCount/maxCount for each cache section
*/
public getSize() : object {
return {
buckets : this.buckets.getSizeObject(),
main : this.main.getSizeObject()
};
}
}