Skip to content

Commit 2d42ce7

Browse files
committed
generic near cache
1 parent 251c6d1 commit 2d42ce7

File tree

5 files changed

+550
-0
lines changed

5 files changed

+550
-0
lines changed

src/Config.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,66 @@ export interface LifecycleListener {
136136
(event: string): void;
137137
}
138138

139+
/**
140+
* Represents the format that objects are kept in this client's memory.
141+
*/
142+
export enum InMemoryFormat {
143+
/**
144+
* Objects are in native JS objects
145+
*/
146+
OBJECT,
147+
148+
/**
149+
* Objects are in serialized form
150+
*/
151+
BINARY
152+
}
153+
154+
export enum LocalUpdatePolicy {
155+
INVALIDATE,
156+
CACHE
157+
}
158+
159+
export class NearCacheConfig {
160+
name: string = 'default';
161+
/**
162+
* 'true' to invalidate entries when they are changed in cluster,
163+
* 'false' to invalidate entries only when they are accessed.
164+
*/
165+
invalidateOnChange: boolean = true;
166+
/**
167+
* Max number of seconds that an entry can stay in the cache until it is acceessed
168+
*/
169+
maxIdleSeconds: number = 0;
170+
inMemoryFormat: InMemoryFormat = InMemoryFormat.BINARY;
171+
/**
172+
* Maximum number of seconds that an entry can stay in cache.
173+
*/
174+
timeToLiveSeconds: number = 0;
175+
evictionPolicy: EvictionPolicy = EvictionPolicy.NONE;
176+
evictionMaxSize: number = Number.MAX_SAFE_INTEGER;
177+
evictionSamplingCount: number = 8;
178+
evictionSamplingPoolSize: number = 16;
179+
180+
toString(): string {
181+
return 'NearCacheConfig[' +
182+
'name: ' + this.name + ', ' +
183+
'invalidateOnChange:' + this.invalidateOnChange + ', ' +
184+
'inMemoryFormat: ' + this.inMemoryFormat + ', ' +
185+
'ttl(sec): ' + this.timeToLiveSeconds + ', ' +
186+
'evictionPolicy: ' + this.evictionPolicy + ', ' +
187+
'evictionMaxSize: ' + this.evictionMaxSize + ', ' +
188+
'maxIdleSeconds: ' + this.maxIdleSeconds + ']';
189+
}
190+
}
191+
192+
export enum EvictionPolicy {
193+
NONE,
194+
LRU,
195+
LFU,
196+
RANDOM
197+
}
198+
139199
/**
140200
* Configurations for LifecycleListeners. These are registered as soon as client started.
141201
*/
@@ -171,4 +231,5 @@ export class ClientConfig {
171231
reliableTopicConfigs: any = {
172232
'default': new ReliableTopicConfig()
173233
};
234+
nearCacheConfigs: {[name: string]: NearCacheConfig} = {};
174235
}

src/NearCache.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import {Data} from './serialization/Data';
2+
import {EvictionPolicy, InMemoryFormat, NearCacheConfig} from './Config';
3+
import {shuffleArray} from './Util';
4+
import {SerializationService} from './serialization/SerializationService';
5+
export class DataRecord {
6+
key: Data | any;
7+
value: Data | any;
8+
private creationTime: number;
9+
private expirationTime: number;
10+
private lastAccessTime: number;
11+
private accessHit: number;
12+
13+
constructor(key: Data | any, value: Data | any, creationTime?: number, ttl?: number) {
14+
this.key = key;
15+
this.value = value;
16+
if (creationTime) {
17+
this.creationTime = creationTime;
18+
} else {
19+
this.creationTime = new Date().getTime();
20+
}
21+
if (ttl) {
22+
this.expirationTime = this.creationTime + ttl * 1000;
23+
} else {
24+
this.expirationTime = undefined;
25+
}
26+
this.lastAccessTime = this.creationTime;
27+
this.accessHit = 0;
28+
}
29+
30+
public static lruComp(x: DataRecord, y: DataRecord) {
31+
return x.lastAccessTime - y.lastAccessTime;
32+
}
33+
34+
public static lfuComp(x: DataRecord, y: DataRecord) {
35+
return x.accessHit - y.accessHit;
36+
}
37+
38+
public static randomComp(x: DataRecord, y: DataRecord) {
39+
return Math.random() - 0.5;
40+
}
41+
42+
isExpired(maxIdleSeconds: number) {
43+
var now = new Date().getTime();
44+
if ( (this.expirationTime > 0 && this.expirationTime < now) ||
45+
(maxIdleSeconds > 0 && this.lastAccessTime + maxIdleSeconds * 1000 < now)) {
46+
return true;
47+
} else {
48+
return false;
49+
}
50+
}
51+
52+
setAccessTime(): void {
53+
this.lastAccessTime = new Date().getTime();
54+
}
55+
56+
hitRecord(): void {
57+
this.accessHit++;
58+
}
59+
}
60+
61+
export interface NearCacheStatistics {
62+
evictedCount: number;
63+
expiredCount: number;
64+
missCount: number;
65+
hitCount: number;
66+
entryCount: number;
67+
}
68+
69+
export class NearCache {
70+
71+
private serializationService: SerializationService;
72+
private name: string;
73+
private invalidateOnChange: boolean;
74+
private maxIdleSeconds: number;
75+
private inMemoryFormat: InMemoryFormat;
76+
private timeToLiveSeconds: number;
77+
private evictionPolicy: EvictionPolicy;
78+
private evictionMaxSize: number;
79+
private evictionSamplingCount: number;
80+
private evictionSamplingPoolSize: number;
81+
private evictionCandidatePool: Array<DataRecord>;
82+
83+
internalStore: Map<any | Data, DataRecord>;
84+
85+
private evictedCount: number = 0;
86+
private expiredCount: number = 0;
87+
private missCount: number = 0;
88+
private hitCount: number = 0;
89+
private compareFunc: (x: DataRecord, y: DataRecord) => number;
90+
91+
constructor(nearCacheConfig: NearCacheConfig, serializationService: SerializationService) {
92+
this.serializationService = serializationService;
93+
this.name = nearCacheConfig.name;
94+
this.invalidateOnChange = nearCacheConfig.invalidateOnChange;
95+
this.maxIdleSeconds = nearCacheConfig.maxIdleSeconds;
96+
this.inMemoryFormat = nearCacheConfig.inMemoryFormat;
97+
this.timeToLiveSeconds = nearCacheConfig.timeToLiveSeconds;
98+
this.evictionPolicy = nearCacheConfig.evictionPolicy;
99+
this.evictionMaxSize = nearCacheConfig.evictionMaxSize;
100+
this.evictionSamplingCount = nearCacheConfig.evictionSamplingCount;
101+
this.evictionSamplingPoolSize = nearCacheConfig.evictionSamplingPoolSize;
102+
if (this.evictionPolicy === EvictionPolicy.LFU) {
103+
this.compareFunc = DataRecord.lfuComp;
104+
} else if (this.evictionPolicy === EvictionPolicy.LRU) {
105+
this.compareFunc = DataRecord.lruComp;
106+
} else if (this.evictionPolicy === EvictionPolicy.RANDOM) {
107+
this.compareFunc = DataRecord.randomComp;
108+
} else {
109+
this.compareFunc = undefined;
110+
}
111+
112+
this.evictionCandidatePool = [];
113+
this.internalStore = new Map();
114+
}
115+
116+
/**
117+
* Creates a new {DataRecord} for given key and value. Then, puts the record in near cache.
118+
* If the number of records in near cache exceeds {evictionMaxSize}, it removes expired items first.
119+
* If there is no expired item, it triggers an invalidation process to create free space.
120+
* @param key
121+
* @param value
122+
*/
123+
put(key: any, value: any): void {
124+
this.doEvictionIfRequired();
125+
if (this.inMemoryFormat === InMemoryFormat.OBJECT) {
126+
value = this.serializationService.toObject(value);
127+
} else {
128+
value = this.serializationService.toData(value);
129+
}
130+
var dr = new DataRecord(key, value, undefined, this.timeToLiveSeconds);
131+
this.internalStore.set(key, dr);
132+
}
133+
134+
/**
135+
*
136+
* @param key
137+
* @returns the value if present in near cache, 'undefined' if not
138+
*/
139+
get(key: Data | any): Data | any {
140+
var dr = this.internalStore.get(key);
141+
if (dr === undefined) {
142+
this.missCount++;
143+
return undefined;
144+
}
145+
if (dr.isExpired(this.maxIdleSeconds)) {
146+
this.expireRecord(key);
147+
this.missCount++;
148+
return undefined;
149+
}
150+
dr.setAccessTime();
151+
dr.hitRecord();
152+
this.hitCount++;
153+
if (this.inMemoryFormat === InMemoryFormat.BINARY) {
154+
return this.serializationService.toObject(dr.value);
155+
} else {
156+
return dr.value;
157+
}
158+
}
159+
160+
protected isEvictionRequired() {
161+
return this.evictionPolicy !== EvictionPolicy.NONE && this.evictionMaxSize <= this.internalStore.size;
162+
}
163+
164+
protected doEvictionIfRequired(): void {
165+
if (!this.isEvictionRequired()) {
166+
return;
167+
}
168+
var internalSize = this.internalStore.size;
169+
if (this.recomputeEvictionPool() > 0) {
170+
return;
171+
} else {
172+
this.evictRecord(this.evictionCandidatePool[0].key);
173+
this.evictionCandidatePool = this.evictionCandidatePool.slice(1);
174+
}
175+
}
176+
177+
/**
178+
* @returns number of expired elements.
179+
*/
180+
protected recomputeEvictionPool(): number {
181+
var arr: Array<DataRecord> = Array.from(this.internalStore, (v: [any, DataRecord]) => { return v[1]; } );
182+
183+
shuffleArray<DataRecord>(arr);
184+
var newCandidates = arr.slice(0, this.evictionSamplingCount);
185+
var cleanedNewCandidates = newCandidates.filter(this.filterExpiredRecord, this);
186+
var expiredCount = newCandidates.length - cleanedNewCandidates.length;
187+
if (expiredCount > 0) {
188+
return expiredCount;
189+
}
190+
191+
this.evictionCandidatePool.push(...cleanedNewCandidates);
192+
193+
this.evictionCandidatePool.sort(this.compareFunc);
194+
195+
this.evictionCandidatePool = this.evictionCandidatePool.slice(0, this.evictionSamplingPoolSize);
196+
return 0;
197+
}
198+
199+
protected filterExpiredRecord(candidate: DataRecord): boolean {
200+
if (candidate.isExpired(this.maxIdleSeconds)) {
201+
this.expireRecord(candidate.key);
202+
return false;
203+
} else {
204+
return true;
205+
}
206+
}
207+
208+
protected expireRecord(key: any | Data): void {
209+
if (this.internalStore.delete(key) ) {
210+
this.expiredCount++;
211+
}
212+
}
213+
214+
protected evictRecord(key: any | Data): void {
215+
if (this.internalStore.delete(key)) {
216+
this.evictedCount++;
217+
}
218+
}
219+
220+
getStatistics(): NearCacheStatistics {
221+
var stats: NearCacheStatistics = {
222+
evictedCount: this.evictedCount,
223+
expiredCount: this.expiredCount,
224+
missCount: this.missCount,
225+
hitCount: this.hitCount,
226+
entryCount: this.internalStore.size
227+
};
228+
return stats;
229+
}
230+
}

src/Util.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ export function assertNotNull(v: any) {
99
export function assertArray(x: any) {
1010
assert(Array.isArray(x), 'Should be array.');
1111
}
12+
13+
export function shuffleArray<T>(array: Array<T>): void {
14+
var randomIndex: number;
15+
var temp: T;
16+
for (var i = array.length; i > 1; i--) {
17+
randomIndex = Math.floor(Math.random() * i);
18+
temp = array[i - 1];
19+
array[i - 1] = array[randomIndex];
20+
array[randomIndex] = temp;
21+
}
22+
}
23+
1224
export function getType(obj: any): string {
1325
assertNotNull(obj);
1426
if (Long.isLong(obj)) {

src/proxy/MapProxy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ import {MapExecuteWithPredicateCodec} from '../codec/MapExecuteWithPredicateCode
6060
import {MapExecuteOnKeyCodec} from '../codec/MapExecuteOnKeyCodec';
6161
import {MapExecuteOnKeysCodec} from '../codec/MapExecuteOnKeysCodec';
6262
import * as SerializationUtil from '../serialization/SerializationUtil';
63+
64+
//TODO this is a temprorary reference to get NearCache compiled
65+
import {NearCache, DataRecord} from '../NearCache';
66+
6367
export class MapProxy<K, V> extends BaseProxy implements IMap<K, V> {
6468

6569
executeOnKeys(keys: K[], entryProcessor: IdentifiedDataSerializable|Portable): Promise<any[]> {

0 commit comments

Comments
 (0)