|
| 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 | +} |
0 commit comments